From d3ae2f66bf96b296d7c0d1b0788fb31b98bd6b8d Mon Sep 17 00:00:00 2001 From: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Date: Sun, 27 Sep 2026 09:19:00 -0400 Subject: [PATCH 1/2] feat(gallery): detail-page navigation, code ergonomics, image polish Detail pages were dead ends: a reader who finished one example had to go back to the grid to find the next. They now page within their own kind (prev/next, arrow keys), list their tags as links back to the filtered index, and end with three related entries chosen by shared topic tags. Source listings gain a line-number gutter and collapse past 120 lines; every code block gets a copy button with a non-secure-context fallback and a visible failure state; README headings get stable anchors. Card images carry intrinsic dimensions so the grid no longer shifts as they load, and the lightbox only closes on backdrop, button or Esc -- clicking the image toggles native size instead. Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> --- docs/gallery/anvil/index.html | 1779 +++++++++++++- docs/gallery/apothecary-shelf/index.html | 1866 +++++++++++++- docs/gallery/armature-bend/index.html | 538 +++- .../gallery/attribute-domain-shear/index.html | 819 ++++++- .../bake-normal-high-to-low/index.html | 798 +++++- docs/gallery/bmesh-gear/index.html | 583 ++++- docs/gallery/book-trolley/index.html | 2047 +++++++++++++++- docs/gallery/bookshelf/index.html | 1928 ++++++++++++++- docs/gallery/brazier/index.html | 1598 +++++++++++- docs/gallery/butter-churn/index.html | 1499 ++++++++++- docs/gallery/campfire/index.html | 1645 ++++++++++++- docs/gallery/car-mirror-symmetry/index.html | 1013 +++++++- docs/gallery/cart/index.html | 2165 +++++++++++++++- docs/gallery/chopping-block/index.html | 2181 ++++++++++++++++- docs/gallery/collision-hull-proxy/index.html | 735 +++++- docs/gallery/color-attribute-wheel/index.html | 572 ++++- docs/gallery/compositor-glare/index.html | 590 ++++- docs/gallery/crate-stack/index.html | 1986 ++++++++++++++- .../cross-version-property-delete/index.html | 562 ++++- docs/gallery/curve-bevel-arc/index.html | 741 +++++- docs/gallery/custom-normals-shade/index.html | 1164 ++++++++- docs/gallery/damped-track-aim/index.html | 618 ++++- docs/gallery/degenerate-bevel-weld/index.html | 810 +++++- docs/gallery/depsgraph-export/index.html | 827 ++++++- docs/gallery/driver-wave/index.html | 427 +++- docs/gallery/export-preset-axis/index.html | 969 +++++++- docs/gallery/fence-kit/index.html | 1600 +++++++++++- docs/gallery/gltf-export-roundtrip/index.html | 951 ++++++- docs/gallery/gltf-skin-roundtrip/index.html | 1185 ++++++++- docs/gallery/gn-instance-grid/index.html | 878 ++++++- docs/gallery/gn-modifier-inputs/index.html | 815 +++++- docs/gallery/gn-sdf-remesh/index.html | 416 +++- docs/gallery/gn-sim-fountain/index.html | 872 ++++++- docs/gallery/gn-socket-rename/index.html | 778 +++++- docs/gallery/gn-zone-iterate/index.html | 665 ++++- docs/gallery/gp-lineart-contour/index.html | 941 ++++++- docs/gallery/grain-sacks/index.html | 1644 ++++++++++++- docs/gallery/grease-pencil-rosette/index.html | 511 +++- docs/gallery/grindstone/index.html | 2003 ++++++++++++++- docs/gallery/hand-pump/index.html | 1602 +++++++++++- docs/gallery/hay-bale/index.html | 1866 +++++++++++++- docs/gallery/hitching-post/index.html | 1771 ++++++++++++- docs/gallery/image-pixels-testcard/index.html | 613 ++++- docs/gallery/index.html | 243 +- docs/gallery/iron-cauldron/index.html | 1939 ++++++++++++++- docs/gallery/light-link-studio/index.html | 642 ++++- docs/gallery/lightmap-uv-channel/index.html | 1064 +++++++- docs/gallery/lod-decimate-chain/index.html | 847 ++++++- docs/gallery/market-stall/index.html | 1725 ++++++++++++- docs/gallery/mesh-hygiene-audit/index.html | 944 ++++++- docs/gallery/modular-kit-snap/index.html | 897 ++++++- docs/gallery/parent-inverse-orrery/index.html | 593 ++++- docs/gallery/park-bench/index.html | 1725 ++++++++++++- docs/gallery/png-exr-alpha/index.html | 896 ++++++- docs/gallery/prop-origin-transform/index.html | 1010 +++++++- docs/gallery/rope-bridge/index.html | 2067 +++++++++++++++- docs/gallery/shader-node-group/index.html | 627 ++++- docs/gallery/shape-key-blend/index.html | 683 +++++- docs/gallery/shipping-crate/index.html | 1790 +++++++++++++- docs/gallery/signpost/index.html | 1456 ++++++++++- .../sky-texture-sun-elevation/index.html | 1003 +++++++- docs/gallery/soccer-ball-goldberg/index.html | 679 ++++- docs/gallery/socket-attach-points/index.html | 1348 +++++++++- docs/gallery/stone-archway/index.html | 1655 ++++++++++++- docs/gallery/stone-well/index.html | 1928 ++++++++++++++- docs/gallery/street-lantern/index.html | 1673 ++++++++++++- docs/gallery/swatch-grid/index.html | 679 ++++- docs/gallery/tavern-stool/index.html | 1756 ++++++++++++- docs/gallery/temp-override-join/index.html | 767 +++++- docs/gallery/terrain-scatter/index.html | 1792 +++++++++++++- docs/gallery/text-version-stamp/index.html | 538 +++- docs/gallery/treasure-chest/index.html | 1730 ++++++++++++- docs/gallery/triangulate-tangents/index.html | 839 ++++++- docs/gallery/turntable/index.html | 430 +++- .../usd-export-evaluation-mode/index.html | 570 ++++- docs/gallery/uv-layer-grid/index.html | 748 +++++- docs/gallery/vertex-color-ao/index.html | 1176 ++++++++- docs/gallery/vertex-weight-limit/index.html | 989 +++++++- docs/gallery/vse-cut-list/index.html | 962 +++++++- docs/gallery/vse-gamma-cross/index.html | 861 ++++++- docs/gallery/wall-torch/index.html | 1678 ++++++++++++- docs/gallery/watchtower/index.html | 1862 +++++++++++++- docs/gallery/water-trough/index.html | 1842 +++++++++++++- docs/gallery/wave-displace/index.html | 430 +++- docs/gallery/wheelbarrow/index.html | 2113 +++++++++++++++- docs/gallery/wooden-barrel/index.html | 1639 ++++++++++++- docs/gallery/wooden-bucket/index.html | 1849 +++++++++++++- docs/gallery/wooden-ladder/index.html | 1836 +++++++++++++- docs/gallery/wooden-yoke/index.html | 1492 ++++++++++- scripts/build_gallery.py | 289 ++- 90 files changed, 103769 insertions(+), 2103 deletions(-) diff --git a/docs/gallery/anvil/index.html b/docs/gallery/anvil/index.html index ae699ebc..2a3f191f 100644 --- a/docs/gallery/anvil/index.html +++ b/docs/gallery/anvil/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

anvil

A procedural blacksmith anvil on a timber stump through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

+

Rendered headless by the showcase piece itself — click to zoom.

witnesses Recomputed: 3052 tris, two materials with 1040 wood and 854 metal faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.621×0.414×0.489 m, LOD ratios in band, convex collider 134 tris, hygiene 0, 16 grounded staves, hoop bite 0.003 m, foot-on-head gap 0.003 m, waist corner reach 0.8572, non-empty glTF. --skip-decimate exits 9; --stray-vert 15; --lift-z/--short-staves 16; --float-anvil 17; --round-band 18; --round-waist 19.
+

tags mesh export showcase

blender --background --python showcase/anvil/anvil.py --
@@ -282,23 +341,23 @@

anvil

The face is one add_slotted_slab so hardy/pritchel are real holes in a single shell, not nubs glued onto stacked boxes. Hoops evaluate the same host_outer_r chord as the stave flats. --round-band is the falsifier: a circle of radius_at(z) instead of the chord.

It asserts budget conformance of the generated result. It does not witness an API contract. "It rendered without error" is not a check.

Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package).

-

Budgets

+

Budgets#

Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.

AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles2500–45003052 / 3052 / 3052
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.5000
LOD2 ratio0.10–0.35 of base0.2195 / 0.2195 / 0.2195
Materialsexactly 2 distinct; ≥400 wood, ≥400 metal faces2 slots; 1040 / 854
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(0.621, 0.414, 0.489) m ± 0.015(0.6210, 0.4137, 0.4890), zmin 0
Collider tris≤ 280134
Exportwritten, size > 0248532 / 248532 / 248516 bytes

Base triangles dropped from 6616 to 2988 in the quality pass: overlapping face boxes and a beveled superellipse horn became one slotted slab plus a circular oval loft. Hidden coincident faces were the waste.

DECIMATE COLLAPSE triangle counts are not identical across series — the gate is a ratio band, not an exact count. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Stave-width jitter uses fixed seed 17, and the per-piece wood tone its own seed. Export byte counts differ on 5.2.1 (glTF serializer), not a gated axis.

-

Hygiene

+

Hygiene#

Recomputed from the generated mesh, not asserted about the script.

AxisDeclaredMeasured (all three)
Non-manifold edges00
Loose verts / edges0 / 00 / 0
Doubles merged at 1e-500
Zero-area faces00
N-gons00
Coplanar disjoint face pairs00
Grounded: zminwithin 1e-4 of 00.0000
Named supports: 16 staveseach zmin ≤ 0.00116, stave_z 0.00000
Body plan0.621 m long × 0.180 m high ± 0.050.6210 × 0.1800
Waist formcorner reach at the pinch ≥ 0.80 (an ellipse is 0.707)0.8572
-

Joint fit and seat

+

Joint fit and seat#

AxisDeclaredMeasured (all three)
Foot-on-head BVH gap≤ 0.006 m0.00300
Hoop bite (host r − inner hoop r)0.0012–0.008 m0.00300
-

Waist, steel and stump

+

Waist, steel and stump#

The waist was lofted from ellipses, so under the rectangular face it read as a turned funnel, not a forged anvil. It is now a superellipse section (exponent 4.5) through five stations that flare from the foot in to a pinch and out to the body. Waist form measures corner reach at the narrowest ring: how far a section vertex fills its bounding rectangle's corner. --round-waist restores the ellipse and exits 19 (0.7071).

The steel was satin (metallic 0.92, roughness 0.32) and read as chrome; it is now dark forged steel with rust. Staves and head carry their own wood tone and grain. The stage grid grew from 14 to 60 m. The temp .glb is removed after its size is measured.

-

Falsifiers

+

Falsifiers#

Each violates one named budget. All seven were run on 4.5.11, 5.1.2 and 5.2.1 and returned the same code on each.

FlagBudget violatedExit
--skip-decimateLOD1 ratio band9
--stray-vertloose vertex count is 015
--lift-zbounding box zmin is 016
--short-stavesnamed stave supports at Z=016
--float-anvilfoot-on-head gap17
--round-bandhoop bite band18
--round-waistwaist form19
-

Run

+

Run#

blender --background --python anvil.py --
 blender --background --python anvil.py -- --skip-decimate
 blender --background --python anvil.py -- --stray-vert
@@ -309,17 +368,1575 @@ 

Run

blender --background --python anvil.py -- --round-waist blender --background --python anvil.py -- --output anvil.png

Smoke passes no flags.

-

Exit codes

+

Exit codes#

File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path. 15–19 are the hygiene and joint-fit family.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 2 distinct slots, or a face-count floor missed
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight
16Not grounded: bounding box zmin off 0, or a named stave floats
17Joint fit: foot-on-head gap (--float-anvil)
18Seat: hoop bite band (--round-band)
19Body length or height off the stated real-world size, or waist form (--round-waist)
-
+

Source

showcase/anvil/anvil.py - View on GitHub → + 1558 lines · View on GitHub →
-
"""Game-ready blacksmith anvil — a showcase piece, not an example.
+      
+
"""Game-ready blacksmith anvil — a showcase piece, not an example.
 
 Asserts budget conformance of a procedural London-pattern anvil on a
 coopered timber stump (stave-chord hoops, sawn head, horn/face/heel
@@ -1878,7 +3495,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -1898,20 +3549,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/apothecary-shelf/index.html b/docs/gallery/apothecary-shelf/index.html index 1c59d7f1..00367832 100644 --- a/docs/gallery/apothecary-shelf/index.html +++ b/docs/gallery/apothecary-shelf/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,44 +325,46 @@

apothecary-shelf

A procedural apothecary shelf — housed shelves, gallery rails, a drawer bank and an arched crest, stocked with fifteen lathed bottles, flasks and glazed jars, each corked or lidded and labelled — carried through UVs, bake, LOD, convex collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

+

Rendered headless by the showcase piece itself — click to zoom.

witnesses Recomputed: 10456 tris, six materials with a face floor each, UVs in 0..1 with zero AABB overlap, outer AABB 0.876×0.274×1.076 m, zmin 0, hygiene 0 including zero coplanar cross-shell pairs, both sides grounded, seven cross members housed 8 mm into each side and 14 mm clear of its far face, fifteen vessels seated 1.0–1.8 mm into their shelves, every cork and lid biting 5 mm, every label 0.3 mm into its body and 0.8 mm proud, no vessel touching another or the carcass, and at least 24 mm of headroom between every vessel and the member above it. --tall-flask exits 20 at −5.5 mm while every seat and the envelope still pass; --short-shelves exits 17 on the dado bite.
+

tags mesh export showcase

blender --background --python showcase/apothecary-shelf/apothecary_shelf.py --

An apothecary shelf: a stained carcass with three shelves housed in its sides, a gallery rail along each shelf, lapped back boards, a bank of three drawers with brass knobs and an arched crest, stocked with fifteen vessels in five forms (bottle, flask, jar, albarello, vial). Every vessel is corked or lidded and carries a paper label. A showcase piece, not an example — it witnesses no API contract. It asserts that generated geometry meets declared asset budgets, recomputed from the finished mesh.

-

What it composes

+

What it composes#

Shipped contentUsed for
skills/mesh-editing-and-bmeshchamfered boards, arched crest, lathed vessels, closures and knobs, labels on the bodies' rings, all in one bmesh
skills/custom-propertiesface attributes (PlankTone, GrainDir, Tint) read by the shaders
skills/procedural-materials-and-shadersgrained stain, tinted glass, glaze, cork, inked paper, aged brass
skills/bake-high-to-lowCycles tangent-space normal bake, high onto low
skills/engine-export-presetsUnity glTF (export_yup=True)
skills/depsgraph-and-evaluated-dataevaluated triangle counts for the LOD ratios
snippets/decimate_to_budget.pyLOD1 / LOD2 COLLAPSE chain
snippets/convex_hull_collider.pyone hull over the carcass and knobs
snippets/lod_chain.pyLOD naming and ratio pattern
examples/mesh-hygiene-audithygiene combinatorics (copied, not imported)
-

The budget that matters

+

The budget that matters#

A stocked shelf fails invisibly when a vessel does not fit between its shelf and the one above. A flask a few centimetres too tall runs its neck up into the next shelf, and nothing else notices:

  • the flask still stands on its shelf, so the seat budget passes;
  • the shelf above is still in its dado, so the joint budget passes;
  • the neck stops inside the shelf, so the bounding box does not move;
  • the triangle count is the same, because only the height changed.

The piece reads each vessel's top (body or closure, whichever is higher) and the underside of the lowest horizontal member above it off the finished mesh, and asserts a headroom floor of 20 mm. The tightest vessel clears by 24.05 mm.

--tall-flask is the falsifier built for exactly this. It stretches the flask on the middle shelf by 1.20. It then runs 5.5 mm into the shelf above and exits 20, while every seat, every joint and the envelope still pass.

-

Budgets

+

Budgets#

Declared in the script as named constants, recomputed from the generated mesh. Measured values are from Blender 5.2.1; every one is byte-identical on 4.5.11 and 5.1.2 (only the glTF file size differs, by up to 8 bytes, which is exporter metadata and not a budget).

BudgetBandMeasured
Base triangles9800–1110010456
LOD1 ratio0.32–0.620.5000
LOD2 ratio0.10–0.350.2200
Material slotsexactly 6, distinct6
Wood / glass / glaze faces≥ 450 / 1800 / 1300525 / 2080 / 1520
Cork / paper / brass faces≥ 850 / 380 / 300960 / 420 / 336
UV boundsinside 0..1(0.0009, 0.0010)–(0.9991, 0.9990)
UV AABB overlap≤ 1e-50.000000
Outer AABB0.876 × 0.274 × 1.076 m ± 0.0200.8760 × 0.2740 × 1.0760
Collider triangles≤ 200138
Normal bake{'FINISHED'} with image data{'FINISHED'}, has_data=True
glTF exportfile written, non-empty~449 kB
Hygieneall zeroloose 0/0, non-manifold 0, zero-area 0, doubles 0, n-gons 0, coplanar cross-shell pairs 0
Grounded AABB\zmin\≤ 1e-40.00000
Named sides2 sides, each zmin ≤ 1e-42 at 0.00000
Dado7 cross members, each ≥ 5 mm into both sides and ≥ 6 mm clear of the far face8.00 mm in, 14.00 mm clear
Closure biteevery cork and lid 2–10 mm into its body5.00 mm
Knob bite3 knobs, each 2–10 mm into its drawer front4.00 mm
Vessel seat15 vessels, each 0.5–3.0 mm into its shelf1.00–1.80 mm
Drawer seat3 drawer fronts, each 1–4 mm into the base2.00 mm
Label seatinner face 0.1–0.8 mm inside the body, outer face ≥ 0.4 mm proud0.30 mm in, 0.80 mm proud
Headroomevery vessel ≥ 20 mm under the member above24.05 mm
Vessel overlaps0, vessel against vessel and against the carcass0
Form spreadwidest / narrowest body ≥ 2.0; tallest / shortest ≥ 1.53.146 / 2.202
Edge treatmentright-angle wood edges0

Real-world size: 0.88 m wide, 1.08 m to the top of the crest and 0.27 m deep. It is a wall shelf for a shop counter or a study, and each compartment is 260 mm clear.

-

Construction

+

Construction#

  • Carcass. Two full-height sides carry everything and are the only named supports. The base, the three shelves and the three gallery rails are each housed DADO (8 mm) into both sides and stop 14 mm short of the outer faces. The top board is seated over the sides and the back boards (TOP_SEAT). The crest is an extruded outline whose top is a shallow arc, seated CREST_BITE into the top board. Every board is chamfered at 2.5 mm with material= pinned to wood, edges sorted by index. The crest's caps are single n-gons, chamfered and then triangulated.
  • Back boards. Four vertical boards, lapped 2 mm into one another rather than butted. An open seam showed daylight through the back in the first close-up. A lap on one plane is a coplanar pair, so every other board steps forward 1.5 mm. The shelves and base bite into them.
  • Drawers. Three fronts stand DRAWER_BITE into the base, with a 4 mm reveal all round. Each has a turned brass knob lathed along −Y, its shank KNOB_BITE into the front.
  • Vessels. Each vessel is a closed lathe of exactly twelve rings between two poles, so every form costs the same triangles. Every foot is chamfered. The five forms:
  • bottle: shoulder, neck and lip
  • flask: a bulb on a small flat base, with a long neck
  • jar: squat and wide-mouthed
  • albarello: waisted
  • vial: small

Sizes vary by a closed-form ±5 % per vessel. Each shelf shares its clear width out evenly. Vessels step front and back alternately inside the depth left between the rail and the back boards.

  • Closures. A cork is narrower than the mouth and flares above it. Its flare stops short of the neck's own radius, because a cork wall on the neck's cylinder is a coplanar pair with it. A lid is wider than the rim: a skirt, a dome and a knob. Both sit CLOSURE_BITE into the body.
  • Labels. Each label is built on the host's arc with the host's segment count. Every label vertex sits at a body vertex's own angle and ring height, so the paper follows a waist or a bulb exactly. The inner face is 0.3 mm inside the body and the outer face 0.8 mm proud. The label is centred on the front and turned with the vessel's yaw.
  • Seats vary by vessel. Each vessel on a shelf sinks by a different step (1.0–1.8 mm). Sunk to one depth, two bottom caps on a shelf lie in one plane, a coplanar cross-shell pair.
-

Findings the budgets forced

+

Findings the budgets forced#

  • Coplanar corks. The first corks flared to 1.10 × their radius, which put a cork wall 0.1 mm off the neck cylinder of the same bottle: 32 coplanar pairs on two bottles. The flare is now 1.20 × and the count is 0.
  • Matching parts to vessels. Closures and labels were first matched to the body with the nearest axis in plan. The three shelves stack vessels in near-identical plan positions, so 12 of 15 labels went to a vessel on another shelf, and the label seat read −23 mm. They are now matched only among bodies at the part's own height.
  • Glass. EEVEE transmission rendered every bottle as grainy speckle. It stayed with and without raytracing, and at 256 samples. The glass now carries its depth in a facing ramp and a clear coat: dark through the middle, lighter at the silhouette.
  • Daylight through the back. A 2 mm butt seam between back boards showed the background as a bright line down the middle of the upper compartment. The boards are now lapped with an alternating step.
-

Conventions walked

+

Conventions walked#

Every convention in showcase/README.md, and whether it applies here.

ConventionAppliesHow
Deterministic, budgets declared, assertions recomputeyesno RNG; every value above is read off the mesh
Falsifier fails the budget it targetsyestable below, proven on all three binaries
Hygiene incl. cross-shell coplanaryesexit 15; seats, lap steps and cork flares chosen to keep it at 0
Named supportsyesthe two sides (--float-side)
Joint-fit budgetsyesdado bite and far-face clearance per cross member (--short-shelves)
A member is tenoned into its seatyessides into the top, crest into the top, back boards into base and top
A platform bears on somethingyesevery shelf housed in both sides and biting the back boards
Carried parts bite their bearersyesvessels in shelves (--float-jars), drawer fronts in the base (--lift-drawers)
A joint bites; touching is not joiningyesclosures (--pop-corks) and knobs (--pull-knobs) as banded bites
Bands on a curved host are built on the host's arcyeslabels on the body's own rings and angles (--float-labels)
Scattered parts do not interpenetrateyesexit 21 (--crowd-jars)
Scatter varies in sizeyesform spread, exit 22 (--uniform-vessels)
Edge treatment: no right anglesyesexit 23 (--sharp-rail), wood only; lathed feet are chamfered by construction
Aim an edge falsifier at one memberyesone rail left square: 12 edges, −32 triangles
Chamfer n-gon caps, then triangulateyescrest caps
Sort bmesh operator inputsyesbevel edges sorted by index
A plank wall is boardsyesfour lapped back boards
Identical boards read as CGyesper-board PlankTone and GrainDir
Shading is part of the modelyesvessels, closures and knobs smooth (blown, turned); every edge over 35° hard, so boards read flat with crisp chamfers
One substance, one slotyeswood, glass, glaze, cork, paper, brass; glass and glaze colour per vessel from Tint
Iron is not chromen/abrass knobs: metallic 0.9, roughness 0.42, darkened by noise
Bake texels per UV cellmeasured, not a budget512 px over a 32-cell grid, about 15 px per cell
The bake cage is narrower than the nearest neighbouryesCAGE_EXTRUSION 0.01 m
Level on the stage; stage 60 myesturned about Z only; 60 m floor and wall
Keep a falsifier's envelope stillyesevery falsifier leaves the AABB unchanged except --pull-knobs (+6 mm in Y, inside BBOX_TOL)
Mirror symmetry, plumb, rope, masonry, roofs, rings, scatter on terrain, vessels with liquidnothe piece has none of these
-

Falsifiers

+

Falsifiers#

Each breaks one pipeline stage so a named budget fails. All fourteen were run on 4.5.11, 5.1.2 and 5.2.1 and exited the same declared code on all three.

FlagTarget budgetBreaksExit
--skip-decimateLOD1 ratiodrops the DECIMATE modifiers, LOD1 ratio goes to 1.00009
--stray-vertmesh hygieneadds one loose vertex inside the carcass15
--lift-zgrounded zminlifts the whole mesh 50 mm16
--float-sidenamed sideslifts the left side 5 mm; the right side still grounds the AABB16
--short-shelvesdado bitestops the three shelves 4 mm short of the sides; −4.0 mm17
--pop-corksclosure bitelifts every cork and lid 10 mm; −5.0 mm17
--pull-knobsknob bitepulls every knob 8 mm out of its front; −4.0 mm17
--float-jarsvessel seatlifts every vessel 6 mm off its shelf; −5.0 to −4.2 mm18
--lift-drawersdrawer seatlifts the drawer fronts 3 mm; −1.0 mm18
--float-labelslabel seatbuilds every label 1.1 mm off its body; −1.1 mm18
--tall-flaskheadroomstretches the middle-shelf flask 1.20 into the shelf above; −5.5 mm20
--crowd-jarsvessel overlapsdraws the bottom shelf's jars to 0.55 of their spacing; 3 overlaps21
--uniform-vesselsform spreadone radius and height for every vessel; 1.000 / 1.00022
--sharp-railedge treatmentleaves the top rail unchamfered; 12 right-angle edges23
-

Exit codes

+

Exit codes#

File-local and sequential. 9 is a valid check code. 1 is the FATAL wrapper — a crash, never a named check. 19 (plumb and real-world size) is reserved across pieces and unused here.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build, or has no UV layer
4Base triangle count outside band
5Material slots, or a material's face floor
6UVs outside 0..1
7UV AABB overlap above tolerance
8Outer AABB off declared size
9LOD1 or LOD2 ratio outside band (--skip-decimate)
10Framing gate (examples/gallery_framing.py, render path only)
11Collider triangles above ceiling
12Normal bake failed or produced no image data
13glTF export missing or empty
14--output produced no file
15Mesh hygiene (--stray-vert)
16Grounded zmin, or a side floating (--lift-z, --float-side)
17Dado, closure or knob bite (--short-shelves, --pop-corks, --pull-knobs)
18Vessel, drawer or label seat (--float-jars, --lift-drawers, --float-labels)
20Headroom under the member above (--tall-flask)
21Vessels overlap each other or the carcass (--crowd-jars)
22Form spread below floor (--uniform-vessels)
23Right-angle wood edges (--sharp-rail)
-

Run it

+

Run it#

# Budget check, no render. ~4.5 s on 5.1 and 5.2.
 blender --background --python apothecary_shelf.py --
 
@@ -318,16 +377,1655 @@ 

Run it

# Render the gallery still (EEVEE; --engine cycles on a GPU-less host). blender --background --python apothecary_shelf.py -- --output apothecary_shelf.webp

Smoke runs the check-only path. It does not pass --output or any falsifier.

-

Cross-version measurements

+

Cross-version measurements#

Value4.5.115.1.25.2.1
Base triangles104561045610456
LOD1 / LOD2 tris5228 / 2300samesame
Face counts (wood / glass / glaze / cork / paper / brass)525 / 2080 / 1520 / 960 / 420 / 336samesame
Outer AABB0.8760 × 0.2740 × 1.0760samesame
Collider tris138138138
Headroom / form spread24.05 mm / 3.146, 2.202samesame
glTF bytes448604448600448596
-
+

Source

showcase/apothecary-shelf/apothecary_shelf.py - View on GitHub → + 1639 lines · View on GitHub →
-
"""Game-ready apothecary shelf — a showcase piece, not an example.
+      
+
"""Game-ready apothecary shelf — a showcase piece, not an example.
 
 Asserts budget conformance of a procedural apothecary shelf: a stained
 carcass with housed shelves, gallery rails, a bank of three drawers and a
@@ -1967,7 +3665,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -1987,20 +3719,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/armature-bend/index.html b/docs/gallery/armature-bend/index.html index c2cb137d..9edb1674 100644 --- a/docs/gallery/armature-bend/index.html +++ b/docs/gallery/armature-bend/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

armature-bend

Rigging end to end in the data API — edit_bones chain construction, name-bound vertex groups with smoothstep blend zones, posing, and depsgraph evaluation — bending a tapered tube through rest, half, and full curl.

+

Rendered headless by the example itself — click to zoom.

witnesses edit_bones exists only in edit mode (empty in object mode), vertex groups bind to bones strictly by name, and the armature modifier is exactly linear blend skinning: every evaluated vertex must equal Σ wᵢ·(pose_bone.matrix @ bone.matrix_local.inverted()) @ rest, root ring pinned, tip deflected — a straight tube fails.
+

tags armature depsgraph

blender --background --python examples/armature-bend/armature_bend.py --
@@ -283,7 +342,7 @@

armature-bend

  1. edit_bones only exists in edit mode — the collection is asserted empty in object mode and holding the full chain (heads/tails at closed-form positions) inside mode_set(mode='EDIT'). Outside edit mode, reads silently return nothing.
  2. Vertex groups bind to bones by name; a typo deforms nothing without erroring. Every group here is named after its bone and the deform proves the binding.
  3. The armature modifier is exactly linear blend skinning: for every vertex, evaluated == Σ wᵢ · (pose_bone.matrix @ bone.matrix_local.inverted()) @ rest. The check re-implements LBS from the pose matrices and compares the whole evaluated mesh (max error ~5e-7). The root ring must stay pinned and the tip must deflect — a straight tube is a failure.

The armature API is identical on Blender 4.5 LTS and 5.1 — no version gate needed. One portability hazard is baked in as a comment: interleaving color_attributes writes with VertexGroup.add() dangles the attribute reference when the deform layer is allocated (hard crash on 4.5, silent luck on 5.1), so the script finishes all weight writes before creating the color attribute.

The render shows rest → half curl → full curl left to right, with per-bone weight bands visualized through a BoneTint color attribute — the smooth color blends at the joints are the same weights the LBS check asserts.

-

Run

+

Run#

# Cheap correctness check (no render) — the CI check:
 blender --background --python armature_bend.py --
 
@@ -293,18 +352,345 @@ 

Run

# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts): blender --background --python armature_bend.py -- --output bend.png blender --background --python armature_bend.py -- --output bend.png --engine cycles
-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3edit_bones populated in object mode
4Edit-mode bone chain off closed form
5Evaluated vertex count changed
6Evaluated mesh off closed-form LBS
7Root ring moved
8Tip did not deflect (--zero-curl lands here)
9--output produced no file

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --zero-curl.

-
+

Source

examples/armature-bend/armature_bend.py - View on GitHub → + 327 lines · View on GitHub →
-
"""An armature bending a weighted tube, checked against closed-form skinning.
+      
+
"""An armature bending a weighted tube, checked against closed-form skinning.
 
 Witnesses the three contracts AI-generated rigging code most often gets wrong:
 
@@ -632,7 +1018,41 @@ 

Source

except Exception as e: import traceback; traceback.print_exc(); print(f"FATAL: {e}", file=sys.stderr); sys.exit(1)
+
+ +
@@ -652,20 +1072,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/attribute-domain-shear/index.html b/docs/gallery/attribute-domain-shear/index.html index 93381007..1c757ec8 100644 --- a/docs/gallery/attribute-domain-shear/index.html +++ b/docs/gallery/attribute-domain-shear/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

attribute-domain-shear

POINT vs CORNER color-attribute domains on a shared-vertex fan: CORNER stays exact per face while a naive per-wedge POINT loop shears to the last write at the hub. A striped parasol keeps crisp stripes under CORNER and smears, with one panel turned the wrong color, under POINT; the measured shear equals the palette closed form.

+

Rendered headless by the example itself — click to zoom.

witnesses Authoring per-face colors into a POINT-domain attribute overwrites shared vertices once per neighbor; the last write wins and intended colors shear across every shared vertex.
+

tags attributes mesh rendering

blender --background --python examples/attribute-domain-shear/attribute_domain_shear.py --
@@ -287,23 +346,631 @@

attribute-domain-shear

Render as proof: two striped patio parasols, painted by the same two authoring functions the check asserts. The canopy is the check's fan grown into fabric: eight gores around one shared apex vertex, each gore a strip of faces whose vertices all sit on the two seams it shares with its neighbours, so the naive loop touches exactly the kind of shared vertex the check measures. CORNER (left) keeps crisp crimson and cream stripes. Naive POINT (right) rewrites every seam and the apex with the later gore's color: the stripes smear pink along the seams, and gore 0, crimson by intent, renders cream because its seam and the apex were last written by gore 7. That gore is turned to face the camera. The broken state is in frame by design: the right parasol is the falsification variant. The canopy fabric is fully matte (Specular IOR Level = 0) so the flat color data carries no specular line, per docs/VISUAL-STYLE.md.

The render uses a two-tone palette (crimson and cream, alternating). It is not the check's palette. The check keeps its eight distinct hues because an off-by-two ordering bug would pass unnoticed under a period-2 palette. A parasol reads as broken only when stripes everyone expects to be crisp go soft. Both palettes go through the same assign_corner and assign_point_naive. Those functions take a faces_per_wedge stride (gore-major face order). It is 1 on the check's fan, so the check's writes are unchanged, and its output is identical before and after.

The staging is render-only and does not touch the checked mesh. Each parasol is eight named parts with five materials: canopy, aluminium ribs and stretchers, brass runner, finial and tilt knuckle, teak upper and lower pole, and a cast-iron base. The canopy tilts 24 degrees toward the camera about the knuckle so the whole top reads. One bold label stands in front of each base. The render path runs the framing gate (exit 10) and the asset-quality floors on one parasol (exit 11).

-

Run

+

Run#

blender --background --python attribute_domain_shear.py --
 blender --background --python attribute_domain_shear.py -- --no-overwrite
 blender --background --python attribute_domain_shear.py -- --output shear.png
 blender --background --python attribute_domain_shear.py -- --output shear.png --engine cycles
-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper and 11 the shared asset-quality helper.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3CORNER or POINT attribute size wrong
4CORNER hub corners off wedge color
5POINT hub is not last-write (--no-overwrite lands here)
6Outer ring verts off last-write order
7Measured shear off palette closed form, or ~0
9--output produced no file
10Gallery framing violation
11Asset-quality floor violation (render path only)

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --no-overwrite.

-
+

Source

examples/attribute-domain-shear/attribute_domain_shear.py - View on GitHub → + 608 lines · View on GitHub →
-
"""Attribute domain shear — POINT vs CORNER color attributes on shared verts.
+      
+
"""Attribute domain shear — POINT vs CORNER color attributes on shared verts.
 
 Witnesses the domain-semantics contract of `Mesh.color_attributes` that
 AI-generated code trips on after learning `color_attributes.new()` exists:
@@ -912,7 +1579,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -932,20 +1633,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/bake-normal-high-to-low/index.html b/docs/gallery/bake-normal-high-to-low/index.html index 312842b8..91768b08 100644 --- a/docs/gallery/bake-normal-high-to-low/index.html +++ b/docs/gallery/bake-normal-high-to-low/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

bake-normal-high-to-low

A collapse-decimated hatch plate receiving a Cycles cage-baked tangent normal map from a ribbed high-poly source

+

Rendered headless by the example itself — click to zoom.

witnesses Statistical gates not byte-identity: detail frac 0.7211 / MAD 0.09356 vs flat 0.0000 / 0.00277; --flat-source exits 5; type=NORMAL not bake_type; RNA identical on 4.5.11, 5.1.2, 5.2.1
+

tags mesh

blender --background --python examples/bake-normal-high-to-low/bake_normal_high_to_low.py --
@@ -286,7 +345,7 @@

bake-normal-high-to-low

The still stages the baked map as an unlit card beside the LOD wearing it. If the bake were flat, the card would be uniform (128, 128, 255) periwinkle and the plate would shade like the undisplaced cage.

Staging is render-only. Each panel stands in a low display plinth, lifted so its lowest evaluated point sits 35 mm in the slot. Both panels used to balance on an edge, and the leaning plate pierced the floor. The solidify rim wears plain bronze instead of the baked map. The slight waviness along the plate's border is the bake itself, normal-map shading inside the UV margin, and is left as the map produces it. Bake statistics are unchanged: frac 0.7211, MAD 0.09356 on all three binaries.

Operator RNA (type='NORMAL', use_selected_to_active, cage_extrusion, cage_object as a string, normal_space='TANGENT', margin_type) is identical on 4.5.11, 5.1.2, and 5.2.1 — no shim.

-

Run

+

Run#

# Cheap correctness check (no render) - the CI check:
 blender --background --python bake_normal_high_to_low.py --
 
@@ -296,18 +355,605 @@ 

Run

# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts): blender --background --python bake_normal_high_to_low.py -- --output hatch.png blender --background --python bake_normal_high_to_low.py -- --output hatch.png --engine cycles
-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Missing UV layer on the target
4Bake did not FINISHED or image has_data is false
5Detail deviant-pixel fraction below 0.40 (--flat-source lands here)
6Detail MAD below 0.05
7Flat control above 0.05 frac / 0.03 MAD
8Monotonic gap below 0.30
9--output produced no file
10Gallery framing violation

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output.

-
+

Source

examples/bake-normal-high-to-low/bake_normal_high_to_low.py - View on GitHub → + 587 lines · View on GitHub →
-
"""High-to-low tangent normal bake — a runnable example.
+      
+
"""High-to-low tangent normal bake — a runnable example.
 
 Witnesses transferring high-poly surface detail onto a collapse-decimated LOD
 via Cycles cage bake. Pixel buffers are stochastic: this example does **not**
@@ -895,7 +1541,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -915,20 +1595,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/bmesh-gear/index.html b/docs/gallery/bmesh-gear/index.html index f564e83e..6bfa1e15 100644 --- a/docs/gallery/bmesh-gear/index.html +++ b/docs/gallery/bmesh-gear/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

bmesh-gear

A 14-tooth gear built entirely with bmesh — profile ring, face, extrude — with bm.free() in a try/finally, exactly as the ownership contract demands.

+

Rendered headless by the example itself — click to zoom.

witnesses Parametric bmesh topology is exactly predictable: verts, edges, and faces match their closed forms, and every edge borders exactly two faces (watertight).
+

tags mesh bmesh

blender --background --python examples/bmesh-gear/bmesh_gear.py --
@@ -282,7 +341,7 @@

bmesh-gear

What it witnesses: parametric bmesh construction has exactly predictable topology. The check asserts the closed-form counts — verts = 2 × (4 × teeth), faces = sides + 2 caps, edges = 3 × profile — and that the result is watertight (every edge borders exactly two faces). If an op leaks geometry or a face fails to close, the math catches it.

The still leans the gear back on a matte inclined display wedge. Both contacts are derived from the posed mesh: the lowest back-cap vertex sits 3 mm into the floor, and the wedge's slope lies in the back-cap plane, so the lean is carried rather than held in the air.

The finish follows how a gear blank is actually cut, and the render path changes only the material and lights — the mesh the check counts is untouched. The caps are lathe-faced: turning marks run concentric about the gear's own axis (object coordinates, so they centre on that axis rather than on a bounding-box corner), far finer than a pixel, so they read only as a narrow satin roughness band and a whisper of bump — the soft highlight that fans from the centre. The tooth flanks are hobbed, a rougher finish that spreads the light across every facet. The key highlight on the face is a softbox placed on the camera ray's reflection about the front cap, and a low bounce card gives the downward flanks something to mirror. There is no bore: the closed-form topology check counts exactly two rings and two caps, so the gear stays a solid blank rather than faking a hole in the shader.

-

Run

+

Run#

# Cheap correctness check (no render) — the CI check:
 blender --background --python bmesh_gear.py --
 
@@ -292,18 +351,390 @@ 

Run

# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts): blender --background --python bmesh_gear.py -- --output gear.png blender --background --python bmesh_gear.py -- --output gear.png --engine cycles
-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Topology ≠ closed form (--no-extrude lands here)
4Non-manifold edges
6--output produced no file
10--output framing violation (Layer 1 fill / margin gate, gallery_framing)

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --no-extrude.

-
+

Source

examples/bmesh-gear/bmesh_gear.py - View on GitHub → + 372 lines · View on GitHub →
-
"""A parametric gear built entirely with bmesh — a runnable example.
+      
+
"""A parametric gear built entirely with bmesh — a runnable example.
 
 Witnesses the bmesh ownership contract from mesh-editing-and-bmesh and the
 always-free-bmesh rule: every `bmesh.new()` is paired with `bm.free()` in a
@@ -676,7 +1107,41 @@ 

Source

except Exception as e: import traceback; traceback.print_exc(); print(f"FATAL: {e}", file=sys.stderr); sys.exit(1)
+
+ +
@@ -696,20 +1161,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/book-trolley/index.html b/docs/gallery/book-trolley/index.html index f28d5ae8..03074832 100644 --- a/docs/gallery/book-trolley/index.html +++ b/docs/gallery/book-trolley/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,43 +325,45 @@

book-trolley

A procedural library book truck — arched end panels standing in skids on four swivel casters, a flat deck, and on each side two sloped troughs, a tilted shelf and a backrest square to it, filled with forty-seven rounded-spine hardbacks spines out and two loose stacks — carried through UVs, bake, LOD, convex collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

+

Rendered headless by the showcase piece itself — click to zoom.

witnesses Recomputed: 17704 tris, seven materials with 568 wood, 2788 cloth, 1476 leather, 312 paper, 2704 gilt, 932 iron and 656 rubber faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.768×0.530×1.000 m, zmin 0, hygiene 0 including zero coplanar cross-shell pairs, four wheels on the floor, ten cross members 8 mm into both end panels, four caster plates 1.0 mm into their skids, carcass and casters mirror-exact in X and Y, 52 books each with one page block 0.6 mm into its boards and two gilt bands 0.3 mm into the spine and 0.8 mm proud, every book seated 1.0–3.2 mm, four troughs each with one backrest and all 47 trough books 0.8 mm into it, thickness and height spread 2.75 and 1.64, zero unplanned book overlaps, zero right-angle wood edges, convex collider 308 tris. --off-back and --deep-back exit 21 on the backrest contact while every seat, the envelope and the overlap budget still pass; --crowd-books exits 22; --skew-caster 19; --drop-plates 18.
+

tags mesh export showcase

blender --background --python showcase/book-trolley/book_trolley.py --

A library book truck: two arched end panels standing in skids on four swivel casters, a flat bottom deck, and on each side two sloped troughs under a turned push rail. Each trough is a shelf tilted 14° back toward the centre and a backrest square to it, both housed in the end panels. Forty-seven hardbacks stand in the troughs spines out, leaning back into the corner; five more lie in two loose stacks on the deck. Every book is a rounded-spine cover in cloth or leather, a page block glued into it, and two gilt bands tooled round the spine. A showcase piece, not an example — it witnesses no API contract. It asserts that generated geometry meets declared asset budgets, recomputed from the finished mesh.

-

What it composes

+

What it composes#

Shipped contentUsed for
skills/mesh-editing-and-bmeshchamfered boards in trough frames, n-gon-capped prisms (arched end panels, caster forks, wheels), U-section covers, all in bmesh
skills/custom-propertiesface attributes (PlankTone, GrainDir, Tint) read by the shaders
skills/procedural-materials-and-shadersgrained stain, cloth, leather, ruled page edges, gilt, caster iron, rubber
skills/bake-high-to-lowCycles tangent-space normal bake, high onto low
skills/engine-export-presetsUnity glTF (export_yup=True)
skills/depsgraph-and-evaluated-dataevaluated triangle counts for the LOD ratios
snippets/decimate_to_budget.pyLOD1 / LOD2 COLLAPSE chain
snippets/convex_hull_collider.pyone hull over the carcass and casters
snippets/lod_chain.pyLOD naming and ratio pattern
examples/mesh-hygiene-audithygiene combinatorics (copied, not imported)
showcase/bookshelfthe book builder, the seat, band and overlap audits (copied, not imported)
-

The budget that matters

+

The budget that matters#

A book on a flat shelf stands on one support. A book in a sloped trough is carried at two: its tail on the shelf and its fore-edge against the backrest. Seated on the shelf and standing off the backrest, it would topple forward on a real trolley, and nothing else sees it:

  • the book still sits its seat into the shelf, so the seat budget passes;
  • it stays inside the carcass, so the bounding box does not move;
  • the triangle count is the same, because only a position changed;
  • the book-overlap budget has to exempt both designed contacts, the shelf and the backrest, so it cannot see how deep either goes.

The piece pairs each tilted shelf with the one backrest whose shell overlaps it, finds each standing book's shelf by the top plane its lowest vertex sits nearest (read off the mesh, on the book's own side of the trolley), and takes the plane of that backrest's face that looks at the book. It asserts how far the book's deepest cover vertex sits behind that plane, as a band of 0.3–2.0 mm, next to the shelf seat's own band. All forty-seven sit at 0.80 mm.

A plane, not the backrest solid, for the same reason as bookshelf's lean contact: a fore-edge driven through the 18 mm board would come out behind it and read as no contact.

--off-back and --deep-back are the falsifiers built for exactly this. One stands every trough book 3 mm off its backrest, the other drives it 4.5 mm in, both along the slope so the shelf seat is untouched. Both exit 21 while every seat, the envelope and the book-overlap budget still pass.

-

Budgets

+

Budgets#

Declared in the script as named constants, recomputed from the generated mesh. Measured values are from Blender 5.2.1; see the cross-version table below.

BudgetBandMeasured
Base triangles16800–1860017704
LOD1 ratio0.32–0.620.5000
LOD2 ratio0.10–0.350.2200
Material slotsexactly 7, distinct7
Wood / cloth / leather faces≥ 510 / 2500 / 1320568 / 2788 / 1476
Paper / gilt faces≥ 280 / 2430312 / 2704
Iron / rubber faces≥ 840 / 590932 / 656
UV boundsinside 0..1(0.0003, 0.0003)–(0.9997, 0.9997)
UV AABB overlap≤ 1e-50.000000
Outer AABB0.768 × 0.530 × 1.000 m ± 0.0200.7680 × 0.5300 × 1.0000
Collider triangles≤ 360308
Normal bake{'FINISHED'} with image data{'FINISHED'}, has_data=True
glTF exportfile written, non-empty~1.30 MB
Hygieneall zeroloose 0/0, non-manifold 0, zero-area 0, doubles 0, n-gons 0, coplanar cross-shell pairs 0
Grounded AABB\zmin\≤ 1e-40.00000
Named wheels4 wheels, each zmin ≤ 1e-44 at 0.00000
Dado2 end panels; 10 cross members (deck, 4 shelves, 4 backrests, rail), each ≥ 5 mm into both panels and ≥ 6 mm clear of the far face8.00 mm in, 10.00 mm clear
Page block52 books, one block and two bands each; every block corner 0.3–1.2 mm inside its boards0.60 mm
Book seat52 books, each 0.5–3.6 mm into its shelf (along the shelf normal), the deck or the book below1.00–3.16 mm
Band seatinner face 0.1–0.8 mm inside the spine, outer face ≥ 0.4 mm proud0.29 mm in, 0.80 mm proud
Caster plates4 plates, each top 0.5–2.0 mm into the skid above1.00 mm
Mirror symmetrycarcass and casters, every vertex's X and Y mirror image on the mesh within 1e-4 m0.0000000
Size spreadthickest / thinnest ≥ 2.5; tallest / shortest ≥ 1.52.750 / 1.640
Backrest contact47 trough and 5 lying books; 4 troughs each with one backrest; every trough book 0.3–2.0 mm into its backrest0.80 mm
Book overlaps0, book against book and against the carcass, designed contacts exempt0
Edge treatmentright-angle wood edges0

Real-world size: 0.77 m long over the skids, 0.53 m deep and 1.00 m tall over the arched ends, on 70 mm casters. Each trough is 170 mm deep and its backrest 170 mm tall; the books run from a 150 mm pocket book to a 245 mm octavo.

-

Construction

+

Construction#

  • End panels and skids. Each end is one prism: a flat-sided outline whose top is a 16-segment arc, extruded 22 mm and chamfered 2.5 mm, standing 12 mm deep in a 70 × 35 mm skid that runs 15 mm past it front and back. The panels are the carcass's only bearers; the casters are the named supports.
  • Cross members. The deck and the four tilted shelves and backrests are each housed DADO (8 mm) into both ends and stop 14 mm short of the outer faces. The push rail is a 16-sided dowel whose ends sit 12 mm blind in both panels, 10 mm short of the outer faces.
  • Troughs. Built in a trough frame: s runs down the slope into the backrest, t up the shelf normal, the corner at the origin. The shelf runs from 170 mm in front of the corner to 6 mm past the backrest's back face, so the two backs never share a plane; the backrest stands 6 mm into the shelf. The two sides are mirror images, and the two tiers of a side stand 320 mm apart, so each tier's books clear the shelf above.
  • Casters. Stacked by a named bite at each joint: a 56 mm plate 1 mm into the skid, a swivel race 1 mm into the plate, a fork (one U-section prism, chamfered 0.8 mm) 1 mm into the race, an axle through both fork legs and a 70 mm rubber wheel whose lowest vertex is the floor. The wheel's rim is rounded with a two-segment bevel and shaded smooth; the iron is near-black with a little rust in the noise, not chrome.
  • Books. Copied from bookshelf: one U-section cover per book with a rounded spine, a page block 0.6 mm into both boards, two gilt bands on the spine's own chain.
  • Trough books. Built in their own frame, turned so the height runs up the shelf normal and the fore-edge down the slope, and yawed about the shelf normal in a four-step cycle (−2.4°, −0.8°, 0.8°, 2.4°) so no two neighbours share a plane. Each is then slid up the normal to a stepped 1.0–3.2 mm seat into the shelf, down the slope until its deepest vertex is BACK_BITE (0.8 mm) into the backrest, and along the trough with a 2.5–4.5 mm gap. A trough that overflows its span is a build error.
  • Loose stacks. Books laid flat on the deck, spine out, yawed ±3° alternately and shifted a few millimetres; each book above the first takes its own 1.0–1.4 mm seat into the board below it.
-

Findings the budgets forced

+

Findings the budgets forced#

  • A 45° plane shared by accident. Dropping the caster plates 2 mm for the plate-seat falsifier put each plate's lower chamfer and the fork's upper chamfer on the same 45° plane — the plate's half-width minus the fork's is 13 mm, and so was the drop between them — and the run exited 15 on eight coplanar pairs instead of 18. The drop is 2.5 mm.
  • An edge falsifier the mirror budget saw first. Leaving one front backrest square broke the carcass's front/back symmetry, so the run exited 19 before the edge budget. The falsifier now leaves the deck square: one centred member, symmetric in both axes, 12 right-angle edges.
  • The bake cage reached the books. At 10 mm the cage from each end panel's inner face reached the high-poly covers 6 mm away, and the books' edges baked onto the panel as pale streaks. Cutting the cage to 3 mm cleared the panels but not the deck: the loose stacks sit 1.0–1.4 mm into it, so rays from round each stack still found the spines and baked every footprint in as a dished groove. Only the wood takes a baked map, so the high-poly bake source is now the carcass and casters alone; the cage stays at 3 mm.
-

Conventions walked

+

Conventions walked#

Every convention in showcase/README.md, and whether it applies here.

ConventionAppliesHow
Deterministic, budgets declared, assertions recomputeyesno RNG; every value above is read off the mesh
Falsifier fails the budget it targetsyestable below, proven on all three binaries
Hygiene incl. cross-shell coplanaryesexit 15; stepped seats, the yaw cycle and the shelf tail keep it at 0
Named supportsyesthe four wheels (--float-caster); the plates, forks and skids stop above the floor
Joint-fit budgetsyesdado bite and far-face clearance per cross member (--short-shelves)
A member is tenoned into its seatyespanels in skids, backrests in shelves, rail blind in the panels, each caster part into the one above
A platform bears on somethingyesdeck and every shelf housed in both end panels
Carried parts bite their bearersyesbooks into shelves, the deck and the book below (--float-books); caster plates into the skids (--drop-plates)
A joint bites; touching is not joiningyespage blocks into their boards (--loose-pages); the backrest contact as a band
Bands on a curved host are built on the host's arcyesgilt bands on the spine's own chain (--float-bands)
A leaning prop rests on somethingyesevery trough book against its backrest, exit 21 (--off-back, --deep-back)
Mirrored assembliesyescarcass and casters mirror in X and Y, exit 19 (--skew-caster, 6 mm inside the 20 mm AABB tolerance)
Scattered parts do not interpenetrateyesexit 22 (--crowd-books), designed contacts exempt
Scatter varies in sizeyesthickness and height spread, exit 20 (--uniform-books)
Edge treatment: no right anglesyesexit 23 (--sharp-deck), wood only; iron chamfered 0.8 mm, rubber rounded
Aim an edge falsifier at one memberyesthe deck left square: 12 edges
Chamfer n-gon caps, then triangulateyesend panels, forks, races, axles, wheels and the rail are n-gon-capped prisms, bevelled then triangulated
Sort bmesh operator inputsyesbevel edges sorted by index, carcass, iron, rubber and covers
Identical boards read as CGyesper-board PlankTone and GrainDir; per-book Tint
Variation goes into surface, never into functionyessizes, tints, gaps and yaw vary; tier pitch, slope and seats are budgets
Shading is part of the modelyesspines, rail and wheels smooth (round); every edge over 35° hard, so boards and chamfers read crisp
One substance, one slotyeswood, cloth, leather, paper, gilt, iron, rubber
Iron is not chromeyesnear-black, roughness 0.52–0.78
Bake texels per UV cellmeasured, not a budget1024 px over a 98-cell grid, about 10 px per cell
The bake cage is narrower than the nearest neighbouryesCAGE_EXTRUSION 3 mm, and the bake source leaves the books out, since they sit inside any cage of the deck and shelves
Level on the stage; stage 60 myesturned about Z only; 60 m floor and wall
Keep a falsifier's envelope stillyesevery falsifier except --lift-z keeps the AABB within tolerance
Plumb, rope, masonry, roofs, rings, terrain, vesselsnothe piece has none of these
-

Falsifiers

+

Falsifiers#

Each breaks one pipeline stage so a named budget fails. All fifteen were run on 4.5.11, 5.1.2 and 5.2.1 and exited the same declared code on all three.

FlagTarget budgetBreaksExit
--skip-decimateLOD1 ratiodrops the DECIMATE modifiers, LOD1 ratio goes to 1.00009
--stray-vertmesh hygieneadds one loose vertex inside the deck15
--lift-zgrounded zminlifts the whole mesh 50 mm16
--float-casternamed wheelslifts one caster 5 mm; the other three still ground the AABB16
--short-shelvesdado bitestops the four shelves 4 mm short of the ends; −4.0 mm17
--loose-pagespage-block bitebuilds every page block 0.9 mm clear of its boards; −0.9 mm17
--float-booksbook seatlifts every book 6 mm off its shelf or the deck; −5.0 mm18
--float-bandsband seatbuilds every band 1.1 mm off its spine; −1.1 mm18
--drop-platescaster plate seatdrops every caster plate 2.5 mm, clear of its skid; −1.5 mm18
--skew-castermirror symmetrymoves one caster 6 mm along the trolley; 6.0 mm19
--uniform-bookssize spreadone height and depth, thickness within ±1.5 %; 1.030 / 1.00020
--off-backbackrest contactstands every trough book 3 mm off its backrest; −3.0 mm21
--deep-backbackrest contactdrives every trough book 4.5 mm into its backrest; 4.5 mm21
--crowd-booksbook overlapscloses the front upper trough's gaps to −6 mm; 13 overlaps22
--sharp-deckedge treatmentleaves the deck unchamfered; 12 right-angle edges23
-

Exit codes

+

Exit codes#

File-local and sequential. 9 is a valid check code. 1 is the FATAL wrapper — a crash, never a named check.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build, or has no UV layer
4Base triangle count outside band
5Material slots, or a material's face floor
6UVs outside 0..1
7UV AABB overlap above tolerance
8Outer AABB off declared size
9LOD1 or LOD2 ratio outside band (--skip-decimate)
10Framing gate (examples/gallery_framing.py, render path only)
11Collider triangles above ceiling
12Normal bake failed or produced no image data
13glTF export missing or empty
14--output produced no file
15Mesh hygiene (--stray-vert)
16Grounded zmin, or a wheel floating (--lift-z, --float-caster)
17Dado or page-block bite, or a book without one block and two bands (--short-shelves, --loose-pages)
18Book, band or caster-plate seat (--float-books, --float-bands, --drop-plates)
19Carcass and casters off mirror symmetry (--skew-caster)
20Size spread below floor (--uniform-books)
21Backrest contact, trough / lying count, or a trough without one backrest (--off-back, --deep-back)
22Books overlap each other or the carcass (--crowd-books)
23Right-angle wood edges (--sharp-deck)
-

Run it

+

Run it#

# Budget check, no render.
 blender --background --python book_trolley.py --
 
@@ -317,17 +376,1837 @@ 

Run it

# Render the gallery still (EEVEE; --engine cycles on a GPU-less host). blender --background --python book_trolley.py -- --output book-trolley.webp

Smoke runs the check-only path. It does not pass --output or any falsifier.

-

Cross-version measurements

+

Cross-version measurements#

Value4.5.115.1.25.2.1
Base triangles177041770417704
LOD1 / LOD2 tris8852 / 38948852 / 38948852 / 3894
Face counts (wood / cloth / leather / paper / gilt / iron / rubber)568 / 2788 / 1476 / 312 / 2704 / 932 / 656samesame
Outer AABB0.7680 × 0.5300 × 1.0000samesame
Collider tris308308308
Seat / backrest contact / mirror1.00–3.16 mm / 0.80 mm / 0.0samesame
glTF bytes130331613033161303308

The glTF size differs by 8 bytes on 5.2 (exporter metadata, not a budget). Every other value, including the DECIMATE COLLAPSE LOD counts, is identical on all three.

-
+

Source

showcase/book-trolley/book_trolley.py - View on GitHub → + 1820 lines · View on GitHub →
-
"""Game-ready library book trolley — a showcase piece, not an example.
+      
+
"""Game-ready library book trolley — a showcase piece, not an example.
 
 Asserts budget conformance of a procedural double-sided library book
 truck: two arched end panels standing in skids on four swivel casters, a
@@ -2148,7 +4027,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -2168,20 +4081,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/bookshelf/index.html b/docs/gallery/bookshelf/index.html index 20e28589..1dd0ea9d 100644 --- a/docs/gallery/bookshelf/index.html +++ b/docs/gallery/bookshelf/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,43 +325,45 @@

bookshelf

A procedural bookcase — two housed shelves, a base on a recessed plinth, lapped back boards and a stepped cornice, filled with thirty-eight rounded-spine hardbacks standing, stacked and leaning — carried through UVs, bake, LOD, convex collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

+

Rendered headless by the showcase piece itself — click to zoom.

witnesses Recomputed: 10908 tris, five materials with 338 wood, 2050 cloth, 1066 leather, 228 paper and 1976 gilt faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.972×0.332×1.176 m, zmin 0, hygiene 0 including zero coplanar cross-shell pairs, both sides on the floor, four cross members 8 mm into both sides, 38 books each with one page block 0.6 mm into its boards and two gilt bands 0.3 mm into the spine and 0.8 mm proud, every book seated 1.0–3.2 mm, 44 mm headroom, thickness and height spread 2.90 and 1.65, both leaning books 0.8 mm onto their neighbour's head edge, zero unplanned book overlaps, zero right-angle wood edges, convex collider 68 tris. --air-lean and --deep-lean exit 22 on the lean contact while every seat, the envelope and the overlap budget still pass; --crowd-books exits 23; --uniform-books 21; --tall-book 20.
+

tags mesh export showcase

blender --background --python showcase/bookshelf/bookshelf.py --

A bookcase: a stained carcass with two shelves housed in its sides, a base on a recessed plinth, lapped back boards and a stepped cornice, filled with thirty-eight hardbacks. Twenty-nine stand upright, seven lie in three stacks, and two lean on a neighbour. Every book is a rounded-spine cover in cloth or leather, a page block glued into it, and two gilt bands tooled round the spine. A showcase piece, not an example — it witnesses no API contract. It asserts that generated geometry meets declared asset budgets, recomputed from the finished mesh.

-

What it composes

+

What it composes#

Shipped contentUsed for
skills/mesh-editing-and-bmeshchamfered boards, U-section covers with rounded spines, page blocks and bands built on the spine's own chain, all in bmesh
skills/custom-propertiesface attributes (PlankTone, GrainDir, Tint) read by the shaders
skills/procedural-materials-and-shadersgrained stain, cloth, leather, ruled page edges, gilt
skills/bake-high-to-lowCycles tangent-space normal bake, high onto low
skills/engine-export-presetsUnity glTF (export_yup=True)
skills/depsgraph-and-evaluated-dataevaluated triangle counts for the LOD ratios
snippets/decimate_to_budget.pyLOD1 / LOD2 COLLAPSE chain
snippets/convex_hull_collider.pyone hull over the carcass
snippets/lod_chain.pyLOD naming and ratio pattern
examples/mesh-hygiene-audithygiene combinatorics (copied, not imported)
-

The budget that matters

+

The budget that matters#

A leaning book has to rest on its neighbour's head edge and on the shelf, not in either. Tip it a few degrees too far and its board cuts through the neighbour's corner; not far enough and it stands in the air against nothing. Neither is visible to any other budget:

  • the leaning book still stands on its shelf, so the seat budget passes;
  • it stays inside the carcass, so the bounding box does not move;
  • the triangle count is the same, because only a position changed;
  • the book-overlap budget has to exempt this very contact, so it cannot see how deep the contact goes.

The piece finds the leaning books on the finished mesh (a book whose largest face, a board, is tilted between 3° and 80°) and the upright neighbour on the side its top leans toward, on the same shelf. It takes the plane of the leaning board's outer face off the mesh and measures how far the neighbour's deepest vertex sits behind it, as a band of 0.3–2.0 mm. Both lean at 0.80 mm.

A plane, not the cover solid: the board is 2.8 mm thick, and a head edge driven right through it lies in the hollow of the book, outside the cover again, so a solid test reads a deep cut as no contact at all. That is how the first --deep-lean passed.

--air-lean and --deep-lean are the falsifiers built for exactly this. One stops each leaning book 3 mm short of its neighbour, the other drives it 4.5 mm in. Both exit 22 while every seat, the envelope and the book overlap budget still pass.

-

Budgets

+

Budgets#

Declared in the script as named constants, recomputed from the generated mesh. Measured values are from Blender 5.2.1; see the cross-version table below.

BudgetBandMeasured
Base triangles10400–1140010908
LOD1 ratio0.32–0.620.5000
LOD2 ratio0.10–0.350.2189 (0.2198 on 4.5 and 5.1)
Material slotsexactly 5, distinct5
Wood / cloth / leather faces≥ 300 / 1850 / 960338 / 2050 / 1066
Paper / gilt faces≥ 200 / 1780228 / 1976
UV boundsinside 0..1(0.0004, 0.0004)–(0.9996, 0.9996)
UV AABB overlap≤ 1e-50.000000
Outer AABB0.972 × 0.332 × 1.176 m ± 0.0200.9720 × 0.3320 × 1.1760
Collider triangles≤ 20068
Normal bake{'FINISHED'} with image data{'FINISHED'}, has_data=True
glTF exportfile written, non-empty~786 kB
Hygieneall zeroloose 0/0, non-manifold 0, zero-area 0, doubles 0, n-gons 0, coplanar cross-shell pairs 0
Grounded AABB\zmin\≤ 1e-40.00000
Named sides2 sides, each zmin ≤ 1e-42 at 0.00000
Dado4 cross members, each ≥ 5 mm into both sides and ≥ 6 mm clear of the far face8.00 mm in, 14.00 mm clear
Page block38 books, one block and two bands each; every block corner 0.3–1.2 mm inside its boards0.60 mm
Book seat38 books, each 0.5–3.6 mm into the shelf or the book it stands on1.00–3.16 mm
Band seatinner face 0.1–0.8 mm inside the spine, outer face ≥ 0.4 mm proud0.29 mm in, 0.80 mm proud
Headroomevery book ≥ 20 mm under the member above44.36 mm
Size spreadthickest / thinnest ≥ 2.5; tallest / shortest ≥ 1.52.900 / 1.645
Lean contact2 leaning and 7 lying books; each neighbour's head edge 0.3–2.0 mm behind the leaning board0.80 mm
Book overlaps0, book against book and against the carcass, designed contacts exempt0
Edge treatmentright-angle wood edges0

Real-world size: 0.97 m wide over the cornice, 1.18 m tall and 0.33 m deep. Each compartment is 318–348 mm clear, and the books run from a 186 mm octavo to a 305 mm folio.

-

Construction

+

Construction#

  • Carcass. Two full-height sides carry everything and are the only named supports. The plinth, the base and the two shelves are each housed DADO (8 mm) into both sides and stop 14 mm short of the outer faces. The plinth stands 18 mm back from the base's front and 4 mm off the floor, so only the sides ground. The top is seated over the sides and the back boards; the cornice steps 10 mm out past it and is seated CORN_BITE into it, its back held 4 mm short of the top's back so the two do not share a plane. Every board is chamfered at 2.5 mm with material= pinned to wood, edges sorted by index.
  • Back boards. Five vertical boards, lapped 2 mm into one another and every other one stepped forward 1.5 mm, as on apothecary-shelf.
  • Covers. One shell per book: a U of front board, half-round spine and back board, extruded up the height (SPINE_ARC = 6 segments). The boards are tangent to the spine, so it reads round and the boards flat. Every edge over 40° is chamfered 0.8 mm; the spine facets (30°) are not, and shade smooth.
  • Page blocks. A block inside the U, SQUARE (4 mm) short of the cover at head, tail and fore-edge, biting PAGE_BITE (0.6 mm) into both boards. Its back stops 1.5 mm short of the line where boards meet spine, which is where the band ends lie.
  • Gilt bands. Two per book, built on the cover's own outer chain round the spine: every band vertex sits on a cover vertex's normal, inner face 0.3 mm inside the spine and outer face 0.8 mm proud.
  • Standing books. Set along each shelf with a 2.5–4.5 mm gap, spines a stepped 6–14 mm back from the shelf front, each seated a stepped 1.0–3.3 mm into the shelf. Each stands a degree or two off square, in a four-step cycle (−2.4°, −0.8°, 0.8°, 2.4°).
  • Stacks. Books laid on a board, spine out, yawed ±3° alternately and shifted a few millimetres. The bottom one seats into the shelf; each one above takes a 1.0–1.4 mm seat into the board below it.
  • Leaning books. Turned about Y by 18° (bottom shelf, onto the book on its left) and 22° (middle shelf, onto the book on its right), seated on the shelf by the lowest vertex of the chamfered cover. Then the leaning book (or, leaning right, its neighbour) is slid along the shelf until the neighbour's deepest vertex is LEAN_BITE (0.8 mm) behind the leaning board's outer face. Each leaning book is taller than its neighbour's head divided by the cosine of its lean, so the head edge meets the board and not its top corner.
-

Findings the budgets forced

+

Findings the budgets forced#

  • Coplanar page blocks and shelves. The first page blocks sat 3 mm short of the cover's tail, and one book seated 2.98 mm into its shelf put its page block's underside 0.02 mm off the shelf top: a coplanar pair. The square is now 4 mm, clear of every seat.
  • Every spine has the same facet angles. A half-round spine of six facets has the same six facet normals on every book. Square to the shelf, two neighbours share a facet plane whenever their offsets happen to line up, and the uniform and crowded falsifiers did so (five and twelve coplanar pairs). Each upright now stands a degree or two off square in a four-step cycle. A half-degree cycle was not enough: on a 45° chamfer the normals differ by half the yaw, inside the 1e-4 parallel tolerance.
  • Planes facing the front. Spine lines, band ends and fore-edges face the shelf front, and two neighbours landed band ends on one plane. Each book is stepped back 0.9 mm at a time until none of its front-facing planes is within 0.4 mm of a neighbour's.
  • A stacked book on a page block's plane. A book stacked on another with the running seat (up to 3.3 mm) could land its underside on the plane of the lower book's page block, 2.2 mm inside its board. A stacked book now takes its own 1.0–1.4 mm seat.
  • Matching parts to books. Page blocks were first matched to the cover nearest their centroid. In a stack, the centroid of a thick book's page block is nearer the board of the book above than its own. Parts are now matched on the mean distance of all their vertices.
  • Deep lean through the board. Measured against the cover solid, a head edge driven 4.5 mm into a 2.8 mm board came out in the hollow of the leaning book and read as no contact, so --deep-lean exited 0. The contact is now measured against the plane of the board's outer face.
  • Black gilt. Fully metallic bands facing the camera mirrored the dark stage and read black except at their edges. The gilt is half metallic.
-

Conventions walked

+

Conventions walked#

Every convention in showcase/README.md, and whether it applies here.

ConventionAppliesHow
Deterministic, budgets declared, assertions recomputeyesno RNG; every value above is read off the mesh
Falsifier fails the budget it targetsyestable below, proven on all three binaries
Hygiene incl. cross-shell coplanaryesexit 15; square, yaw cycle, front-plane step and stack seat chosen to keep it at 0
Named supportsyesthe two sides (--float-side); the plinth stops 4 mm off the floor
Joint-fit budgetsyesdado bite and far-face clearance per cross member (--short-shelves)
A member is tenoned into its seatyestop over sides and back boards, cornice into the top, back boards into the base
A platform bears on somethingyesevery shelf housed in both sides and biting the back boards
Carried parts bite their bearersyesbooks into shelves and into the book below (--float-books)
A joint bites; touching is not joiningyespage blocks into their boards (--loose-pages); the lean contact as a band
Bands on a curved host are built on the host's arcyesgilt bands on the spine's own chain (--float-bands)
A leaning prop rests on somethingyesthe lean contact, exit 22 (--air-lean, --deep-lean)
Scattered parts do not interpenetrateyesexit 23 (--crowd-books), designed contacts exempt and counted
Scatter varies in sizeyesthickness and height spread, exit 21 (--uniform-books)
The subject hangs where it can be seen / headroomyesevery book under the member above, exit 20 (--tall-book)
Edge treatment: no right anglesyesexit 24 (--sharp-shelf), wood only; covers chamfered 0.8 mm by construction
Aim an edge falsifier at one memberyesone shelf left square: 12 edges
Sort bmesh operator inputsyesbevel edges sorted by index, carcass and covers
A plank wall is boardsyesfive lapped back boards
Identical boards read as CGyesper-board PlankTone and GrainDir; per-book Tint
Variation goes into surface, never into functionyessizes, tints, gaps and yaw vary; shelf pitch and seats are budgets
Shading is part of the modelyesspines smooth (round); every edge over 35° hard, so boards and chamfers read crisp
One substance, one slotyeswood, cloth, leather, paper, gilt
Iron is not chromen/ano iron; gilt is half metallic, roughness 0.45
Bake texels per UV cellmeasured, not a budget1024 px over a 76-cell grid, about 13 px per cell
The bake cage is narrower than the nearest neighbouryesCAGE_EXTRUSION 0.01 m
Level on the stage; stage 60 myesturned about Z only; 60 m floor and wall
Keep a falsifier's envelope stillyesevery falsifier leaves the AABB unchanged
Mirror symmetry, plumb, rope, masonry, roofs, rings, terrain, vesselsnothe piece has none of these
-

Falsifiers

+

Falsifiers#

Each breaks one pipeline stage so a named budget fails. All fifteen were run on 4.5.11, 5.1.2 and 5.2.1 and exited the same declared code on all three.

FlagTarget budgetBreaksExit
--skip-decimateLOD1 ratiodrops the DECIMATE modifiers, LOD1 ratio goes to 1.00009
--stray-vertmesh hygieneadds one loose vertex inside the carcass15
--lift-zgrounded zminlifts the whole mesh 50 mm16
--float-sidenamed sideslifts the left side 5 mm; the right side still grounds the AABB16
--short-shelvesdado bitestops the two shelves 4 mm short of the sides; −4.0 mm17
--loose-pagespage-block bitebuilds every page block 0.9 mm clear of its boards; −0.9 mm17
--float-booksbook seatlifts every book 6 mm; −5.0 mm18
--float-bandsband seatbuilds every band 1.1 mm off its spine; −1.1 mm18
--tall-bookheadroomstretches one middle-shelf folio 1.20 into the shelf above; −3.9 mm20
--uniform-bookssize spreadone height and depth, thickness within ±1.5 %; 1.030 / 1.00021
--air-leanlean contactstops each leaning book 3 mm short of its neighbour; −3.0 mm22
--deep-leanlean contactdrives each leaning book 4.5 mm into its neighbour; 4.5 mm22
--crowd-booksbook overlapscloses the top shelf's gaps to −4.1 mm; 11 overlaps23
--sharp-shelfedge treatmentleaves the top shelf unchamfered; 12 right-angle edges24
-

Exit codes

+

Exit codes#

File-local and sequential. 9 is a valid check code. 1 is the FATAL wrapper — a crash, never a named check. 19 (plumb and real-world size) is reserved across pieces and unused here.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build, or has no UV layer
4Base triangle count outside band
5Material slots, or a material's face floor
6UVs outside 0..1
7UV AABB overlap above tolerance
8Outer AABB off declared size
9LOD1 or LOD2 ratio outside band (--skip-decimate)
10Framing gate (examples/gallery_framing.py, render path only)
11Collider triangles above ceiling
12Normal bake failed or produced no image data
13glTF export missing or empty
14--output produced no file
15Mesh hygiene (--stray-vert)
16Grounded zmin, or a side floating (--lift-z, --float-side)
17Dado or page-block bite, or a book without one block and two bands (--short-shelves, --loose-pages)
18Book or band seat (--float-books, --float-bands)
20Headroom under the member above (--tall-book)
21Size spread below floor (--uniform-books)
22Lean contact, or leaning / lying count (--air-lean, --deep-lean)
23Books overlap each other or the carcass (--crowd-books)
24Right-angle wood edges (--sharp-shelf)
-

Run it

+

Run it#

# Budget check, no render.
 blender --background --python bookshelf.py --
 
@@ -317,17 +376,1718 @@ 

Run it

# Render the gallery still (EEVEE; --engine cycles on a GPU-less host). blender --background --python bookshelf.py -- --output bookshelf.webp

Smoke runs the check-only path. It does not pass --output or any falsifier.

-

Cross-version measurements

+

Cross-version measurements#

Value4.5.115.1.25.2.1
Base triangles109081090810908
LOD1 / LOD2 tris5454 / 23985454 / 23985454 / 2388
Face counts (wood / cloth / leather / paper / gilt)338 / 2050 / 1066 / 228 / 1976samesame
Outer AABB0.9720 × 0.3320 × 1.1760samesame
Collider tris686868
Headroom / spread / lean44.36 mm / 2.900, 1.645 / 0.80 mmsamesame
glTF bytes785836785836785828

LOD2 differs by ten triangles on 5.2 (DECIMATE COLLAPSE, the known divergence; the gate is a ratio band) and the glTF size by 8 bytes (exporter metadata, not a budget). Every other value is identical.

-
+

Source

showcase/bookshelf/bookshelf.py - View on GitHub → + 1701 lines · View on GitHub →
-
"""Game-ready bookshelf with leaning books — a showcase piece, not an example.
+      
+
"""Game-ready bookshelf with leaning books — a showcase piece, not an example.
 
 Asserts budget conformance of a procedural bookcase: a stained carcass
 with two housed shelves, a base on a recessed plinth, lapped back boards
@@ -2029,7 +3789,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -2049,20 +3843,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/brazier/index.html b/docs/gallery/brazier/index.html index 39e13e24..23321a96 100644 --- a/docs/gallery/brazier/index.html +++ b/docs/gallery/brazier/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,42 +325,44 @@

brazier

A procedural iron brazier — spun bowl with a rolled rim on three forged legs, a bed of ash and a heap of broken glowing charcoal — carried through UVs, bake, LOD, compound collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

+

Rendered headless by the showcase piece itself — click to zoom.

witnesses Recomputed: 6960 tris, three materials with 1680 iron, 2880 coal and 448 ash faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.696×0.696×0.612 m, zmin 0, hygiene 0 including zero coplanar cross-shell pairs, three feet grounded, every leg biting 2.6 mm into the bowl wall, 36 coals seated 6.4–15.7 mm into the ash with none interpenetrating, 54 mm of freeboard under the rim, and a 51.9 kg density-weighted mass centre 0.466 m up and 154 mm inside the support polygon — an 18.3° tip angle. --tuck-legs exits 19 at 10.2° while the feet still ground and the envelope is unchanged; --overfill exits 18 on the freeboard.
+

tags mesh export showcase

blender --background --python showcase/brazier/brazier.py --

A fire brazier: a spun-iron bowl with a rolled rim, three forged legs swept in one piece from the bowl wall to curled feet, a bed of ash in the bowl and a heap of thirty-six broken, glowing charcoal lumps on it. A showcase piece, not an example — it witnesses no API contract. It asserts that generated geometry meets declared asset budgets, recomputed from the finished mesh.

-

What it composes

+

What it composes#

Shipped contentUsed for
skills/mesh-editing-and-bmeshlathed bowl and ash, swept legs, cleaved coal lumps in one bmesh
skills/procedural-materials-and-shadersrusted rough iron, pale ash, charcoal with emissive cracks masked by noise
skills/bake-high-to-lowCycles tangent-space normal bake, high onto low
skills/engine-export-presetsUnity glTF (export_yup=True)
skills/depsgraph-and-evaluated-dataevaluated triangle counts for the LOD ratios
snippets/decimate_to_budget.pyLOD1 / LOD2 COLLAPSE chain
snippets/convex_hull_collider.pyone hull for the bowl, one per leg, merged into a compound
snippets/lod_chain.pyLOD naming and ratio pattern
examples/mesh-hygiene-audithygiene combinatorics (copied, not imported)
-

The budget that matters

+

The budget that matters#

A brazier holds burning coals, so the thing it must not do is tip when someone knocks it. That is invisible to every envelope check: legs tucked in under the bowl still reach the floor, still ground the piece, and still fit inside the bowl's own bounding box.

The piece recomputes stability from the finished mesh:

  • Centre of mass. Signed tetrahedra over every closed shell, each triangle weighted by its material's density (iron 7850, coal 500, ash 600 kg/m³). The bowl dominates: 51.9 kg in all, centred on the axis at 0.466 m.
  • Support polygon. The convex hull, in plan, of the leg vertices that lie on the floor.
  • Tip angle. atan(d / z), where d is the distance from the mass centre's plan position to the nearest support edge and z is its height. This is the lean the brazier survives before it goes over. Measured 18.30° against a 13° floor.

--tuck-legs is the falsifier built for exactly this. It brings the feet in from 0.29 m to 0.15 m of the axis. They still ground (zmin and the three-feet gate pass), the rim still sets the envelope (the AABB is unchanged), and every other budget passes. Only the tip angle sees it: 10.23°.

-

Budgets

+

Budgets#

Declared in the script as named constants, recomputed from the generated mesh. Measured values are from Blender 5.2.1; every one is byte-identical on 4.5.11 and 5.1.2 (only the glTF file size differs, by 4 bytes, which is exporter metadata and not a budget).

BudgetBandMeasured
Base triangles6600–82006960
LOD1 ratio0.32–0.620.5000
LOD2 ratio0.10–0.350.2198
Material slotsexactly 3, distinct3
Iron faces≥ 14001680
Coal faces≥ 28002880
Ash faces≥ 380448
UV boundsinside 0..1(0.0006, 0.0006)–(0.9994, 0.9994)
UV AABB overlap≤ 1e-50.000000
Outer AABB0.696 × 0.696 × 0.612 m ± 0.0200.6960 × 0.6960 × 0.6118
Collider triangles≤ 400240 (four hulls)
Normal bake{'FINISHED'} with image data{'FINISHED'}, has_data=True
glTF exportfile written, non-empty~450 kB
Hygieneall zeroloose 0/0, non-manifold 0, zero-area 0, doubles 0, n-gons 0, coplanar cross-shell pairs 0
Grounded AABB\zmin\≤ 1e-40.00000
Named feet3 legs, each zmin ≤ 1e-33 at 0.00000
Legs bite the bowldeepest leg vertex inside the bowl ≥ 1.5 mm2.58 mm
Coal seat36 coals, deepest vertex inside the ash 4–30 mm6.40–15.69 mm
Freeboardrim above the highest coal or ash ≥ 30 mm53.95 mm
Tip angle≥ 13°18.30°
Coal interpenetration0 pairs0
Coal size spreadlargest footprint over smallest ≥ 1.355.64

Real-world size: a 0.70 m bowl, rim 0.61 m off the floor — a courtyard or hall brazier.

-

Construction

+

Construction#

  • Bowl. A half-ellipsoid spun from sheet iron, with a rolled bead at the lip. The inner wall is the outer one offset along its own normal by WALL (8 mm), so the wall keeps its thickness where the bowl runs flat.
  • Legs. A chamfered-square bar (no 90° edges), swept in one piece:
  • The bar leaves the bowl along the wall's normal, starting LEG_BITE inside the iron.
  • It bends down and out on a Bézier whose last handle is level, so it arrives flat.
  • It runs level on the floor with a flat face down, and ends in a curled toe.
  • The section frame is fixed to each leg's own vertical plane.
  • Ash. A lathed dome whose edge follows the inner wall ASH_BITE outside it. The bed is held by the bowl, not resting beside it.
  • Coals. Each coal is an icosphere scaled to a lump, tumbled a few degrees and cleaved by three closed-form planes, so it reads as split charcoal and not a pebble ("rock is broken"). They are flat-shaded. Thirty-six are packed on a golden-angle spiral: each walks outward until it clears every placed coal by the two lumps' own plan radii (read off their vertices) plus COAL_GAP, and stays inside the inner wall. Each is then sunk into the ash by a fraction that varies with its index.
-

Findings the budgets forced

+

Findings the budgets forced#

  • Coal count. Packing "as many coals as fit" made the coal count, and so the triangle budget, move with any falsifier that changed the space or the spacing. --overfill, --pile-coals and --uniform-coals all exited 4 or 5 instead of their targets. The heap is now a fixed N_COALS = 36.
  • Coplanar faces. One coplanar cross-shell pair: two buried underside triangles on neighbouring lumps, 0.2° apart in normal and 0.04 mm apart in plane. Stood level, similar lumps put near-parallel faces side by side. Each lump now leans a closed-form few degrees, and the count is 0.
  • Stale face references. A dict keyed by BMFace returned the wrong face after create_icosphere(subdivisions=2) freed and reallocated face memory. UV islands now live in a face int layer, and the provisional strip UVs are written straight into the loop layer.
-

Conventions walked

+

Conventions walked#

Every convention in showcase/README.md, and whether it applies here.

ConventionAppliesHow
Deterministic, budgets declared, assertions recomputeyesno RNG; every value above is read off the mesh
Falsifier fails the budget it targetsyestable below, proven on all three binaries
Hygiene incl. cross-shell coplanaryesexit 15
Named supportsyesthree feet (--float-foot)
A joint bitesyeseach leg's deepest vertex inside the bowl by signed distance (--short-legs)
A bent bar is one sweepyeseach leg is one sweep from bowl to toe
Iron is not chromeyesnear-black and rust, roughness 0.55–0.85, metallic 0.65
A vessel has a base and a bellyyesthe bowl's wall is offset along its normal
A vessel holds its contentsyesthe ash bites the inner wall; freeboard is asserted (--overfill)
Carried parts bite their bearersyescoals seated in the ash, banded (--float-coals)
Rock is broken, not smoothyescleaved, flat-shaded lumps
Scattered parts do not interpenetrateyesexit 20 (--pile-coals)
Scatter varies in sizeyesexit 21 (--uniform-coals)
Shading is part of the modelyesbowl and ash smooth (spun, powdery); legs flat (forged bar); coals flat (broken)
One substance, one slotyesiron, coal, ash
Edge treatment: no right anglesyes, by constructionchamfered-square legs (45° steps), lathed bowl; not a separate budget
Sort bmesh operator inputsn/ano set-fed operator is run
Level on the stage; stage 60 myesturned about Z only; 60 m floor and wall
Keep a falsifier's envelope stillyesevery falsifier leaves the AABB unchanged
Timber, masonry, rope, fixtures, roofs, rings, paintnothe piece has none of these
-

Falsifiers

+

Falsifiers#

Each breaks one pipeline stage so a named budget fails. All ten were run on 4.5.11, 5.1.2 and 5.2.1 and exited the same declared code on all three.

FlagTarget budgetBreaksExit
--skip-decimateLOD1 ratiodrops the DECIMATE modifiers, LOD1 ratio goes to 1.00009
--stray-vertmesh hygieneadds one loose vertex under the bowl15
--lift-zgrounded zminlifts the whole mesh 50 mm16
--float-footnamed feetlifts one foot 12 mm; the other two still ground the AABB16
--short-legsleg-to-bowl bitestarts every leg 12 mm off the wall; −10.0 mm17
--float-coalscoal seatlifts every coal 30 mm off the ash; −13.9 to −23.3 mm18
--overfillfreeboardraises the ash and heap 45 mm, still below the rim; 8.3 mm18
--tuck-legstip anglebrings the feet in to 0.15 m of the axis; 10.23°19
--pile-coalscoal interpenetrationpacks coals at 0.6 of their spacing; 40 pairs20
--uniform-coalscoal size spreadone size for every coal; 1.19821
-

Exit codes

+

Exit codes#

File-local and sequential. 9 is a valid check code. 1 is the FATAL wrapper — a crash, never a named check.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build, or has no UV layer
4Base triangle count outside band
5Material slots, or a material's face floor
6UVs outside 0..1
7UV AABB overlap above tolerance
8Outer AABB off declared size
9LOD1 or LOD2 ratio outside band (--skip-decimate)
10Framing gate (examples/gallery_framing.py, render path only)
11Collider triangles above ceiling
12Normal bake failed or produced no image data
13glTF export missing or empty
14--output produced no file
15Mesh hygiene (--stray-vert)
16Grounded zmin, or a foot floating (--lift-z, --float-foot)
17A leg not biting the bowl (--short-legs)
18Coal seat or freeboard (--float-coals, --overfill)
19Tip angle below floor (--tuck-legs)
20Coals interpenetrate (--pile-coals)
21Coal size spread below floor (--uniform-coals)
-

Run it

+

Run it#

# Budget check, no render. ~2.2 s on 4.5, ~2.0 s on 5.1, ~2.4 s on 5.2.
 blender --background --python brazier.py --
 
@@ -317,16 +376,1387 @@ 

Run it

blender --background --python brazier.py -- --output brazier.webp

Smoke runs the check-only path. It does not pass --output or any falsifier.

The render adds two render-only lights of its own: a point light above the coals for the fire, and a spot for the warm floor pool. The camera is low and close, so the house area wedge landed on the back wall, and an area lamp brought near lit the whole floor.

-

Cross-version measurements

+

Cross-version measurements#

Value4.5.115.1.25.2.1
Base triangles696069606960
LOD1 / LOD2 tris3480 / 1530samesame
Face counts (iron / coal / ash)1680 / 2880 / 448samesame
Outer AABB0.6960 × 0.6960 × 0.6118samesame
Collider tris240240240
Mass / tip angle51.87 kg / 18.30°samesame
glTF bytes450940450940450936
Check wall-clock~2.2 s~2.0 s~2.4 s
-
+

Source

showcase/brazier/brazier.py - View on GitHub → + 1371 lines · View on GitHub →
-
"""Game-ready brazier — a showcase piece, not an example.
+      
+
"""Game-ready brazier — a showcase piece, not an example.
 
 Asserts budget conformance of a procedural iron brazier: a spun bowl
 with a rolled rim on three forged legs, a bed of ash in the bowl and a
@@ -1698,7 +3128,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -1718,20 +3182,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/butter-churn/index.html b/docs/gallery/butter-churn/index.html index 8896ee6f..560f7bf3 100644 --- a/docs/gallery/butter-churn/index.html +++ b/docs/gallery/butter-churn/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,47 +325,49 @@

butter-churn

A procedural plunge butter churn — sixteen tapered oak staves, three iron hoops, a three-board bottom, a two-board lid and a maple cross dasher — carried through UVs, bake, LOD, compound collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

+

Rendered headless by the showcase piece itself — click to zoom.

witnesses Recomputed: 5684 tris, three materials with 2240 oak, 160 maple and 576 iron faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.340×0.340×1.050 m, zmin 0, hygiene 0 including zero coplanar cross-shell pairs, all sixteen staves on the floor, three bottom boards with 1.2 mm seams, the handle 14 mm into the slats, three hoops 2.5 mm into the staves and 4.9 mm proud, the lid 1.4–2.0 mm onto the stave tops, 5 mm of clearance round the handle, and a 514 mm plunge stroke for a 101 mm reach. --wide-dasher exits 20 at 140 mm while the plunger still clears the staves at rest; --tight-hole exits 18.
+

tags mesh export showcase

blender --background --python showcase/butter-churn/butter_churn.py --

A plunge butter churn: a tall body of sixteen oak staves that narrows toward the top, three forged iron hoops, a bottom of three boards set in a croze, a lid of two boards with a hole for the handle, and a maple cross dasher whose handle runs up through the lid. A showcase piece, not an example — it witnesses no API contract. It asserts that generated geometry meets declared asset budgets, recomputed from the finished mesh.

-

What it composes

+

What it composes#

Shipped contentUsed for
skills/mesh-editing-and-bmeshlofted staves, banded hoops, clipped board polygons, lathed handle, all in one bmesh
skills/custom-propertiesface attributes (PlankTone, GrainDir) read by the wood shaders
skills/procedural-materials-and-shadersoak with grain along each stave, pale maple, rusted rough iron
skills/bake-high-to-lowCycles tangent-space normal bake, high onto low
skills/engine-export-presetsUnity glTF (export_yup=True)
skills/depsgraph-and-evaluated-dataevaluated triangle counts for the LOD ratios
snippets/decimate_to_budget.pyLOD1 / LOD2 COLLAPSE chain
snippets/convex_hull_collider.pyone hull for the body and lid, one for the handle above it
snippets/lod_chain.pyLOD naming and ratio pattern
examples/mesh-hygiene-audithygiene combinatorics (copied, not imported)
-

The budget that matters

+

The budget that matters#

A churn is for plunging. The body narrows toward the top, so a plunger that clears the staves at the bottom can jam partway up the stroke, and nothing else notices:

  • the plunger still sits inside the body without touching it;
  • the handle still clears the lid's hole;
  • the bounding box does not move.

The piece reads the inner wall off the staves ring by ring and the plunger's reach off the dasher's slats, about the handle's axis. It finds the height at which the wall comes within WALL_CLEAR (10 mm) of the plunger, stops at the lid if that comes first, and asserts the travel from the plunger's rest height as a stroke floor of 400 mm. Measured: 513.8 mm, for a 100.6 mm reach.

--wide-dasher is the falsifier built for exactly this. It widens the slats to 125 mm. The plunger still clears the staves at rest, but it can travel only 140.4 mm before it would meet the wall, and the run exits

  1. Every other budget passes and the AABB is unchanged.
-

Budgets

+

Budgets#

Declared in the script as named constants, recomputed from the generated mesh. Measured values are from Blender 5.2.1; every one is byte-identical on 4.5.11 and 5.1.2 (only the glTF file size differs, by 8 bytes, which is exporter metadata and not a budget).

BudgetBandMeasured
Base triangles5000–65005684
LOD1 ratio0.32–0.620.5000
LOD2 ratio0.10–0.350.2199
Material slotsexactly 3, distinct3
Oak / maple / iron faces≥ 2000 / 140 / 5002240 / 160 / 576
UV boundsinside 0..1(0.0013, 0.0013)–(0.9987, 0.9987)
UV AABB overlap≤ 1e-50.000000
Outer AABB0.340 × 0.340 × 1.050 m ± 0.0200.3401 × 0.3401 × 1.0500
Collider triangles≤ 200178 (two hulls)
Normal bake{'FINISHED'} with image data{'FINISHED'}, has_data=True
glTF exportfile written, non-empty~240 kB
Hygieneall zeroloose 0/0, non-manifold 0, zero-area 0, doubles 0, n-gons 0, coplanar cross-shell pairs 0
Grounded AABB\zmin\≤ 1e-40.00000
Named staves16 staves, each zmin ≤ 1e-416 at 0.00000
Bottom boards3 boards, seams 0.6–2.5 mm3, 1.20 mm
Handle bitehandle's foot 8–20 mm into the slats14.0 mm
Hoop seat3 hoops, inner face 1.0–4.5 mm into the staves, outer face ≥ 3 mm proud2.50 mm in, 4.90 mm proud
Lid seat2 boards, each 1–4 mm onto the stave tops1.40–2.00 mm
Handle clearancelid hole over handle radius, 2–10 mm5.00 mm
Plunge stroke≥ 400 mm513.8 mm

Real-world size: 0.34 m across the foot, 0.75 m to the rim and 1.05 m to the top of the handle. It is a farmhouse plunge churn.

-

Construction

+

Construction#

  • Staves. Sixteen staves are lofted on a straight taper (R_BOT 170 mm to R_TOP 120 mm), with a gap of STAVE_GAP between neighbours.
  • Each stave's section has four vertices across its width at fractions 0, ¼, ¾ and 1, outer face over inner face, and quads close both ends.
  • Ring heights are a uniform run plus both edges of every hoop and of the croze, so a hoop's edge and the bottom's faces meet stave vertices.
  • Each stave's foot and top are cut sloping inward (END_CANT, doubled on alternate staves), so the churn stands on its outer edges.
  • Every real edge is chamfered at 1.5 mm; the few-degree edges across a stave's width are left alone.
  • Hoops. Each hoop is a banded iron section: flat inner face, chamfered outer edges. It is sampled at every stave's two interior vertex angles, so every hoop vertex lies radially over a stave vertex at the same height. The inner face is HOOP_BITE inside the stave's outer face, read from the same taper; the outer face is HOOP_PROUD outside it.
  • Bottom. A 48-gon disk sized to the inner wall plus CROZE, cut across X into three boards with a named seam (Sutherland–Hodgman clip). Caps are chamfered as n-gons and then triangulated.
  • Lid. Two half-annulus boards with the seam across the hole. The hole is HOLE_CLEAR wider than the handle. The lid rests LID_SEAT on the stave tops and overhangs by LID_OVER.
  • Dasher. Two crossed maple slats, the second a step higher. The handle's foot is driven HANDLE_BITE into them. The handle is a 12-segment lathe with rings at both faces of the lid, so its clearance is measured where it passes, and it ends in a swelled grip.
-

Findings the budgets forced

+

Findings the budgets forced#

  • Neighbouring ends in one plane. The first build had 240 coplanar cross-shell pairs:
  • adjacent stave feet and tops;
  • the two lid boards;
  • the bottom boards' faces and a rim chord the seams split.

The fixes:

  • stave ends are canted inward, doubled on alternate staves (at one cant, neighbours 5.6° apart in bearing still matched as one plane);
  • one lid board stands 0.6 mm proud;
  • the middle bottom board steps up and in by 0.6 mm.

The count is 0.

  • A falsifier stolen by the collider. --float-hoops exited 11 on the collider ceiling (242 against 240) instead of 18. The hoops stand 5 mm proud and add nothing a character could hit, so they were taken out of the body hull rather than the ceiling raised. The collider is now 178 triangles, and the falsifier exits 18.
-

Conventions walked

+

Conventions walked#

Every convention in showcase/README.md, and whether it applies here.

ConventionAppliesHow
Deterministic, budgets declared, assertions recomputeyesno RNG; every value above is read off the mesh
Falsifier fails the budget it targetsyestable below, proven on all three binaries
Hygiene incl. cross-shell coplanaryesexit 15; canted stave ends and stepped boards keep it at 0
Named supportsyesall sixteen staves (--short-stave)
Seat conformance, sampled per segmentyesevery hoop vertex against the stave vertex under it (--float-hoops)
Bands on a curved host are built on the host's arcyeshoop vertices at the staves' own angles
A band wraps its host, never sunk into ityesinner face bites, outer face asserted proud
A head or bottom is boards, not a slabyesthree bottom boards with a banded seam (--one-piece-bottom); the lid is two boards
A vessel holds its contentsn/athe churn shows no contents
A joint bites; touching is not joiningyeshandle into the slats (--short-handle)
Carried parts bite their bearersyeslid on the stave tops (--float-lid)
What passes through a part must clear its mountyeshandle through the lid's hole (--tight-hole)
Identical boards read as CGyesper-stave and per-board PlankTone, grain along each piece's longest extent
Hoops are forged iron, not brightyesnear-black and rust, roughness 0.55–0.85, metallic 0.65
Shading is part of the modelyesstaves smooth across their width with every edge over 35° hard, so they read as coopered boards; handle smooth (turned)
One substance, one slotyesoak, maple, iron
Chamfer n-gon caps, then triangulateyesbottom and lid boards
Sort bmesh operator inputsyesbevel edges sorted by index
Level on the stage; stage 60 myesturned about Z only; 60 m floor and wall
Keep a falsifier's envelope stillyesevery falsifier leaves the AABB unchanged
Rope, masonry, roofs, rings, scatter, mirrored assembliesnothe piece has none of these
-

Falsifiers

+

Falsifiers#

Each breaks one pipeline stage so a named budget fails. All ten were run on 4.5.11, 5.1.2 and 5.2.1 and exited the same declared code on all three.

FlagTarget budgetBreaksExit
--skip-decimateLOD1 ratiodrops the DECIMATE modifiers, LOD1 ratio goes to 1.00009
--stray-vertmesh hygieneadds one loose vertex inside the body15
--lift-zgrounded zminlifts the whole mesh 50 mm16
--short-stavenamed stavesstops one stave's foot 6 mm above the floor; the rest ground the AABB16
--one-piece-bottombottom boardsone disk instead of three boards17
--short-handlehandle bitestarts the handle 5 mm above the slats; −5.0 mm17
--float-hoopshoop seatbuilds every hoop 4 mm off the staves; −3.5 mm18
--float-lidlid seatlifts the lid 5 mm; −3.6 to −3.0 mm18
--tight-holehandle clearancecuts the hole 2 mm inside the handle; −2.0 mm18
--wide-dasherplunge strokewidens the slats to 125 mm; 140.4 mm20
-

Exit codes

+

Exit codes#

File-local and sequential. 9 is a valid check code. 1 is the FATAL wrapper — a crash, never a named check. 19 (plumb and real-world size) is reserved across pieces and unused here.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build, or has no UV layer
4Base triangle count outside band
5Material slots, or a material's face floor
6UVs outside 0..1
7UV AABB overlap above tolerance
8Outer AABB off declared size
9LOD1 or LOD2 ratio outside band (--skip-decimate)
10Framing gate (examples/gallery_framing.py, render path only)
11Collider triangles above ceiling
12Normal bake failed or produced no image data
13glTF export missing or empty
14--output produced no file
15Mesh hygiene (--stray-vert)
16Grounded zmin, or a stave floating (--lift-z, --short-stave)
17Bottom boards or handle bite (--one-piece-bottom, --short-handle)
18Hoop seat, lid seat or handle clearance (--float-hoops, --float-lid, --tight-hole)
20Plunge stroke below floor (--wide-dasher)
-

Run it

+

Run it#

# Budget check, no render. ~1.8 s on 4.5 and 5.2.
 blender --background --python butter_churn.py --
 
@@ -321,16 +380,1288 @@ 

Run it

# Render the gallery still (EEVEE; --engine cycles on a GPU-less host). blender --background --python butter_churn.py -- --output butter_churn.webp

Smoke runs the check-only path. It does not pass --output or any falsifier.

-

Cross-version measurements

+

Cross-version measurements#

Value4.5.115.1.25.2.1
Base triangles568456845684
LOD1 / LOD2 tris2842 / 1250samesame
Face counts (oak / maple / iron)2240 / 160 / 576samesame
Outer AABB0.3401 × 0.3401 × 1.0500samesame
Collider tris178178178
Plunge stroke / reach513.8 mm / 100.6 mmsamesame
glTF bytes239804239804239796
-
+

Source

showcase/butter-churn/butter_churn.py - View on GitHub → + 1272 lines · View on GitHub →
-
"""Game-ready butter churn — a showcase piece, not an example.
+      
+
"""Game-ready butter churn — a showcase piece, not an example.
 
 Asserts budget conformance of a procedural plunge churn: a tall staved
 body that narrows toward the top, three iron hoops, a bottom of three
@@ -1603,7 +2934,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -1623,20 +2988,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/campfire/index.html b/docs/gallery/campfire/index.html index 8e5197dc..c298cbce 100644 --- a/docs/gallery/campfire/index.html +++ b/docs/gallery/campfire/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

campfire

A procedural campfire through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

+

Rendered headless by the showcase piece itself — click to zoom.

witnesses Recomputed: 2760 tris, three materials with 216 wood, 48 ash and 1416 stone faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.802×0.802×0.404 m, LOD ratios in band, convex collider 162 tris, hygiene 0, 14 grounded bottom stones, teepee kiss 0.00034 m, course seat 0, stone variation 0.01269 m, non-empty glTF. --skip-decimate exits 9; --stray-vert 15; --lift-z/--short-stones 16; --float-logs 17; --gap-courses 18; --uniform-stones 20.
+

tags mesh export showcase

blender --background --python showcase/campfire/campfire.py --
@@ -282,24 +341,24 @@

campfire

Courses share a Z plane; --gap-courses is the falsifier. Teepee axes aim at a ring of radius (STICK_R * 0.55) / sin(π/N) and overshoot so the caps cross; --float-logs inflates that ring.

It asserts budget conformance of the generated result. It does not witness an API contract. "It rendered without error" is not a check.

Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package).

-

Budgets

+

Budgets#

Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.

AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles1400–36002760 / 2760 / 2760
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.4993
LOD2 ratio0.10–0.35 of base0.2196 / 0.2196 / 0.2196
Materialsexactly 3 distinct; ≥80 wood, ≥24 ash, ≥200 stone3 slots; 216 / 48 / 1416
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(0.807, 0.808, 0.404) m ± 0.015(0.8024, 0.8022, 0.4037), zmin 0
Collider tris≤ 360162
Exportwritten, size > 0231952 / 231952 / 231944 bytes

Base triangles dropped from 3616 to 2200 in the quality pass: the old piece stacked identical boxes with a mortar gap and a teepee that met at one point. The rebuild is two seated courses plus crossing sticks; hidden overlapping volume went away. The second pass took it to 2760: each stone is now a lofted, chamfered lump instead of a four-sided wedge.

DECIMATE COLLAPSE triangle counts are not guaranteed identical across series — the gate is a ratio band, not an exact count. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Stone-width jitter uses fixed seed 17; each stone's shape draws from its own seeded generator. glTF byte size differs on 5.2.1.

-

Hygiene

+

Hygiene#

Recomputed from the generated mesh, not asserted about the script.

AxisDeclaredMeasured (all three)
Non-manifold edges00
Loose verts / edges0 / 00 / 0
Doubles merged at 1e-500
Zero-area faces00
N-gons00
Coplanar disjoint face pairs00
Grounded: zminwithin 1e-4 of 00.0000
Named supports: 14 bottom stoneseach zmin ≤ 0.00114, stone_z 0.00000
Ring plan0.807 m dia × 0.176 m high ± 0.040.8023 × 0.1759
Stone variation (exit 20)per course, spread of stone outer radius, and of crown height on the top course, each ≥ 0.006 m0.01269 (smallest spread)
-

Joint fit and seat

+

Joint fit and seat#

AxisDeclaredMeasured (all three)
Teepee kiss (BVH nearest at caps)≤ 0.008 m0.00034
Course seat (upper zmin − lower zmax)abs ≤ 0.004 m0.00000
-

Stones, wood and ash

+

Stones, wood and ash#

The ring was 28 identical curved bricks: flat tops, flat faces, one height, one grey. It read as a concrete kerb. Each stone is now a chamfered octagon section lofted along its span, swelling to a seeded belly mid-stone and pinched and capped at the ends, with a seeded radial offset and outer-face wobble. Top-course stones get a seeded domed crown. The beds stay flat, and the bottom course's crown stays at the bed height, so the upper course still seats (course seat 0.00000). The stated ring height rose from 0.156 to 0.176 m for the crowns.

Stone variation asserts the ring is not one stone repeated. --uniform-stones gives every stone the same draw — each jitter at its upper bound, so the ring keeps the envelope of its widest seeded stone and only this budget fails (exit 20, spread 0.00000).

Stone is isotropic mottling, speckle and pitting with a seeded tone per stone (stone-archway's recipe, reading PieceTone); sticks and logs carry their own tone and grain along their axis; the ash has sparse ember glow. The temp .glb is removed after its size is measured.

-

Falsifiers

+

Falsifiers#

Each violates one named budget. All seven were run on 4.5.11, 5.1.2 and 5.2.1 and returned the same code on each.

FlagBudget violatedExit
--skip-decimateLOD1 ratio band9
--stray-vertloose vertex count is 015
--lift-zbounding box zmin is 016
--short-stonesnamed bottom-course supports at Z=016
--float-logsteepee kiss17
--gap-coursescourse seat18
--uniform-stonesstone variation20
-

Run

+

Run#

blender --background --python campfire.py --
 blender --background --python campfire.py -- --skip-decimate
 blender --background --python campfire.py -- --stray-vert
@@ -310,17 +369,1441 @@ 

Run

blender --background --python campfire.py -- --uniform-stones blender --background --python campfire.py -- --output campfire.png

Smoke passes no flags.

-

Exit codes

+

Exit codes#

File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path. 15–19 are the hygiene and joint-fit family; 20 is file-local.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 3 distinct slots, or a face-count floor missed
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight
16Not grounded: bounding box zmin off 0, or a named bottom stone floats
17Joint fit: teepee kiss (--float-logs)
18Seat: course gap (--gap-courses)
19Ring diameter or height off the stated real-world size
20Stone variation: the ring stones are identical (--uniform-stones)
-
+

Source

showcase/campfire/campfire.py - View on GitHub → + 1424 lines · View on GitHub →
-
"""Game-ready campfire — a showcase piece, not an example.
+      
+
"""Game-ready campfire — a showcase piece, not an example.
 
 Asserts budget conformance of a procedural campfire (two-course
 wedge-stone ring stacked on a shared plane, pit-floor cobbles and ash,
@@ -1745,7 +3228,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -1765,20 +3282,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/car-mirror-symmetry/index.html b/docs/gallery/car-mirror-symmetry/index.html index 9a7ba07d..e92d364b 100644 --- a/docs/gallery/car-mirror-symmetry/index.html +++ b/docs/gallery/car-mirror-symmetry/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

car-mirror-symmetry

A stylized hatchback lofted as one half (52 stations, 13-point rings) and completed by the Mirror modifier, evaluated through the depsgraph. Wheels, lamps, grille, door mirrors and handles mirror about object origins parked on the symmetry plane; the grille is authored as a half and welded on it.

+

Rendered headless by the example itself — click to zoom.

witnesses Mirror is a modifier, not applied data: the original keeps the authored half while the evaluated mesh is the welded, watertight, exactly symmetric whole.
+

tags modifiers depsgraph

blender --background --python examples/car-mirror-symmetry/car_mirror_symmetry.py --
@@ -285,7 +344,7 @@

car-mirror-symmetry

Version witness: check output is byte-identical on Blender 4.5.11 LTS, 5.1.2 and 5.2.1 LTS. Mirror, evaluated_get / to_mesh / to_mesh_clear (no argument — passing the mesh raises TypeError on both), BVHTree, and TRACK_TO constraint behavior are stable across them; only the EEVEE engine id is version-gated.

The render is the proof: hide the mirror and the still is literally half a car — one headlamp, one door mirror, half a grille, a hood cut open at the centerline. The camera stands on the key light's side, high enough to look down the hood centerline so both halves read at once. The weld itself is not a pixel witness — the hood and roof are nearly flat across the plane, so an unwelded seam renders within 12/255 of the welded one; the 2n − c count is what catches it.

Render notes: the loft is smooth-shaded with sharp edges set on the authored half by dihedral angle (35°) and material border — no bevel or weighted-normal modifier, so the stack stays Mirror only and every count stays closed-form. Boundary edges on the plane take the angle to their own mirror image, acos(1 − 2nₓ²), so the centerline shades smooth. The previous faceted loft had a thin pink-white streak on the hood by the windshield: not a seam (it sat at |x| 0.11–0.38, on the mirrored half, and all weld checks passed) but a sliver facet — the loft's greenhouse points collapsing onto the hood — flat-shaded and facing the key almost head on (n · −key = 0.967). The redesigned ring lerps a hood layout into a cabin layout, so no such sliver exists. Panel classes are by construction position: underbody, rocker and arch cladding are trim; side glass is the sill-to-glass-top segment under the roof, broken by a trim B-pillar; the drip rail becomes the A-pillar; windshield and hatch glass are the roof bands where the cabin factor ramps. The glass is a dark dielectric whose base colour ramps toward steel blue at grazing angles (Layer Weight → Color Ramp) — on a near-black stage there is little to reflect, and flat dark glass read as dull paint. Lamps, grille, mirrors and handles are closed superellipse pods seated on the skin by a BVHTree ray cast, with a chrome bezel and an inset lens rather than flat boxes.

-

Run

+

Run#

# Cheap correctness check (no render) — the CI check:
 blender --background --python car_mirror_symmetry.py --
 
@@ -295,18 +354,820 @@ 

Run

# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts): blender --background --python car_mirror_symmetry.py -- --output car.png blender --background --python car_mirror_symmetry.py -- --output car.png --engine cycles
-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Body datablock is not the authored half
4Authored centerline vert count ≠ 28
5Evaluated verts ≠ 2n − c (--no-mirror lands here)
6Evaluated on-plane verts ≠ centerline; also --output produced no file
7Evaluated Euler characteristic ≠ 2
8Non-manifold edges in the evaluated shell
9Evaluated verts lack a mirrored partner
10Mirror partner deviation above tolerance
11Evaluated bbox not symmetric about X
12Mirrored-part origin off the plane
13Mirrored-part datablock is not the authored half (verts, faces, on-plane verts)
14Mirrored-part evaluated counts ≠ 2n − weld / 2f, or on-plane verts ≠ weld
15Mirrored-part partner check failed
16Mirrored-part evaluated mesh stayed on one side
17Gallery framing violation (--output path only; gallery_framing returns 10, remapped)
18Asset-quality floor violation (--output path only; gallery_asset_quality returns 11, remapped)

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --no-mirror.

-
+

Source

examples/car-mirror-symmetry/car_mirror_symmetry.py - View on GitHub → + 802 lines · View on GitHub →
-
"""A stylized hatchback built as one half and completed by the Mirror modifier
+      
+
"""A stylized hatchback built as one half and completed by the Mirror modifier
 — a runnable example.
 
 Witnesses the Mirror + depsgraph contract from depsgraph-and-evaluated-data:
@@ -1109,7 +1970,41 @@ 

Source

except Exception as e: import traceback; traceback.print_exc(); print(f"FATAL: {e}", file=sys.stderr); sys.exit(1)
+
+ +
@@ -1129,20 +2024,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/cart/index.html b/docs/gallery/cart/index.html index a7bfebe3..60ce6f92 100644 --- a/docs/gallery/cart/index.html +++ b/docs/gallery/cart/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

cart

A procedural two-wheel handcart through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

+

Rendered headless by the showcase piece itself — click to zoom.

witnesses Recomputed: 5284 tris, two materials with metal and wood face floors, UVs in 0..1 with zero AABB overlap, outer AABB 1.580×0.842×0.640 m, LOD ratios in band, convex collider 192 tris, hygiene zero (loose, non-manifold, doubles, n-gons), zero coplanar cross-shell face pairs, both tyres and both prop legs grounded, two wall boards per side with 3 mm seams, tyre seat depth in band over 48 angular stations, every shell in one BVH-contact assembly, the two wheels mirrored within 5e-5 m, no unchamfered right-angle edge, 17.45 bake texels per UV cell, non-empty glTF. --flush-tyre exits 15 on the z-fight budget; --float-wheel and --short-props exit 16 on the named-support budget; --wide-seams exits 17 on the board seams; --sink-tyre exits 18 on the tyre seat band; --lift-bed exits 18 on the one-assembly budget; --skew-wheel exits 19 on the wheel mirror; --sharp-bar exits 20 on edge treatment; --low-bake exits 21 on texel density; --skip-decimate exits 9 on the LOD1 ratio budget; --lift-z exits 16 on the grounded budget.
+

tags mesh export showcase

blender --background --python showcase/cart/cart.py --
@@ -282,19 +341,19 @@

cart

The chassis is stacked from the axle up, and every member bites the one below it by a named depth: axle, bolster, rails, floor planks, walls. The cart stands level on four named supports: two tyres and two legs. Stated size is 1.58 m long, 0.84 m across the axle ends and 0.64 m tall. The wheels are 0.64 m in diameter and the bed is 0.92 × 0.50 m.

It asserts budget conformance of the generated result. It does not witness an API contract. "It rendered without error" is not a check.

Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package).

-

Budgets

+

Budgets#

Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.

AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles5160–54105284 / 5284 / 5284
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.5000
LOD2 ratio0.10–0.35 of base0.2199 / 0.2199 / 0.2146
Materialsexactly 2 distinct, metal ≥ 800, wood ≥ 1200 faces2 slots; 1196 metal, 1694 wood
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(1.580, 0.842, 0.640) m ± 0.01(1.5800, 0.8420, 0.6400)
Collider tris≤ 360192
Bake texelssmallest UV cell ≥ 12 px at the baked resolution17.45 px at 1024 px
Exportwritten, size > 0, removed after measuring403176 / 403176 / 403160 bytes
-

Hygiene, supports and joints

+

Hygiene, supports and joints#

Recomputed from the generated mesh on all three binaries, identical on each.

AxisDeclaredMeasured
Loose V/E, non-manifold, zero-area, doubles @1e-5, n-gonsall 0all 0
Z-fightingcoplanar cross-shell face pairs = 00
Groundedbbox min Z within 1e-4 of 00.0000
Named supports2 legs found; each tyre and leg zmin within 1e-4 of the floor2 legs; all four 0.000000
Material-island gapmetal↔wood min distance ≤ 0.008 m0.00023
Wall boards2 boards per side wall, seams 0.0015–0.005 m(2, 2), seams 0.00300
Tyre seat0.0030–0.0055 m interference, per angular station[0.00400, 0.00400] over 48 stations
One assemblythe shells' BVH-overlap graph is 1 component1 component, 59 shells
Wheel mirrorthe two wheels match in X, Z and extent within 5e-5 m0.00000 mm
Edge treatmentmanifold edges within 5° of 90° = 00 wood, 0 iron

DECIMATE COLLAPSE triangle counts are not identical across series. 5.2.1 is more aggressive on LOD2, so the gate is a ratio band, not an exact count. Bake pixels are stochastic; the gate is has_data plus operator FINISHED plus the texel floor, not byte-identity. Per-board tone comes from random.Random(TONE_SEED), so it is identical run to run. Export byte counts differ by a few bytes across series (glTF serializer). The gate is "written and non-empty", not a byte count, and the file is removed once measured.

The bake cage is 0.01 m: it only has to cover the chamfer difference between the 3-segment high and the 1-segment low. At 0.08 m it reached past the 74 mm between a side wall and its wheel, so rays from the wall hit the high-poly felloe. The wheel's rim baked onto the board as a black staircase.

-

Falsifiers

+

Falsifiers#

Each flag breaks one stage so a named budget fails and the piece exits that code. Magnitudes are sized so no earlier gate can steal the failure. The wheel, leg and bed moves stay inside BBOX_TOL, so the AABB gate (exit 8) still passes, and no flag changes the triangle count by more than the band allows.

FlagTarget budgetExit
--skip-decimateLOD1 ratio band (ratio becomes 1.0)9
--flush-tyreCoplanar cross-shell pairs: sets the tyre's inner radius equal to the felloe's outer radius, which is the construction bug this piece was rebuilt to remove (0 → 144 pairs)15
--lift-zAABB grounded zmin (whole mesh up 0.05 m)16
--float-wheelNamed supports: lifts one wheel 3 mm while the other still grounds the AABB16
--short-propsNamed supports: stops both prop legs 3 mm above the floor while the tyres still ground the AABB16
--wide-seamsWall board seams: opens every seam to 10 mm, same triangle count17
--sink-tyreTyre seat band: buries the hoop 9 mm into the felloe so it reads as one body18
--lift-bedOne assembly: raises the box (planks, walls, straps) 7 mm off its rails, so the contact graph splits in two18
--skew-wheelWheel mirror: pushes one wheel 6 mm out along the track19
--sharp-barEdge treatment: leaves the pull bar's rims unchamfered (24 right-angle edges, 48 triangles short)20
--low-bakeBake texel density: bakes at 256 px (4.36 px per cell)21

--lift-z, --float-wheel and --short-props share exit 16 and fail different budgets. --lift-z fails the AABB gate, which fires first. The other two leave the AABB grounded, so only the per-support check can catch them. --sharp-bar replaced a flag that skipped the whole iron chamfer pass. That flag dropped 1200 triangles and tripped the triangle floor (exit 4) instead of the edge budget.

-

Run

+

Run#

blender --background --python cart.py --
 blender --background --python cart.py -- --skip-decimate
 blender --background --python cart.py -- --flush-tyre
@@ -309,17 +368,1965 @@ 

Run

blender --background --python cart.py -- --low-bake blender --background --python cart.py -- --output cart.png

Smoke passes none of the falsifier flags and no --output.

-

Exit codes

+

Exit codes#

File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer / wheels not found
4Base triangle count outside range
5Material count ≠ 2 distinct slots, or a face-count floor missed
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Hygiene: loose geometry, non-manifold, zero-area, doubles, n-gons, or coplanar cross-shell face pairs (--flush-tyre lands here)
16Bbox min Z not grounded (--lift-z), or a named support off the floor (--float-wheel, --short-props)
17Material-island gap above tolerance, or wall boards miscounted or their seams out of band (--wide-seams)
18Tyre seat depth outside its band (--sink-tyre), or the shells not one connected assembly (--lift-bed)
19Wheel mirror deviation above epsilon (--skew-wheel lands here)
20A right-angle edge survived the chamfer passes (--sharp-bar lands here)
21Bake texel density below the floor (--low-bake lands here)
-
+

Source

showcase/cart/cart.py - View on GitHub → + 1948 lines · View on GitHub →
-
"""Game-ready two-wheel handcart — a showcase piece, not an example.
+      
+
"""Game-ready two-wheel handcart — a showcase piece, not an example.
 
 Asserts budget conformance of a procedural handcart (planked bed on two
 rails that run on as shafts, board walls with iron corner straps, prop
@@ -2268,7 +4275,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -2288,20 +4329,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/chopping-block/index.html b/docs/gallery/chopping-block/index.html index b9f50e54..7e950537 100644 --- a/docs/gallery/chopping-block/index.html +++ b/docs/gallery/chopping-block/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

chopping-block

A procedural log-round chopping block with a riveted iron hoop and a felling axe buried in the sawn face, through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

+

Rendered headless by the showcase piece itself — click to zoom.

witnesses Recomputed: 1800 tris, four materials with 180 bark / 216 grain / 404 metal / 176 haft faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.526 x 0.526 x 1.006 m, LOD ratios in band, convex collider 538 tris, non-empty glTF. Hygiene: zero non-manifold edges, loose geometry, doubles, zero-area faces, n-gons and coplanar disjoint face pairs, grounded at zmin 0, log plumb to 1e-7, oval haft 1.35 wide-to-thick. Joint fit: six shells, 18.0 mm haft clearance in the eye, 78.7 mm bit bury, hoop biting 3.7-4.2 mm into the log on every segment, two lap rivets seated 1.50 and 1.54 mm. --fat-haft exits 17 on eye clearance; --round-band and --float-rivets exit 18 on their seats; --round-haft exits 19 on the haft section.
+

tags mesh export showcase

blender --background --python showcase/chopping-block/chopping_block.py --
@@ -282,27 +341,27 @@

chopping-block

The log is an out-of-round loft whose radius is a closed-form function of angle and height; the hoop, its rivets, the top rim and the drying checks are all generated from that same function, so they stay seated when a dimension changes. The hoop is a lapped strip with two domed rivets through the lap. The axe head is one lofted shell from poll to bit with its chamfers modelled into the section profile, and the haft is swept through the eye rather than pushed into it. The haft is oval, bows away from the bit, and hooks toward it at the knob.

It asserts budget conformance of the generated result. It does not witness an API contract. "It rendered without error" is not a check.

Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package).

-

Budgets

+

Budgets#

Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.

AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles1730–18701800 / 1800 / 1800
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.5000
LOD2 ratio0.10–0.35 of base0.2200 / 0.2200 / 0.2200
Materialsexactly 4 distinct; ≥160 bark, ≥90 grain, ≥170 metal, ≥150 haft faces4 slots; 180 / 216 / 404 / 176
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(0.526, 0.526, 1.006) m ± 0.012(0.5264, 0.5264, 1.0064)
Collider tris≤ 560538
Exportwritten, size > 0180588 / 180588 / 180572 bytes

The triangle band, the outer AABB and the collider ceiling were re-fitted in the quality pass that added the lap, the rivets and the 16-segment oval haft (1472 → 1800 triangles, collider 434 → 538). The triangle band is centred on the new measurement, ± 4 %.

-

Hygiene

+

Hygiene#

Recomputed from the generated mesh, not asserted about the script.

AxisDeclaredMeasured (all three)
Non-manifold edges00
Loose verts / edges0 / 00 / 0
Doubles merged at 1e-500
Zero-area faces00
N-gons00
Coplanar disjoint face pairs00
Grounded: zminwithin 1e-4 of 00.0000
Log axis out of plumb≤ 1e-50.0000000
Log size0.535 × 0.360 m ± 0.030 / ± 0.0100.5464 × 0.3600
Haft section, wide over thick at the grip1.25–1.601.3500
-

Joint fit

+

Joint fit#

Six shells that have to meet correctly: log, hoop, head, haft, and the two rivets through the hoop's lap.

AxisDeclaredMeasured (all three)
Shell countexactly 66
Haft clearance inside the eye≥ 0.006 m0.01800
Haft engagement through the eye≥ 0.020 m0.08225
Haft breakout margin below the head≥ 0.006 m0.02175
Bit bury below the sawn top≥ 0.030 m0.07871
Bit inset from the rim≥ 0.030 m0.14647
Hoop bite into the log, every segment0.002–0.007 m0.00371–0.00422
Haft clearance above the log≥ 0.015 m, 0 verts inside0.07531, 0
Rivet seat into the hoop, each rivet0.0008–0.0025 m0.00150, 0.00154
Rivet dome proud of the hoop, each rivet≥ 0.0025 m0.00419, 0.00432

The hoop bite is binned by angular segment against the log's own radius function. A single global midpoint radius misclassifies outer chamfer vertices as inner ones on an out-of-round log, which is how a hoop that visibly floated on one side still passed.

The rivet seat is measured radially at each rivet vertex's own angle, against the hoop's outer face read off the mesh by a ray cast from the axis. Each rivet is aimed down the hoop's surface normal, not the radial: the log is out of round, so across one 12 mm head its surface falls by up to 1.4 mm, and radially aimed heads seated 1.87 and 2.82 mm against a 1.5 mm design bite, one edge sunk and the other lifted.

DECIMATE COLLAPSE triangle counts happen to agree across all three series here; the gate is still a ratio band, not an exact count. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Construction uses no RNG: two runs on each binary print identical measurements, and the three binaries agree on every gated axis. Export byte counts differ by 16 B on 5.2.1 (glTF serializer), not a gated axis.

-

Shading and materials

+

Shading and materials#

Only the axe head is flat-shaded: it is a chamfered forging, and its facets are its form. The log and the hoop are smooth-shaded. Flat-shaded, the log's 36 equal facets read as coopered staves and the block as a tub, and the hoop threw one highlight per facet. The log's wobble still reads in the silhouette, and its surface is carried by the materials: vertical bark furrows with a bump, growth rings about an off-centre pith on the sawn face, and dark radial checks drawn at the same angles the geometry notches. Every material boundary is a hard edge, so the sawn rim stays crisp against the bark.

The haft has its own material slot. It used to share the bark's, which made the hickory handle the darkest wood on the piece.

-

Falsifiers

+

Falsifiers#

Each violates one named budget. All seven were run on 4.5.11, 5.1.2 and 5.2.1 and returned the same code on each.

FlagBudget violatedExit
--skip-decimateLOD1 ratio band9
--stray-vertloose vertex count is 015
--lift-zbounding box zmin is 016
--fat-hafthaft clearance inside the eye17
--round-bandhoop bite into the log18
--float-rivetsrivet seat into the hoop18
--round-hafthaft section wide over thick19

--fat-haft thickens the haft across the cheeks only, which is the dimension the eye clearance measures. Scaling the whole oval section grew the knob 16 mm past the bounding box, so it exited 8 instead.

-

Run

+

Run#

blender --background --python chopping_block.py --
 blender --background --python chopping_block.py -- --skip-decimate
 blender --background --python chopping_block.py -- --stray-vert
@@ -313,17 +372,1977 @@ 

Run

blender --background --python chopping_block.py -- --round-haft blender --background --python chopping_block.py -- --output chopping-block.png

Smoke passes no flags.

-

Exit codes

+

Exit codes#

File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path. 15–19 are the hygiene and joint-fit family.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 4 distinct slots, or a face-count floor missed
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight
16Not grounded: bounding box zmin off 0
17Joint fit: shell count, eye clearance, engagement, breakout, bury, inset
18Contact fit: hoop bite band, haft fouling the log, or a rivet off its seat band
19Log out of plumb, off its stated real-world size, or a round haft section
-
+

Source

showcase/chopping-block/chopping_block.py - View on GitHub → + 1960 lines · View on GitHub →
-
"""Game-ready chopping block — a showcase piece, not an example.
+      
+
"""Game-ready chopping block — a showcase piece, not an example.
 
 Asserts budget conformance of a procedural splitting block (a hooped log
 round with a felling axe standing in it) after composing shipped pipeline
@@ -2284,7 +4303,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -2304,20 +4357,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/collision-hull-proxy/index.html b/docs/gallery/collision-hull-proxy/index.html index 3adddb5a..2351b650 100644 --- a/docs/gallery/collision-hull-proxy/index.html +++ b/docs/gallery/collision-hull-proxy/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

collision-hull-proxy

A fire hydrant street prop inside its compound collision shell: four convex pieces hulled by bmesh.ops.convex_hull from a coarse inflated cage. The dense render mesh is never hulled - its hull would measure 380 faces, over the 255-face per-piece engine budget. Closed-form plane tests prove containment, convexity, watertightness, outward winding, and Euler characteristic 2 per piece.

+

Rendered headless by the example itself — click to zoom.

witnesses A hull of the dense render mesh measures 380 faces, over the 255-face per-piece budget - the cage is a coarser lathe with rings inflated by sec(pi/n) so each cage ring circumscribes its render ring exactly (containment 4.4e-08). Proud details cost cage rows; concave grooves are free under the hull. Piece budgets: body 70, caps 60+60+60, compound 250.
+

tags bmesh mesh

blender --background --python examples/collision-hull-proxy/collision_hull_proxy.py --
@@ -286,7 +345,7 @@

collision-hull-proxy

What each check catches on failure: an artist editing the render mesh after the hull was generated (probe: pumper lug stretched 2×, exit 3, measured escape 0.730000); inverted hull winding (probe: polygons flipped, exit 5, signed volume −1.022381, the exact negation); a non-watertight piece (probe: one face deleted, exit 4, three border edges).

Version witness: output is byte-identical on Blender 4.5.11 LTS and 5.1.2 — same piece counts, same volumes, same excursions. bmesh.ops.convex_hull and foreach_get are stable across the pair.

The render shows the prop inside its four faceted translucent hull pieces: if any piece failed to enclose its geometry, painted metal would poke through the shell in frame.

-

Run

+

Run#

# Cheap correctness check (no render) — the CI check:
 blender --background --python collision_hull_proxy.py --
 
@@ -296,18 +355,542 @@ 

Run

# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts): blender --background --python collision_hull_proxy.py -- --output hydrant.png blender --background --python collision_hull_proxy.py -- --output hydrant.png --engine cycles
-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Render vertex escapes its hull (--shrink-hull lands here)
4Hull edge does not border exactly two faces
5Signed volume ≤ 0 (inverted winding)
6Hull vertex off its own face plane
7Euler characteristic ≠ 2
8Piece over the 255-face collision budget
9--output produced no file

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --shrink-hull.

-
+

Source

examples/collision-hull-proxy/collision_hull_proxy.py - View on GitHub → + 524 lines · View on GitHub →
-
"""A fire hydrant prop plus its convex collision proxies — a runnable example.
+      
+
"""A fire hydrant prop plus its convex collision proxies — a runnable example.
 
 Witnesses the collision contract every game prop pipeline (engines generally,
 FiveM/GTA-style prop workflows specifically) depends on: a prop's collision is
@@ -832,7 +1415,41 @@ 

Source

except Exception as e: import traceback; traceback.print_exc(); print(f"FATAL: {e}", file=sys.stderr); sys.exit(1)
+
+ +
@@ -852,20 +1469,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/color-attribute-wheel/index.html b/docs/gallery/color-attribute-wheel/index.html index edd38600..a9d0e5e0 100644 --- a/docs/gallery/color-attribute-wheel/index.html +++ b/docs/gallery/color-attribute-wheel/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

color-attribute-wheel

The modern color-attributes API — mesh.color_attributes.new() on the CORNER domain, filled by expanding per-vertex HSV across face corners with foreach_get/foreach_set, then wired into a shader Attribute node.

+

Rendered headless by the example itself — click to zoom.

witnesses CORNER-domain attributes are sized to loop count, not vertex count; the check asserts the sizing, that active_color points at it, and that the Attribute node is actually linked to Base Color, not just present.
+

tags mesh materials attributes

blender --background --python examples/color-attribute-wheel/color_attribute_wheel.py --
@@ -280,9 +339,9 @@

color-attribute-wheel

A runnable example that builds an HSV color wheel disc entirely with bmesh and colors it with Mesh.color_attributes.new() — the modern attributes API, not the deprecated Mesh.vertex_colors alias AI code keeps reaching for. It witnesses the domain trap that comes with it: a CORNER-domain attribute is sized to len(mesh.loops), not len(mesh.vertices), so per-vertex data has to be expanded across face corners before it is written. The material wires the same attribute into a Shader Attribute node (attribute_type='GEOMETRY') feeding Base Color — the step AI code most often skips, leaving the mesh gray even when the attribute data is correct.

What it witnesses: a FLOAT_COLOR attribute on the CORNER domain, filled with one foreach_get (loop → vertex index) and one foreach_set (loop color), never a per-loop Python assignment. The check asserts the attribute is sized to the loop count and not the vertex count, that it is color_attributes.active_color (so a renderer or exporter actually picks it up), and that several probe loops match the closed-form HSV value for the vertex they reference. A separate check in the render path confirms the Attribute node is actually linked to Base Color, not just present in the node tree.

-

Staging

+

Staging#

The disc is zero-thickness, and the still used to show it as a paper-thin oval hovering 8 cm above the floor. The render path now gives it a render-only Solidify body (5 cm) and stands it at the same 52° lean on a dark plinth, with a strut behind it, easel-style. The height is computed so the lowest point of the rim rests on the plinth. The mesh check() asserts is unchanged: the modifier is added after the check and only in the render path. Measured framing on 5.2.1: fill 0.794 y, every margin ≥ 0.094.

-

Run

+

Run#

# Cheap correctness check (no render) — the CI check:
 blender --background --python color_attribute_wheel.py --
 
@@ -292,18 +351,377 @@ 

Run

# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts): blender --background --python color_attribute_wheel.py -- --output wheel.png blender --background --python color_attribute_wheel.py -- --output wheel.png --engine cycles
-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Topology ≠ closed form
4Color attribute missing
5Domain/type ≠ CORNER/FLOAT_COLOR (--point-domain lands here)
6Attribute sized to verts, not loops
7active_color not set
8Probe loop color off HSV closed form
9--output produced no file

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --point-domain.

-
+

Source

examples/color-attribute-wheel/color_attribute_wheel.py - View on GitHub → + 359 lines · View on GitHub →
-
"""HSV color-attribute wheel -- a runnable example.
+      
+
"""HSV color-attribute wheel -- a runnable example.
 
 Witnesses the modern color-attribute contract that AI-generated Blender code
 routinely gets wrong: `Mesh.color_attributes.new()` (not the deprecated
@@ -663,7 +1081,41 @@ 

Source

except Exception as e: import traceback; traceback.print_exc(); print(f"FATAL: {e}", file=sys.stderr); sys.exit(1)
+
+ +
@@ -683,20 +1135,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/compositor-glare/index.html b/docs/gallery/compositor-glare/index.html index b906ec99..8cfbbd17 100644 --- a/docs/gallery/compositor-glare/index.html +++ b/docs/gallery/compositor-glare/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

compositor-glare

Bloom where it actually lives — a compositor Glare (Fog Glow) node fed by Render Layers, wired via scene.compositing_node_group on 5.x and scene.node_tree on 4.x, with the Glare node's legacy properties vs 5.x menu sockets.

+

Rendered headless by the example itself — click to zoom.

witnesses scene.node_tree is gone in 5.x and EEVEE has no use_bloom on either side: the halo beyond the ring silhouette falls off strictly with the compositor on and is exactly zero with it off.
+

tags compositor rendering

blender --background --python examples/compositor-glare/compositor_glare.py --
@@ -281,7 +340,7 @@

compositor-glare

A runnable example that puts bloom where it actually lives: the compositor. Three emissive rings are rendered, then a Glare (Fog Glow) node fed by Render Layers grows the halo — wired through scene.compositing_node_group on Blender 5.x and through scene.node_tree on 4.x, because the 5.0 compositor rewrite removed the scene-level tree entirely (scene.use_nodes = True now raises AttributeError).

The Glare node changed shape with it: on 4.x it takes legacy enum properties (glare_type='FOG_GLOW', quality='HIGH', size=6), while on 5.x the same choices are menu/float input sockets (inputs['Type'].default_value = 'Fog Glow'). The 4.x threshold property is a dead shim — the real threshold is the Threshold input socket on both versions. And EEVEE has no use_bloom toggle on either version (removed in 4.2): bloom is a compositor node, period.

What it witnesses: the halo is the compositor's output, proven with pixels. The check renders the scene twice at 96×54 (single-sample Cycles, noise-free) — once with use_compositing on, once off — and samples a column of pixels above the middle ring's silhouette via world_to_camera_view. With the compositor on, the halo luminance just outside the silhouette is far above zero and falls off strictly with distance (the Fog Glow kernel signature); with it off, those same pixels are exactly zero. The structural check pins the wiring (Render Layers → Glare → output), the version-correct tree plumbing, and the absence of any EEVEE bloom toggle.

-

Run

+

Run#

# Cheap correctness check (two tiny renders) — the CI check:
 blender --background --python compositor_glare.py --
 
@@ -291,18 +350,397 @@ 

Run

# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts): blender --background --python compositor_glare.py -- --output rings.png blender --background --python compositor_glare.py -- --output rings.png --engine cycles
-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Compositor tree / Glare / Threshold / link-chain contract (--threshold-high lands here)
4Check render missing, tube dark, no halo, or halo does not fall off
5Halo present with compositing off
6--output produced no file

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --threshold-high.

-
+

Source

examples/compositor-glare/compositor_glare.py - View on GitHub → + 379 lines · View on GitHub →
-
"""Bloom through the compositor across the 4.x -> 5.x boundary — a runnable example.
+      
+
"""Bloom through the compositor across the 4.x -> 5.x boundary — a runnable example.
 
 Witnesses the compositor-plumbing contract that AI-generated Blender code breaks
 constantly:
@@ -682,7 +1120,41 @@ 

Source

except Exception as e: import traceback; traceback.print_exc(); print(f"FATAL: {e}", file=sys.stderr); sys.exit(1)
+
+ +
@@ -702,20 +1174,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/crate-stack/index.html b/docs/gallery/crate-stack/index.html index 51d900f5..e26f066c 100644 --- a/docs/gallery/crate-stack/index.html +++ b/docs/gallery/crate-stack/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,50 +325,52 @@

crate-stack

A stack of three procedural shipping crates built by one generator called three times with a per-instance seed, carried through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

+

Rendered headless by the showcase piece itself — click to zoom.

witnesses Recomputed: 4380 tris, two materials with 216 iron and 2436 timber faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.658×0.524×0.704 m, zmin 0, hygiene 0 including zero coplanar disjoint pairs, three ground runners at z=0, both crate-to-crate seats overlapping, each crate's own footprint 0.585×0.425 m measured in its own frame, yaw spread 0.026 rad and plank spread 2.05 mm across instances, LOD ratios in band, convex collider 202 tris, non-empty glTF. --float-stack exits 18 on the seat; --same-seed exits 20 on per-instance variation.
+

tags mesh export showcase

blender --background --python showcase/crate-stack/crate_stack.py --

A stack of three shipping crates. A showcase piece, not an example — it witnesses no API contract. It asserts that generated geometry meets declared asset budgets, recomputed from the finished mesh.

-

What it composes

+

What it composes#

One build_crate generator, invoked three times with a per-instance seed, then the shipped pipeline:

Shipped contentUsed for
skills/mesh-editing-and-bmeshbox, bevel and UV construction in one bmesh
skills/procedural-materials-and-shadersattribute-driven timber (per-plank tone, grain along each board) and rusted iron
skills/bake-high-to-lowCycles tangent-space normal bake, high onto low
skills/engine-export-presetsUnity glTF (export_yup=True)
skills/depsgraph-and-evaluated-dataevaluated triangle counts for the LOD ratios
snippets/decimate_to_budget.pyLOD1 / LOD2 COLLAPSE chain
snippets/convex_hull_collider.pyconvex collider
snippets/lod_chain.pyLOD naming and ratio pattern
examples/mesh-hygiene-audithygiene combinatorics (copied, not imported)
-

Reuse with variation

+

Reuse with variation#

The point of the piece. Each crate comes out of the same generator; what differs is the seed. That seed drives the yaw, the plank widths, and the board heights, so the three read as three of the same design rather than one model pasted three times.

True instancing — three objects sharing one mesh datablock — and per-instance geometry variation are mutually exclusive. This piece takes the variation, and the shipped asset is the flattened single mesh an engine would receive. The generator is reused; the geometry is not.

Two independent random streams keep that honest. The design stream (yaw, plank widths) is what --same-seed collapses. The placement stream (lateral offsets) stays per-instance either way, so a falsified stack has the same footprint as a good one and fails on the variation budget rather than on the bounding box.

-

Budgets

+

Budgets#

Declared in the script as named constants, recomputed from the generated mesh. Measured values below are from Blender 5.2.1; see the table at the end for the 4.5.11 / 5.1.2 figures.

BudgetBandMeasured
Base triangles5200–70006060
LOD1 ratio0.32–0.620.5000
LOD2 ratio0.10–0.350.2198
Material slotsexactly 2, distinct2
Iron faces≥ 9001344
Timber faces≥ 18002418
UV boundsinside 0..1(0.0006, 0.0007)–(0.9994, 0.9993)
UV AABB overlap≤ 1e-50.000000
Outer AABB0.658 × 0.524 × 0.704 m ± 0.0200.6583 × 0.5235 × 0.7040
Crate body footprint0.588 × 0.428 m ± 0.015, in the crate's own frame0.5882 × 0.4282 (all three)
Collider triangles≤ 260230
Normal bake{'FINISHED'} with image data{'FINISHED'}, has_data=True
glTF exportfile written, non-empty~490 kB
Hygieneall zeroloose 0/0, non-manifold 0, zero-area 0, doubles 0, n-gons 0, coplanar disjoint pairs 0
Grounded AABB\zmin\≤ 1e-40.00000
Ground runners3 named supports, each zmin ≤ 1e-33 at 0.00000
Crate-to-crate seatboth seats, surface gap ≤ 0.0015 m0.00000 (overlapping)
Yaw bandeach \yaw\in 0.040–0.192 rad0.0681, 0.0943, 0.1093
Per-instance variationyaw spread ≥ 0.020 rad, plank spread ≥ 0.0008 m0.0262 rad, 0.00205 m
Nail seat48 nails; bite 0.3–1.0 mm into the strap, head ≥ 1.0 mm proud48; bite 0.60 mm, proud 1.80 mm
Edge treatmentmanifold edges within 5° of a right angle: 0timber 0, iron 0

Real-world size: each crate is 0.58 × 0.42 m at the timber and 0.588 × 0.428 m over the corner iron and its nail heads, 0.244 m tall including runners and lid. Three stacked come to 0.70 m — knee height, wider than tall.

-

Why the footprint is measured in the crate's own frame

+

Why the footprint is measured in the crate's own frame#

The stack AABB is the union of three yawed boxes, so a crate could drift to any size underneath it and the outer budget would not notice. Each crate's footprint is therefore measured after un-rotating by the yaw that crate was measured to have, not the yaw it was built with. All three land on 0.5882 × 0.4282 m exactly, which is what makes the un-rotation trustworthy.

-

Why parts are classified by principal axes

+

Why parts are classified by principal axes#

Every part is an axis-aligned box in its crate's frame and then rotated about Z. A world-AABB shape filter measures the rotated bounding box, not the part: a 0.554 m board yawed 0.09 rad reports 0.061 m of depth instead of its 0.013 m thickness, and every filter keyed to thickness silently matches nothing. xy_principal recovers each box's own axes, and hands back the yaw as a by-product — which is where the variation budget's yaw numbers come from.

-

Why stack levels come from the runners

+

Why stack levels come from the runners#

Binning parts against the declared stack pitch put a lid — which sits 8 mm below the next crate's base — on the wrong level the moment a falsifier shifted a crate, and the seat budget then compared a crate against itself. The bands are clustered from the runner heights the mesh actually has, so they follow the geometry including when a falsifier moves it.

-

Why the corner iron is one shell

+

Why the corner iron is one shell#

Each strap used to be two overlapping boxes, one per leg of the L. Both boxes had a vertical edge on the strap's outer corner, and once the iron was chamfered each box laid a chamfer strip on that same line: a coplanar cross-shell pair at all twelve corners, which the z-fight budget caught. The strap is now a single L-section extrusion — each cap two convex quads meeting on the inner-corner diagonal, so no n-gon — with one outer corner. The iron bevel passes material=METAL_IDX; left to its default, bevel gave the chamfer faces slot 0 and the plates rendered and classified as timber.

-

Why nails are told from plates by the strap's size

+

Why nails are told from plates by the strap's size#

Nail and plate shells are both iron; the nail audit separates them by world-AABB extent. The threshold is IRON_WRAP * 0.5, derived from the strap. A fixed 10 mm threshold dropped 8 of 48 nails as soon as the heads grew to 10 mm, because a yawed head's world AABB is wider than the head.

-

Timber surface

+

Timber surface#

paint_planks writes two face attributes: PlankTone, a seeded tone per shell, and GrainDir, the shell's own long axis recovered from its vertices. The wood shader samples noise in object space with the component along GrainDir compressed, so the grain streaks run along each board whatever the crate's yaw, and scales the colour by the plank's tone. Neither attribute is a budget: a tone cannot be seen by an assertion, which is why the inspection sheet looked at it.

-

Determinism

+

Determinism#

Fixed seed 41; no unseeded randomness. Identical geometry across runs on one binary and across 4.5.11, 5.1.2 and 5.2.1 — every measured value above is byte-identical on the three. LOD2 used to differ — DECIMATE COLLAPSE produced 878 triangles on 5.2 and 934 on 4.5 and 5.1 on the earlier mesh — and on the current mesh lands on 1332 on all three. The LOD gate stays a ratio band (0.10–0.35) rather than an exact count, because that agreement is a property of this mesh, not of the modifier.

-

Falsifiers

+

Falsifiers#

Each breaks one pipeline stage so a named budget fails. All eight were run on 4.5.11, 5.1.2 and 5.2.1 and produced the same exit code on all three.

FlagBreaksExit
--skip-decimatedrops the DECIMATE modifiers, LOD1 ratio goes to 1.00009
--stray-vertadds one loose vertex inside the silhouette, so hygiene catches it rather than the bounding box15
--lift-zlifts the whole mesh 50 mm off the floor16
--short-skidsfloats one of the three ground runners 12 mm; the other two still ground the AABB, so only the named-support budget sees it16
--float-stacklifts the top crate 9 mm clear of the lid below, opening a 34 mm seat gap18
--same-seedgives all three crates the same design seed; yaw spread and plank spread both go to 020
--float-nailslifts every nail head 1.5 mm along its plate normal; bite goes to −0.90 mm, nail seat budget18
--sharp-ironskips the iron chamfer pass; 216 right-angle iron edges, edge-treatment budget21
-

Exit codes

+

Exit codes#

File-local and sequential. 9 is a valid check code; there is no rule against it. 1 is the FATAL wrapper — a crash, never a named check.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build, or has no UV layer
4Base triangle count outside band
5Material slots, or a material's face floor
6UVs outside 0..1
7UV AABB overlap above tolerance
8Outer AABB off declared size
9LOD1 or LOD2 ratio outside band (--skip-decimate)
10Framing gate (examples/gallery_framing.py, render path only)
11Collider triangles above ceiling
12Normal bake failed or produced no image data
13glTF export missing or empty
14--output produced no file
15Mesh hygiene (--stray-vert)
16Grounded zmin, or a named ground runner floating (--lift-z, --short-skids)
18Crate-to-crate seat gap (--float-stack), or a nail head off its strap seat band (--float-nails)
19Crate body footprint off declared size
20Per-instance variation collapsed (--same-seed)
21A manifold edge within 5° of a right angle: a chamfer pass skipped (--sharp-iron)

17 is unused here: this piece has no diagonal member. 15–19 are reserved across showcase pieces for the hygiene family, so the numbering skips rather than reuses.

-

Run it

+

Run it#

# Budget check, no render. ~1.4-1.7 s on 4.5 / 5.1 / 5.2.
 blender --background --python crate_stack.py --
 
@@ -324,16 +383,1767 @@ 

Run it

# Render the gallery still (EEVEE; --engine cycles on a GPU-less host). blender --background --python crate_stack.py -- --output stack.webp

Smoke runs the check-only path. It does not pass --output or any falsifier.

-

Cross-version measurements

+

Cross-version measurements#

Value4.5.115.1.25.2.1
Base triangles606060606060
LOD1 tris / ratio3030 / 0.50003030 / 0.50003030 / 0.5000
LOD2 tris / ratio1332 / 0.21981332 / 0.21981332 / 0.2198
Outer AABB0.6583 × 0.5235 × 0.7040samesame
Collider tris230230230
Yaws (rad)0.0943, −0.1093, 0.0681samesame
Seat gap0.000000.000000.00000
Nails / bite / proud48 / 0.60 mm / 1.80 mmsamesame
Right-angle edges000
Check wall-clock~1.59 s~1.41 s~1.65 s
-
+

Source

showcase/crate-stack/crate_stack.py - View on GitHub → + 1751 lines · View on GitHub →
-
"""Game-ready stack of three shipping crates — a showcase piece, not an example.
+      
+
"""Game-ready stack of three shipping crates — a showcase piece, not an example.
 
 Asserts budget conformance of a procedural crate *stack*: one crate
 generator invoked three times with a per-instance seed, each instance
@@ -2085,7 +3895,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -2105,20 +3949,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/cross-version-property-delete/index.html b/docs/gallery/cross-version-property-delete/index.html index f2a7a4eb..3c5eac1c 100644 --- a/docs/gallery/cross-version-property-delete/index.html +++ b/docs/gallery/cross-version-property-delete/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

cross-version-property-delete

Custom ID properties are removed with del, not property_unset. The IDs are built through bpy.data.objects.new so the check does not depend on active_object.

+

Rendered headless by the example itself — click to zoom.

witnesses Factory-empty has no active_object; property_unset raises TypeError and leaves the key; del removes it (clear_has=False). --skip-delete and --unset-instead exit 7.
+

tags context properties

blender --background --python examples/cross-version-property-delete/cross_version_property_delete.py --
@@ -283,25 +342,392 @@

cross-version-property-delete

What it witnesses: property_unset is a TypeError on a custom ID key and does not remove it. del id_block[key] does. After factory-empty, there is no active_object.

What failure each check would catch:

  • exit 2 — someone set an active object; the snippet __main__ would have been the only path
  • exit 3 — the ID property never landed
  • exit 4 — property_unset removed the key or failed to raise
  • exit 7 — del did not run (--skip-delete / --unset-instead falsify this: measured clear_has=True)
-

Staging

+

Staging#

The two tags now hang from the stand's bar on steel rods. The bar used to run 0.17 m behind the plates with nothing joining them, and the "rings" were solid discs, so the plates hung on air. The bar spans both plates, the post rises to it, and the base is as wide as what it carries. Render path only: the check reads ID properties, never geometry.

-

Run

+

Run#

blender --background --python cross_version_property_delete.py --
 blender --background --python cross_version_property_delete.py -- --output tags.png

--skip-delete and --unset-instead are falsification switches: both leave the Clear plate tagged and exit 7.

The --output render path measures framing against the Layer 1 band via examples/gallery_framing.py (exit 10 on violation) before writing the still.

-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage; also an active object existed after the data-API build
3Custom ID property did not land
4property_unset on a custom ID key did not TypeError, or it deleted the key
5del did not report removal
6Keep plate lost the ID property
7Clear plate still has the ID property (--skip-delete / --unset-instead land here)
10Gallery framing violation
12--output produced no file

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output, --skip-delete, or --unset-instead.

-
+

Source

examples/cross-version-property-delete/cross_version_property_delete.py - View on GitHub → + 367 lines · View on GitHub →
-
"""Custom ID-property delete — a runnable example.
+      
+
"""Custom ID-property delete — a runnable example.
 
 Witnesses the contract the snippet ``cross-version-property-delete`` teaches,
 not the snippet's ``__main__`` (which keys off ``context.active_object`` and
@@ -669,7 +1095,23 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -689,20 +1131,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/curve-bevel-arc/index.html b/docs/gallery/curve-bevel-arc/index.html index 73eaeb08..bc620057 100644 --- a/docs/gallery/curve-bevel-arc/index.html +++ b/docs/gallery/curve-bevel-arc/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

curve-bevel-arc

A beveled Bezier semicircle authored on bpy.types.Curve — splines.new('BEZIER'), bezier_points, bevel_depth, use_fill_caps — so the curve renders as a solid tube without a prior mesh conversion.

+

Rendered headless by the example itself — click to zoom.

witnesses Curve tubes are curve datablocks: eight Bezier points, bevel_depth == 0.15, filled caps, and the evaluated mesh has deterministic topology (1044 verts, 1028 faces) resting on the floor.
+

tags curves bevel

blender --background --python examples/curve-bevel-arc/curve_bevel_arc.py --
@@ -281,7 +340,7 @@

curve-bevel-arc

A runnable example that builds a beveled Bezier semicircle entirely through the curve data API — splines.new('BEZIER'), per-point bezier_points, bevel_depth, and use_fill_caps — so the curve renders as a solid tube without a prior mesh conversion.

What it witnesses: renderable curve tubes are curve datablocks, not meshes. The check asserts eight Bezier points, bevel_depth == 0.15, use_fill_caps, and that the depsgraph-evaluated mesh has the deterministic topology (1044 verts, 1028 faces for these resolution settings) with a Z span that rests on the floor ([0, 2 × bevel]) and an X span of 2 × radius + 2 × bevel.

Staging: the checked curve is presented as what a capped, beveled Bezier semicircle is in the world — a round-bar horseshoe magnet lying on the floor, open end to the camera. The two filled caps (use_fill_caps, the witness) are its ground-steel pole faces, turned face-on; an uncapped build would show two hollow pipe ends exactly there. The red enamel / bare-steel split is one material keyed on object-space Y, so the magnet stays a single curve datablock. The paper clips are beveled Bezier curves too (explicit FREE handles: straight legs plus quarter-circle bends), each lying along the field line that leaves its pole; the iron filings trace those lines — for two opposite line poles they are exact circular arcs through both pole faces. The cap's 12-sided outline is the pinned tessellation (bevel_resolution 4, locked by the 1044-vertex gate), not a presentation choice. The render path rotates the object only; the check reads object-space evaluated geometry and runs before any staging.

-

Run

+

Run#

# Cheap correctness check (no render) — the CI check:
 blender --background --python curve_bevel_arc.py --
 
@@ -291,18 +350,566 @@ 

Run

# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts): blender --background --python curve_bevel_arc.py -- --output arc.png blender --background --python curve_bevel_arc.py -- --output arc.png --engine cycles
-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is also the shared framing helper.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Spline type is not BEZIER
4Bezier point count ≠ 8
5bevel_depth ≠ 0.15
6use_fill_caps is False (--no-caps lands here)
7Evaluated vert/face count off measured tessellation
8Tube does not rest on the floor
9Tube height ≠ 2 × bevel
10X span off closed form; also gallery framing violation
11--output produced no file; also asset-quality floor violation (gallery_asset_quality)

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --no-caps.

-
+

Source

examples/curve-bevel-arc/curve_bevel_arc.py - View on GitHub → + 548 lines · View on GitHub →
-
"""Beveled Bezier arc via the curve data API — a runnable example.
+      
+
"""Beveled Bezier arc via the curve data API — a runnable example.
 
 Witnesses that renderable tubes are authored on `bpy.types.Curve` directly
 (`splines.new('BEZIER')`, `bezier_points`, `bevel_depth`, `use_fill_caps`) —
@@ -851,7 +1458,23 @@ 

Source

except Exception as e: import traceback; traceback.print_exc(); print(f"FATAL: {e}", file=sys.stderr); sys.exit(1)
+
+ +
@@ -871,20 +1494,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/custom-normals-shade/index.html b/docs/gallery/custom-normals-shade/index.html index 0d557c67..8f1aa591 100644 --- a/docs/gallery/custom-normals-shade/index.html +++ b/docs/gallery/custom-normals-shade/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

custom-normals-shade

A jerry can prop shaded three ways to prove the post-4.1 shading contract: hard edges are mesh data, landing exactly where the dihedral crosses. Face smooth flags plus a sharp_edge attribute, verified against an independently recomputed dihedral test, and per-loop custom normals surviving depsgraph evaluation within their int16 storage quantization (3.904e-05 over 8196 loops, not float-exact).

+

Rendered headless by the example itself — click to zoom.

witnesses use_auto_smooth, use_custom_normals and calc_normals are AttributeError on BOTH 4.5 LTS and 5.1 - AI code still emits them. The legacy shade_auto_smooth operator CANCELS headless on 4.5 (asset load never finishes; mesh untouched; no exception) while 5.1 FINISHES with the Smooth by Angle NODES modifier. By-angle sharp sets match the independent dihedral recompute exactly (1304 of 4738 edges over 3 meshes).
+

tags mesh attributes

blender --background --python examples/custom-normals-shade/custom_normals_shade.py --
@@ -286,7 +345,7 @@

custom-normals-shade

What each check catches on failure: author/audit threshold drift (probe: sharp marks applied at 20° but audited at 30°, exit 5, 48 extra edges on the shell); the two halves of the contract out of sync (probe: sharp_edge set but face smooth flags lost, exit 6, smooth-edge loops split by 3.902e-01); float-exactness assumed of custom normals (probe: tolerance 1e-6, exit 7, measured 3.904e-05); and any future version that resurrects the legacy API or changes the operator's headless behavior (exit 3/8). The render path adds the Layer 1 gates: framing (exit 10) and asset quality (exit 11).

Version witness: every value above is identical on Blender 4.5.11 LTS, 5.1.2 and 5.2.1 LTS except the shade_auto_smooth operator behavior, which is asserted per version (CANCELLED + untouched on 4.5, FINISHED + NODES modifier on 5.1 and 5.2).

The render shows three fresh builds of the same can, one per shading treatment. Left, flat: the side-band corners, cap and handle break into facets. Middle, smooth-everywhere (the shade-smooth-and-forget AI habit): the flat fields smear into gradients and the pressed panels go pillowy. Right, by-angle (the contract): crisp creases and smooth rounds, with the sharp_edge attribute read back from the mesh and traced as thin cyan lines, so the viewer sees exactly which edges the data marked hard. A low strip light makes the highlight shapes diverge between the three.

-

Run

+

Run#

# Cheap correctness check (no render) — the CI check:
 blender --background --python custom_normals_shade.py --
 
@@ -296,18 +355,971 @@ 

Run

# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts): blender --background --python custom_normals_shade.py -- --output cans.png blender --background --python custom_normals_shade.py -- --output cans.png --engine cycles
-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Legacy shading API present, or modern path missing
4Non-manifold edges (dihedral test undefined)
5Sharp set ≠ independent dihedral (--mismatch-angle lands here)
6Evaluated loop normals not welded/split as the sharp set promises
7Custom split normals lost or dequantized in evaluation
8shade_auto_smooth operator behavior drifted from the version split
9--output produced no file
10Render path: Layer 1 framing violation (gallery_framing)
11Render path: asset-quality floor violation (gallery_asset_quality)

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --mismatch-angle.

-
+

Source

examples/custom-normals-shade/custom_normals_shade.py - View on GitHub → + 953 lines · View on GitHub →
-
"""A jerry can prop shaded three ways — a runnable example.
+      
+
"""A jerry can prop shaded three ways — a runnable example.
 
 Witnesses the shading contract a game prop's silhouette depends on (engines
 generally, FiveM/GTA-style prop workflows specifically): which edges read
@@ -1261,7 +2273,41 @@ 

Source

except Exception as e: import traceback; traceback.print_exc(); print(f"FATAL: {e}", file=sys.stderr); sys.exit(1)
+
+ +
@@ -1281,20 +2327,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/damped-track-aim/index.html b/docs/gallery/damped-track-aim/index.html index 337783e9..811f1b18 100644 --- a/docs/gallery/damped-track-aim/index.html +++ b/docs/gallery/damped-track-aim/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

damped-track-aim

Aim constraints via the data API — Object.constraints.new('DAMPED_TRACK') with target and TRACK_Z, not bpy.ops.object.constraint_add in a headless loop. Gallery still: brass spike cage around an ember core.

+

Rendered headless by the example itself — click to zoom.

witnesses Every needle has one unmuted DAMPED_TRACK on the core; evaluated local +Z aligns toward the core (dot ≥ 0.998). TRACK_TO stand-ins and flipped axes fail.
+

tags constraints animation

blender --background --python examples/damped-track-aim/damped_track_aim.py --
@@ -280,9 +339,9 @@

damped-track-aim

A runnable example that aims twelve brass spikes at an ember core with Object.constraints.new('DAMPED_TRACK') — the data-API path, not bpy.ops.object.constraint_add (which needs an active object and fails in headless loops). Damped Track is the twist-stable aim constraint: it points one local axis at a target without the roll fights Track To is known for.

What it witnesses: every spike carries exactly one unmuted DAMPED_TRACK bound to the core on TRACK_Z. After a depsgraph update, each evaluated local +Z aligns with the world vector toward the core (dot ≥ 0.998 ≈ 3.6°). A missing constraint, a muted one, a TRACK_TO stand-in, or a flipped axis fails the check.

-

Framing deviation

+

Framing deviation#

Radiating composition — the twelve spikes read as converging on the core from beyond the frame, so their tails bleed past the left, right, and bottom edges by design (measured fill 1.000x/0.917y with edge touch on three sides). If wired to examples/gallery_framing.py, call it with deviation="radiating composition; spikes read as extending past frame".

-

Run

+

Run#

# Cheap correctness check (no render) — the CI check:
 blender --background --python damped_track_aim.py --
 
@@ -292,18 +351,432 @@ 

Run

# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts): blender --background --python damped_track_aim.py -- --output aim.png blender --background --python damped_track_aim.py -- --output aim.png --engine cycles
-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Needle count ≠ 12
4Needle does not carry exactly one DAMPED_TRACK
5Constraint target is not Core
6track_axis is not TRACK_Z
7Constraint muted or influence < 1 (--mute lands here)
8TRACK_TO still present
9Evaluated aim dot below 0.998
10--output produced no file

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --mute.

-
+

Source

examples/damped-track-aim/damped_track_aim.py - View on GitHub → + 414 lines · View on GitHub →
-
"""Damped Track needles aiming at an emissive core — a runnable example.
+      
+
"""Damped Track needles aiming at an emissive core — a runnable example.
 
 Witnesses that aim constraints are authored on the data API
 (`Object.constraints.new('DAMPED_TRACK')`, `target`, `track_axis`) — not via
@@ -718,7 +1191,32 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -738,20 +1236,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/degenerate-bevel-weld/index.html b/docs/gallery/degenerate-bevel-weld/index.html index d6d3104b..d8356726 100644 --- a/docs/gallery/degenerate-bevel-weld/index.html +++ b/docs/gallery/degenerate-bevel-weld/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

degenerate-bevel-weld

Bevel offset >= half the min box dimension collapses the band into zero-area faces — and they ship: a stdlib GLB re-parse counts the degenerate triangles crossing the export boundary. Two rugged cases whose shells are the check's meshes: flat end panel versus a rolled knife ridge, the collapsed seam traced hot from live mesh data.

+

Rendered headless by the example itself — click to zoom.

witnesses At offset == min_dimension/2 the bevel band pinches to exactly 12 zero-area faces (4 edges x 3 segments); 32 degenerate triangles cross into the exported GLB, waiting for any merge-by-distance to weld them.
+

tags bmesh mesh game-pipeline

blender --background --python examples/degenerate-bevel-weld/degenerate_bevel_weld.py --
@@ -285,23 +344,622 @@

degenerate-bevel-weld

What each check catches on failure: safe offset pushed to the threshold (exit 3); degenerate offset backed below it (exit 4); coincident closed form corrupted (exit 6); GLB degenerate-tri expectation off (exit 8, e.g. if a future Blender changes triangulation — re-measure and update deliberately).

Version witness: output is byte-identical on Blender 4.5.11 LTS and 5.1.2 — same counts, same min_area values, same GLB triangle census.

Render as proof: two rugged hard-shell equipment cases whose shells are the check's two meshes — beveled_box(DIMS, 0.10) and beveled_box(DIMS, 0.20) — with every fitting (handle, latches, molded frame, purge valve, ID plate, feet) kept inside the flat front land that survives both offsets, so the bevel is the only difference. The cases turn their end panels to the camera: the left keeps a flat end framed by clean chamfer bands; on the right the end and top lands are gone and the band rolls into a knife ridge at mid-depth. That collapsed seam is traced in hot red from live mesh data (every edge of every face thinner than 1e-6), with an orange bead on each of the 12 zero-area faces the check counts — change the offset and the overlay moves or disappears. The clean case is the designed asset and passes the asset-quality floors (exit 11).

-

Run

+

Run#

blender --background --python degenerate_bevel_weld.py --
 blender --background --python degenerate_bevel_weld.py -- --both-safe
 blender --background --python degenerate_bevel_weld.py -- --output bevel.png
 blender --background --python degenerate_bevel_weld.py -- --output bevel.png --engine cycles
-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper, 11 the shared asset-quality helper.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Safe bevel produced zero-area faces
4Degenerate bevel zero-area count ≠ closed form (--both-safe lands here)
5min_area collapse under 1e5×
6Coincident-position count off the closed form
7Safe GLB carries degenerate triangles
8Degenerate GLB triangle or position count drifted
9--output produced no file
10Gallery framing violation
11Asset-quality floor violation (render path only)

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --both-safe.

-
+

Source

examples/degenerate-bevel-weld/degenerate_bevel_weld.py - View on GitHub → + 599 lines · View on GitHub →
-
"""Degenerate bevel weld — the half-dimension collapse that ships to disk.
+      
+
"""Degenerate bevel weld — the half-dimension collapse that ships to disk.
 
 Witnesses the bevel-threshold hazard a prop pipeline hits when a width is
 authored against one box and later used on a thinner one: `bmesh.ops.bevel`
@@ -901,7 +1559,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -921,20 +1613,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/depsgraph-export/index.html b/docs/gallery/depsgraph-export/index.html index 8c5c77d8..c2178ba8 100644 --- a/docs/gallery/depsgraph-export/index.html +++ b/docs/gallery/depsgraph-export/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

depsgraph-export

The depsgraph lifetime contract — evaluated_get().to_mesh() paired with to_mesh_clear() — measured against an OBJ export of the same object.

+

Rendered headless by the example itself — click to zoom.

witnesses Exports ship evaluated geometry: the controller shell subdivides to exactly its Catmull-Clark closed form, and the exported vertex count equals the subsurf-applied count, strictly greater than the base mesh.
+

tags depsgraph export

blender --background --python examples/depsgraph-export/depsgraph_export.py --
@@ -281,10 +340,10 @@

depsgraph-export

A runnable example that proves modifiers actually ship in exports and demonstrates the depsgraph-and-evaluated-data lifetime contract. It builds a game controller whose shell is a sparse quad control cage (90 vertices) under a level-2 SUBSURF modifier, with the sticks, d-pad, face buttons and bumpers modeled as ordinary parts parented to it. It measures every mesh via evaluated_get().to_mesh() (each paired with to_mesh_clear()), exports the scene through wm.obj_export, and asserts:

  • the evaluated shell has exactly the vertex count the Catmull-Clark closed form predicts from the cage's own topology — per level V' = V + E + F, E' = 2E + S, F' = S (S = face corners) — which for this cage (V=90, E=176, F=88, S=352) is 1,410;
  • the exported OBJ vertex count equals the summed evaluated counts of every mesh (3,666), not the summed base counts (2,346).

What it witnesses: the evaluated_get → to_mesh → to_mesh_clear contract, and that wm.obj_export writes the depsgraph-evaluated geometry (so modifiers are baked into the export) rather than the unmodified base mesh.

-

The render

+

The render#

Left: the shell datablock as the .blend stores it — its 90-vertex control cage drawn as orange wire with a bead on every vertex, over faint blue facets. Right: the same object as the depsgraph evaluates it and the OBJ contains it — the smooth subdivided cobalt controller with its controls. If the export shipped the base mesh, the right-hand piece would be the blocky cage on the left.

Both pieces lean toward the camera on low satin display stands (render-only, added after the check and export have run).

-

Run

+

Run#

# Cheap correctness check (writes an OBJ to a temp path, asserts the counts) — the CI check:
 blender --background --python depsgraph_export.py --
 
@@ -297,20 +356,634 @@ 

Run

# Write the exported OBJ to a specific path: blender --background --python depsgraph_export.py -- --obj exported.obj
-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 and 11 are the shared framing and asset-quality helpers.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Evaluated shell did not apply the modifier (evaluated count not above the cage)
4No OBJ written
5Export vert count ≠ evaluated (--unevaluated lands here)
6--output produced no file
7Evaluated shell count ≠ Catmull-Clark closed form
10Gallery framing violation
11Asset-quality floor violation (render path only)

--obj is a path selector, not a falsifier.

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output, --obj, or --unevaluated.

The --output render path additionally measures framing against the Layer 1 band via examples/gallery_framing.py (exit 10) and the asset-quality floors on the shipped controller via examples/gallery_asset_quality.py (exit 11) before writing the still.

-
+

Source

examples/depsgraph-export/depsgraph_export.py - View on GitHub → + 614 lines · View on GitHub →
-
"""Depsgraph-evaluated export — a runnable example.
+      
+
"""Depsgraph-evaluated export — a runnable example.
 
 Witnesses the depsgraph lifetime contract AND that modifiers actually ship in
 exports. Builds a game controller whose shell is a sparse quad control cage
@@ -925,7 +1598,41 @@ 

Source

except Exception as e: import traceback; traceback.print_exc(); print(f"FATAL: {e}", file=sys.stderr); sys.exit(1)
+
+ +
@@ -945,20 +1652,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/driver-wave/index.html b/docs/gallery/driver-wave/index.html index ec1dd1b5..fa50a8e3 100644 --- a/docs/gallery/driver-wave/index.html +++ b/docs/gallery/driver-wave/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

driver-wave

A driver_namespace function driving sixteen column heights through SCRIPTED drivers — the sine skyline is entirely driver-evaluated.

+

Rendered headless by the example itself — click to zoom.

witnesses Driven values appear after a view-layer update in two places that must agree: the evaluated copy and the original datablock the animation system flushes for display.
+

tags drivers animation

blender --background --python examples/driver-wave/driver_wave.py --
@@ -281,9 +340,9 @@

driver-wave

A runnable example that drives sixteen column heights from a custom function registered in bpy.app.driver_namespace — the pattern from drivers-and-app-handlers. Each column gets a SCRIPTED driver on Z scale whose expression calls wave_scale(i), producing a sine skyline.

What it witnesses: the driver evaluation contract. Driven values appear only after a view-layer update, and they land in two places that must agree: the depsgraph-evaluated copy (evaluated_get(dg).scale) and the original datablock, which the animation system flushes for display. The check asserts both against the closed-form profile.

Note for real add-ons: driver_namespace entries do not persist in .blend files — re-register them from a load_post handler, or every driver that calls them fails on file open. Headless, registering before driver creation (as here) is enough.

-

Staging

+

Staging#

The camera is raised to look down about 18 degrees, so the column tops and their shadows read as a wave. From nearly level (86 degrees) the skyline flattened into a bar chart. Render path only.

-

Run

+

Run#

# Cheap correctness check (no render) — the CI check:
 blender --background --python driver_wave.py --
 
@@ -293,18 +352,241 @@ 

Run

# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts): blender --background --python driver_wave.py -- --output driver.png blender --background --python driver_wave.py -- --output driver.png --engine cycles
-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Evaluated Z scale ≠ wave_scale (--flat-expr lands here)
4Original datablock was not flushed
6--output produced no file
10Gallery framing violation

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --flat-expr.

-
+

Source

examples/driver-wave/driver_wave.py - View on GitHub → + 223 lines · View on GitHub →
-
"""Driver-namespace scale drivers, evaluated through the depsgraph — a runnable example.
+      
+
"""Driver-namespace scale drivers, evaluated through the depsgraph — a runnable example.
 
 Witnesses the drivers-and-app-handlers contract end to end: a custom function
 is registered in `bpy.app.driver_namespace`, sixteen columns get a SCRIPTED
@@ -528,7 +810,32 @@ 

Source

except Exception as e: import traceback; traceback.print_exc(); print(f"FATAL: {e}", file=sys.stderr); sys.exit(1)
+
+ +
@@ -548,20 +855,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/export-preset-axis/index.html b/docs/gallery/export-preset-axis/index.html index 572ab86a..05669891 100644 --- a/docs/gallery/export-preset-axis/index.html +++ b/docs/gallery/export-preset-axis/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

export-preset-axis

A radio mast exported under Unity and Godot glTF presets and re-imported, proving the two files have different vertex orientation

+

Rendered headless by the example itself — click to zoom.

witnesses Unity export_yup=True round-trips standing (x,y,z); Godot export_yup=False reimports lying along -Y as (x,-z,y); --same-axis exits 9; exporter RNA is guarded so a future kwarg rename fails loudly
+

tags export

blender --background --python examples/export-preset-axis/export_preset_axis.py --
@@ -284,7 +343,7 @@

export-preset-axis

Neighbor of gltf-export-roundtrip (Y-up bake vs Z-up on disk for one file) and unapplied-scale-gltf (export_apply is modifiers, not object scale). This example names the Unity vs Godot presets and asserts the re-imported orientations diverge.

The subject is a radio mast chosen because "up" is unmistakable on it: a stepped concrete footing, a bolted base flange, a tapered mast in red and white aviation bands with steel collars, three sector panel antennas, a shrouded microwave dish on a raked arm, an equipment cabinet, and a red obstruction lamp under a lightning rod. The rod's point is the witnessed tip vertex.

The still stages the two re-imports side by side: the Unity copy standing on the left, the Godot copy lying along the floor on the right. Beside each sits a modelled X/Y/Z axis gizmo (red, green, blue) showing that re-import's frame. The gizmo is measured, not placed: the render path tries all 24 axis-aligned rotations and keeps the one that maps the source vertices onto the re-imported vertices (worst nearest-vertex distance, printed as fit_err; exit 12 if it exceeds 1 mm). The Unity gizmo comes out as the identity, blue Z up. The Godot gizmo comes out as Y -> +Z, Z -> -Y: green Y points up and blue Z runs along the lying mast. The mast didn't fall over. The Godot file stores Z-up coordinates, and the importer reads them as Y-up. If export_yup were the same on both, the two copies and both gizmos would match.

-

Run

+

Run#

# Cheap correctness check (no render) - the CI check:
 blender --background --python export_preset_axis.py --
 
@@ -294,18 +353,776 @@ 

Run

# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts): blender --background --python export_preset_axis.py -- --output mast.png blender --background --python export_preset_axis.py -- --output mast.png --engine cycles
-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper and 11 on the render path is the shared asset-quality helper.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage; also exporter RNA missing expected glTF kwargs
3Source mast is not Z-dominant, or tip drifted
4glTF reimport produced no mesh
5Unity disk POSITION is not (x, z, -y), or Unity node has rotation
6Godot disk POSITION is not raw Z-up; also --output produced no file
7Unity reimport is not standing
8Godot reimport is not lying along Y, or reimported tip mismatch
9Reimported orientations did not differ (--same-axis lands here)
10Gallery framing violation
11--same-axis did not collapse the axis difference; also asset-quality floor violation (render path)
12Render path: no axis-aligned rotation maps the source onto a re-import, so the gizmo frame could not be measured

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --same-axis.

-
+

Source

examples/export-preset-axis/export_preset_axis.py - View on GitHub → + 758 lines · View on GitHub →
-
"""Unity vs Godot glTF presets: a runnable example.
+      
+
"""Unity vs Godot glTF presets: a runnable example.
 
 Same source mesh, two presets. Unity is Y-up (`export_yup=True`). Godot in this
 repo is Z-up glTF (`export_yup=False`). Re-importing each file through Blender's
@@ -1064,7 +1881,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -1084,20 +1935,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/fence-kit/index.html b/docs/gallery/fence-kit/index.html index f64be0cc..25f84280 100644 --- a/docs/gallery/fence-kit/index.html +++ b/docs/gallery/fence-kit/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

fence-kit

A procedural post-and-rail fence section that fits its 1.60 m tile, with one-shell iron bands and a board brace housed in both posts, through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

+

Rendered headless by the showcase piece itself — click to zoom.

witnesses Recomputed: 1012 tris, two materials with 116 metal and 390 wood faces, UVs in 0..1 with zero AABB overlap, outer AABB 1.596×0.134×1.232 m inside the 1.60 m tile, grounded zmin, 2 iron shoes, brace housed 12 mm in both posts and bearing 2 mm on the middle rail, rail-to-post gap 0.93 mm, rail span 1.362 m, LOD ratios in band (5.2 COLLAPSE leaner on LOD2), convex collider 24 tris, hygiene all zero, non-empty glTF. --skip-decimate exits 9; --stray-vert 15; --lift-z/--short-shoes 16; --short-brace 17; --gap-rails 18; --long-rails 19; --float-brace 20; --wide-tile 21.
+

tags mesh export showcase

blender --background --python showcase/fence-kit/fence_kit.py --
@@ -283,22 +342,22 @@

fence-kit

It asserts budget conformance of the generated result. It does not witness an API contract. "It rendered without error" is not a check. This piece does not re-witness the modular-kit-snap snap contract.

Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package). Hygiene combinatorics match examples/mesh-hygiene-audit (copied, not imported).

Intended size: 1.60 m tile, 1.14 m posts, 0.11 m post stock. Outer AABB 1.596 × 0.134 × 1.232 m.

-

Budgets

+

Budgets#

Declared as named constants. Every gate recomputes its value from the mesh, materials, UVs, evaluated LOD, collider or export file.

AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles850–12001012 / 1012 / 1012
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.5000
LOD2 ratio0.10–0.35 of base0.2194 / 0.2194 / 0.2174
Materialsexactly 2 distinct, ≥48 metal, ≥48 wood2 slots, 116 metal / 390 wood
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(1.596, 0.134, 1.232) m ± 0.015(1.5960, 0.1340, 1.2320), zmin 0
Collider tris≤ 18024
Exportwritten, size > 075900 / 75900 / 75888 bytes

Base triangles dropped from 1092 to 1012 in the second quality pass. Each band became one mitred shell instead of three or four separate plates, and the brace became a six-face board. The band was re-fitted around the measurement and narrowed from 1000–1400.

DECIMATE COLLAPSE triangle counts are not identical across series: 5.2.1 is more aggressive on LOD2. The gate is a ratio band, not an exact count. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Construction is closed-form and plank tones are seeded, so repeated runs on one binary print identical measurements. Export byte counts differ by 12 B on 5.2.1 (glTF serializer), not a gated axis.

-

Hygiene

+

Hygiene#

Recomputed from the generated mesh, not asserted about the script.

AxisDeclaredMeasured (all three)
Non-manifold edges00
Loose verts / edges0 / 00 / 0
Doubles merged at 1e-500
Zero-area faces00
N-gons00
Coplanar disjoint face pairs00
Grounded: zminwithin 1e-4 of 00.0000
Named shoes2, each zmin ≤ 0.0022, 0.00000
-

Joint fit, bearing and tiling

+

Joint fit, bearing and tiling#

AxisDeclaredMeasured (all three)
Brace-to-post BVH gap (both posts)≤ 0.008 m0.00531
Rail-to-post BVH gap≤ 0.008 m0.00093
Rail span vs 2·RAIL_HALF± 0.04 m1.3620 vs 1.362
Brace housing (exit 20): deepest brace vertex inside each post≥ 0.004 m0.01200
Brace bearing (exit 20): brace back plane past the middle rail's front plane, where their heights overlap≥ 0.001 m0.00200
Tile fit (exit 21): section width along the tiling axis1.590–1.600 m1.5960

The brace bearing is measured plane to plane. The brace and the middle rail cross mid-span, and neither has a vertex there, so a vertex-depth metric reports a false gap of 0.15 m.

-

Falsifiers

+

Falsifiers#

Each violates one named budget. All were run on 4.5.11, 5.1.2 and 5.2.1 and returned the same code on each.

FlagBudget violatedExit
--skip-decimateLOD1 ratio band9
--stray-vertloose vertex count is 015
--lift-zbounding box zmin is 016
--short-shoesnamed shoes plant at z=016
--short-bracebrace-to-post BVH gap (both posts)17
--gap-railsrail-to-post BVH gap18
--long-railsrail span vs 2·RAIL_HALF19
--float-bracebrace bearing on the middle rail (brace 6 mm forward; measured −0.00400)20
--wide-tiletile fit (the first build's post stations; measured 1.6240)21

--short-shoes lifts only the iron shoes. After recentring, a post or the kick is the AABB zmin, so the AABB gate would still pass. The named-shoe stations fail. --short-brace shortens the left end; brace_gap is the maximum of the per-post minima, so a brace that still reaches the right post cannot sneak through. --long-rails adds an extra spanning board, and the tenoned rails stay put, so the gap budget still passes. --wide-tile moves the posts out to where the first build had them and moves the rails with them. It is checked before the AABB gate, which it would also fail.

-

Run

+

Run#

blender --background --python fence_kit.py --
 blender --background --python fence_kit.py -- --skip-decimate
 blender --background --python fence_kit.py -- --stray-vert
@@ -311,17 +370,1398 @@ 

Run

blender --background --python fence_kit.py -- --wide-tile blender --background --python fence_kit.py -- --output fence.png

Smoke passes no flags.

-

Exit codes

+

Exit codes#

File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path. 15–19 are the hygiene and joint-fit family. 20 and 21 are this piece's own.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 2 distinct slots, or wood/metal faces missing
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Hygiene (--stray-vert lands here)
16Grounded zmin / named shoes (--lift-z, --short-shoes)
17Brace-to-post gap (--short-brace)
18Rail-to-post gap (--gap-rails)
19Rail span (--long-rails)
20Brace not housed in both posts or not bearing on the middle rail (--float-brace)
21Section does not fit its tile (--wide-tile)
-
+

Source

showcase/fence-kit/fence_kit.py - View on GitHub → + 1381 lines · View on GitHub →
-
"""Game-ready post-and-rail fence kit — a showcase piece, not an example.
+      
+
"""Game-ready post-and-rail fence kit — a showcase piece, not an example.
 
 Asserts budget conformance of a tiling timber fence section after
 composing shipped pipeline pieces: bmesh construction, UVs, two
@@ -1703,7 +3143,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -1723,20 +3197,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/gltf-export-roundtrip/index.html b/docs/gallery/gltf-export-roundtrip/index.html index 1efafb09..82f09013 100644 --- a/docs/gallery/gltf-export-roundtrip/index.html +++ b/docs/gallery/gltf-export-roundtrip/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

gltf-export-roundtrip

A sci-fi supply crate exported to glTF and re-imported, verifying the round-trip against the depsgraph-evaluated mesh within float tolerances. Positions, loop normals, box-mapped UVs, and per-triangle material bindings must all survive; the on-disk JSON proves the +Y-up conversion and the V-flipped UV layout.

+

Rendered headless by the example itself — click to zoom.

witnesses export_yup bakes (x, y, z) -> (x, z, -y) into the vertex data with no node rotation (probed identical on 4.5.11 and 5.1.2); export_apply ships the evaluated mesh, not the base cage; exporter/importer RNA is guarded so a future kwarg rename fails loudly.
+

tags export uv

blender --background --python examples/gltf-export-roundtrip/gltf_export_roundtrip.py --
@@ -287,7 +346,7 @@

gltf-export-roundtrip

The render shows one crate. The solid crate is the authored one, rebuilt in place after the check. Laid over it is the actual re-imported mesh — the file's own triangles — drawn as a thin amber wire cage (a render-only Weld + Wireframe display stack on the imported object) and masked to the right of a vertical cut, so the left half reads as the authored asset and the right half as the file's geometry sitting on it. Neither object is moved: both keep the identity transform check 13 proved, and the camera does the turning. The cage lands on every bevel, rivet and vent slat only because the importer undid the +Y-up bake exactly and the file carried the evaluated (beveled) mesh. If the axis conversion broke, the cage would be rotated a quarter turn off the solid crate; a probe that re-applies that quarter turn to the imported object before rendering drops the cage through the floor and also fails the framing gate (bottom margin 0.000). If the modifier contract broke, the cage would trace square corners across the rounded ones.

Presentation only, after every check has run: the authored crate's bevel is baked into its display mesh (the asset-quality edge measure reads Object.data, which is still the square cage while the bevel lives in the modifier stack), and its paint and trim get a noise-mottled base colour and roughness. The exported file only ever carried the flat Principled values.

The render path gates framing through gallery_framing.check_framing and the asset floors through gallery_asset_quality.check_asset_quality on the authored crate. Both helpers' codes (10, 11) are already check codes here, so the call site remaps them to 22 and 23.

-

Run

+

Run#

# Cheap correctness check (no render) — the CI check:
 blender --background --python gltf_export_roundtrip.py --
 
@@ -297,18 +356,758 @@ 

Run

# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts): blender --background --python gltf_export_roundtrip.py -- --output crate.png blender --background --python gltf_export_roundtrip.py -- --output crate.png --engine cycles
-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Exporter/importer RNA kwargs drifted
4Base cage drifted from its closed form
5Authored UVs drifted from the box-map closed form
6Bevel produced no evaluated geometry
7On-disk node/mesh/generator contract drifted
8On-disk primitive/material binding count drifted
9On-disk POSITION bounds ≠ axis-converted bbox (--no-yup lands here)
10On-disk POSITION count ≠ evaluated loop count
11On-disk UV V-flip failed
12Re-import did not produce exactly one mesh
13Re-imported object carries a transform
14Material names drifted on re-import
15Re-import vert count ≠ evaluated loop count
16Round-trip position drift
17Round-trip normal drift
18Round-trip UV drift
19Re-import triangle count drifted
20Per-triangle material bindings drifted
21--output produced no file
22Gallery framing violation (render path only; the helper's 10, remapped)
23Gallery asset-quality violation (render path only; the helper's 11, remapped)

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --no-yup.

-
+

Source

examples/gltf-export-roundtrip/gltf_export_roundtrip.py - View on GitHub → + 740 lines · View on GitHub →
-
"""A sci-fi supply crate round-tripped through glTF export/import — a runnable example.
+      
+
"""A sci-fi supply crate round-tripped through glTF export/import — a runnable example.
 
 Witnesses the contracts of the game-asset interchange path that AI-generated
 code most often gets silently wrong:
@@ -1049,7 +1848,41 @@ 

Source

except Exception as e: import traceback; traceback.print_exc(); print(f"FATAL: {e}", file=sys.stderr); sys.exit(1)
+
+ +
@@ -1069,20 +1902,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/gltf-skin-roundtrip/index.html b/docs/gallery/gltf-skin-roundtrip/index.html index 5682760a..298f9b65 100644 --- a/docs/gallery/gltf-skin-roundtrip/index.html +++ b/docs/gallery/gltf-skin-roundtrip/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

gltf-skin-roundtrip

A rigged mech scorpion exported to glTF with skins and re-imported, verifying the skinning contract the geometry round-trip left uncovered. Skeleton, weights, and deformation must all survive the format.

+

Rendered headless by the example itself — click to zoom.

witnesses skins[0].joints names all 21 bones; JOINTS_0/WEIGHTS_0 per primitive with unit sums; every split copy's weights bit-exact on re-import; deformation matches within 6.0e-07. The mesh must be parented to the armature or the exporter binds by name.
+

tags export armature

blender --background --python examples/gltf-skin-roundtrip/gltf_skin_roundtrip.py --
@@ -287,7 +346,7 @@

gltf-skin-roundtrip

Version witness: the skins pipeline is stable across Blender 4.5 LTS, 5.1 and 5.2 LTS — every measured value matches to the digit on 4.5.11, 5.1.2 and 5.2.1.

The render is a standoff. On the left, the authored scorpion holds the guard pose the check compares: tail coiled tight over its back, claws tucked, pincers shut. On the right is the actual re-imported mesh, driven through its imported armature into a different pose: the tail rears high to strike, the claws are raised and the pincers gape. A file that lost the skin, the joints, or the weights could not follow that pose. The rubber boots at the joints bend smoothly and the plates stay rigid, which is the linear blend skinning surviving the format. The two poses differ on purpose, so the eye has something to compare. Two identical copies would render the same whether or not the round-trip worked.

Both rigs are posed by rest-space axes in quaternions, and the yaw is composed into each armature's world matrix, because the glTF importer leaves its armature in quaternion mode, where rotation_euler does nothing. Framing is measured by gallery_framing (fill 0.856 x, minimum margin 0.072). The asset floors are measured by gallery_asset_quality (5 materials: hazard paint, gunmetal, chrome, rubber, venom glow; edge90 0.001).

-

Run

+

Run#

# Cheap correctness check (no render) — the CI check:
 blender --background --python gltf_skin_roundtrip.py --
 
@@ -297,19 +356,993 @@ 

Run

# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts): blender --background --python gltf_skin_roundtrip.py -- --output scorp.png blender --background --python gltf_skin_roundtrip.py -- --output scorp.png --engine cycles
-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Exporter RNA missing expected kwargs
4Vertex group count ≠ bone count
5Disk skins ≠ 1 (--no-skins lands here)
6Skin joints ≠ bone names
7Missing JOINTS_0/WEIGHTS_0, or accessor length mismatch
8Disk weight sums off 1.0
9Disk verts exceed evaluated loops
10Armature count after import ≠ 1
11Bone count drifted
12Named bone lost
13Bone parent drifted
14Rest matrices drifted
15Skinned mesh count after import ≠ 1
16Re-import vert count ≠ disk
17Vertex group names drifted
18Weight round-trip drifted
19Deformation round-trip drifted
20--output produced no file

On the --output path only, which runs after every check has passed, 10 also means a gallery_framing violation and 11 a gallery_asset_quality floor violation. The shared helpers own those codes, and the log line says which one fired.

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --no-skins.

-
+

Source

examples/gltf-skin-roundtrip/gltf_skin_roundtrip.py - View on GitHub → + 974 lines · View on GitHub →
-
"""A rigged mech scorpion round-tripped through glTF skins — a runnable example.
+      
+
"""A rigged mech scorpion round-tripped through glTF skins — a runnable example.
 
 Witnesses the skinned-mesh export contract that gltf-export-roundtrip left
 uncovered: bones, hierarchy, weights, and deformation must all survive the
@@ -1284,7 +2317,41 @@ 

Source

except Exception as e: import traceback; traceback.print_exc(); print(f"FATAL: {e}", file=sys.stderr); sys.exit(1)
+
+ +
@@ -1304,20 +2371,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/gn-instance-grid/index.html b/docs/gallery/gn-instance-grid/index.html index 693a77ec..d9cd73c9 100644 --- a/docs/gallery/gn-instance-grid/index.html +++ b/docs/gallery/gn-instance-grid/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

gn-instance-grid

A generative Geometry Nodes tree — Mesh Grid → Instance on Points (a modeled keycap via Object Info) → Realize Instances → Set Shade Smooth → Set Material — attached as a NODES modifier with no Group Input geometry.

+

Rendered headless by the example itself — click to zoom.

witnesses Realized instances produce closed-form topology (9 keycaps × 121 = 1089 verts, 1089 faces), Set Material carries the keycap plastic, a position-field Set Material lands the accent on exactly one keycap, and the corner keycap sits at its closed-form grid coordinate.
+

tags geometry-nodes instancing

blender --background --python examples/gn-instance-grid/gn_instance_grid.py --
@@ -281,7 +340,7 @@

gn-instance-grid

A runnable example that builds a generative Geometry Nodes tree — Mesh Grid → Instance on Points → Realize Instances → Transform → Set Shade Smooth → Set Material — and attaches it as a NODES modifier, following the geometry-nodes-python skill. The tree has no Group Input: the grid lives inside the node group, and the instance is a modeled DSA-profile keycap read in through an Object Info node (original transform space). The realized grid is the key field of a 3x3 macropad.

What it witnesses: instancing is not free geometry until you realize it. The check asserts the closed-form evaluated topology — verts = grid points × keycap verts (9 × 121 = 1089), faces = 9 × 121 = 1089 — where the keycap counts come from its construction parameters (six rounded-rectangle rings of 4 × (4 + 1) = 20 verts, plus the dish centre; five ring-to-ring quad bands, a 20-triangle dish fan and one bottom n-gon), not from measuring the mesh. It also asserts that the (+x, +y) corner cell holds exactly one keycap, centred on its closed-form grid point with its foot at the Transform lift, that Set Material carries Keycap.PBT, and that a second Set Material, selected by a per-face position field, puts Keycap.Accent on exactly one keycap at the front-right grid point. If Realize Instances is bypassed, the evaluated mesh is empty and the count gate fails.

What the render shows: nine identical keycaps, one per grid point, seated on switch housings in the recessed well of an anodized case, and the one orange "enter" key the position-field selection picked out. The count gate is numeric: an unrealized grid renders the same keycaps, because the renderer draws instances either way. What does show is the grid itself — --one-cell renders a single keycap in the centre of an empty well — and where the accent selection landed. The knob, OLED, case and switch housings are render-only staging built in bmesh; only the key field comes from the tree.

-

Run

+

Run#

# Cheap correctness check (no render) — the CI check:
 blender --background --python gn_instance_grid.py --
 
@@ -291,18 +350,685 @@ 

Run

# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts): blender --background --python gn_instance_grid.py -- --output grid.png blender --background --python gn_instance_grid.py -- --output grid.png --engine cycles
-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Carrier vertex count ≠ 1
4Evaluated topology ≠ 1089 verts / 1089 faces (--one-cell lands here)
5Corner grid cell does not hold exactly one keycap (121 verts)
6Set Material did not carry Keycap.PBT and Keycap.Accent
7Corner keycap off its closed-form grid point or lift
8--output produced no file
9Accent selection did not cover exactly one keycap at the front-right point
10Render path: Layer 1 framing violation (gallery_framing)
11Render path: asset-quality floor violation (gallery_asset_quality)

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --one-cell.

-
+

Source

examples/gn-instance-grid/gn_instance_grid.py - View on GitHub → + 667 lines · View on GitHub →
-
"""Geometry Nodes Instance-on-Points grid — a runnable example.
+      
+
"""Geometry Nodes Instance-on-Points grid — a runnable example.
 
 Witnesses the geometry-nodes-python construction contract for instancing:
 a generative GeometryNodeTree (Mesh Grid → Instance on Points → Realize
@@ -970,7 +1696,41 @@ 

Source

except Exception as e: import traceback; traceback.print_exc(); print(f"FATAL: {e}", file=sys.stderr); sys.exit(1)
+
+ +
@@ -990,20 +1750,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/gn-modifier-inputs/index.html b/docs/gallery/gn-modifier-inputs/index.html index 9debf41b..0987dc16 100644 --- a/docs/gallery/gn-modifier-inputs/index.html +++ b/docs/gallery/gn-modifier-inputs/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

gn-modifier-inputs

Per-modifier Geometry Nodes Float inputs on a shared tree — 4.5/5.1 write mod[identifier], 5.2 writes mod.properties.inputs.Socket_1.value.

+

Rendered headless by the example itself — click to zoom.

witnesses The 5.1 dict form raises TypeError on 5.2 rather than silently no-opping; RNA properties are AttributeError on 4.5. Readback and evaluated Z-extent match written heights 1/2/3, which the shared tree turns into 7/17/27 treads.
+

tags geometry-nodes modifiers

blender --background --python examples/gn-modifier-inputs/gn_modifier_inputs.py --
@@ -283,7 +342,7 @@

gn-modifier-inputs

What it witnesses: Blender 5.2 removed ID-property assignment on NodesModifier. mod["Socket_1"] = 2.0 raises TypeError rather than silently no-opping. 4.5 LTS and 5.1 still require that dict form; mod.properties does not exist there (AttributeError). 5.2+ writes mod.properties.inputs.Socket_1.value. After the write, the depsgraph must be updated or evaluated_get still sees the previous height.

Follows geometry-nodes-python.

Staging: the default dark studio. The three staircases stand on 1.62 m centres (XS); the check reads only each object's evaluated Z extent and zmin, never X, so the layout is free. All three share one tree and one set of materials — the only thing that differs between them is the modifier input. The key carries a 36° spread so its pool stays on the stairs, and the rim sits high behind them so it does not wash the foreground floor.

-

Run

+

Run#

# Cheap correctness check (no render) — the CI check:
 blender --background --python gn_modifier_inputs.py --
 
@@ -298,20 +357,622 @@ 

Run

# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts): blender --background --python gn_modifier_inputs.py -- --output stairs.png blender --background --python gn_modifier_inputs.py -- --output stairs.png --engine cycles
-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Height input identifier missing on the tree interface
4Modifiers do not share one node_group
5Version-path write raised (--api dict on 5.2, --api rna on 4.5)
6Version-path read raised
7Readback ≠ intended height (--same-height lands here)
8Evaluated Z-extent ≠ intended height
9Evaluated mesh not sitting on z=0
10Gallery framing violation
11Evaluated extents not distinct
12--output produced no file

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output, --same-height, or --api dict/rna.

-

Falsification

+

Falsification#

ProbeBinaryResult
--api dict5.2.1 LTSexit 5, TypeError: id properties not supported for this type
--api rna4.5.11 LTSexit 5, AttributeError: 'NodesModifier' object has no attribute 'properties'
--api dict5.1.2exit 0 (5.1 still takes the dict form)
--api rna5.1.2exit 5, AttributeError: 'NodesModifier' object has no attribute 'properties'
--same-height5.2.1, 5.1.2, 4.5.11exit 7, readback 1.0 != written 2.0 on SpiralStair.H2
RAIL_H = -0.5 (treads climb past the post)5.2.1, 4.5.11exit 8, evaluated Z-extent 1.934334 != height 1.0
--api auto5.2.1, 5.1.2, 4.5.11exit 0, extents 1.000 / 2.000 / 3.000
-
+

Source

examples/gn-modifier-inputs/gn_modifier_inputs.py - View on GitHub → + 602 lines · View on GitHub →
-
"""Geometry Nodes per-modifier input write — a runnable example.
+      
+
"""Geometry Nodes per-modifier input write — a runnable example.
 
 Witnesses the 5.1→5.2 removal of dict assignment on a NODES modifier.
 A shared GeometryNodeTree builds a spiral staircase from one exposed Float
@@ -914,7 +1575,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -934,20 +1629,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/gn-sdf-remesh/index.html b/docs/gallery/gn-sdf-remesh/index.html index 830f8416..0b6334fd 100644 --- a/docs/gallery/gn-sdf-remesh/index.html +++ b/docs/gallery/gn-sdf-remesh/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

gn-sdf-remesh

A Geometry Nodes SDF remesh (MeshToSDFGrid → GridToMesh at the SDF zero-level), with a Set Material node carrying the material through the remesh.

+

Rendered headless by the example itself — click to zoom.

witnesses An SDF grid is meshed with Grid to Mesh, not Volume to Mesh; GN geometry needs Set Material or it renders untextured.
+

tags geometry-nodes materials

blender --background --python examples/gn-sdf-remesh/gn_sdf_remesh.py --
@@ -281,7 +340,7 @@

gn-sdf-remesh

A runnable example that remeshes an input mesh through an OpenVDB SDF grid using the build_remesh_via_sdf pattern from the geometry-nodes-python skill: GeometryNodeMeshToSDFGrid → GeometryNodeGridToMesh at the SDF zero-level, attached as a NODES modifier and evaluated via the depsgraph.

Which fix it witnesses: an SDF grid is meshed with Grid to Mesh, not Volume to Mesh (the Mesh to SDF Grid output is a grid socket; Volume to Mesh takes a volume-geometry socket, so wiring the grid there is an invalid link that yields no geometry). Grid to Mesh has the matching grid input.

Materials gotcha: geometry generated by Geometry Nodes carries no material, so the input mesh's material is dropped on remesh (the result renders with the default white). Re-apply it inside the tree with a Set Material node (GeometryNodeSetMaterial) on the remeshed output — this example does that and asserts the material survives onto the evaluated mesh.

-

Run

+

Run#

# Cheap correctness check only (no render) — the CI smoke check:
 blender --background --python gn_sdf_remesh.py --
 
@@ -291,18 +350,223 @@ 

Run

# Also render the remeshed result (EEVEE on a GPU host; --engine cycles on GPU-less hosts): blender --background --python gn_sdf_remesh.py -- --output remesh.png blender --background --python gn_sdf_remesh.py -- --output remesh.png --engine cycles
-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3SDF remesh produced no/unchanged geometry (--no-sdf lands here)
4--output produced no file
5Wrong-era EEVEE engine id was accepted
6Input material dropped by remesh

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --no-sdf.

-
+

Source

examples/gn-sdf-remesh/gn_sdf_remesh.py - View on GitHub → + 205 lines · View on GitHub →
-
"""Geometry Nodes SDF remesh -- a runnable BDT example.
+      
+
"""Geometry Nodes SDF remesh -- a runnable BDT example.
 
 Builds the `build_remesh_via_sdf` pattern from the geometry-nodes-python skill
 (`GeometryNodeMeshToSDFGrid` -> `GeometryNodeGridToMesh` at the SDF zero-level), attaches it
@@ -508,7 +772,41 @@ 

Source

import traceback; traceback.print_exc() print(f"FATAL: {type(exc).__name__}: {exc}", file=sys.stderr); sys.exit(1)
+
+ +
@@ -528,20 +826,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/gn-sim-fountain/index.html b/docs/gallery/gn-sim-fountain/index.html index cb028ed8..07517d9d 100644 --- a/docs/gallery/gn-sim-fountain/index.html +++ b/docs/gallery/gn-sim-fountain/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

gn-sim-fountain

A Simulation Zone only advances one step per consecutive frame_set. A direct jump to frame N runs a single step, and only a bake gives random access to the stepped state.

+

Rendered headless by the example itself — click to zoom.

witnesses 24 stepped frames match p0 + v0 t - g t^2/2 within 6.6e-7 m. An unbaked jump 1->25 runs one step and sits 1.68-2.42 m off the arc; calculate_to_frame returns PASS_THROUGH headless; after a bake, random-access frames match within 5.6e-7 m.
+

tags geometry-nodes

blender --background --python examples/gn-sim-fountain/gn_sim_fountain.py --
@@ -283,7 +342,7 @@

gn-sim-fountain

p += v*dt - 0.5*g*dt^2 * z
 v -= g*dt * z

so at frame f every point must lie on the closed-form ballistic arc p0 + v0*t - 0.5*g*t^2*z and carry v0 - g*t*z, with t = (f - frame_start) / fps. The check recomputes both from the launch data (8 crown jets thrown up and out, 12 rim jets arcing inward; g = 9.81, 24 fps, 24 steps = 1.0 s).

-

Measured behavior

+

Measured behavior#

Identical on 4.5.11 LTS, 5.1.2 and 5.2.1 LTS. The Simulation Zone and its cache/bake operators have not diverged across the three, so there is no version branch; the probes that established this are the checks below.

Call sequenceWhat the zone does
frame_set(frame_start)body runs once with Delta Time 0 — state reset to the input
frame_set(f) then frame_set(f + 1)one step, dt = 1/fps
frame_set(start) then frame_set(N), no cacheone step, dt = 1/fps: frame N shows the frame-2 state
simulation_nodes_cache_calculate_to_frame(selected=True) headlesspoll fails: logs Invalid operator call, returns {'PASS_THROUGH'}, does not raise; frames between the two cached endpoints then read as a linear interpolation of them
simulation_nodes_cache_bake(selected=True) under temp_overridebakes frame_start..frame_end (PACKED, in memory, no directory); any frame_set order then reads the exact per-frame state

Two silent traps, then. The jump is 1.68–2.42 m off the true frame-25 arc here. The tempting cache operator "succeeds" without raising and leaves interpolated garbage between cached frames (2.42 m / 9.40 m/s off).

@@ -291,25 +350,682 @@

Measured behavior

What failure each check would catch:

  • exit 3 — the zone is unpaired or drops points: pair_with_output not called evaluates to 0 points (--unpair)
  • exit 4 — the integrator is not the exact constant-gravity update: naive p += v*dt drifts by g*dt*t/2, 0.2044 m at t = 1 s (--euler)
  • exit 5 — a direct frame jump did not reproduce the one-step trap — e.g. because a bake was already present (--prebake-trap). This is what keeps the trap claim above honest on every version CI runs
  • exit 6 — random-access reads after a cache fill miss the arc, or the fill operator did not return FINISHED: calculate_to_frame headless (--calc-to-frame)

The render is the proof. Every droplet bead in the still is a recorded simulated position from the stepped run (check 4). The thin line each string of beads is threaded on is the closed-form arc, drawn from the launch data, not from the simulation. A drifting integrator would pull the late beads off their line — by 0.20 m at the pool for --euler, as check 4 measures (that flag exits before rendering, so this is computed, not a rendered still).

-

Run

+

Run#

blender --background --python gn_sim_fountain.py --
 blender --background --python gn_sim_fountain.py -- --output fountain.png

The --output render path measures framing via examples/gallery_framing.py (exit 10 on violation).

-

Falsifiers

+

Falsifiers#

Each breaks one thing and exits the code of the check it targets, on all three versions.

FlagBreaksTarget checkExit
--unpairSimulation Input never paired with its Outputzone evaluates one point per jet3
--eulerdrops the -0.5*g*dt^2 position termstepped frames on the closed-form arc4
--prebake-trapbakes before the direct jumpunbaked jump runs exactly one step5
--calc-to-framefills the cache with calculate_to_frame, not a bakebaked random access on the arc6
-

Exit codes

+

Exit codes#

Per-script sequential checks. 10 is the shared framing helper.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Point count or frame-start state off the launch data
4Stepped frames off the closed-form arc (position or velocity)
5Direct jump did not reproduce the one-step trap
6After the cache fill, random-access frames off the arc (or fill not FINISHED)
10Gallery framing violation
12--output produced no file

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or any falsifier.

-
+

Source

examples/gn-sim-fountain/gn_sim_fountain.py - View on GitHub → + 657 lines · View on GitHub →
-
"""Geometry Nodes Simulation Zone ballistic fountain — a runnable example.
+      
+
"""Geometry Nodes Simulation Zone ballistic fountain — a runnable example.
 
 Witnesses the Simulation Zone frame contract from
 ``skills/geometry-nodes-python``. A zone integrates state only when the
@@ -967,7 +1683,41 @@ 

Source

print(f"ERROR: {type(exc).__name__}: {exc}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -987,20 +1737,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/gn-socket-rename/index.html b/docs/gallery/gn-socket-rename/index.html index 83cb928c..56a830c4 100644 --- a/docs/gallery/gn-socket-rename/index.html +++ b/docs/gallery/gn-socket-rename/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

gn-socket-rename

Compare and Random Value socket identifiers collapsed onto reused names in 5.2; enabled-name lookup wires on 4.5, 5.1, and 5.2

+

Rendered headless by the example itself — click to zoom.

witnesses A_INT and Min_001 present on 4.5.11 and 5.1.2, absent on 5.2.1. --legacy-ids exits 5 only on 5.2. Evaluated 16 verts and gauge_h 1.80 on eight column verts; the still draws the column graduations from gauge_h, so an unwired Random Value renders a blank column
+

tags geometry-nodes

blender --background --python examples/gn-socket-rename/gn_socket_rename.py --
@@ -286,25 +345,590 @@

gn-socket-rename

  • exit 4 — identifier inventory wrong for this Blender
  • exit 5 — pre-5.2 identifier lookup failed (--legacy-ids on 5.2)
  • exit 6 — enabled-name lookup failed
  • exit 7 — Compare did not switch the column in (8 verts)
  • exit 8 — column not sitting on the plinth
  • exit 9 — gauge_h not 1.80 on eight verts (Random unwired)

--legacy-ids is the falsifier: wire by Min_001 / A_INT. It exits 0 on 4.5.11 and 5.1.2 (those identifiers still exist) and 5 on 5.2.1. Unlike --same-axis, it is not red on every binary.

No SMOKE_SKIP. Do not pass deviation= to check_framing.

-

Run

+

Run#

blender --background --python gn_socket_rename.py --
 blender --background --python gn_socket_rename.py -- --legacy-ids
 blender --background --python gn_socket_rename.py -- --output gauge.png

The --output render path measures framing against the Layer 1 band via examples/gallery_framing.py (exit 10 on violation) before writing the still.

-

Render

+

Render#

The still witnesses both axes, not only the Compare one. The column's graduations — a tick every 0.1 m in from each vertical corner, a heavier one every 0.5 m — are drawn by the column shader from the evaluated POINT gauge_h attribute, and only up to its stored height. With Compare broken the column is missing; with Random Value unwired the Store default leaves gauge_h at 0 and the column renders blank copper. Both were rendered to confirm it. The shader is render-path only and nothing in it feeds back into check(); the column is also turned to 18° for the still so two faces show, which check() cannot see because it reads object-space vertices.

-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
4Identifier inventory wrong for this Blender version
5Pre-5.2 identifier missing (--legacy-ids on 5.2)
6Enabled-name socket lookup failed
7Evaluated vert count off (Compare did not switch the column in)
8Evaluated zmax off closed form
9POINT gauge_h not 1.80 on eight column verts
10Gallery framing violation
12--output produced no file

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --legacy-ids.

-
+

Source

examples/gn-socket-rename/gn_socket_rename.py - View on GitHub → + 565 lines · View on GitHub →
-
"""GN Compare / Random Value socket identifier rename — a runnable example.
+      
+
"""GN Compare / Random Value socket identifier rename — a runnable example.
 
 Witnesses the 5.2 collapse of typed sockets onto reused identifiers.
 ``FunctionNodeRandomValue`` FLOAT used ``Min_001`` / ``Max_001`` / ``Value_001``
@@ -870,7 +1494,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -890,20 +1548,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/gn-zone-iterate/index.html b/docs/gallery/gn-zone-iterate/index.html index eeb86327..f8299322 100644 --- a/docs/gallery/gn-zone-iterate/index.html +++ b/docs/gallery/gn-zone-iterate/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

gn-zone-iterate

Repeat Zone and For Each Element only iterate after pair_with_output. Evaluated cube counts follow 8 times (1+N) and 8 times P, not tree structure.

+

Rendered headless by the example itself — click to zoom.

witnesses Repeat 32/24 verts/faces with X-centers at k*1.2; For Each 48/36 with Z-centers at i*0.6+0.21. Unpaired evaluates empty; For Each main Geometry passthrough is 6 verts.
+

tags geometry-nodes

blender --background --python examples/gn-zone-iterate/gn_zone_iterate.py --
@@ -286,22 +345,476 @@

gn-zone-iterate

Same on 4.5 LTS and 5.x (zones are 4.3+). No skip.

What failure each check would catch:

  • exit 3 — Repeat pairing broken or iterations wrong (--unpair-repeat → 0/0; --repeat-iterations 1 → 16/12)
  • exit 4 — Repeat count matches but cubes collapsed (--no-offset)
  • exit 5 — For Each pairing broken, element count wrong, or main-socket passthrough (--unpair-foreach → 0/0; --foreach-count 3 → 24/18; --foreach-main → 6/0)
  • exit 6 — For Each count matches but Z-centers do not
-

Run

+

Run#

blender --background --python gn_zone_iterate.py --
 blender --background --python gn_zone_iterate.py -- --output zones.png

The --output render path measures framing via examples/gallery_framing.py (exit 10 on violation).

-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage; also carrier mesh was rewritten
3Repeat evaluated vert/face count off closed form
4Repeat X-centers off closed form
5For Each evaluated vert/face count off closed form
6For Each Z-centers off closed form
10Gallery framing violation
12--output produced no file

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output, --no-offset, --unpair-foreach, or --unpair-repeat.

-
+

Source

examples/gn-zone-iterate/gn_zone_iterate.py - View on GitHub → + 454 lines · View on GitHub →
-
"""Geometry Nodes zone pairing — a runnable example.
+      
+
"""Geometry Nodes zone pairing — a runnable example.
 
 Witnesses Repeat Zone and For Each Element from
 ``skills/geometry-nodes-python``: ``pair_with_output`` is load-bearing, and
@@ -756,7 +1269,41 @@ 

Source

print(f"ERROR: {type(exc).__name__}: {exc}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -776,20 +1323,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/gp-lineart-contour/index.html b/docs/gallery/gp-lineart-contour/index.html index e6f3904b..28ca493b 100644 --- a/docs/gallery/gp-lineart-contour/index.html +++ b/docs/gallery/gp-lineart-contour/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

gp-lineart-contour

Grease Pencil LINEART modifier ink via the depsgraph on a cel-shaded lighthouse diorama. source_object is load-bearing (clear yields 0 strokes); every edge type off yields 0; the drawing is 255 strokes / 1393 points on 4.5.11, 5.1.2 and 5.2.1, gated above the count left when any one of contour, crease, material-border or intersection edges is dropped. Stroke width: thickness exists on 4.5, AttributeError on 5.1 — portable path is radius.

+

Rendered headless by the example itself — click to zoom.

witnesses AI code still sets LINEART.thickness on 5.1 (AttributeError) or hand-draws strokes / enables Freestyle instead of modifiers.new(..., 'LINEART'). Clearing source_object or leaving every edge type off silently yields an empty drawing — the check proves both failure modes and recovery.
+

tags grease-pencil line-art modifiers

blender --background --python examples/gp-lineart-contour/gp_lineart_contour.py --
@@ -284,9 +343,9 @@

gp-lineart-contour

What each check catches on failure: wrong GPv3 collection (exit 2), thickness present/absent on the wrong side of 5.0 (exit 3), lost source_object assignment or use_contour (exit 4), drawing below the gates (exit 5), source clear not zeroing strokes (exit 6), flags-off not zeroing strokes or a rebuild not recovering the same counts (exit 7).

Version witness: stroke counts match on 4.5.11 LTS, 5.1.2 and 5.2.1 LTS (255 strokes / 1393 points). The divergence is thickness vs radius.

The still: the check and the still share one camera, because Line Art is view-dependent — the strokes the check counts are the strokes the still draws. The mesh is shaded flat (three cel tones), so the black ink is what makes it read as an inked illustration: clear source_object and the same render is a flat, lineless low-poly model — no band borders, no waterline, no outlines.

-

Stage deviation

+

Stage deviation#

Line Art is a non-photoreal contract: the hero's materials are cel-shaded (Diffuse → Shader to RGB → constant ramp, emitted) instead of Principled, so the ink reads against flat tone fields. The stage itself — floor, wall, world, key, fill and warm wedge — is the default. The cel shading uses Shader to RGB, which is EEVEE-only, so --engine cycles does not render the look.

-

Run

+

Run#

# Depsgraph contour check — the CI check:
 blender --background --python gp_lineart_contour.py --
 
@@ -295,18 +354,746 @@ 

Run

# Also render the gallery still: blender --background --python gp_lineart_contour.py -- --output lineart.png
-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage; also GPv3 collection address contract
3LINEART thickness / radius trap for this Blender
4LINEART type, source, or use_contour (--no-contour lands here)
5Evaluated drawing below the stroke/point gates
6Cleared source_object still produced strokes
7Every edge type off still produced strokes, or the rebuild did not recover the same counts
8--output produced no file
10Gallery framing violation

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --no-contour.

-
+

Source

examples/gp-lineart-contour/gp_lineart_contour.py - View on GitHub → + 728 lines · View on GitHub →
-
"""Grease Pencil Line Art contours — a runnable example.
+      
+
"""Grease Pencil Line Art contours — a runnable example.
 
 Witnesses the Line Art modifier contract AI-generated NPR code most often
 gets wrong across the 4.5 LTS → 5.1 window:
@@ -1035,7 +1822,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -1055,20 +1876,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/grain-sacks/index.html b/docs/gallery/grain-sacks/index.html index ae4e03ea..7a39e755 100644 --- a/docs/gallery/grain-sacks/index.html +++ b/docs/gallery/grain-sacks/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,40 +325,42 @@

grain-sacks

A procedural pile of burlap grain sacks — settled onto flat bases, pressed into one another, gathered under twine ties with pleated crowns — carried through UVs, bake, LOD, compound collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

+

Rendered headless by the showcase piece itself — click to zoom.

witnesses Recomputed: 8640 tris, two materials with 3264 burlap and 1152 twine faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.914×0.905×0.636 m, zmin 0, hygiene 0 including zero coplanar cross-shell pairs, all three sacks grounded, every tie in a neck waist 13–15 mm narrower than the cloth 40 mm either side, six twine turns seated 3.6–5.2 mm into the cloth, sacks pressing 12 mm into one another, bellies settled low (0.25–0.32 of the body), and flat contact patches of 0.035, 0.099 and 0.117 m² under the three sacks. --round-bottom exits 19 with every patch at 0 while zmin and the supports still pass; --slip-tie exits 17 on the waist.
+

tags mesh export showcase

blender --background --python showcase/grain-sacks/grain_sacks.py --

A pile of three burlap grain sacks. Two stand, one lies slumped in front of them, and each is gathered at the neck under a two-turn twine tie with a pleated crown above it. The sacks have settled onto flat bases and press into each other where they lean. A showcase piece, not an example — it witnesses no API contract. It asserts that generated geometry meets declared asset budgets, recomputed from the finished mesh.

-

What it composes

+

What it composes#

Shipped contentUsed for
skills/mesh-editing-and-bmeshlofted, pleated sack bodies and laid twine in one bmesh
skills/procedural-materials-and-shadershessian weave and stripes on the cloth's own coordinates (a second UV layer), per-sack tone
skills/bake-high-to-lowCycles tangent-space normal bake, high onto low
skills/engine-export-presetsUnity glTF (export_yup=True)
skills/depsgraph-and-evaluated-dataevaluated triangle counts for the LOD ratios
snippets/decimate_to_budget.pyLOD1 / LOD2 COLLAPSE chain
snippets/convex_hull_collider.pyone hull per sack, merged into a compound
snippets/lod_chain.pyLOD naming and ratio pattern
examples/mesh-hygiene-audithygiene combinatorics (copied, not imported)
-

The budget that matters

+

The budget that matters#

A sack full of grain settles. Its contents slump into the bottom and it rests on a flat patch of cloth pressed against the floor. An egg-bottomed sack still touches the floor at its lowest vertex, so the grounded-zmin gate and the per-sack support gate both pass it, but it reads as a balloon rocking on a point. The piece sums, per sack, the area of the faces that lie flat on the floor (every vertex at z = 0) and asserts each is at least 0.012 m².

--round-bottom is the falsifier built for exactly this. It sinks each sack by the same amount and folds the same height, but squashes the base linearly instead of flattening it, so every sack keeps its height, its footprint and its single lowest point on the floor. Every other budget passes. Only the patch sees it: 0 m² on all three.

-

Settling and pressing

+

Settling and pressing#

  • Settle. Each sack is built standing (or laid on its flat side and squashed 16%), sunk SINK = 60 mm into the floor, and everything under SETTLE_C = 30 mm is folded back up by g(u) = ((u + 1) / 2)², clamped to 0 below u = −1. The fold is C1 at its top and exactly flat where the grain presses the floor. The squashed height goes outward from the sack's own axis (SPREAD), so the base bulges the way a filled sack does.
  • Press. Sacks are placed, then slid along the line between centres until the moving sack's deepest vertex inside the sack it leans on reaches PRESS = 12 mm. This uses bisection on the real meshes, so the press is exact whatever the lumps and pleats do where the two meet.
  • Tie. Each twine turn's centreline is the host's own section at its station, pushed out along each vertex's radial by the twine radius less TIE_BITE. The turn follows the pleats instead of standing proud of the valleys and sinking into the crests.
-

Budgets

+

Budgets#

Declared in the script as named constants, recomputed from the generated mesh. Measured values are from Blender 5.2.1; every one is byte-identical on 4.5.11 and 5.1.2.

BudgetBandMeasured
Base triangles8000–93008640
LOD1 ratio0.32–0.620.5000
LOD2 ratio0.10–0.350.2199
Material slotsexactly 2, distinct2
Cloth faces≥ 26003264
Twine faces≥ 9001152
UV boundsinside 0..1(0.0133, 0.0133)–(0.9867, 0.9867)
UV AABB overlap≤ 1e-50.000000
Outer AABB0.914 × 0.905 × 0.636 m ± 0.0200.9139 × 0.9054 × 0.6360
Collider triangles≤ 450376 (three hulls)
Normal bake{'FINISHED'} with image data{'FINISHED'}, has_data=True
glTF exportfile written, non-empty~280 kB
Hygieneall zeroloose 0/0, non-manifold 0, zero-area 0, doubles 0, n-gons 0, coplanar cross-shell pairs 0
Grounded AABB\zmin\≤ 1e-40.00000
Named supports3 sacks, each zmin ≤ 1e-33 at 0.00000
Tie in the waisthost under the tie ≥ 8 mm narrower than 40 mm either side, per sack14.25 / 15.04 / 13.11 mm
Tie seat6 turns, deepest twine vertex inside the cloth 1.0–6.0 mm3.57–5.21 mm
Press between sackseach sack's deepest vertex inside another 6–20 mm11.96 / 12.00 / 12.00 mm
Contact patcheach sack ≥ 0.012 m² flat on the floor0.0352 / 0.0988 / 0.1168 m²
Settled bellywidest station ≤ 0.42 of the body from its base0.251 / 0.280 / 0.316

Real-world size: the tall sack stands 0.64 m and is 0.47 m across the belly — a 50 kg grain sack. The outer AABB is the three sacks themselves, with no appendage above or beside them, so the AABB gate already holds the bodies to their stated size.

-

Surface

+

Surface#

  • Weave. Hessian is two perpendicular sine gratings on a second UV layer, ClothCo: metres round the sack from its front, and metres up it. The weave follows the cloth whichever way up the sack lies. A 6 mm period aliased into moiré at hero distance, so the period is 12 mm with a soft bump.
  • Stripes. Stripes are the old feed-sack marking, centred on each sack's front face (the broad face a viewer sees): one broad blue band on the tall sack, three thin red ones on the lying sack, none on the third. Each sack also has its own tone.
  • Twine. Twine is paler and yellower than the hessian, so the tie reads against the neck.
-

Conventions walked

+

Conventions walked#

Every convention in showcase/README.md, and whether it applies here.

ConventionAppliesHow
Deterministic, budgets declared, assertions recomputeyesno RNG at all; every value above is read off the mesh
Falsifier fails the budget it targetsyestable below, proven on all three binaries
Hygiene incl. cross-shell coplanaryesexit 15. A twine face parallel to the cloth it hugs, one column over, first shared its plane (2 pairs); the turns now sit a quarter step off the body's columns
Named supportsyesevery sack grounded (--float-sack)
Wrappers follow the host's profileyesthe tie is built on the host's own pleated section
Band hooped, never flush / seat conformanceyestie bite banded (--loose-tie)
One connected assembly / carried parts biteyessacks press a banded depth into one another (--part-sacks)
A vessel has a base and a bellyyessettled belly low (--high-belly) and the contact patch (--round-bottom)
Plumb and real-world sizepartlya slumped sack is not plumb by design; size is held by the AABB, which is the bodies themselves
Shading is part of the modelyescloth and twine smooth-shaded: nothing on a sack is faceted in life
One substance, one slotyesburlap, twine
Rope is laid, not pipedyesthree-lobed 6-vertex twine section, turned per ring
Two identical sections stacked share planesyesthe two tie turns are half a step apart
Identical parts read as CGyeseach sack has its own height, width, depth, pleat phase, tone and stripe
Sort bmesh operator inputsn/ano bevel or other set-fed operator is run
Edge treatment: no right anglesn/ano boxes; every surface is lofted cloth or twine
Bake texels per UV celln/anine UV islands, one per shell, each a ninth of the sheet
Level on the stage; stage 60 myesturned about Z only; 60 m floor and wall
Keep a falsifier's envelope stillyesworst deltas: --high-belly −16.2 mm Y, --part-sacks +15.5 mm Y, against 20 mm
Timber, iron, masonry, scatter, fixtures, roofs, ringsnothe piece has none of these
-

Falsifiers

+

Falsifiers#

Each breaks one pipeline stage so a named budget fails. All nine were run on 4.5.11, 5.1.2 and 5.2.1 and exited the same declared code on all three.

FlagTarget budgetBreaksExit
--skip-decimateLOD1 ratiodrops the DECIMATE modifiers, LOD1 ratio goes to 1.00009
--stray-vertmesh hygieneadds one loose vertex inside the pile15
--lift-zgrounded zminlifts the whole mesh 50 mm16
--float-sacknamed supportsfloats the lying sack 12 mm; the standing ones still ground the AABB16
--slip-tietie in the waistslides every tie 50 mm up onto the crown, still seated on it; margin −13.4 to −44.6 mm17
--loose-tietie seatsizes each turn 6 mm wider; shallowest turn −1.67 mm18
--part-sackspress between sacksstops the lying sack 4 mm short of the one it leans on18
--round-bottomcontact patchsquashes the bases round instead of flat; 0 m² on all three19
--high-bellysettled bellymoves the widest station up to 0.52 of the body, same widths; 0.535–0.58619
-

Exit codes

+

Exit codes#

File-local and sequential. 9 is a valid check code. 1 is the FATAL wrapper — a crash, never a named check.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build, or has no UV layer
4Base triangle count outside band
5Material slots, or a material's face floor
6UVs outside 0..1
7UV AABB overlap above tolerance
8Outer AABB off declared size
9LOD1 or LOD2 ratio outside band (--skip-decimate)
10Framing gate (examples/gallery_framing.py, render path only)
11Collider triangles above ceiling
12Normal bake failed or produced no image data
13glTF export missing or empty
14--output produced no file
15Mesh hygiene (--stray-vert)
16Grounded zmin, or a sack floating (--lift-z, --float-sack)
17A tie not in its neck's waist (--slip-tie)
18Tie seat, or press between sacks (--loose-tie, --part-sacks)
19Contact patch or settled belly (--round-bottom, --high-belly)
-

Run it

+

Run it#

# Budget check, no render. ~1.9 s on 4.5, ~1.7 s on 5.1, ~2.1 s on 5.2.
 blender --background --python grain_sacks.py --
 
@@ -314,17 +373,1434 @@ 

Run it

# Render the gallery still (EEVEE; --engine cycles on a GPU-less host). blender --background --python grain_sacks.py -- --output sacks.webp

Smoke runs the check-only path. It does not pass --output or any falsifier.

-

Cross-version measurements

+

Cross-version measurements#

Value4.5.115.1.25.2.1
Base triangles864086408640
LOD1 / LOD2 tris4320 / 1900samesame
Face counts (cloth / twine)3264 / 1152samesame
Outer AABB0.9139 × 0.9054 × 0.6360samesame
Collider tris376376376
Contact patches (m²)0.0352 / 0.0988 / 0.1168samesame
glTF bytesidenticalidenticalidentical
Check wall-clock~1.9 s~1.7 s~2.1 s

DECIMATE COLLAPSE is the usual cross-version suspect. Here it produces identical LOD counts on all three binaries; the gate is still a ratio band, not an exact count.

-
+

Source

showcase/grain-sacks/grain_sacks.py - View on GitHub → + 1417 lines · View on GitHub →
-
"""Game-ready grain sacks — a showcase piece, not an example.
+      
+
"""Game-ready grain sacks — a showcase piece, not an example.
 
 Asserts budget conformance of a procedural pile of three burlap grain
 sacks: two standing, one lying slumped against them, each gathered at the
@@ -1742,7 +3218,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -1762,20 +3272,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/grease-pencil-rosette/index.html b/docs/gallery/grease-pencil-rosette/index.html index 3c670f87..6518c554 100644 --- a/docs/gallery/grease-pencil-rosette/index.html +++ b/docs/gallery/grease-pencil-rosette/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

grease-pencil-rosette

Grease Pencil v3's attribute-based API — layer → frames.new(1).drawing → add_strokes → per-point position/radius/opacity/vertex_color — drawing five nested neon rose curves.

+

Rendered headless by the example itself — click to zoom.

witnesses The GPv3 address break: 4.5 keeps GPv3 at grease_pencils_v3 while grease_pencils is still legacy GPencil (frame.strokes, no .drawing); 5.x deletes legacy and GPv3 takes over the grease_pencils name. Point writes lazily materialize attribute layers, and every position round-trips through the raw POINT buffer.
+

tags grease-pencil attributes

blender --background --python examples/grease-pencil-rosette/grease_pencil_rosette.py --
@@ -282,7 +341,7 @@

grease-pencil-rosette

What it witnesses: GPv3 lives at different addresses on the two supported versions, and the check asserts each side's actual contract:

  • Blender 4.5 LTS — GPv3 is bpy.data.grease_pencils_v3 (GreasePencilv3), while bpy.data.grease_pencils still holds legacy GPencil whose frames carry .strokes directly and have no .drawing. Same collection name, incompatible API — the trap is asserted, not just avoided.
  • Blender 5.x — legacy is gone: GPv3 took over the bpy.data.grease_pencils name, and grease_pencils_v3 / bpy.types.GPencilStroke no longer exist.

On the shared GPv3 surface it then asserts that stroke points are views over attribute layers: writing pt.radius / pt.opacity / pt.vertex_color lazily materializes radius, opacity, vertex_color (POINT) and cyclic (CURVE) layers in drawing.attributes, and every closed-form rose position round-trips exactly through the raw position attribute buffer via foreach_get.

-

Run

+

Run#

# Cheap correctness check (no render) — the CI check:
 blender --background --python grease_pencil_rosette.py --
 
@@ -292,18 +351,318 @@ 

Run

# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts): blender --background --python grease_pencil_rosette.py -- --output rosette.png blender --background --python grease_pencil_rosette.py -- --output rosette.png --engine cycles
-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3GPv3 address / legacy trap for this Blender
4Object type, layer count, or frame number
5Stroke topology or cyclic (--open-strokes lands here)
6Lazy attribute layers missing or wrong domain/type
7Position buffer length or closed-form round-trip
8Grease Pencil material missing gpencil settings
9--output produced no file

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --open-strokes.

-
+

Source

examples/grease-pencil-rosette/grease_pencil_rosette.py - View on GitHub → + 300 lines · View on GitHub →
-
"""A neon rose-curve set drawn with GPv3 strokes — a runnable example.
+      
+
"""A neon rose-curve set drawn with GPv3 strokes — a runnable example.
 
 Witnesses the Grease Pencil v3 rewrite, the largest bpy API break in the
 4.x-to-5.x window. GPv3 is present on both supported versions but lives at
@@ -604,7 +963,41 @@ 

Source

except Exception as e: import traceback; traceback.print_exc(); print(f"FATAL: {e}", file=sys.stderr); sys.exit(1)
+
+ +
@@ -624,20 +1017,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/grindstone/index.html b/docs/gallery/grindstone/index.html index ff6296ad..2c498acd 100644 --- a/docs/gallery/grindstone/index.html +++ b/docs/gallery/grindstone/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

grindstone

A procedural grindstone on a timber trestle through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

+

Rendered headless by the showcase piece itself — click to zoom.

witnesses Recomputed: 1668 tris, three materials with 400 wood / 240 stone / 364 metal faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.722×0.419×0.610 m, zmin 0, hygiene 0 including zero coplanar cross-shell pairs, four shoes planted, trough seated in the sills and biting both stretchers 4 mm, stone dip 24 mm, no unchamfered right-angle edge, 29.44 bake texels per UV cell, LOD ratios in band, convex collider 304 tris, non-empty glTF. --skip-decimate exits 9 on LOD1; --stray-vert / --flush-shoes exit 15 on hygiene; --lift-z / --short-legs exit 16 on grounded; --float-crank / --float-legs exit 17 on joint fit; --no-dip / --narrow-trough / --low-stretchers exit 18 on seat; --sharp-handle exits 20 on edge treatment; --low-bake exits 21 on texel density.
+

tags mesh export showcase

blender --background --python showcase/grindstone/grindstone.py --
@@ -282,22 +341,22 @@

grindstone

Each A-frame is a king post with two diagonals tenoned 12 mm into its faces and into a fatter sill. It is not two sticks meeting at a point. The sills stand in iron shoes, cups one 6 mm reveal proud of the sill on every side. The trough is one manifold basin seated into the sills and carried on two end stretchers, which it bites by 4 mm. Every timber and iron edge is chamfered. The stone is 48 smooth segments with hard chamfer rings, in a mottled sandstone material. Stated size is a 0.50 m × 0.095 m stone in a 0.72 × 0.42 × 0.61 m frame.

It asserts budget conformance of the generated result. It does not witness an API contract. "It rendered without error" is not a check.

Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package).

-

Budgets

+

Budgets#

Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.

AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles1560–17801668 / 1668 / 1668
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.5000
LOD2 ratio0.10–0.35 of base0.2194 / 0.2194 / 0.2194
Materialsexactly 3 distinct; ≥ 280 wood, ≥ 190 stone, ≥ 260 metal faces3 slots; 400 / 240 / 364
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(0.722, 0.419, 0.610) m ± 0.015(0.7220, 0.4185, 0.6100), zmin 0
Collider tris≤ 320304
Bake texelssmallest UV cell ≥ 12 px at the baked resolution29.44 px at 1024 px
Exportwritten, size > 0152272 / 152272 / 152260 bytes

The collider ceiling moved from 240 to 320 with the stone. 48 segments put more of its rim on the hull (198 → 304 triangles).

-

Hygiene

+

Hygiene#

Recomputed from the generated mesh, not asserted about the script.

AxisDeclaredMeasured (all three)
Non-manifold edges00
Loose verts / edges0 / 00 / 0
Doubles merged at 1e-500
Zero-area faces00
N-gons00
Coplanar cross-shell face pairs00
Grounded: zminwithin 1e-4 of 00.0000
Named supports: 4 shoeseach zmin ≤ 1e-34, shoe_z 0.00000
Stone size0.500 m dia × 0.095 m thick ± 0.02 / ± 0.0150.5000 × 0.0950
Edge treatmentmanifold edges within 5° of 90° = 00 wood, 0 stone, 0 iron

The coplanar budget counts pairs from different shells within 0.05 m, using hay-bale's constants. The first build matched only faces whose centres fell within 0.1 mm of each other, and no real overlap ever does. It reported 0 while the shoes shared three planes with every sill end, seen as a lit slit in each foot close-up.

-

Joint fit and seat

+

Joint fit and seat#

AxisDeclaredMeasured (all three)
Crank-axle gap≤ 0.008 m0.00376
Shoe-wood BVH gap≤ 0.008 m (overlap is 0)0.00000
Trough-sill per-side gap≤ 0.008 m (overlap is 0)0.00000
Stone dip into trough0.015–0.045 m0.02416
Trough floor zmin≥ 0.040 m0.05800
Trough bearing2 end stretchers, each biting the trough floor 0.002–0.008 m2, [0.00400, 0.00400]

DECIMATE COLLAPSE triangle counts can differ across series, so the gate is a ratio band, not an exact count. Bake pixels are stochastic. The gate is has_data plus operator FINISHED plus the texel floor, not byte-identity. Per-member wood tone comes from random.Random(TONE_SEED), so it is identical run to run. Export byte counts differ by 12 B on 5.2.1 (glTF serializer), which is not a gated axis.

-

Falsifiers

+

Falsifiers#

Each violates one named budget. All thirteen were run on 4.5.11, 5.1.2 and 5.2.1 and returned the same code on each.

FlagBudget violatedExit
--skip-decimateLOD1 ratio band9
--stray-vertloose vertex count is 015
--flush-shoescoplanar cross-shell pairs: shoes sized to the sill and flush with its end (0 → 4 pairs)15
--lift-zbounding box zmin is 016
--short-legsnamed shoe supports at Z=016
--float-crankcrank-axle gap17
--float-legsshoe-wood join17
--narrow-troughtrough-sill seat18
--no-dipstone dip band18
--low-stretcherstrough bearing: stretchers back at sill height, 5.9 mm under the trough18
--sharp-handleedge treatment: the crank handle's rims left square (20 right-angle edges)20
--low-bakebake texel density: 256 px bake (7.36 px per cell)21

--flush-shoes moves the shoe envelope 12 mm inside BBOX_TOL (15 mm), so the AABB gate cannot steal it. --sharp-handle leaves the triangle count inside its band, so only the edge budget sees it.

-

Run

+

Run#

blender --background --python grindstone.py --
 blender --background --python grindstone.py -- --skip-decimate
 blender --background --python grindstone.py -- --stray-vert
@@ -313,17 +372,1801 @@ 

Run

blender --background --python grindstone.py -- --low-bake blender --background --python grindstone.py -- --output grindstone.png

Smoke passes no flags.

-

Exit codes

+

Exit codes#

File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path. 15–19 are the hygiene and joint-fit family.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 3 distinct slots, or a face-count floor missed
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, coplanar cross-shell pairs
16Not grounded: bounding box zmin off 0, or a named shoe floats
17Joint fit: crank-axle gap, or shoe-wood join
18Seat: trough-sill gap, stone dip band, trough on the dirt, or trough not bearing on its stretchers
19Stone diameter or thickness off the stated real-world size
20A right-angle edge survived the chamfer passes (--sharp-handle lands here)
21Bake texel density below the floor (--low-bake lands here)
-
+

Source

showcase/grindstone/grindstone.py - View on GitHub → + 1784 lines · View on GitHub →
-
"""Game-ready grindstone — a showcase piece, not an example.
+      
+
"""Game-ready grindstone — a showcase piece, not an example.
 
 Asserts budget conformance of a procedural grindstone (thick sandstone
 wheel, timber A-frame, open water trough, iron axle/hubs/crank) after
@@ -2108,7 +3951,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -2128,20 +4005,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/hand-pump/index.html b/docs/gallery/hand-pump/index.html index b6a921c4..a52f948e 100644 --- a/docs/gallery/hand-pump/index.html +++ b/docs/gallery/hand-pump/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

hand-pump

A procedural village hand pump whose gooseneck is a 6-gon tube from the lower-column radius, through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

+

Rendered headless by the showcase piece itself — click to zoom.

witnesses Recomputed: 1064 tris, two materials with 564 metal and 144 wood faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.610×0.382×1.052 m, grounded zmin, spout-to-column gap 2.7 mm, flange-to-plinth gap 6.0 mm, column radius 62 mm, LOD ratios in band, convex collider 114 tris, hygiene all zero, non-empty glTF. --skip-decimate exits 9; --stray-vert 15; --lift-z 16; --float-spout 17; --float-flange 18; --skinny-col 19.
+

tags mesh export showcase

blender --background --python showcase/hand-pump/hand_pump.py --
@@ -283,22 +342,22 @@

hand-pump

It asserts budget conformance of the generated result. It does not witness an API contract. "It rendered without error" is not a check.

Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package). Hygiene combinatorics match examples/mesh-hygiene-audit (copied, not imported).

Intended size: ~1.05 m village pump, 0.34 m plinth, 0.76 m column; outer AABB 0.610 × 0.382 × 1.052 m.

-

Budgets

+

Budgets#

Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.

AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles900–22001064 / 1064 / 1064
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.5000
LOD2 ratio0.10–0.35 of base0.2199 / 0.2199 / 0.2162
Materialsexactly 2 distinct, ≥48 metal, ≥24 wood2 slots, 564 metal / 144 wood
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(0.610, 0.382, 1.052) m ± 0.015(0.6100, 0.3820, 1.0520), zmin 0
Collider tris≤ 220114
Exportwritten, size > 097304 / 97304 / 97296 bytes

Base triangles dropped from 1164 to 1064 in the quality pass: seven sausage cylinders became one gooseneck tube, and the wood grip is no longer beveled with the plinth. Outer AABB is 0.610 × 0.382 × 1.052 m (was 0.610 × 0.364 × 1.049 m).

DECIMATE COLLAPSE triangle counts are not identical across series — 5.2.1 is slightly leaner on LOD2. The gate is a ratio band, not an exact count. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Construction is closed-form; the only RNG is the seeded per-piece wood tone. Bevel inputs are sorted by edge index, so the face order is the same on every run; a Python set of edges handed to the bevel had made it vary. Export byte counts differ on 5.2.1 (glTF serializer), not a gated axis.

-

Surface and stage

+

Surface and stage#

The quality pass found no geometric defect: every joint checked in the inspection sheet (spout root, flange bolts, head and handle pivot, column step, plinth) seats as the joint budgets say. The defects were on the surface. The iron was polished metal (metallic 1.0, roughness 0.38) and read as chrome; it is now near-black painted cast iron with dark rust in the pores. The plinth slabs and the handle grip carry their own wood tone and grain. The stage grid grew from 14 to 60 m, because the wall's left edge showed in the corner of the hero. The temp .glb is removed after its size is measured. With no new geometric defect, there is no new budget.

-

Hygiene

+

Hygiene#

Recomputed from the generated mesh, not asserted about the script.

AxisDeclaredMeasured (all three)
Non-manifold edges00
Loose verts / edges0 / 00 / 0
Doubles merged at 1e-500
Zero-area faces00
N-gons00
Coplanar disjoint face pairs00
Grounded: zminwithin 1e-4 of 00.0000
Plinth zminwithin 1e-4 of 00.00000
-

Joint fit and column

+

Joint fit and column#

AxisDeclaredMeasured (all three)
Spout-to-column BVH gap≤ 0.008 m0.00272
Flange-to-plinth BVH gap≤ 0.008 m0.00600
Lower-column radius vs COL_R_LO± 0.008 m0.06200
-

Falsifiers

+

Falsifiers#

Each violates one named budget. All were run on 4.5.11, 5.1.2 and 5.2.1 and returned the same code on each.

FlagBudget violatedExit
--skip-decimateLOD1 ratio band9
--stray-vertloose vertex count is 015
--lift-zbounding box zmin is 016
--float-spoutspout-to-column BVH gap17
--float-flangeflange-to-plinth BVH gap18
--skinny-collower-column radius19
-

Run

+

Run#

blender --background --python hand_pump.py --
 blender --background --python hand_pump.py -- --skip-decimate
 blender --background --python hand_pump.py -- --stray-vert
@@ -308,17 +367,1398 @@ 

Run

blender --background --python hand_pump.py -- --skinny-col blender --background --python hand_pump.py -- --output hand-pump.png

Smoke passes no flags.

-

Exit codes

+

Exit codes#

File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path. 15–19 are the hygiene and joint-fit family.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 2 distinct slots, or wood/metal faces missing
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Hygiene (--stray-vert lands here)
16Grounded zmin / plinth (--lift-z)
17Spout-to-column gap (--float-spout)
18Flange-to-plinth gap (--float-flange)
19Column radius (--skinny-col)
-
+

Source

showcase/hand-pump/hand_pump.py - View on GitHub → + 1381 lines · View on GitHub →
-
"""Game-ready village hand pump — a showcase piece, not an example.
+      
+
"""Game-ready village hand pump — a showcase piece, not an example.
 
 Asserts budget conformance of a procedural cast-iron hand pump (plinth,
 column, gooseneck, stuffing-box head, handle, wooden grip) after
@@ -1700,7 +3140,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -1720,20 +3194,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/hay-bale/index.html b/docs/gallery/hay-bale/index.html index 55e21007..5cb771d2 100644 --- a/docs/gallery/hay-bale/index.html +++ b/docs/gallery/hay-bale/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

hay-bale

A bound hay bale whose twine belts follow the loaf's own raycast cross-section, asserting recomputed geometry budgets, not an API contract.

+

Rendered headless by the showcase piece itself — click to zoom.

witnesses Recomputed: 2044 tris, two materials with 1436 hay and 304 twine faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.921x0.508x0.417 m, zmin 0, hygiene and cross-shell z-fight 0, 5 shells, each belt wrapping under the loaf at zmin 1.5 mm, banded belt seat depth 4.0-11.1 mm over 20/20 stations on both belts, belt mirror error 0, cinch depth 10.5 mm, loaf 0.921x0.393 m, 22.4 bake texels per UV cell, LOD ratios in band, convex collider 140 tris, non-empty glTF. --skip-decimate exits 9; --stray-vert 15; --lift-z and --float-belts 16; --slack-belt 18; --skew-belt and --odd-loaf 19; --low-bake 20.
+

tags mesh export showcase

blender --background --python showcase/hay-bale/hay_bale.py --
@@ -282,27 +341,27 @@

hay-bale

It asserts budget conformance of the generated result. It does not witness an API contract. "It rendered without error" is not a check.

Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package). Hygiene combinatorics match examples/mesh-hygiene-audit (copied, not imported).

Intended size: 0.90 × 0.48 × 0.38 m nominal bale. The loaf measures 0.921 × 0.508 × 0.393 m with its pillow bulge, and the envelope is 0.921 × 0.508 × 0.417 m once the belts and knot loops are on it.

-

How the belts are built

+

How the belts are built#

The belt is not a constant rectangle. The loaf's cross-section at a belt station is rounded, bulged and waisted, so a rectangle stands proud at the middle of each face and is swallowed at the corners — which is exactly what the previous build did, leaving the wrap hanging 2.9–8.3 mm above the floor with a visible notch at the bottom edge.

Instead the loaf is built, bevelled and grounded first, then each belt raycasts the finished loaf at its own station (loaf_profile, BELT_SEGS samples) and follows that profile at a named bite. Where the run passes near the ground it is pressed flush and a little past flush (BELT_PRESS_H, BELT_UNDER_SINK) so the loop closes underneath without breaking the Z=0 plane the bale is grounded on.

Every closed-form term in the loaf shaping is even in x. An odd term (sin(x*24)) put the two belt stations on different surface heights and left the wraps 5.4 mm out of step, which no budget could see.

-

Budgets

+

Budgets#

Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file. Measurements below are identical on 4.5.11, 5.1.2 and 5.2.1 — construction uses no RNG and no version-dependent operator.

AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles1700–24002044 on all three
LOD1 ratio0.32–0.62 of base0.5000
LOD2 ratio0.10–0.35 of base0.2192
Materialsexactly 2 distinct, ≥24 hay, ≥24 twine2 slots, 1436 hay, 304 twine
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(0.921, 0.508, 0.417) m ± 0.01(0.9208, 0.5075, 0.4165)
Grounded zminwithin 1e-4 of 00.000000
Hygieneloose/nonman/zero-area/doubles/n-gons/z-fight = 0all 0
Shell countexactly 5 (loaf, 2 belts, 2 loops)5
Belt wrap zmineach belt shell ≤ 0.006 m0.001460 both
Belt seat depthevery station in [0.002, 0.020] m0.00400..0.01114 over 20/20 stations, both belts
Mirror symmetrybelts and loops matched within 5e-4 m0.000000
Loaf mirrorevery loaf vertex has an X-mirror partner within 2e-4 m0.000093
Cinch depthmid-span minus waist in [0.006, 0.020] m0.01053
Loaf X × Z(0.921, 0.393) m ± 0.02(0.9208, 0.3931)
Hay–twine gapBVH surface < 0.008 m0.00009
Collider tris≤ 400140
Bake texelssmallest UV cell ≥ 12 px at the baked resolution22.43 px at 1024 px
Exportwritten, size > 0203508 / 203508 / 203496 bytes

DECIMATE COLLAPSE triangle counts are not guaranteed identical across series — the gate is a ratio band, not an exact count. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity, plus the texel floor. The bake was 256 px, which left 5.6 texels per UV cell and smeared neighbouring cells' normals into streaks across the straw; at 1024 px with a 0.02 m cage (was 0.04, which reached the twine standing proud of the loaf) every cell gets 22. Export byte counts differ by 12 B on 5.2.1 (glTF serializer), which is not a gated axis; every geometric figure above is identical on all three binaries.

The export is deleted once its size is measured. Blender sets TMPDIR from its own preference, which resolves to the working directory on a stock portable build, so tempfile.gettempdir() is the repo root under CI and every run used to leave a .glb behind.

-

Why these budgets and not an AABB

+

Why these budgets and not an AABB#

zmin alone cannot see a belt that stops at the bottom edge: the loaf still grounds the box. A single global hay-twine gap cannot see a wrap that hugs one side and gaps the other. A bounding box cannot see two belts 5.4 mm out of step, or a flank with no waist in it. Each of the five budgets in bold above exists because a defect got past everything else and had to be found by looking at a render.

The z-fight count is taken over cross-shell face pairs, not merely over faces that share no vertex: two quads two steps apart on one flat cap share no vertex and are coplanar by construction, and counting those reported 96 pairs, none of them a hazard.

-

Why the loaf is triangulated along its short diagonals

+

Why the loaf is triangulated along its short diagonals#

The twine used to be a 16 × 24 mm band, and it read as packing tape on a parcel. Thinned to an 8 × 10 mm cord, the two belts came out 0.45 mm out of step, right against the 0.5 mm mirror budget. The loaf's vertices were mirror-symmetric to 71 um. The belt profile is raycast from the loaf at each station, though, and the loaf's quads are not planar: split along a fixed diagonal, the surface at -x and at +x differed by up to 5.6 mm on the same vertices. The old wide band happened to hide that.

The loaf is now triangulated with quad_method="SHORT_EDGE". On mirror-symmetric vertices the shorter diagonal is mirror-symmetric too, and a tie only happens where a quad is planar, where either split is the same surface. The belt mirror error went back to exactly 0 and the seats to identical values at both stations. This is the surface an engine renders, so it is fixed in the mesh, not only in the sampling. Averaging the two stations' profiles was tried first and rejected: it put one belt 0.52 mm into the loaf, below the seat band, because neither station's real surface is the mean.

The loaf's own symmetry is now asserted directly (Loaf mirror, exit 19). Every loaf vertex needs a partner at (-x, y, z) within LOAF_MIRROR_EPS. The belt budget only ever caught an odd shaping term second-hand, through the belts' extents. --odd-loaf adds a 2 mm odd sin displacement to the finished loaf and fails it. It is applied after the bevel because, put into the shaping function, even 0.3 mm of odd term flipped which edges cleared the bevel's 50-degree selection and grew the envelope 16 mm, so the bounding box fired instead.

-

Falsifiers

+

Falsifiers#

Each breaks one stage so a named budget fails and the piece exits that code. All eight proven on 4.5.11, 5.1.2 and 5.2.1.

FlagTarget budgetExit
--skip-decimateLOD1 ratio band9
--stray-verthygiene: loose vertices15
--lift-zgrounded AABB zmin16
--float-beltsper-belt wrap zmin16
--slack-beltbanded belt seat depth18
--skew-beltbelt mirror symmetry19
--odd-loafloaf X-mirror symmetry19
--low-bakebake texel density: 256 px bake (under 12 px per cell)20

--stray-vert parks its loose vertex inside the bale: above it, the bounding-box budget (exit 8) fired first and the run proved nothing about hygiene. --skew-belt shifts one belt along X rather than Z for the same reason — a Z skew landed a belt face coplanar with a loaf face and tripped the z-fight budget (exit 15) instead of its target. --float-belts moves twine only, so the loaf still grounds the AABB and the per-belt gate has to be the thing that fires. With the ridge at 6 mm the knot loops set the envelope top, so the two twine falsifiers are sized to their own budgets and kept inside BBOX_TOL: --float-belts lifts 8 mm (was 20) against a 6 mm wrap gate, and --slack-belt backs off 6 mm (was 10) against a 2 mm seat floor. At the old sizes both exited 8.

-

Run

+

Run#

blender --background --python hay_bale.py --
 blender --background --python hay_bale.py -- --skip-decimate
 blender --background --python hay_bale.py -- --stray-vert
@@ -314,24 +373,1665 @@ 

Run

blender --background --python hay_bale.py -- --low-bake blender --background --python hay_bale.py -- --output bale.png

Smoke does not pass --output or any falsifier flag.

-

Shading

+

Shading#

Decided per part, not globally. The loaf is flat-shaded: straw is a chunky matte mass and the facets read as compressed flakes. Smoothing the whole bale erased every ridge and nap in it and left a featureless pillow that read as a bar of soap. Twine is cord, so the belts and hitch loops stay smooth.

The low mesh carries the form — bulge, waist, ridges, end strata — and the high mesh carries the straw: LOAF_CUTS_HIGH subdivision plus per-band flake offsets, moved onto the low mesh by the bake stage. A low mesh dense enough to model flakes directly would cost thousands of triangles for detail this piece already has a bake for.

-

Surface

+

Surface#

The bale used to read as a wrapped parcel: flat mustard hay, and a wide dark band for twine. The hay is now straw_material:

  • a dense fibre field stretched along the bale (noise with X compressed), and a weaker crossing field for the strands that lie the other way;
  • colour pulled between pale straw and dark tan by the fibres, with a faint green cast from low-frequency noise;
  • roughness and a bump from the same fibres, so every strand catches light.

The baked normal is chained under that bump (wire_normal). The twine is a 10 × 14 mm orange polypropylene cord standing 6 mm proud. As an 8 × 10 mm dark brown cord it stood 4 mm proud of the waisted loaf and read as a scored cut in every view. The longitudinal ridge dropped from 14 mm to 6 mm, because at 14 mm it crumpled the top edge in profile and the bale read as a stuffed sack. Each knot is a tight loop, KNOT_MAJOR 13 mm, standing in the belt's plane with its tube sized from TWINE_T. A 60 mm ring lying flat on the top read as a printed symbol. The earlier note here, that the surface belonged to the material and bake and not to more geometry, was right. The cord was the other half of the parcel read.

-

Exit codes

+

Exit codes#

File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 2 distinct slots, or hay/twine faces missing
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Hygiene: loose, non-manifold, zero-area, doubles, n-gons, or cross-shell z-fight (--stray-vert)
16Grounded: AABB zmin (--lift-z), or a belt that does not wrap under (--float-belts)
17Shell count, or hay–twine BVH gap above 8 mm
18Banded belt seat depth (--slack-belt)
19Loaf X-mirror symmetry (--odd-loaf), belt mirror symmetry (--skew-belt), cinch depth, or loaf real-world size
20Bake texel density below the floor (--low-bake)
-
+

Source

showcase/hay-bale/hay_bale.py - View on GitHub → + 1641 lines · View on GitHub →
-
"""Game-ready bound hay bale — a showcase piece, not an example.
+      
+
"""Game-ready bound hay bale — a showcase piece, not an example.
 
 Asserts budget conformance of a procedural straw bale after composing
 shipped pipeline pieces: bmesh construction, UVs, two materials,
@@ -1973,7 +3673,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -1993,20 +3727,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/hitching-post/index.html b/docs/gallery/hitching-post/index.html index 6a144f11..df09b089 100644 --- a/docs/gallery/hitching-post/index.html +++ b/docs/gallery/hitching-post/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

hitching-post

A procedural hitching post (eaved cap, one rail, cup shoe, rings hung through eyes) through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

+

Rendered headless by the showcase piece itself — click to zoom.

witnesses Recomputed: 1680 tris, two materials with 114 wood and 742 metal faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.500×0.149×1.246 m, zmin 0, hygiene and coplanar pairs 0, post seated at z 0.010, ring centerline error 0, bands 6.5 mm proud of the post, LOD ratios in band, convex collider 80 tris, non-empty glTF. --skip-decimate exits 9; --stray-vert and --twin-sole exit 15; --lift-z exits 16; --clip-ring and --sunk-bands exit 18; --short-post exits 19.
+

tags mesh export showcase

blender --background --python showcase/hitching-post/hitching_post.py --
@@ -282,18 +341,18 @@

hitching-post

It asserts budget conformance of the generated result. It does not witness an API contract. "It rendered without error" is not a check.

Intended size: post 0.125 m square and 1.16 m of timber, cap 0.096 m, cross-rail 0.50 m. Outer AABB 0.500 × 0.149 × 1.246 m.

Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package). Hygiene combinatorics match examples/mesh-hygiene-audit (copied, not imported). Coplanar-pair counting matches showcase/signpost.

-

Budgets

+

Budgets#

Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.

AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles1400–24001680 / 1680 / 1680
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.5000
LOD2 ratio0.10–0.35 of base0.2190 / 0.2190 / 0.2190
Materialsexactly 2 distinct, ≥70 wood, ≥560 metal2 slots, 114 wood, 742 metal
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(0.500, 0.149, 1.246) m ± 0.01(0.5000, 0.1490, 1.2460)
Grounded zminwithin 1e-4 of 00 / 0 / 0
Hygieneloose/nonman/zero-area/doubles/n-gons/coplanar pairs = 0all 0
Post seatworld zmin in (0.004, 0.014), XY centroid within 0.010 of origin, arm axis cos ≥ cos(0.5°)0.01000, 0, 1.000
Hung ringcenterline error ≤ 0.008 m, ring–wood overlap 0, eye–wood overlap 0, shank–ring overlap 0, shank bites the eye0, 0, 0, 0, 40
Shoeeach band overlaps the post, shoe overlaps the soleband gap 0, sole–shoe 8
Band seateach band above the shoe stands ≥ 0.004 m proud of the post faces0.00650
Wood–metal gapBVH surface < 0.008 m0.00075
Collider tris≤ 28080
Exportwritten, size > 0127424 / 127424 / 127412 bytes

Base triangles rose from 1598 to 1680. The old rings were faceted tori clipped into the arm; the new eyes, shanks, and 24-segment hung rings replace that mesh. Cylinder caps are triangulated so the n-gon budget stays at 0.

DECIMATE COLLAPSE triangle counts are not identical across series — the gate is a ratio band, not an exact count. On this mesh the three binaries agreed. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Construction is closed-form; the only RNG is the seeded per-piece wood tone. Bevel inputs are sorted by edge index, so the face order is the same on every run; a Python set of edges handed to the bevel had made it vary. Export byte counts differ on 5.2.1 (glTF serializer), not a gated axis. Euler is 12 (six balls and seven tori) and is not gated to 2.

-

Bands, wood and stage

+

Bands, wood and stage#

The two iron bands were sized from half - grip: an inner half-width of 53 mm and an outer one of 61 mm, against a 62.5 mm post face. They sat inside the post, and only the chamfered corners broke the surface, showing as small black slits up the post. They now wrap it: the inner face bites 1.5 mm into the post (BAND_BITE), so each band stands 6.5 mm proud. Band seat asserts it; --sunk-bands restores the old bands and exits 18 (proud −0.0015 m).

The post, rail and cap each carry their own tone and grain, and the iron is rusted. The stage grid grew from 14 to 60 m, because the wall's left edge showed as a bright band in the corner of the hero. The temp .glb is removed after its size is measured.

Each falsifier violates exactly one named budget. Proven on Blender 4.5.11 LTS, 5.1.2, and 5.2.1 LTS (the binaries' own --version):

FalsifierBudget violatedExitMeasured failure
--skip-decimateLOD1 ratio band9ratio 1.0000
--stray-vertMesh hygiene, loose verts15loose_v=1
--twin-soleCoplanar face pairs15zfight=6
--lift-zGrounded zmin16zmin 0.050000
--clip-ringHung-ring centerline18ring_err 0.024, ring–wood overlap 32
--short-postSeated post19post zmin 0.030
--sunk-bandsBand seat18band proud −0.0015

Default exit is 0 on all three. --clip-ring also overlaps the rail; the centerline gate is the one that fires.

-

Run

+

Run#

blender --background --python hitching_post.py --
 blender --background --python hitching_post.py -- --skip-decimate
 blender --background --python hitching_post.py -- --stray-vert
@@ -304,17 +363,1573 @@ 

Run

blender --background --python hitching_post.py -- --sunk-bands blender --background --python hitching_post.py -- --output preview.webp --engine cycles

Smoke does not pass --output or a falsifier.

-

Exit codes

+

Exit codes#

File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 2 distinct slots, or wood/metal faces missing
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Hygiene, including coplanar pairs (--stray-vert, --twin-sole)
16Grounded zmin (--lift-z)
17Wood–metal BVH gap above 8 mm
18Hung ring, shoe or band seat (--clip-ring, --sunk-bands)
19Post plumb, origin, and cup seat (--short-post)
-
+

Source

showcase/hitching-post/hitching_post.py - View on GitHub → + 1556 lines · View on GitHub →
-
"""Game-ready hitching post — a showcase piece, not an example.
+      
+
"""Game-ready hitching post — a showcase piece, not an example.
 
 Asserts budget conformance of a procedural timber hitching post (square
 post, pyramidal cap, cross-arm, iron shoe, bands, and hitching rings)
@@ -1871,7 +3486,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -1891,20 +3540,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/image-pixels-testcard/index.html b/docs/gallery/image-pixels-testcard/index.html index 7dde54f2..9005d7ff 100644 --- a/docs/gallery/image-pixels-testcard/index.html +++ b/docs/gallery/image-pixels-testcard/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

image-pixels-testcard

The Image pixel-buffer contract — a procedural broadcast test card written into bpy.data.images.new() with one pixels.foreach_set (589,824 floats), byte vs float_buffer storage, scale() reallocation, and the save() vs save_render() lifecycle.

+

Rendered headless by the example itself — click to zoom.

witnesses pixels is always flat RGBA (channels == 4 even with alpha=False), byte storage quantizes at exactly ≤ 0.5/255 and strictly > 0, stale-size bulk reads raise after scale() — and save() silently flips source to FILE and drops the buffer, so later pixels reads come from whatever sits on disk (proven with an imposter file).
+

tags images performance rendering

blender --background --python examples/image-pixels-testcard/image_pixels_testcard.py --
@@ -285,7 +344,7 @@

image-pixels-testcard

  • Buffer geometry — an API change to per-image channel counts, or code assuming an RGB stride (falsified: a W*H*3 write raises and the check exits 3).
  • Byte/float round-trip vs the closed-form card — any stride, orientation, or packing bug in the bulk path; a one-pixel shift was deliberately introduced once and the check exited 4 with measured error 0.97 against tolerance 0.00196.
  • Quantization floor — byte_err > 0 proves 8-bit storage really quantizes; byte and float images swapping behavior cannot hide.
  • Reallocation — scale() no longer reallocating (stale-size read succeeding).
  • save() lifecycle — the source-flip/buffer-drop behavior changing (falsified: substituting save_render() for save() exits 7 because source stays GENERATED).
  • Disk round-trip — a byte sRGB image saved to PNG and reloaded must match at quantization tolerance.

Version divergence: none — every contract above, including the save() trap, was probed and asserts identically on Blender 4.5.11 LTS and 5.1.2. The only gate in the file is the EEVEE engine id for the optional render (BLENDER_EEVEE_NEXT on 4.x, BLENDER_EEVEE on 5.x).

Render hazard worth knowing: a bmesh-built plane has no UV map, and bmesh.ops.create_grid(..., calc_uvs=True) silently creates none unless a UV layer already exists — without one, an Image Texture samples texel (0,0) for every fragment and the screen renders as one flat color. The render path creates the layer explicitly.

-

Run

+

Run#

# Cheap correctness check (no render) — the CI check:
 blender --background --python image_pixels_testcard.py --
 
@@ -295,19 +354,421 @@ 

Run

# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts): blender --background --python image_pixels_testcard.py -- --output card.png blender --background --python image_pixels_testcard.py -- --output card.png --engine cycles
-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Pixel buffer is not always RGBA
4Byte round-trip vs closed-form card (--wrong-origin lands here)
5Float-buffer round-trip failed
6scale() did not reallocate, or stale-size read succeeded
7save() source/buffer-drop contract drifted
8save_render() flipped source or disturbed the buffer
9Byte PNG save/reload error
10--output produced no file

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --wrong-origin.

In the render, Closest interpolation keeps the pixel grid honest — the jagged circle edge is the 512 × 288 buffer itself, and the white marker in the PLUGE row sits at the bottom-left because that is where pixel (0, 0) lives. The monitor is staged as a designed object — beveled dark-polymer case, machined metal stand, teal power LED — on the dark studio stage from docs/VISUAL-STYLE.md (Standard view transform; warm key, cool fill and rim; a warm pool raking the back wall). The screen stays emissive and matte — specular off, emission strength 1.0 — so the card's values read exactly.

-
+

Source

examples/image-pixels-testcard/image_pixels_testcard.py - View on GitHub → + 402 lines · View on GitHub →
-
"""A procedural broadcast test card written with one foreach_set — a runnable example.
+      
+
"""A procedural broadcast test card written with one foreach_set — a runnable example.
 
 Witnesses the `bpy.types.Image` pixel-buffer contract. `Image.pixels` is a flat,
 row-major, bottom-left-origin float buffer that is ALWAYS RGBA: `channels == 4`
@@ -710,7 +1171,41 @@ 

Source

except Exception as e: import traceback; traceback.print_exc(); print(f"FATAL: {e}", file=sys.stderr); sys.exit(1)
+
+ +
@@ -730,20 +1225,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/index.html b/docs/gallery/index.html index 8166a1b3..59c0754e 100644 --- a/docs/gallery/index.html +++ b/docs/gallery/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -343,7 +400,7 @@

Examples and Showcase

cross-version-property-delete

@@ -860,7 +917,7 @@

cross-version-property-delete

book-trolley

diff --git a/docs/gallery/iron-cauldron/index.html b/docs/gallery/iron-cauldron/index.html index e467fa5a..2493b6dd 100644 --- a/docs/gallery/iron-cauldron/index.html +++ b/docs/gallery/iron-cauldron/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

iron-cauldron

A procedural hanging iron cauldron with a round-bellied pot, a bail threaded through flat ear rings, and a timber tripod through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

+

Rendered headless by the showcase piece itself — click to zoom.

witnesses Recomputed: 3664 tris, two materials with 224 wood and 1768 metal faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.987×0.961×0.860 m, pot–leg clearance 0.053 m, pot base radius 0.075 m, 3 coaxial ferrules at zmin 0, ferrule bite −0.00185 m, hook-bail join 0, bail threads both ear rings, mesh hygiene all zero, LOD ratios in band, convex collider 246 tris, non-empty glTF. --skip-decimate exits 9; --stray-vert 15; --lift-z/--short-legs 16; --float-hook/--edge-on-ears 17; --pipe-ferrule 18; --pointed-pot 19.
+

tags mesh export showcase

blender --background --python showcase/iron-cauldron/iron_cauldron.py --
@@ -282,24 +341,24 @@

iron-cauldron

Ferrules are lofted along the pole axis and shifted so the downhill rim sits at Z=0. A world-Z bucket lets a leaning pole slice the lip. ferrule_bite is measured in that same frame, and only samples wood inside the cup's own axis span — a pole hovering above the well cannot fake a seat.

It asserts budget conformance of the generated result. It does not witness an API contract. "It rendered without error" is not a check.

Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package).

-

Budgets

+

Budgets#

Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.

AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles2800–42003664 / 3664 / 3664
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.5000
LOD2 ratio0.10–0.35 of base0.2200 / 0.2200 / 0.2200
Materialsexactly 2 distinct; ≥80 wood, ≥200 metal faces2 slots; 224 / 1768
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(0.987, 0.961, 0.860) m ± 0.015(0.9866, 0.9605, 0.8600), zmin 0
Collider tris≤ 280246
Exportwritten, size > 0287380 / 287380 / 287368 bytes

Base triangles rose from 3388 to 3664 in the quality pass: coaxial ferrule cups and a dual-wall lathe with a rolled rim replaced the old cone-tripod / overlapping-torus construction.

-

Hygiene

+

Hygiene#

Recomputed from the generated mesh, not asserted about the script.

AxisDeclaredMeasured (all three)
Non-manifold edges00
Loose verts / edges0 / 00 / 0
Doubles merged at 1e-500
Zero-area faces00
N-gons00
Coplanar disjoint face pairs00
Grounded: zminwithin 1e-4 of 00.0000
Named supports: 3 ferruleseach zmin ≤ 1e-33, cup_z 0.00000
Pot size0.355 m dia × 0.334 m high ± 0.040.3600 × 0.3340
Pot formbase radius ≥ 0.05 m; widest point in the lower 60%0.0750; at 0.420 of the height
Pot–leg clearance≥ 0.04 m0.0528
-

Joint fit and seat

+

Joint fit and seat#

AxisDeclaredMeasured (all three)
Hook-bail BVH gap≤ 0.010 m0.00000
Bail through the ears2 rings; 0 ring vertices inside the bail; bail within the hole (< 0.0135 m of the ring centre)2; 0; 0.00641
Ferrule bite (pole r − inner wall, pole frame)−0.006–0.000 m−0.00185

DECIMATE COLLAPSE triangle counts are not identical across series — the gate is a ratio band, not an exact count. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Construction is closed-form; the only RNG is the seeded per-pole wood tone. Export byte counts differ on 5.2.1 (glTF serializer), not a gated axis.

-

Pot, ears and surface

+

Pot, ears and surface#

The pot was a quadratic profile from a 28 mm tip: an onion that ran to a point. It is now a superellipse belly from a 75 mm flat base out to 0.180 m at 0.42 of the height, then a rounded shoulder in to a 0.128 m mouth. The inner wall is offset along the profile's normal, not sideways, so it keeps its thickness where the wall runs nearly flat. Pot form asserts it; --pointed-pot restores the old profile (base 0.028 m, widest at 0.727 of the height) and exits 19.

The ear rings stood in the bail's own XZ plane, so the bail ran through the ring's tube rather than its hole, and its ends hung into the pot's mouth. The rings now lie flat, bite 2 mm into the rim's outer edge, and the bail (vertical where it passes) threads them. Past each ear the bail is mirrored about the ring, so its ends hook outward. Bail through the ears asserts it; --edge-on-ears restores the old rings (36 ring vertices inside the bail) and exits 17.

Cast iron is near-black and rough with rust in the pores; each pole has its own tone and grain along its axis. The temp .glb is removed after its size is measured.

-

Falsifiers

+

Falsifiers#

Each violates one named budget. All nine were run on 4.5.11, 5.1.2 and 5.2.1 and returned the same code on each.

FlagBudget violatedExit
--skip-decimateLOD1 ratio band9
--stray-vertloose vertex count is 015
--lift-zbounding box zmin is 016
--short-legsnamed ferrule supports at Z=016
--float-hookhook-bail gap17
--pipe-ferruleferrule bite (pole starts above the well)18
--pointed-potpot form19
--edge-on-earsbail through the ears17
-

Run

+

Run#

blender --background --python iron_cauldron.py --
 blender --background --python iron_cauldron.py -- --skip-decimate
 blender --background --python iron_cauldron.py -- --stray-vert
@@ -311,17 +370,1735 @@ 

Run

blender --background --python iron_cauldron.py -- --edge-on-ears blender --background --python iron_cauldron.py -- --output cauldron.png

Smoke passes no flags.

-

Exit codes

+

Exit codes#

File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path. 15–19 are the hygiene and joint-fit family.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer / pot clips tripod
4Base triangle count outside range
5Material count ≠ 2 distinct slots, or a face-count floor missed
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight
16Not grounded: bounding box zmin off 0, or a named ferrule floats
17Joint fit: hook-bail gap, or the bail not threading the ear rings
18Seat: ferrule bite band
19Pot diameter or height off the stated real-world size, or pot form (base radius, belly height)
-
+

Source

showcase/iron-cauldron/iron_cauldron.py - View on GitHub → + 1718 lines · View on GitHub →
-
"""Game-ready hanging iron cauldron — a showcase piece, not an example.
+      
+
"""Game-ready hanging iron cauldron — a showcase piece, not an example.
 
 Asserts budget conformance of a procedural cauldron (lathed pot with a
 rolled rim, pipe bail through ear rings, timber tripod tenoned into a
@@ -2040,7 +3817,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -2060,20 +3871,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/light-link-studio/index.html b/docs/gallery/light-link-studio/index.html index 90d4ee1e..dcedbc3b 100644 --- a/docs/gallery/light-link-studio/index.html +++ b/docs/gallery/light-link-studio/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

light-link-studio

One key, one hero: a light linked to a receiver collection lights only the hero, proven by two pixel renders in one pass. Linked: 3.6x luminance ratio; unlinked in the same check: the decoy rises 233% while the hero holds at 0.3% drift.

+

Rendered headless by the example itself — click to zoom.

witnesses The API is obj.light_linking on the light OBJECT (ld.light_linking is an AttributeError). Luminance is sampled at each sphere's projected center; EEVEE honors linking too (probe-measured 3.8x on 4.5.11 EEVEE Next and 5.1.2 EEVEE), Cycles pinned for deterministic samples.
+

tags rendering objects

blender --background --python examples/light-link-studio/light_link_studio.py --
@@ -284,7 +343,7 @@

light-link-studio

What each check catches on failure: inverting the link to the decoy collection (exit 6 — hero/decoy ratio collapses to 0.39x), an RNA move of the API onto the Light datablock (exit 3, guarded), a lost assignment read-back (exit 5), a link that doesn't restrict (exit 7, rise below 50%), and a non-surgical restriction (exit 8, hero drift above 5%).

Version witness: ObjectLightLinking API and the measured ratios match on Blender 4.5 LTS and 5.1 (3.7x vs 3.6x linked; rise 238% vs 233% — inside the rise noise described above).

The render is the contract at a glance: the spheres staged as studio specimens on matching turned-metal pedestals — the blazing orange hero over its LINKED placard with the warm floor inlay marking the linked light's footprint, the cold steel decoy over UNLINKED with its inlay dark, a warm pool raking the wall behind them — one key, one hero.

-

Run

+

Run#

# Two-render correctness check (tiny Cycles CPU renders) — the CI check:
 blender --background --python light_link_studio.py --
 
@@ -293,18 +352,449 @@ 

Run

# Also render the gallery still (Cycles, deterministic samples): blender --background --python light_link_studio.py -- --output linked.png
-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Light datablock gained light_linking
4Light objects lost light_linking
5receiver_collection assignment did not read back (--skip-link lands here)
6Linked state: hero dark or hero/decoy ratio below gate
7Decoy rise below gate when unlinked
8Hero drifted across the unlink
9--output produced no file

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --skip-link.

-
+

Source

examples/light-link-studio/light_link_studio.py - View on GitHub → + 431 lines · View on GitHub →
-
"""One key, one hero: light linking, proven by pixels — a runnable example.
+      
+
"""One key, one hero: light linking, proven by pixels — a runnable example.
 
 Witnesses the light-linking contract AI-generated staging code most often
 misses: a light's contribution can be restricted to a receiver collection,
@@ -736,7 +1226,41 @@ 

Source

except Exception as e: import traceback; traceback.print_exc(); print(f"FATAL: {e}", file=sys.stderr); sys.exit(1)
+
+ +
@@ -756,20 +1280,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/lightmap-uv-channel/index.html b/docs/gallery/lightmap-uv-channel/index.html index 0e502c4a..edbfe2a2 100644 --- a/docs/gallery/lightmap-uv-channel/index.html +++ b/docs/gallery/lightmap-uv-channel/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

lightmap-uv-channel

A market cart carrying the two-channel UV contract for baked lighting: UV0 untouched, UVLight packed with no overlaps and a respected margin.

+

Rendered headless by the example itself — click to zoom.

witnesses Per-part UVMap/UVLight with active vs active_render pinned (ops clear both flags), UV0 drift 0.0, 3680 islands with 0 SAT overlaps, min island distance 0.00401.
+

tags uv game-pipeline rendering

blender --background --python examples/lightmap-uv-channel/lightmap_uv_channel.py --
@@ -287,22 +346,875 @@

lightmap-uv-channel

Pipeline arc neighbors: UV-layer authoring in uv-layer-grid, tangent-space from UVs in triangulate-tangents, export round-trip in gltf-export-roundtrip, pivot discipline in prop-origin-transform.

Version witness: check output is byte-identical on Blender 4.5.11 LTS and 5.1.2 (21 parts, 3680 islands, drift 0, overlap 0, min island distance 0.00401). The UV layout is packer-version-dependent by design; the contracts are layout-independent invariants.

Render as proof: the cart beside its enlarged UV1 atlas board — the Bed's packed lightmap built from live UV data, so a change in the atlas moves the board geometry. The falsification variant (--falsify) translates the second-largest island onto the largest: a big emissive-red island visibly stacked over the atlas (15 SAT hits in the check probe). The render path also gates the asset itself through examples/gallery_asset_quality.py (naming, material variation, edge treatment — exit 11): 21 named parts, 5 materials, right-angle share 0.069.

-

Run

+

Run#

blender --background --python lightmap_uv_channel.py --
 blender --background --python lightmap_uv_channel.py -- --output atlas.png
 blender --background --python lightmap_uv_channel.py -- --falsify overlap.png
-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper. 11 is the shared asset-quality helper.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3UV layer names ≠ UV0 / UV1
4UVMap / UV1 render/clone flags wrong after re-assert
5Channel 0 (UV0) touched by the UV1 unwrap
6UV1 loops outside [0,1]
7SAT overlap self-test failed, or UV1 triangle pairs overlap
8Min island distance below margin
9Part is not watertight
10Gallery framing violation; also --output produced no file
11Gallery asset-quality violation

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --falsify.

-
+

Source

examples/lightmap-uv-channel/lightmap_uv_channel.py - View on GitHub → + 853 lines · View on GitHub →
-
"""Lightmap UV channel — the second UV layer engines need for baked lighting.
+      
+
"""Lightmap UV channel — the second UV layer engines need for baked lighting.
 
 Witnesses the two-channel UV contract a bake pipeline depends on, on an
 asset built to be reused (a market cart: bed, axle, two wheels, four posts,
@@ -1156,7 +2068,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -1176,20 +2122,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/lod-decimate-chain/index.html b/docs/gallery/lod-decimate-chain/index.html index 34085579..0a1a485b 100644 --- a/docs/gallery/lod-decimate-chain/index.html +++ b/docs/gallery/lod-decimate-chain/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

lod-decimate-chain

A retro rocket at LOD0/1/2 via the Decimate modifier evaluated through the depsgraph, with a wireframe of each evaluated mesh. The check proves the reduction is non-destructive, the triangle count hits ratio x base within bounds, and silhouette-critical dimensions survive.

+

Rendered headless by the example itself — click to zoom.

witnesses Evaluated-vs-original: the depsgraph carries the reduction while obj.data keeps the closed-form counts. A stacked Decimate halves the effective ratio (caught); a fin extreme on an oblique plate corner is shaved at ratio 0.18 (caught).
+

tags depsgraph mesh

blender --background --python examples/lod-decimate-chain/lod_decimate_chain.py --
@@ -284,7 +343,7 @@

lod-decimate-chain

  • Decimate is non-destructive and lives in the depsgraph. The evaluated mesh carries the reduction; the original datablock is byte-identical before and after evaluation. The check proves both — evaluated counts drop while obj.data keeps the closed-form counts (2,511 verts / 2,488 faces / 4,784 tris) — and every evaluated reference is released with to_mesh_clear(). An LOD chain that bakes the reduction into obj.data destroys the asset's LOD0 (applying the modifier instead: exit 7, caught).
  • The COLLAPSE ratio is a target, not a guarantee. Each LOD's evaluated triangle count must land within 5% of ratio x base_tris — measured 0.00% at ratio 0.5 (2,392 tris) and 0.13% at ratio 0.18 (860 tris). A stacked second Decimate halves the effective ratio (50.0% excursion, caught); a dropped modifier leaves evaluated == original (caught).
  • LODs preserve silhouette-critical dimensions. Height (the antenna tip at 3.40) and fin span (2.04) are the rocket's readability; the evaluated bbox holds the base bbox within 1e-3 at every level (measured 2.6e-10). Decimate has no vertex pinning, so this is a real risk, not a formality, and it shaped the model: each fin ends in a straight tip cap and a flat foot, and the four fins sit on the local axes, so every bbox extreme is a fin's mid-rim vertex on a straight edge. The three-fin layout this replaced put an extreme on an oblique fin's plate corner, and a collapse at 0.18 shaved it by 4.7e-3 (caught).

The Decimate modifier API (decimate_type='COLLAPSE', ratio) is stable across Blender 4.5 LTS, 5.1 and 5.2 — the example runs identically on all three, which is itself the version witness (measured values match to the digit).

The render stages the three LODs left to right with a dark triangle wireframe laid over each: the same mesh through the same Decimate, then Triangulate and Wireframe, so the overlay draws exactly the triangles the check counts (measured 4,784 / 2,392 / 860 faces on the overlay's input). The density drop reads at thumbnail size — a fine mesh on LOD0, large facets on LOD2, where the porthole flange collapses to an octagon — while the silhouettes stay identical, which is contract 3 made visible. One label per LOD carries the evaluated triangle count from the check.

-

Run

+

Run#

# Cheap correctness check (no render) — the CI check:
 blender --background --python lod_decimate_chain.py --
 
@@ -294,18 +353,654 @@ 

Run

# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts): blender --background --python lod_decimate_chain.py -- --output rocket.png blender --background --python lod_decimate_chain.py -- --output rocket.png --engine cycles
-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Base topology ≠ closed form
4Base bbox ≠ closed form
5LOD0 evaluated counts ≠ base
6Evaluated tris ≥ base (--no-decimate lands here)
7Original datablock mutated after evaluation
8LOD tris outside ratio bounds
9LOD bbox lost silhouette-critical dimensions
10--output framing outside the Layer 1 band (gallery_framing)
11--output asset below the quality floors (gallery_asset_quality)
12--output produced no file

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --no-decimate.

-
+

Source

examples/lod-decimate-chain/lod_decimate_chain.py - View on GitHub → + 636 lines · View on GitHub →
-
"""A retro rocket at three LODs via the Decimate modifier — a runnable example.
+      
+
"""A retro rocket at three LODs via the Decimate modifier — a runnable example.
 
 Witnesses the modifier-based LOD contract that game pipelines rely on and
 AI-generated code most often gets wrong:
@@ -942,7 +1637,41 @@ 

Source

except Exception as e: import traceback; traceback.print_exc(); print(f"FATAL: {e}", file=sys.stderr); sys.exit(1)
+
+ +
@@ -962,20 +1691,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/market-stall/index.html b/docs/gallery/market-stall/index.html index 2f9dccfa..3805b54a 100644 --- a/docs/gallery/market-stall/index.html +++ b/docs/gallery/market-stall/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

market-stall

A procedural timber market stall through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

+

Rendered headless by the showcase piece itself — click to zoom.

witnesses Recomputed: 4324 tris, three materials with 34 faces per awning stripe slot, UVs in 0..1 with zero AABB overlap, outer AABB 1.389×1.011×1.740 m, grounded zmin, 4 wrap feet, brace-counter overlap 0, both side braces seated 0.0418 m into their posts, awning seat 4 mm, post plumb 0, LOD ratios in band, convex collider 58 tris, non-empty glTF. Hygiene all zero. --skip-decimate exits 9; --stray-vert 15; --lift-z/--short-feet 16; --low-brace/--short-brace 17; --float-awning 18; --rake-posts 19.
+

tags mesh export showcase

blender --background --python showcase/market-stall/market_stall.py --
@@ -283,19 +342,19 @@

market-stall

The side braces used to start at the back post and stop at mid-depth, 0.43 m short of the front post, nearly level and joined to nothing at the front: in the side elevation they read as pegs hanging in the air. The audit that should have caught it never did, because a level brace fails its brace shape test and was never measured. Each brace now runs from the front post, just above the counter, up to the back post under the header, so the side bay is triangulated; the brace-in-post seat below asserts both ends. Wood carries grain and a tone per piece (PlankTone / GrainDir face attributes), and the awning is a matte canvas with faint dirt, not two flat paints.

It asserts budget conformance of the generated result. It does not witness an API contract. "It rendered without error" is not a check.

Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package).

-

Budgets

+

Budgets#

Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.

AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles3900–52004324 / 4324 / 4324
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.5000
LOD2 ratio0.10–0.35 of base0.2197 / 0.2197 / 0.2197
Materialsexactly 3 distinct; ≥16 faces per stripe slot3 slots; 34 / 34 stripe faces
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(1.389, 1.011, 1.740) m ± 0.01(1.3892, 1.0107, 1.7402), zmin 0
Collider tris≤ 8058
Exportwritten, size > 0, removed after measuring312400 / 312400 / 312376 bytes
-

Hygiene

+

Hygiene#

Recomputed from the generated mesh, not asserted about the script.

AxisDeclaredMeasured (all three)
Non-manifold edges00
Loose verts / edges0 / 00 / 0
Doubles merged at 1e-500
Zero-area faces00
N-gons00
Coplanar disjoint face pairs00
Grounded: zminwithin 1e-4 of 00.0000
Named supports: 4 wrap feeteach zmin ≤ 1e-34, foot_z 0.00000
Frame plan1.36 × 0.92 m ± 0.041.3600 × 0.9200
-

Joint fit and seat

+

Joint fit and seat#

AxisDeclaredMeasured (all three)
Brace-vs-counter overlap≤ 1e-6 m³0.000000
Brace-in-post seat: 2 side braces, each end past the post's inner face≥ 0.020 m0.0418
Header-post tenon engage≥ 0.4 × tenon0.0595
Awning-on-header seat−0.002–0.010 m0.00378
Post plumb (XY drift)≤ 0.008 m0.00000

DECIMATE COLLAPSE triangle counts are not identical across series — the gate is a ratio band, not an exact count. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Construction uses no RNG; the per-piece wood tone is drawn from a seeded random.Random(TONE_SEED), the same every run. Export byte counts differ by series (glTF serializer), not a gated axis.

-

Falsifiers

+

Falsifiers#

Each violates one named budget. Every falsifier was run on 4.5.11, 5.1.2 and 5.2.1 and returned the same code on each.

FlagBudget violatedExit
--skip-decimateLOD1 ratio band9
--stray-vertloose vertex count is 015
--lift-zbounding box zmin is 016
--short-feetnamed wrap-foot supports at Z=016
--low-bracebrace-vs-counter overlap17
--short-bracebrace-in-post seat (front end stops at mid-depth, -0.4181 m)17
--float-awningawning-on-header seat18
--rake-postspost plumb19
-

Run

+

Run#

blender --background --python market_stall.py --
 blender --background --python market_stall.py -- --skip-decimate
 blender --background --python market_stall.py -- --stray-vert
@@ -307,17 +366,1523 @@ 

Run

blender --background --python market_stall.py -- --rake-posts blender --background --python market_stall.py -- --output stall.png

Smoke passes no flags.

-

Exit codes

+

Exit codes#

File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path. 15–19 are the hygiene and joint-fit family.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 3 distinct slots, or stripe faces missing
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight
16Not grounded: bounding box zmin off 0, or a named wrap foot floats
17Joint fit: brace occupying the counter volume, or a side brace not seated in both posts
18Seat: awning-on-header gap
19Post plumb or frame plan off the stated real-world size
-
+

Source

showcase/market-stall/market_stall.py - View on GitHub → + 1506 lines · View on GitHub →
-
"""Game-ready market stall — a showcase piece, not an example.
+      
+
"""Game-ready market stall — a showcase piece, not an example.
 
 Asserts budget conformance of a procedural timber stall after composing
 shipped pipeline pieces: bmesh construction, UVs, three materials,
@@ -1824,7 +3389,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -1844,20 +3443,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/mesh-hygiene-audit/index.html b/docs/gallery/mesh-hygiene-audit/index.html index 4d48672d..99d5c7e2 100644 --- a/docs/gallery/mesh-hygiene-audit/index.html +++ b/docs/gallery/mesh-hygiene-audit/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

mesh-hygiene-audit

Engine-ingest mesh hygiene on every part of a flanged street valve: no ngons, no loose verts, manifold edges, no zero-area faces, contiguous and outward winding, Euler V-E+F==2 on the body casting. A dirty copy carries a hole, a flipped patch, an ngon and loose verts, each marked from live audit incidence; the paint glows red wherever the renderer sees a back face.

+

Rendered headless by the example itself — click to zoom.

witnesses Pipelines that skip topology gates ship open boundaries, flipped patches, ngons, loose verts or stray shells into engine ingest. The still shows the first four on a dirty copy; zero-area faces and a buried second shell remain check-only.
+

tags bmesh mesh game-pipeline

blender --background --python examples/mesh-hygiene-audit/mesh_hygiene_audit.py --
@@ -287,24 +346,757 @@

mesh-hygiene-audit

Version witness: the check output is identical on Blender 5.2.1 LTS, 5.1.2 and 4.5.11 LTS — same counts, same volume, same min_area.

Render as proof: a dirty copy of the same valve (left) beside the clean valve (right), same paint on both. The dirty body casting carries four real gate failures, each placed on the camera-facing side: a hole through the globe (open boundary), a flipped patch on the bonnet dome, an ngon merged into the base-flange annulus, and three stray loose vertices. The markers are read back from the dirty mesh's audit incidence, not from the staging code: boundary edges become glowing red tubes, loose verts red beads, ngon faces an amber material slot. The flipped patch and the inside of the hole need no marker — the paint mixes to red on the shader's own Backfacing output, so the renderer shows every face it sees from behind. On the clean valve that same material shows no red anywhere, because no back side is visible on a closed, outward-wound shell. The render path refuses to write a still (exit 9) if staging loses any of the four defect classes.

Not depicted in the still (check-proven only): zero-area collapse and the buried second shell — neither changes a single pixel.

-

Run

+

Run#

blender --background --python mesh_hygiene_audit.py --
 blender --background --python mesh_hygiene_audit.py -- --inject ngon
 blender --background --python mesh_hygiene_audit.py -- --inject flip_patch --inject-part Valve.HandwheelRim
 blender --background --python mesh_hygiene_audit.py -- --output hygiene.png
 blender --background --python mesh_hygiene_audit.py -- --output hygiene.png --engine cycles
-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper, 11 the shared asset-quality helper.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Ngon present (--inject ngon lands here)
4Loose vertices
5Non-manifold or boundary edges
6Zero-area faces
7Winding seam edges, or signed volume ≤ 0
8Body casting Euler characteristic ≠ 2
9--output produced no file, or dirty staging lost a defect class
10Gallery framing violation
11Gallery asset-quality floor violation

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --inject.

-
+

Source

examples/mesh-hygiene-audit/mesh_hygiene_audit.py - View on GitHub → + 733 lines · View on GitHub →
-
"""Mesh hygiene audit — engine-ingest topology as a runnable contract.
+      
+
"""Mesh hygiene audit — engine-ingest topology as a runnable contract.
 
 Witnesses the mesh-cleanliness checklist a prop pipeline relies on before
 engine ingest, on every part of an authored upright street valve (flanged
@@ -1038,7 +1830,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -1058,20 +1884,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/modular-kit-snap/index.html b/docs/gallery/modular-kit-snap/index.html index 36af3e8e..cac7f91d 100644 --- a/docs/gallery/modular-kit-snap/index.html +++ b/docs/gallery/modular-kit-snap/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

modular-kit-snap

A tiling corridor kit whose open-end boundary verts snap to the tile grid, so instances at 4 m multiples join with zero gap or overlap.

+

Rendered headless by the example itself — click to zoom.

witnesses 16 boundary verts on x∈{0,4} within 1e-6 m, linked-duplicate joint deviation 0.0, bbox == declared tile; unsnapped probe fails at 3e-3 m.
+

tags mesh bmesh game-pipeline instancing

blender --background --python examples/modular-kit-snap/modular_kit_snap.py --
@@ -287,23 +346,709 @@

modular-kit-snap

Version witness: check output is byte-identical on Blender 4.5.11 LTS and 5.1.2 — same counts, same zero measured deviations.

Render as proof: a four-segment run — the joints vanish. The falsification variant (--falsify) accumulates a 120 mm gap, 50 mm lateral jogs, and 40 mm floor steps at each joint: floor plates split with dark seams and the trim rails visibly break. The check measures the same failure class at 3 mm; the render exaggerates it to read at frame scale. The render path also gates the asset through examples/gallery_asset_quality.py (naming, material variation, edge treatment — exit 11). An earlier revision shipped flat grey wall panels and was remodeled under that gate: each panel is now an assembly (backing plate + inset panel + four bolt heads), the palette shifted teal-slate so the card does not twin with lightmap-uv-channel's warm cart, and mean luminance was brought into the calibration range (82.7 → 66.1, ceiling 77.7) — no luminance deviation needed.

Framing deviation: the still is an interior corridor run — the envelope surrounds the camera on five sides and the tiling joints are the proof, so the subject reads as extending past the frame. The helper measures; this example enforces a projection-space fill-over cap (FRAME_DEV_MAX = 8.0). Gallery camera scores ~5.06 (pass). --close-camera scores ~30.9 and exits 10. Silhouette saturates at fill 1.0, so the cap is measured with strategy="projection".

-

Run

+

Run#

blender --background --python modular_kit_snap.py --
 blender --background --python modular_kit_snap.py -- --output corridor.png
 blender --background --python modular_kit_snap.py -- --falsify seams.png
 blender --background --python modular_kit_snap.py -- --output close.png --close-camera
-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is this example's framing-deviation cap (call site, not the shared Layer 1 helper). 11 is the shared asset-quality helper.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Boundary vert count or end-plane membership
4End rings do not partition evenly, or opposing loops differ
5Tiled instances do not share boundary positions
6Shell or whole-asset bbox off the declared tile
7Boundary edge count, non-manifold edges, or rim off the end planes
8Detail part not watertight
9Detail part reaches a tile boundary; also --output produced no file
10Framing deviation score exceeds cap (--close-camera)
11Kit part count, unapplied scale, namespace, or origin; also gallery asset-quality violation

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output, --falsify, or --close-camera.

-
+

Source

examples/modular-kit-snap/modular_kit_snap.py - View on GitHub → + 686 lines · View on GitHub →
-
"""Modular kit snap — boundary vertices welded to the tile grid so segments tile seamlessly.
+      
+
"""Modular kit snap — boundary vertices welded to the tile grid so segments tile seamlessly.
 
 Witnesses the contract a tiling modular kit lives or dies by: a corridor
 segment whose open-end boundary vertices sit EXACTLY on the declared tile
@@ -990,7 +1735,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -1010,20 +1789,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/parent-inverse-orrery/index.html b/docs/gallery/parent-inverse-orrery/index.html index 273c66b3..ff525f07 100644 --- a/docs/gallery/parent-inverse-orrery/index.html +++ b/docs/gallery/parent-inverse-orrery/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

parent-inverse-orrery

Data-API parenting for a brass orrery — the keep-world idiom (child.parent = pivot; child.matrix_parent_inverse = pivot.matrix_world.inverted()) carrying arms, planets, and a two-level moon through spinning pivots.

+

Rendered headless by the example itself — click to zoom.

witnesses Bare `.parent =` really does teleport the child; the idiom restores world position exactly; matrix_world stays stale until view_layer.update(); every orbit lands on its closed form.
+

tags objects transforms

blender --background --python examples/parent-inverse-orrery/parent_inverse_orrery.py --
@@ -282,7 +341,7 @@

parent-inverse-orrery

child.parent = pivot
 child.matrix_parent_inverse = pivot.matrix_world.inverted()

What it witnesses: the check first proves the trap is real (a bare-parented probe jumps by more than half a unit), then that the idiom restores the probe's world position to within 1e-5. It also asserts the second half of the contract — matrix_world is the last-evaluated matrix, stale after any transform edit until bpy.context.view_layer.update() — and finally that every planet and the two-level moon land exactly on their closed-form orbit positions after the pivots spin.

-

Run

+

Run#

# Cheap correctness check (no render) — the CI check:
 blender --background --python parent_inverse_orrery.py --
 
@@ -292,20 +351,400 @@ 

Run

# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts): blender --background --python parent_inverse_orrery.py -- --output orrery.png blender --background --python parent_inverse_orrery.py -- --output orrery.png --engine cycles
-

Staging

+

Staging#

Render path only; the rig the checks measure is untouched. The still uses the Standard view transform: under the default AgX the orange sun clipped to a peach-white bulb and read as a lamp. Lighting follows the house recipe (shaped key, faint fill, rim, warm wedge on the back wall) on the dark matte studio floor; the old rig (key 1500 W, fill 500 W, a glossy grey floor) lit the frame to mean luma 0.33, above the gallery's calibration band. The render path gates framing through gallery_framing.check_framing (exit 10), which it never did before.

-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Bare parenting did not jump
4Keep-world idiom off
5Stale matrix_world contract broken
6Planet off closed-form orbit (--skip-mpi lands here)
7Moon off closed-form orbit
8--output produced no file
10Gallery framing violation (render path only)

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --skip-mpi.

-
+

Source

examples/parent-inverse-orrery/parent_inverse_orrery.py - View on GitHub → + 380 lines · View on GitHub →
-
"""A brass orrery parented through the data API — a runnable example.
+      
+
"""A brass orrery parented through the data API — a runnable example.
 
 Witnesses the object-parenting contract that generated code gets wrong most
 often. Assigning `child.parent = pivot` alone re-interprets the child's local
@@ -686,7 +1125,41 @@ 

Source

except Exception as e: import traceback; traceback.print_exc(); print(f"FATAL: {e}", file=sys.stderr); sys.exit(1)
+
+ +
@@ -706,20 +1179,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/park-bench/index.html b/docs/gallery/park-bench/index.html index dd8ba4f7..7c166c4b 100644 --- a/docs/gallery/park-bench/index.html +++ b/docs/gallery/park-bench/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

park-bench

A procedural wrought-iron park bench whose legs are each one bent bar from scroll toe to reclined back, through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

+

Rendered headless by the showcase piece itself — click to zoom.

witnesses Recomputed: 3272 tris, two materials with 1008 metal and 756 wood faces, UVs in 0..1 with zero AABB overlap, outer AABB 1.295×0.632×0.887 m, grounded zmin, 4 named toes, 4 legs each one bar from toe to seat, back reclined 12°, 28 slat and armrest bites of at least 2.0 mm, stretcher gap 1.1 mm, seat span 1.272 m, LOD ratios in band, convex collider 64 tris, hygiene zero, non-empty glTF. --skip-decimate exits 9; --stray-vert 15; --lift-z/--short-feet 16; --float-stretcher 17; --gap-slats 18; --narrow-seat/--upright-back 19; --float-slats 20; --split-feet 21.
+

tags mesh export showcase

blender --background --python showcase/park-bench/park_bench.py --
@@ -283,22 +342,22 @@

park-bench

It asserts budget conformance of the generated result. It does not witness an API contract. "It rendered without error" is not a check.

Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package). Hygiene combinatorics match examples/mesh-hygiene-audit (copied, not imported).

Intended size: 1.40 m sitting width, 0.50 m seat depth, 0.43 m seat height, 0.64 m arm height, back reclined 12°. Outer AABB 1.295 × 0.632 × 0.887 m.

-

Budgets

+

Budgets#

Declared as named constants. Every gate recomputes its value from the mesh, materials, UVs, evaluated LOD, collider or export file.

AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles2900–37003272 / 3272 / 3272
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.5000
LOD2 ratio0.10–0.35 of base0.2194 / 0.2194 / 0.2194
Materialsexactly 2 distinct, ≥48 metal, ≥24 wood2 slots, 1008 metal / 756 wood
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(1.295, 0.632, 0.887) m ± 0.015(1.2945, 0.6316, 0.8870), zmin 0
Collider tris≤ 18064
Exportwritten, size > 0245620 / 245620 / 245604 bytes

Base triangles rose from 2520 to 3272 in the second quality pass. The square posts and 6-gon scroll tubes became 8-gon round bar swept along each leg, the back gained its bend, and armrest brackets were added. The band was re-fitted around the measurement and narrowed from 2200–4200.

DECIMATE COLLAPSE triangle counts are not guaranteed identical across series — the gate is a ratio band, not an exact count. This mesh matched on 4.5.11 / 5.1.2 / 5.2.1. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Slat widths are closed-form and plank tones are seeded, so repeated runs on one binary print identical measurements. Export byte counts differ by 16 B on 5.2.1 (glTF serializer), not a gated axis.

-

Hygiene

+

Hygiene#

Recomputed from the generated mesh, not asserted about the script.

AxisDeclaredMeasured (all three)
Non-manifold edges00
Loose verts / edges0 / 00 / 0
Doubles merged at 1e-500
Zero-area faces00
N-gons00
Coplanar disjoint face pairs00
Grounded: zminwithin 1e-4 of 00.0000
Named toes4, each zmin ≤ 0.0024, 0.00000
-

Joint fit, bearing and form

+

Joint fit, bearing and form#

AxisDeclaredMeasured (all three)
Stretcher-to-leg BVH gap≤ 0.008 m0.00111
Slat-to-leg BVH gap≤ 0.008 m0.00016
Seat span vs 2·hx± 0.08 m1.2720 vs 1.260
Bearing bite (exit 20): 7 seat slats × 2 side rails, 5 back slats × 2 uprights, 2 armrests × (front leg + arm bar), overlap depth either way round28 bites, each ≥ 0.0015 m28, min 0.00201
Legs in one piece (exit 21): the shell under each toe station is a leg reaching the seat44
Back recline (exit 19): each upright, from a low and a high slab centroid9°–16°12.00° / 12.00°

The bearing bite takes the overlap either way round. A round rail has vertices only at its end rings, so under a mid-span slat it is the slat's corner that sits in the rail, not a rail vertex in the slat.

-

Falsifiers

+

Falsifiers#

Each violates one named budget. All were run on 4.5.11, 5.1.2 and 5.2.1 and returned the same code on each.

FlagBudget violatedExit
--skip-decimateLOD1 ratio band9
--stray-vertloose vertex count is 015
--lift-zbounding box zmin is 016
--short-feetnamed toes plant at z=016
--float-stretcherstretcher-to-leg BVH gap17
--gap-slatsslat-to-leg BVH gap18
--narrow-seatseat span vs 2·hx19
--upright-backback recline band (measured 0.00°)19
--float-slatsbearing bite (seat slats 6 mm up; measured −0.00354)20
--split-feetlegs in one piece (measured 0 of 4)21

--short-feet lifts only the outward toe verts. After recentring, the bottom of the scroll is the AABB zmin, so the AABB gate would still pass. The named-toe stations fail. --split-feet restores the first build: each foot is an arc stopped short of its post, and each post starts at the arc's top. The toes still ground and every gap budget still passes. Only the one-piece budget sees the 14 mm of daylight. --upright-back keeps the back top where it was, so the envelope does not move.

-

Run

+

Run#

blender --background --python park_bench.py --
 blender --background --python park_bench.py -- --skip-decimate
 blender --background --python park_bench.py -- --stray-vert
@@ -312,17 +371,1523 @@ 

Run

blender --background --python park_bench.py -- --split-feet blender --background --python park_bench.py -- --output bench.png

Smoke passes no flags.

-

Exit codes

+

Exit codes#

File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path. 15–19 are the hygiene and joint-fit family. 20 and 21 are this piece's own.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 2 distinct slots, or wood/metal faces missing
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Hygiene (--stray-vert lands here)
16Grounded zmin / named toes (--lift-z, --short-feet)
17Stretcher-to-leg gap (--float-stretcher)
18Slat-to-leg gap (--gap-slats)
19Seat span (--narrow-seat) or back recline (--upright-back)
20Bearing bite on slats and armrests (--float-slats)
21Legs not bent in one piece from toe to seat (--split-feet)
-
+

Source

showcase/park-bench/park_bench.py - View on GitHub → + 1506 lines · View on GitHub →
-
"""Game-ready park bench — a showcase piece, not an example.
+      
+
"""Game-ready park bench — a showcase piece, not an example.
 
 Asserts budget conformance of a procedural wrought-iron bench after
 composing shipped pipeline pieces: bmesh construction, UVs, two
@@ -1829,7 +3394,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -1849,20 +3448,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/png-exr-alpha/index.html b/docs/gallery/png-exr-alpha/index.html index 697a4a0d..06b14bbb 100644 --- a/docs/gallery/png-exr-alpha/index.html +++ b/docs/gallery/png-exr-alpha/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

png-exr-alpha

Float-image PNG save trap — float_buffer=True Image.save() writes RGBA16 and unpremultiplies as if associated-alpha, clamping straight-authored dark values at low alpha to white (closed-form err 0.98 at RGB 0.02 / a=1/255).

+

Rendered headless by the example itself — click to zoom.

witnesses float→PNG IHDR is RGBA16 and matches q16(min(1, c/q16(a))); OpenEXR round-trips within 1e-5; byte→PNG is straight RGBA8; EXR color_mode=RGB drops alpha to opaque.
+

tags images rendering alpha

blender --background --python examples/png-exr-alpha/png_exr_alpha.py --
@@ -286,7 +345,7 @@

png-exr-alpha

  • RGBA16 IHDR — float PNG bit-depth or color-type change (falsified expectation: 8-bit would exit 4).
  • Error floor — float PNG path stops destroying straight mid-tones / dark values at low alpha (exit 5).
  • Closed-form residual — encoding no longer matches false-unpremul+clamp (falsified once by swapping in the byte straight-alpha model; exited 6 with residual 0.9803922).
  • EXR fidelity — OpenEXR float RGBA round-trip regressing (exit 7).
  • Byte straight path — byte images starting to false-unpremul, or bit-depth flipping to 16 (exits 9–11).
  • RGB EXR alpha drop — color_mode='RGB' preserving authored alpha (exit 12).

Version divergence: none probed on the save/reload contracts above — they assert on Blender 5.1.1 locally. Blender 4.5 LTS is exercised by the blender-smoke CI job (4.5 is not installed on this authoring host; do not treat a 4.4 run as a 4.5 substitute). The only version gate in the file is the EEVEE engine id for the optional render (BLENDER_EEVEE_NEXT on 4.x, BLENDER_EEVEE on 5.x).

Render: two framed verification displays on a shared plinth — the left face bakes the closed-form PNG mangling (dark rows flash to white at low-alpha columns), the right face shows the authored straight buffer (EXR-clean); nameplates read FLOAT → PNG and FLOAT → EXR. If the contract failed, both panels would read the same.

-

Run

+

Run#

# Cheap correctness check (no render) — the CI check:
 blender --background --python png_exr_alpha.py --
 
@@ -296,18 +355,703 @@ 

Run

# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts): blender --background --python png_exr_alpha.py -- --output alpha.png blender --background --python png_exr_alpha.py -- --output alpha.png --engine cycles
-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is also the shared framing helper.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage; also closed-form palette no longer stresses unpremul
3float_buffer=True image reports is_float=False
4Float→PNG IHDR is not RGBA16
5Float→PNG RGB error below floor (--opaque-alpha lands here)
6Float→PNG disagrees with closed-form false-unpremul model
7Float→EXR round-trip above tolerance
8float_buffer=False image reports is_float=True
9Byte→PNG IHDR is not RGBA8
10Byte→PNG disagrees with straight-alpha 8-bit model; also gallery framing violation
11Byte→PNG stress cell looks false-unpremul-mangled
12EXR color_mode=RGB alpha-drop contract
13--output produced no file

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --opaque-alpha.

-
+

Source

examples/png-exr-alpha/png_exr_alpha.py - View on GitHub → + 685 lines · View on GitHub →
-
"""PNG vs EXR alpha round-trip — a runnable example.
+      
+
"""PNG vs EXR alpha round-trip — a runnable example.
 
 Witnesses that a `float_buffer=True` image saved via `Image.save()` to PNG
 is written as **16-bit RGBA** and, critically, is **unpremultiplied as if
@@ -993,7 +1737,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -1013,20 +1791,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/prop-origin-transform/index.html b/docs/gallery/prop-origin-transform/index.html index f9259d03..53a585cd 100644 --- a/docs/gallery/prop-origin-transform/index.html +++ b/docs/gallery/prop-origin-transform/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

prop-origin-transform

Street pedestal origin-to-base-center + data-API scale apply + matrix_parent_inverse for a flanged conduit elbow. After bake: scale (1,1,1), local min.z==0, world AABB unchanged. Bare parent throws the elbow off its mount; MPI keeps it seated.

+

Rendered headless by the example itself — click to zoom.

witnesses Bare child.parent = parent teleports accessories off their mounts; skipping view_layer.update() before matrix_world, or leaving non-uniform scale / centered origin, breaks engine ingest placement.
+

tags transforms parenting game-pipeline

blender --background --python examples/prop-origin-transform/prop_origin_transform.py --
@@ -287,23 +346,822 @@

prop-origin-transform

Version witness: identical numbers on Blender 5.2.1 LTS, 5.1.2 and 4.5.11 LTS.

The asset. The pedestal is one mesh with five materials: a precast concrete footing, a galvanised base plate with anchor nuts, a green cabinet on a black kick skirt, a hipped lid, a door with hinges, a T-handle and a hazard plate, louvred side vents, a conduit boss on the +X face and a sleeve in the pad. It is authored the way an import often arrives: origin at the geometric centre and coordinates divided by a non-uniform object scale. The bake lands it on exactly the designed shape. The accessory is a flanged PVC conduit elbow with bolt heads, a strap clamp and a coupling that drops into the sleeve.

The render. Two baked pedestals stand on a sidewalk slab. On the left the elbow is parented with MPI and stays bolted to its boss, seated in the sleeve. On the right, bare child.parent = parent applies the pedestal's transform a second time. The elbow is thrown 0.65 m, past the slab edge and into the air, and a glowing cyan outline (a Wireframe modifier on a copy) marks the seat it left. The slab raises both origins 0.20 m, so the doubled translation lifts the elbow instead of sliding it along the floor. A pivot ring on the floor is centred on each pedestal's origin, with red notches on its X axis and green notches on Y. The origin is under the base, so the ring frames it. There are no text labels: the ghost and the stranded elbow carry the proof. The check's closed forms are unchanged.

-

Run

+

Run#

blender --background --python prop_origin_transform.py --
 blender --background --python prop_origin_transform.py -- --skip-mpi
 blender --background --python prop_origin_transform.py -- --output origin.png
 blender --background --python prop_origin_transform.py -- --output origin.png --engine cycles
-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Stale matrix_world contract broken
4World bbox moved across bake
5Scale after bake is not (1,1,1)
6Origin not at local base center
7Bare parenting did not jump
8MPI did not restore world location (--skip-mpi lands here)
9--output produced no file
10Gallery framing violation
11Gallery asset-quality violation (render path only)

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --skip-mpi.

-
+

Source

examples/prop-origin-transform/prop_origin_transform.py - View on GitHub → + 799 lines · View on GitHub →
-
"""Prop origin transform — base-center origin + scale apply + MPI accessory.
+      
+
"""Prop origin transform — base-center origin + scale apply + MPI accessory.
 
 Witnesses the object-level contract a prop pipeline relies on before engine
 ingest: origin at the local base center, scale applied through the data API
@@ -1103,7 +1961,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -1123,20 +2015,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/rope-bridge/index.html b/docs/gallery/rope-bridge/index.html index 6d2abce6..2a23588e 100644 --- a/docs/gallery/rope-bridge/index.html +++ b/docs/gallery/rope-bridge/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,40 +325,42 @@

rope-bridge

A procedural rope footbridge — log posts, through-tenoned sills, twenty planks on two sagging foot ropes, lashed hand ropes, suspenders and staked tie-offs — carried through UVs, bake, LOD, compound collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

+

Rendered headless by the showcase piece itself — click to zoom.

witnesses Recomputed: 8768 tris, two materials with 1356 timber and 3348 rope faces, UVs in 0..1 with zero AABB overlap and 18.8 baked texels per island, outer AABB 4.619×1.073×1.480 m, zmin 0, hygiene 0 including zero coplanar cross-shell pairs, four posts and four stakes grounded, every rope end and sill tenon biting its host, all 20 planks seated 4.0 mm on both foot ropes, 12 lashing turns hooped 2.4 mm, one connected assembly of 60 shells, posts plumb, plank pitch within 0.08 mm, hand ropes 0.87 m over the deck, and every plank top within 2.97 mm of a least-squares parabola with a 0.220 m sag. --vee-deck exits 19 on the parabola fit with its sag still in band; --float-suspenders exits 18 on the contact graph.
+

tags mesh export showcase

blender --background --python showcase/rope-bridge/rope_bridge.py --

A rope footbridge — two pairs of log posts, a squared sill through-tenoned across each end, twenty planks laid on two foot ropes that hang between the sills, two hand ropes lashed to the posts, suspenders tying hand rope to foot rope, and the foot ropes run back over the sills to ground stakes. A showcase piece, not an example — it witnesses no API contract. It asserts that generated geometry meets declared asset budgets, recomputed from the finished mesh.

-

What it composes

+

What it composes#

Shipped contentUsed for
skills/mesh-editing-and-bmeshlathed logs, laid-rope sweeps, chamfered boards, UVs in one bmesh
skills/procedural-materials-and-shaderstimber grain along each member (GrainDir), per-member tone (PlankTone), hemp with a fibre bump
skills/bake-high-to-lowCycles tangent-space normal bake, high onto low
skills/engine-export-presetsUnity glTF (export_yup=True)
skills/depsgraph-and-evaluated-dataevaluated triangle counts for the LOD ratios
snippets/decimate_to_budget.pyLOD1 / LOD2 COLLAPSE chain
snippets/lod_chain.pyLOD naming and ratio pattern
examples/mesh-hygiene-audithygiene combinatorics (copied, not imported)

snippets/convex_hull_collider.py is deliberately not used; see Collider below.

-

The budget that matters

+

The budget that matters#

A rope bridge can fail invisibly. A deck of planks laid on two straight ramps that meet at midspan has the same ends, the same sag, the same planks and the same bounding box as one that hangs; only the shape is wrong. A uniformly loaded cable hangs as a parabola, so the piece fits a least-squares parabola to the plank top-face centres read off the finished mesh — not to the function the generator used — and asserts every plank lands within 6 mm of it, with the fitted sag within 15 mm of the declared 0.22 m.

--vee-deck is the falsifier built for exactly this. It keeps the sill heights, the sag, the plank count, the pitch, every seat and the envelope, and swaps the parabola for two straight ramps. Its fitted sag is 0.215 m — inside the sag tolerance — and every other budget passes. Only the residual sees it: 32.5 mm against a 6 mm band.

The measured residual on the true deck is 2.97 mm, not zero, and that is the model rather than noise: each plank's underside is set from the foot rope's own reach at its station (below), and the three-strand lay turns under the deck, so the planks ride a few millimetres up and down on the strands.

-

Budgets

+

Budgets#

Declared in the script as named constants, recomputed from the generated mesh. Measured values are from Blender 5.2.1; every one is byte-identical on 4.5.11 and 5.1.2.

BudgetBandMeasured
Base triangles8300–92008768
LOD1 ratio0.32–0.620.5000
LOD2 ratio0.10–0.350.2199
Material slotsexactly 2, distinct2
Timber faces≥ 11001356
Rope faces≥ 28003348
UV boundsinside 0..1(0.0016, 0.0016)–(0.9984, 0.9984)
UV AABB overlap≤ 1e-50.000000
Baked texels per UV island≥ 1218.84 (610 islands, 512 px)
Outer AABB4.619 × 1.073 × 1.480 m ± 0.0204.6185 × 1.0733 × 1.4800
Collider triangles≤ 400360
Normal bake{'FINISHED'} with image data{'FINISHED'}, has_data=True
glTF exportfile written, non-empty~300 kB
Hygieneall zeroloose 0/0, non-manifold 0, zero-area 0, doubles 0, n-gons 0, coplanar cross-shell pairs 0
Grounded AABB\zmin\≤ 1e-40.00000
Named supports4 posts + 4 stakes, each zmin ≤ 1e-38 at 0.00000
Hand-rope ends inside their postsdeepest vertex ≥ 0.030 m0.05669
Foot-rope ends inside their stakesdeepest vertex ≥ 0.020 m0.03347
Sill tenons inside their postsdeepest vertex ≥ 0.010 m0.01980
Plank seat on each foot rope20 planks, bite 0.002–0.008 m0.00400 on all 40
Lashing hoop12 turns, bite 0.0005–0.005 m0.00238–0.00246
One connected assembly1 component1 (60 shells)
Plumb (posts and stakes)bottom-to-top slab centroid ≤ 0.004 m0.00000
Deck parabolaevery plank top within 0.006 m; sag 0.22 ± 0.015 m0.00297; 0.22037
Plank pitchevery gap within 0.003 m of the mean0.00008 (mean 0.1689)
Rail height at midspan0.80–1.00 m above the deck0.86521
Right-angle edges00

Real-world size: a 3.6 m span between post centres carrying a 3.2 m walkable deck of 0.80 m planks, hand ropes 0.87 m over the deck at midspan, posts 1.48 m tall — a garden or gorge footbridge module, 4.6 m overall with its stakes.

-

Construction

+

Construction#

  • Laid rope. Every rope is a three-lobed section turned one vertex step per ring, so each lobe winds as a strand. The main ropes use 9 vertices; suspenders and lashings use 6. The section frame is fixed to each path's plane: +Y for the ropes that run the span, +X for the suspenders, +Z for the lashings.
  • Rope ends close in a blunt cone. A fan cap maps to overlapping UV triangles. A cone half the radius long maps as one more row of the rope's strip island, so every rope is a single UV island with no overlap.
  • The foot rope wraps the sill with one mitred ring. The bend radius over the arris is smaller than the rope. With a ring every few degrees of arc the section folded through itself and left six right-angle creases, which the edge budget found.
  • Planks are seated from the host. Each plank's underside is the foot rope's own reach along the plank normal at its station, less PLANK_BITE. The bite is therefore exact (4.00 mm on all 40 seats) whatever the lay does under the plank.
  • Lashings are hooped. Each turn's inner radius is the post's (or stake's) own radius at that height, taken from its lathe profile, less LASH_BITE. The two turns on each post are rotated half a vertex step against each other. Stacked identically, their outer faces shared planes and the coplanar budget counted 84 pairs.
  • Collider. A single convex hull over a sagging deck is a lens whose top is the chord between the sills: a character would walk 0.22 m above the planks at midspan. The collider is a compound, one oriented box per plank, sill, post and stake (30 boxes, 360 triangles). Ropes are left out.
-

Conventions walked

+

Conventions walked#

Every convention in showcase/README.md, and whether it applies here.

ConventionAppliesHow
Deterministic, budgets declared, assertions recomputeyesseed 31; every value above is read off the mesh
Falsifier fails the budget it targetsyestable below, proven on all three binaries
Hygiene incl. cross-shell coplanaryesexit 15; KD-tree range query, cross-shell
Named supportsyes4 posts + 4 stakes (--float-post)
Joint-fit / a joint bitesyesrope ends and sill tenons, deepest vertex by signed distance (--short-rails)
Diagonal from stationsyessuspenders run from the foot-rope centre to the hand-rope centre at a plank gap
Even shaping terms / mirror symmetrynothe laid rope is chiral: a right-hand lay mirrors to a left-hand one, so the body has no mirror partner by design
Wrappers follow the host's profileyeslashings take the lathe profile's radius at their height
Seat conformance (banded)yesplank seats and lashing hoops, both banded (--float-planks, --loose-lashings)
Plumb and real-world sizeyesposts and stakes plumb (--lean-post); rail height (--slack-rails)
Band hooped, never flushyeslashings bite 2.5 mm
Member tenoned into its seatyessills end at the post centres
Segment counts are a silhouette budgetyes16-segment posts, 12-segment stakes, smooth-shaded
Material face floorsyestimber ≥ 1100, rope ≥ 2800
Shading is part of the modelyesposts, stakes and ropes smooth (round, organic); planks and sills flat (sawn)
One substance, one slotyestimber (posts, sills, planks, stakes), rope
Edge treatment: no right anglesyesexit 20 (--sharp-plank)
Sort bmesh operator inputsyesbevel edges sorted by index
Variation into surface, never functionyesplank length, tone and post wobble vary; plank pitch is asserted (--drift-planks)
A platform bears on somethingyesevery plank seats on both foot ropes, counted
One connected assemblyyesexit 18 (--float-suspenders)
Bake texels per UV cellyesexit 21 (--low-bake)
Bake cage narrower than the nearest neighbouryesCAGE_EXTRUSION 0.01 m
Rope is laid, not pipedyesabove
Identical boards read as CGyesPlankTone and GrainDir per shell
Level on the stage; stage 60 myesturned about Z only; 60 m floor and wall
Keep a falsifier's envelope stillyesworst deltas: --float-post +12 mm Z, --loose-lashings +13 mm X/Y, against 20 mm
Masonry, vessels, scatter, fixtures, roofs, forgings, rings, iron, paintnothe piece has no stone, vessel, scatter, plate, roof, forging, ring hardware or paint
-

Falsifiers

+

Falsifiers#

Each breaks one pipeline stage so a named budget fails. All fourteen were run on 4.5.11, 5.1.2 and 5.2.1 and exited the same declared code on all three.

FlagTarget budgetBreaksExit
--skip-decimateLOD1 ratiodrops the DECIMATE modifiers, LOD1 ratio goes to 1.00009
--stray-vertmesh hygieneadds one loose vertex above the deck, inside the envelope15
--lift-zgrounded zminlifts the whole mesh 50 mm16
--float-postnamed supportsfloats one post 12 mm; the others still ground the AABB16
--short-railsjoint bitestops each hand rope 20 mm short of its post; deepest end vertex −19.3 mm17
--float-planksplank seatlifts every plank 7 mm off the ropes; bite −3.0 mm18
--loose-lashingslashing hoopsizes every lashing 6.5 mm wider; bite −4.0 mm18
--float-suspendersone connected assemblystops both ends of every suspender 30 mm short; 15 components18
--lean-postplumbleans one post 20 mm at the top, lashings and all; 19.4 mm off plumb19
--vee-deckdeck parabolatwo straight ramps with the same ends and sag; 32.5 mm off the fit19
--drift-planksplank pitchshifts alternate planks ±15 mm along the rope; 31.7 mm off the mean gap19
--slack-railsrail heighthangs the hand ropes 0.40 m instead of 0.10 m; rail 0.565 m19
--sharp-plankedge treatmentleaves the middle plank unchamfered; 12 right-angle edges20
--low-bakebaked texels per UV islandbakes at 256 px; 9.42 texels21
-

Exit codes

+

Exit codes#

File-local and sequential. 9 is a valid check code. 1 is the FATAL wrapper — a crash, never a named check.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build, or has no UV layer
4Base triangle count outside band
5Material slots, or a material's face floor
6UVs outside 0..1
7UV AABB overlap above tolerance
8Outer AABB off declared size
9LOD1 or LOD2 ratio outside band (--skip-decimate)
10Framing gate (examples/gallery_framing.py, render path only)
11Collider triangles above ceiling
12Normal bake failed or produced no image data
13glTF export missing or empty
14--output produced no file
15Mesh hygiene (--stray-vert)
16Grounded zmin, or a named support floating (--lift-z, --float-post)
17A rope end or sill tenon not biting its host (--short-rails)
18Plank seat, lashing hoop, or the contact graph (--float-planks, --loose-lashings, --float-suspenders)
19Plumb, deck parabola, plank pitch or rail height (--lean-post, --vee-deck, --drift-planks, --slack-rails)
20Right-angle edges (--sharp-plank)
21Baked texels per UV island below floor (--low-bake)
-

Run it

+

Run it#

# Budget check, no render. ~2.2 s on 4.5, ~2.0 s on 5.1, ~2.2 s on 5.2.
 blender --background --python rope_bridge.py --
 
@@ -314,17 +373,1859 @@ 

Run it

# Render the gallery still (EEVEE; --engine cycles on a GPU-less host). blender --background --python rope_bridge.py -- --output bridge.webp

Smoke runs the check-only path. It does not pass --output or any falsifier.

-

Cross-version measurements

+

Cross-version measurements#

Value4.5.115.1.25.2.1
Base triangles876887688768
LOD1 tris / ratio4384 / 0.5000samesame
LOD2 tris / ratio1928 / 0.2199samesame
Face counts (timber / rope)1356 / 3348samesame
Outer AABB4.6185 × 1.0733 × 1.4800samesame
Collider tris360360360
Deck fit residual / sag0.00297 / 0.22037samesame
glTF bytes300212300212300212
Check wall-clock~2.2 s~2.0 s~2.2 s

DECIMATE COLLAPSE is the usual cross-version suspect. Here it happens to produce identical LOD counts on all three binaries; the gate is still a ratio band, not an exact count.

-
+

Source

showcase/rope-bridge/rope_bridge.py - View on GitHub → + 1842 lines · View on GitHub →
-
"""Game-ready rope bridge — a showcase piece, not an example.
+      
+
"""Game-ready rope bridge — a showcase piece, not an example.
 
 Asserts budget conformance of a procedural rope footbridge: two log posts
 and a sill at each end, twenty planks laid on two foot ropes that hang
@@ -2167,7 +4068,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -2187,20 +4122,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/shader-node-group/index.html b/docs/gallery/shader-node-group/index.html index 8c4b076f..51f846f6 100644 --- a/docs/gallery/shader-node-group/index.html +++ b/docs/gallery/shader-node-group/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

shader-node-group

One reusable shader group declared via tree.interface.new_socket, instanced in five materials with different Tint values — one dipped-glaze group, five stoneware mugs, five colors.

+

Rendered headless by the example itself — click to zoom.

witnesses Grouping contract: interface sockets appear on every instance, all five materials share one group datablock (users == 5), and per-material parameters live on the group node, not inside the tree.
+

tags materials node-groups

blender --background --python examples/shader-node-group/shader_node_group.py --
@@ -281,9 +340,9 @@

shader-node-group

A runnable example that declares a reusable DippedGlaze shader group through tree.interface.new_socket — the 4.x/5.x API that replaced tree.inputs/tree.outputs — and instances it in five mug materials with different parameters, following procedural-materials-and-shaders and the shader-node-group snippet.

What it witnesses: the grouping contract. Sockets declared on the interface appear on every group-node instance; all five materials share ONE group datablock (users == 5); and the per-material Tint lives on the group node, not inside the group — set it inside the tree and every mug changes at once.

The render is the proof. The group is a dipped stoneware glaze: bare buff clay at the foot, a wavy dip line, iron speckle through clay and glaze alike, and a glaze that breaks back toward clay over the rim. All of that lives inside the group, so the five mugs — a warm-to-cool lineup of oxblood, amber, celadon, teal and cobalt — carry the same foot band, the same dip line, the same speckle field and the same rim break. Only the instance-level Tint differs. If Tint were baked into the group, the lineup would be one colour; if each material built its own tree, the shared character would be a copy rather than one datablock (the check's users gate).

-

Staging

+

Staging#

A staggered shop-display lineup on the dark house stage: the odd mugs stand on a low walnut riser behind the even ones, so five mugs fill the frame without shrinking to a thin strip. Handles all turn the same way so the only thing that changes along the row is colour. The still renders under the Standard view transform (AgX would wash the five tints toward pastel); the key's spread is narrowed so it lights the mugs, not the back wall, and a warm wedge sits between the riser and the wall. Render path only.

-

Run

+

Run#

# Cheap correctness check (no render) — the CI check:
 blender --background --python shader_node_group.py --
 
@@ -293,18 +352,432 @@ 

Run

# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts): blender --background --python shader_node_group.py -- --output mugs.png blender --background --python shader_node_group.py -- --output mugs.png --engine cycles
-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Interface sockets missing Tint / Roughness / Shader
4Group datablock users ≠ number of mug materials (5)
5Instance points at a different node tree
6Instance Tint values not pairwise distinct (--same-tint lands here)
7--output produced no file
10Gallery framing violation

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --same-tint.

-
+

Source

examples/shader-node-group/shader_node_group.py - View on GitHub → + 414 lines · View on GitHub →
-
"""One shader node group, five materials — a runnable example.
+      
+
"""One shader node group, five materials — a runnable example.
 
 Witnesses the node-group socket workflow from procedural-materials-and-shaders:
 group sockets are declared through `tree.interface.new_socket` (the 4.x/5.x
@@ -719,7 +1192,41 @@ 

Source

except Exception as e: import traceback; traceback.print_exc(); print(f"FATAL: {e}", file=sys.stderr); sys.exit(1)
+
+ +
@@ -739,20 +1246,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/shape-key-blend/index.html b/docs/gallery/shape-key-blend/index.html index 220f73ed..3b8dd633 100644 --- a/docs/gallery/shape-key-blend/index.html +++ b/docs/gallery/shape-key-blend/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

shape-key-blend

A relative Tall shape key that turns a squat ceramic jar into a trumpet vase — lifting and flaring the rim — authored via shape_key_add / key_blocks / .value and read back from the depsgraph-evaluated mesh.

+

Rendered headless by the example itself — click to zoom.

witnesses Shape keys do not rewrite mesh.vertices: every evaluated vert matches basis + value × (key − basis), including the lifted, flared rim ring.
+

tags mesh shape-keys

blender --background --python examples/shape-key-blend/shape_key_blend.py --
@@ -280,9 +339,9 @@

shape-key-blend

A runnable example that authors a relative shape key entirely through the data API — shape_key_add, key_blocks["Tall"].data (bulk foreach_get / foreach_set on the key's co), and .value — then reads the blend back from the depsgraph-evaluated mesh. The subject is a lathe-turned ceramic vase: its Basis is a squat jar, and the Tall key lifts the rim (stretching the neck), flares the lip into a trumpet, and slims the belly.

What it witnesses: shape keys do not rewrite mesh.vertices. The undeformed mesh stays at Basis (every vertex, plus the rim ring at z = 1.0, r = 0.335); every evaluated vertex matches the closed-form blend co = basis + value × (key − basis). The check also pins the rim ring — lifted to 1.0 + value × 1.5 and flared to 0.335 + value × 0.38 — with the foot still on the floor, so a uniform scale or a lift without the flare cannot pass.

-

The render

+

The render#

The same vase at Tall.value 0, 0.5 (the checked object) and 1, left to right: squat jar, amphora, trumpet vase. The ochre band is part of the lathe profile, so it rides the key — thin on the jar's shoulder, stretched tall and higher on the full key. Around the full-key vase, three thin pale rings are read straight from the Basis key block: the jar's belly it was blended from. If the blend broke, the three vases would render as three identical jars.

-

Run

+

Run#

# Cheap correctness check (no render) — the CI check:
 blender --background --python shape_key_blend.py --
 
@@ -292,18 +351,488 @@ 

Run

# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts): blender --background --python shape_key_blend.py -- --output blend.png blender --background --python shape_key_blend.py -- --output blend.png --engine cycles
-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 and 11 are the shared render-path gates' own codes.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3No shape keys on mesh
4Key names ≠ Basis, Tall
5Tall.value ≠ 0.5 (--zero-blend lands here)
6Undeformed mesh.vertices not at Basis
7Evaluated vert off closed-form blend
8Evaluated Z span off closed form
9Rim radius (flare) off closed form
10Framing gate (gallery_framing, --output only)
11Asset-quality gate (gallery_asset_quality, --output only)
12--output produced no file

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --zero-blend.

-
+

Source

examples/shape-key-blend/shape_key_blend.py - View on GitHub → + 470 lines · View on GitHub →
-
"""Relative shape-key blend via the data API — a runnable example.
+      
+
"""Relative shape-key blend via the data API — a runnable example.
 
 Witnesses that shape keys are authored and driven on the mesh datablock
 (`shape_key_add`, `key_blocks["Tall"].data`, `.value`), not through
@@ -774,7 +1303,41 @@ 

Source

except Exception as e: import traceback; traceback.print_exc(); print(f"FATAL: {e}", file=sys.stderr); sys.exit(1)
+
+ +
@@ -794,20 +1357,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/shipping-crate/index.html b/docs/gallery/shipping-crate/index.html index 1b01a9aa..e4eed5e2 100644 --- a/docs/gallery/shipping-crate/index.html +++ b/docs/gallery/shipping-crate/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

shipping-crate

A procedural shipping crate through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

+

Rendered headless by the showcase piece itself — click to zoom.

witnesses Recomputed: 2508 tris, two materials with 1170 wood and 280 metal faces, UVs in 0..1 with zero AABB overlap, outer AABB 1.146×0.726×0.612 m, grounded zmin, 3 skids, handle-end and slat-post join 0, mesh hygiene all zero, LOD ratios in band (5.2 COLLAPSE leaner on LOD2), convex collider 76 tris, non-empty glTF. --skip-decimate exits 9; --stray-vert 15; --lift-z/--short-skids 16; --float-handle 17; --omit-slats 18.
+

tags mesh export showcase

blender --background --python showcase/shipping-crate/shipping_crate.py --
@@ -282,24 +341,24 @@

shipping-crate

Skids sit under the posts so the outer corner is a mortise, not a cave. Handle bails use quarter-circle corners so a 90-degree loft cannot sit the bar on the plate.

It asserts budget conformance of the generated result. It does not witness an API contract. "It rendered without error" is not a check.

Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package).

-

Budgets

+

Budgets#

Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.

AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles2900–39003388 / 3388 / 3388
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.5000
LOD2 ratio0.10–0.35 of base0.2196 / 0.2196 / 0.2196
Materialsexactly 2 distinct; ≥900 wood, ≥600 metal faces2 slots; 1170 / 904
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(1.146, 0.726, 0.612) m ± 0.015(1.1460, 0.7260, 0.6120), zmin 0
Collider tris≤ 220116
Exportwritten, size > 0273816 / 273816 / 273800 bytes

Base triangles rose from 624 to 2508 in the quality pass: the old beveled cube with glued-on slats became a post-and-slat crate with tenons, sills, L-straps, and pipe handles. The second pass took them to 3388: single-shell chamfered straps, 24 nails, chamfered mounting plates.

DECIMATE COLLAPSE triangle counts are not guaranteed identical across series; on this mesh they happen to match (LOD2 744 on all three). The gate is a ratio band, not an exact count. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Slat-width jitter uses fixed seed 17. Export byte counts differ by 16 B on 5.2.1 (glTF serializer), not a gated axis.

-

Hygiene

+

Hygiene#

Recomputed from the generated mesh, not asserted about the script.

AxisDeclaredMeasured (all three)
Non-manifold edges00
Loose verts / edges0 / 00 / 0
Doubles merged at 1e-500
Zero-area faces00
N-gons00
Coplanar disjoint face pairs00
Grounded: zminwithin 1e-4 of 00.0000
Named supports: 3 skidseach zmin ≤ 1e-33, skid_z 0.00000
Body plan1.056 × 0.716 m ± 0.051.0560 × 0.7160
-

Joint fit and seat

+

Joint fit and seat#

AxisDeclaredMeasured (all three)
Handle-end BVH gap≤ 0.008 m0.00000
Slat-post BVH gap≤ 0.006 m0.00000
Nail seat24 nails; bite 0.3–1.0 mm into the strap, head ≥ 1.0 mm proud24; 0.60 mm; 1.80 mm
Handle plates on one boardeach plate inside a single end board with ≥ 4 mm clearance at both edges4 plates; 32.4 mm
-

Second pass (straps, nails, handle placement, surface)

+

Second pass (straps, nails, handle placement, surface)#

The inspection sheet found the iron and the handles built the way crate-stack used to build them:

  • Straps were two unbevelled boxes per corner. That left a seam line down the outer corner, razor edges, and an inner face sitting flush on the post face. Each strap is now one L-section extrusion that bites IRON_BITE into the post. It is chamfered at IRON_BEVEL with material=METAL_IDX, since bevel otherwise hands chamfer faces slot 0. Code copied from crate-stack, not imported.
  • No fasteners. Three nail stations per plate, 24 nails, placed from each plate's own outer face and normal. nail_audit asserts the seat band (exit 18, --float-nails).
  • The handle mounting plates straddled the gap between two end boards. The handle sat at a fixed 52% of post height, which landed on a gap. It now snaps to the centre of the end board nearest that height, from the same _span_layout the boards come from. plate_board_audit asserts each plate sits inside one board with clearance at both edges (exit 20, --straddle-handle, which puts the handle on a gap and measures −26.9 mm).
  • Every plank was the same flat tone. PlankTone / GrainDir face attributes drive a grain-along-the-board wood shader. The iron gets rust wear.
  • The temp glTF export is removed after it is measured.

The right-angle-edge budget from crate-stack is not applied here: the handle bail's pipe ends are square discs buried in the mounting plates, so "every box is chamfered" does not hold for this piece.

-

Falsifiers

+

Falsifiers#

Each violates one named budget. All nine were run on 4.5.11, 5.1.2 and 5.2.1 and returned the same code on each.

FlagBudget violatedExit
--skip-decimateLOD1 ratio band9
--stray-vertloose vertex count is 015
--lift-zbounding box zmin is 016
--short-skidsnamed skid supports at Z=016
--float-handlehandle-end gap17
--omit-slatsslat-post seat18
--float-nailsnail seat band (heads lifted 1.5 mm off the straps)18
--straddle-handlehandle plates on one end board20
-

Run

+

Run#

blender --background --python shipping_crate.py --
 blender --background --python shipping_crate.py -- --skip-decimate
 blender --background --python shipping_crate.py -- --stray-vert
@@ -311,17 +370,1586 @@ 

Run

blender --background --python shipping_crate.py -- --straddle-handle blender --background --python shipping_crate.py -- --output crate.png

Smoke passes no flags.

-

Exit codes

+

Exit codes#

File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path. 15–19 are the hygiene and joint-fit family.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 2 distinct slots, or a face-count floor missed
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight
16Not grounded: bounding box zmin off 0, or a named skid floats
17Joint fit: handle-end gap
18Seat: slat-post gap, or a nail head off its strap seat band (--float-nails)
19Body plan off the stated real-world size
20A handle mounting plate not seated on a single end board (--straddle-handle)
-
+

Source

showcase/shipping-crate/shipping_crate.py - View on GitHub → + 1569 lines · View on GitHub →
-
"""Game-ready shipping crate — a showcase piece, not an example.
+      
+
"""Game-ready shipping crate — a showcase piece, not an example.
 
 Asserts budget conformance of a procedural crate (skids, corner posts,
 tenoned slats with seeded width jitter, nailed single-shell L-straps,
@@ -1891,7 +3519,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -1911,20 +3573,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/signpost/index.html b/docs/gallery/signpost/index.html index 7a6b7dba..cacc311f 100644 --- a/docs/gallery/signpost/index.html +++ b/docs/gallery/signpost/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

signpost

A procedural timber fingerboard signpost through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

+

Rendered headless by the showcase piece itself — click to zoom.

witnesses Recomputed: 376 tris, three materials with 108 metal, 22 board and 60 wood faces, UVs in 0..1 with zero AABB overlap, outer AABB 1.260×0.142×1.551 m at zmin 0, LOD ratios in band (5.2 COLLAPSE leaner on LOD1/LOD2), convex collider 52 tris, non-empty glTF. Hygiene all zero. Board-to-post gap 5 mm, strap gap 3 mm, board AABB Y 28 mm. --skip-decimate exits 9; --stray-vert 15; --lift-z / --short-shoe 16; --gap-board 17; --float-strap 18; --yaw-boards 19.
+

tags mesh export showcase

blender --background --python showcase/signpost/signpost.py --
@@ -283,23 +342,23 @@

signpost

It asserts budget conformance of the generated result. It does not witness an API contract. "It rendered without error" is not a check.

Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package). Hygiene combinatorics match examples/mesh-hygiene-audit (copied, not imported).

Intended size: 1.48 m post, boards 0.62 m and 0.54 m, 0.118 m post stock; outer AABB 1.260 × 0.142 × 1.551 m.

-

Budgets

+

Budgets#

Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.

AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles320–480376 / 376 / 376
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.4681
LOD2 ratio0.10–0.35 of base0.2181 / 0.2181 / 0.1649
Materialsexactly 3 distinct, ≥24 metal, ≥12 board, ≥12 wood3 slots, 108 metal, 22 board, 60 wood
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(1.260, 0.142, 1.551) m ± 0.015(1.2600, 0.1420, 1.5510), zmin 0
Collider tris≤ 18052
Exportwritten, size > 035648 / 35648 / 35640 bytes

Base triangles dropped from 504 to 376 in the quality pass: glued diamond cubes and a solid shoe block became wedges and a four-plate collar. Outer AABB tightened from 1.368 × 0.201 × 1.566 m.

DECIMATE COLLAPSE triangle counts are not identical across series — 5.2.1 is leaner on LOD1 and LOD2. The gate is a ratio band, not an exact count. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Construction is closed-form; the only RNG is the seeded per-piece wood tone. Bevel inputs are sorted by edge index, so the face order is the same on every run. Export byte counts differ on 5.2.1 (glTF serializer), not a gated axis.

-

Surface and stage

+

Surface and stage#

The quality pass found no geometric defect: the boards, straps, bands and shoe seat as their budgets say. The defects were surface. The finger boards were flat pale yellow; they are now cream paint worn through to the wood by a noise mask, a shade apart per board. The iron read as chrome (metallic 1.0, roughness 0.38) and is now rusted. The post has its own tone and grain. The stage grid grew from 14 to 60 m, because the wall's left edge showed in the corner of the hero. The temp .glb is removed after its size is measured. With no new geometric defect, there is no new budget.

-

Hygiene

+

Hygiene#

Recomputed from the generated mesh, not asserted about the script.

AxisDeclaredMeasured (all three)
Non-manifold edges00
Loose verts / edges0 / 00 / 0
Doubles merged at 1e-500
Zero-area faces00
N-gons00
Coplanar disjoint face pairs00
Grounded: zminwithin 1e-4 of 00.0000
Named shoe plates≥1, each zmin ≤ 0.0024, 0.00000
-

Joint fit and orthogonal boards

+

Joint fit and orthogonal boards#

AxisDeclaredMeasured (all three)
Board-to-post BVH gap≤ 0.008 m0.00500
Strap-to-post BVH gap≤ 0.008 m0.00300
Board AABB Y (axis-aligned)≤ 0.055 m0.0280
-

Falsifiers

+

Falsifiers#

Each violates one named budget. All were run on 4.5.11, 5.1.2 and 5.2.1 and returned the same code on each.

FlagBudget violatedExit
--skip-decimateLOD1 ratio band9
--stray-vertloose vertex count is 015
--lift-zbounding box zmin is 016
--short-shoenamed shoes plant at z=016
--gap-boardboard-to-post BVH gap17
--float-strapstrap-to-post BVH gap18
--yaw-boardsboard AABB Y (orthogonal)19

--short-shoe lifts only the iron shoe plates. After zmin snap the post is the AABB zmin, so the AABB gate would still pass; the named shoe stations fail. --gap-board moves the board inner station outside the post. --float-strap adds Y offset to the U-wrap so it no longer bites. --yaw-boards restores a 0.12 rad yaw; the AABB secondary extent exceeds the thickness bound.

-

Run

+

Run#

blender --background --python signpost.py --
 blender --background --python signpost.py -- --skip-decimate
 blender --background --python signpost.py -- --stray-vert
@@ -310,17 +369,1252 @@ 

Run

blender --background --python signpost.py -- --yaw-boards blender --background --python signpost.py -- --output signpost.png

Smoke passes no flags.

-

Exit codes

+

Exit codes#

File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path. 15–19 are the hygiene and joint-fit family.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 3 distinct slots, or wood/board/metal faces missing
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Hygiene (--stray-vert lands here)
16Grounded zmin / named shoes (--lift-z, --short-shoe)
17Board-to-post gap (--gap-board)
18Strap-to-post gap (--float-strap)
19Board yaw / AABB Y (--yaw-boards)
-
+

Source

showcase/signpost/signpost.py - View on GitHub → + 1235 lines · View on GitHub →
-
"""Game-ready timber signpost — a showcase piece, not an example.
+      
+
"""Game-ready timber signpost — a showcase piece, not an example.
 
 Asserts budget conformance of a procedural fingerboard signpost (post,
 two painted boards, iron shoe and straps) after composing shipped
@@ -1556,7 +2850,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -1576,20 +2904,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/sky-texture-sun-elevation/index.html b/docs/gallery/sky-texture-sun-elevation/index.html index 774b1b71..4d989ed2 100644 --- a/docs/gallery/sky-texture-sun-elevation/index.html +++ b/docs/gallery/sky-texture-sun-elevation/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

sky-texture-sun-elevation

World ShaderNodeTexSky driving Background Color — the sky contract across 4.5 LTS and 5.1. sky_type is NISHITA on 4.5 and MULTIPLE_SCATTERING on 5.1 (NISHITA gone); dust_density exists only on 4.5 (aerosol_density on 5.1). Two tiny Cycles OPEN_EXR zenith probes prove sun_elevation 8 deg to 55 deg brightens zenith (rise 2.25x on 5.1.2, 1.50x on 4.5.11, gate >= 1.25).

+

Rendered headless by the example itself — click to zoom.

witnesses AI code still assigns sky_type='NISHITA' and dust_density on 5.1 — both are wrong (NISHITA removed; dust_density is AttributeError; use MULTIPLE_SCATTERING + aerosol_density). A near-black Background Strength alone is not a sky: Sky must link into Background Color. Elevation that does not brighten zenith fails the rise gate.
+

tags world sky rendering

blender --background --python examples/sky-texture-sun-elevation/sky_texture_sun_elevation.py --
@@ -283,14 +342,14 @@

sky-texture-sun-elevation

  • Sky must drive Background Color. A near-black Background Strength alone is not a sky — the check asserts the Sky → Background → World Output links.
  • sky_type renamed. 4.5 LTS uses NISHITA; 5.1 replaced it with MULTIPLE_SCATTERING (Nishita's successor). Assigning NISHITA on 5.1 fails; the enum no longer lists it.
  • dust_density → aerosol_density. 4.5 exposes dust_density; 5.1 raises AttributeError and ships aerosol_density instead. AI code still emits dust_density.
  • sun_elevation brightens zenith. Two CPU Cycles OPEN_EXR probes with a straight-up camera, Background Strength 0.05 (unclipped), compare zenith luminance at 8° vs 55°. Measured rise 2.25x on 5.1.2 and 1.50x on 4.5.11 LTS (gate ≥ 1.25).

What each check catches on failure: broken Sky→Background link (exit 6), wrong sky_type for the running version (exit 3), lost sun_elevation round-trip (exit 4), dust_density / NISHITA present on the wrong side of 5.0 (exit 5), a non-working sky (zenith floor, exit 7), and an elevation that does not brighten zenith (exit 8 — rise below 1.25).

Version witness: sky_type and the dust/aerosol rename are the divergence; sun_elevation itself is stable. Zenith rise differs by model (Nishita vs multiple scattering) but clears the same gate on both binaries.

-

Stage deviation

+

Stage deviation#

The World carries a Nishita / multiple-scattering Sky Texture instead of the default near-black Background, and there are no studio lights: the contract is the sky, so the sky's sun disc is the key and its dome is the fill. The still is a diptych, 8° | 55°, split by a dark 12 px rule, with one Background Strength (0.035) for both panels so sun_elevation is the only variable. If it did not take, the two panels would render identical.

What reads at a glance: at 8° a deep navy sky with a dusk glow on the left horizon, the obelisk's sun face raked orange, the plaza nearly dark and the shadow running long out of frame; at 55° a bright blue sky, a sunlit plaza, and a short shadow ending on the bronze meridian scale inlaid in the paving. The ground runs to the horizon, so there is no void below it.

Layer 1 still holds: view_transform='Standard', designed materials (red granite, limestone, bronze, matte gilt, brick-pattern paving), a chosen 35 mm camera on a TRACK_TO aim, no helpers in frame. Standard does not compress highlights, so the render path fails with exit 12 if more than 0.05 % of the still clips to white. The committed still has zero clipped pixels. The pyramidion is matte gilt rather than metal: a metallic cap Fresnel-glinted the 55° sun to pure white at grazing angles, whatever its roughness. Mean luma is about 0.20, reported against the calibration set as information under this deviation.

The sky's sun_rotation is a compass bearing, measured clockwise from +Y. The first draft assumed the opposite convention and put the shadow on the wrong side. _sun_rotation_for_shadow holds the measured mapping.

-

Framing

+

Framing#

Each 640 × 720 panel is its own frame, and the gnomon is measured in it through examples/gallery_framing.py (exit 10). Both panels share geometry and camera, so one measurement covers both. On 5.2.1 the fill is x 0.566 / y 0.794 and the margins are left 0.100, right 0.334, bottom 0.044, top 0.161. The ground, plaza and meridian inlay are stage.

-

Run

+

Run#

# Zenith-luminance correctness check (tiny Cycles CPU EXR probes) — the CI check:
 blender --background --python sky_texture_sun_elevation.py --
 
@@ -299,18 +358,806 @@ 

Run

# Also render the gallery diptych (Cycles; framing gate exit 10, clip gate exit 12): blender --background --python sky_texture_sun_elevation.py -- --output sky.png
-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3sky_type is not the identifier for this Blender
4sun_elevation round-trip failed
5dust_density / aerosol_density / NISHITA / MULTIPLE_SCATTERING trap
6Sky → Background → World Output links broken (--unlink-sky lands here)
7High-elevation zenith luma below floor
8Zenith luma did not rise with elevation
9--output produced no file
10--output framing violation (gallery_framing, per panel)
12--output still clips to white under Standard (> 0.05 % of pixels)

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --unlink-sky, so exits 10 and 12 are render-path only.

-
+

Source

examples/sky-texture-sun-elevation/sky_texture_sun_elevation.py - View on GitHub → + 788 lines · View on GitHub →
-
"""Nishita / multiple-scattering sky + sun_elevation — a runnable example.
+      
+
"""Nishita / multiple-scattering sky + sun_elevation — a runnable example.
 
 Witnesses the world Sky Texture contract AI-generated lighting code most often
 gets wrong across the 4.5 LTS → 5.1 window:
@@ -1099,7 +1946,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -1119,20 +2000,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/soccer-ball-goldberg/index.html b/docs/gallery/soccer-ball-goldberg/index.html index f487d641..825680d3 100644 --- a/docs/gallery/soccer-ball-goldberg/index.html +++ b/docs/gallery/soccer-ball-goldberg/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

soccer-ball-goldberg

A soccer ball as a Goldberg polyhedron: a bmesh icosphere truncated at 1/3 per edge, faces ordered by link-topology walks, panels bound by face vertex count.

+

Rendered headless by the example itself — click to zoom.

witnesses Goldberg invariants are closed forms — counts, degree 3, uniform edges, planar faces, one circumsphere — and panels bind by vertex count, never enumeration order.
+

tags bmesh mesh

blender --background --python examples/soccer-ball-goldberg/soccer_ball_goldberg.py --
@@ -285,10 +344,10 @@

soccer-ball-goldberg

Tolerance basis: mesh coordinates are float32 and create_icosphere's own trig lands the invariants at a deterministic ~9e-6 noise floor (measured values are byte-identical on 4.5.11 and 5.1.2). The 3e-5 gates sit ~3x above that floor; any genuine contract break is orders of magnitude larger.

Version witness: check output is byte-identical on Blender 4.5.11 LTS and 5.1.2 — same counts, same deviations. One authoring hazard surfaced and is noted in the code: Object.to_mesh_clear() takes no argument on current Blender (passing the mesh raises TypeError) — see depsgraph-and-evaluated-data.

The render is the proof: the faceted Goldberg cage is smoothed by an unapplied Subsurf modifier (panel materials carry through Catmull-Clark per face class), and the ball is grounded by its depsgraph-evaluated lowest vertex. Invert the panel binding and the still inverts with it: white pentagons on a black ball, wrong on sight.

-

Staging (render path only)

+

Staging (render path only)#

None of this touches the checked mesh, its binding, or any check.

  • Round, not lumpy. Catmull-Clark on a 60-vertex cage leaves flats over every hexagon; the clay pass read as a lumpy stone. An unapplied Cast modifier after the Subsurf pulls the surface onto a sphere of the cage's own circumradius.
  • Every panel visible. The white hexagons used to merge into one white shell, so the still showed 12 of the 32 panels the check counts. A render-only copy of the mesh, turned into a wire, subdivided and cast onto a sphere 2 mm proud of the ball, draws every panel boundary as a stitched seam. It is parented to the ball and bound to the dark slot on the copy only.
  • Framing gate. The render path calls gallery_framing.check_framing before writing the still. The helper returns 10, which this example already spends on the centroid check, so the call site maps a violation to 14. The larger, rounder ball touched the top edge at the old 55 mm lens; the lens is now 50 mm.
-

Run

+

Run#

# Cheap correctness check (no render) — the CI check:
 blender --background --python soccer_ball_goldberg.py --
 
@@ -298,18 +357,484 @@ 

Run

# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts): blender --background --python soccer_ball_goldberg.py -- --output ball.png blender --background --python soccer_ball_goldberg.py -- --output ball.png --engine cycles
-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Topology ≠ 60/90/32
4Euler characteristic ≠ 2
5Face census ≠ 12 pentagons + 20 hexagons
6Vertex degree not uniform 3; also --output produced no file
7Non-manifold edges
8Edge lengths not uniform
9Face planarity off
10Centroid off origin
11Circumradius not uniform
12Panel material count ≠ 2
13Panel binding not by vertex count (--invert-bind lands here)
14Framing gate (--output path only; gallery_framing returns 10, remapped)

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --invert-bind.

-
+

Source

examples/soccer-ball-goldberg/soccer_ball_goldberg.py - View on GitHub → + 466 lines · View on GitHub →
-
"""A soccer ball built as a Goldberg polyhedron with bmesh — a runnable example.
+      
+
"""A soccer ball built as a Goldberg polyhedron with bmesh — a runnable example.
 
 A truncated icosahedron (12 pentagons, 20 hexagons) derived by cutting every
 edge of a bmesh icosphere at exactly 1/3, with the pentagon/hexagon faces
@@ -776,7 +1301,41 @@ 

Source

except Exception as e: import traceback; traceback.print_exc(); print(f"FATAL: {e}", file=sys.stderr); sys.exit(1)
+
+ +
@@ -796,20 +1355,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/socket-attach-points/index.html b/docs/gallery/socket-attach-points/index.html index 285b125b..06f9b15b 100644 --- a/docs/gallery/socket-attach-points/index.html +++ b/docs/gallery/socket-attach-points/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

socket-attach-points

A survey drone whose named SKT_ empties are the spawn contract: modules parented with an identity local transform seat exactly on their mount pads.

+

Rendered headless by the example itself — click to zoom.

witnesses 7 socket world matrices within 1.788e-07 of the authored transform, socket +Z == the pad's Newell normal (1.794e-07), seating offset 0.0; the no-parent-inverse probe jumps 0.690128 m.
+

tags mesh transforms game-pipeline instancing

blender --background --python examples/socket-attach-points/socket_attach_points.py --
@@ -287,29 +346,1164 @@

socket-attach-points

Pipeline arc neighbours: pivot and origin discipline in prop-origin-transform, parent-inverse under animation in parent-inverse-orrery, tile-grid boundary contracts in modular-kit-snap, collision packaging in collision-hull-proxy.

What it witnesses (all closed form or independently re-derived):

  • Socket matrices. All 7 evaluated matrix_world values equal root.matrix_world @ authored_local within 1e-6 (measured 1.788e-07); every basis is orthonormal (Gram error 3.576e-07) and right-handed (det − 1 = 2.980e-07).
  • Orientation vs geometry. Each socket's world +Z equals the Newell normal of its mount pad's mount face — computed from raw vertex coordinates, and the pad mesh is built by a quaternion swing from +Z while the socket basis is built by explicit Gram-Schmidt, so the two derivations are independent (normal deviation 1.794e-07). The socket origin sits on that face's centroid (6.687e-08), and the up axis matches the documented fallback rule (1.943e-07).
  • Seating. Every module's mount origin coincides with its socket origin (offset exactly 0.0) with its mount axis on the socket +Z (dot − 1 = 1.735e-07), and carries a strictly identity local matrix — seated by the socket, not nudged into place.
  • Rigid invariance. Re-posing the chassis root to a second arbitrary transform moves every socket to root.matrix_world @ authored_local (2.384e-07) and every module stays on its socket (0.0).
  • Transform apply. Freezing loc/rot/scale on the root leaves every socket's world matrix put (2.235e-07) — see the hazard below for what it does to the local ones.
  • Reuse hygiene. 14 chassis parts at identity scale, Drone.Survey.* / SKT_* names, no default datablocks, min z == 0.00e+00.
-

Hazards found while authoring

+

Hazards found while authoring#

  • Applying a transform on an Empty root pushes it down into the children. An Empty has no object data to bake into, so transform_apply on the root resets 7/7 children's matrix_parent_inverse to identity and hands each child the root's scale locally (measured 0.35 for an applied 1.35). World matrices survive to float32; local transforms do not. A spawn system that reads matrix_local, or an exporter that trusts "transforms are applied", reads different numbers after an artist freezes the rig. Both halves are asserted, so the behaviour cannot change under us silently.
  • Only the root may be selected during the apply. A child left selected gets the parent transform applied twice — measured 2.335 m of socket drift on this asset.
  • bm.normal_update() does not fix winding. It recomputes normals from the existing winding. The lofted fuselage wound its side quads inward and rendered as a flat unshaded white panel rather than an obvious hole; bmesh.ops.recalc_face_normals is what fixes it, and Part.finish now always calls it.
  • Bevel offsets near half the local edge length collapse. The skid rails at a 5 mm offset on a 9.9 mm octagon edge produced 3 zero-area faces each — the collapse degenerate-bevel-weld witnesses. 2.2 mm is clean. Note that examples/gallery_asset_quality.py raises ValueError: zero length vectors have no valid angle on such a mesh rather than reporting it.

What each check catches on failure (every one probed, with the measured error): sockets parented without the parent-inverse — worst jump 0.690128 m (exit 3); a left-handed basis from z.cross(y) — det == −1.000000 (exit 3); a rotor pad built flat instead of canted — 1.395e-01 off the Newell normal (exit 4); an 11° roll on the basis — 1.917e-01 off the up rule (exit 4); a module nudged 4 mm off its socket (exit 5); a default Cube datablock name (exit 6); a socket left unparented so it ignores the re-pose — 1.417e+00 (exit 7); a child selected during the apply — 1.252e+00 to 2.244e+00 of socket drift (exit 8).

Version witness: check output is byte-identical on Blender 4.5.11 LTS and 5.1.2 — same counts, same measured deviations to every printed digit, and the same framing numbers on the render path.

Render as proof: the drone on the dark stage with every module seated, the orange mount pads reading as the socket signature. The falsification variant (--falsify) parents the sockets without matrix_parent_inverse: the pads are left bare, the rotors and mast vanish from the airframe, and the camera pod and battery hang in space near the top of frame. A broken matrix is not a subtle pixel difference here — the asset comes apart.

-

Run

+

Run#

blender --background --python socket_attach_points.py --
 blender --background --python socket_attach_points.py -- --output drone.png
 blender --background --python socket_attach_points.py -- --falsify adrift.png
 blender --background --python socket_attach_points.py -- --probe

The --output render path additionally gates framing via examples/gallery_framing.py (fill 0.881x, margins 0.066/0.053/0.122/0.106, no edge touched) and the asset floors via examples/gallery_asset_quality.py (32 materials, edge90 0.027, no default names). --falsify is a diagnostic, not a gallery hero: it measures and prints framing but does not enforce the Layer 1 band — modules leave the frame on purpose. --falsify and --probe still exit 0.

-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper. 11 is the shared asset-quality helper.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Socket world matrix, orthonormal basis, or determinant
4Socket pad normal, origin, or up-axis rule
5Module seating / mount axis / leftover local transform
6Population, namespace, default datablock name, SKT_ prefix, or skid plane
7Re-pose: socket or module lost the root
8Parent-inverse clear, child scale pushdown, or post-apply module drift
9--output produced no file
10Gallery framing violation
11Gallery asset-quality violation

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output, --falsify, or --probe.

-
+

Source

examples/socket-attach-points/socket_attach_points.py - View on GitHub → + 1135 lines · View on GitHub →
-
"""Socket attach points — named empties parented into an asset as spawn mounts.
+      
+
"""Socket attach points — named empties parented into an asset as spawn mounts.
 
 Witnesses the contract a spawn system depends on when it bolts modules onto a
 prop at runtime: an attachment placed at socket S with an identity local
@@ -1445,7 +2639,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -1465,20 +2693,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/stone-archway/index.html b/docs/gallery/stone-archway/index.html index 4a4a204a..0df16a9a 100644 --- a/docs/gallery/stone-archway/index.html +++ b/docs/gallery/stone-archway/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,47 +325,49 @@

stone-archway

A procedural masonry archway — coursed piers, projecting imposts, nine voussoirs and a proud keystone — carried through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

+

Rendered headless by the showcase piece itself — click to zoom.

witnesses Recomputed: 924 tris, two materials with 182 dressed and 364 ashlar faces re-stamped after the chamfer, UVs in 0..1 with zero AABB overlap, outer AABB 1.644×0.510×2.057 m, clear opening 1.203 m, zmin 0, hygiene 0 including zero coplanar disjoint pairs, both piers grounded, the springing joint an overlap, all eight mortar joints at 9.70 mm inside the 6–17 mm band, and every intrados vertex within 0.97 mm of the declared 0.60 m radius over 178°. --off-circle exits 19 on the circle fit; --wide-mortar exits 18 on the joint band.
+

tags mesh export showcase

blender --background --python showcase/stone-archway/stone_archway.py --

A freestanding masonry arch — two coursed piers, projecting imposts, nine voussoirs turning a semicircle, and a keystone standing proud at the crown, every joint filled with a recessed mortar bed. A showcase piece, not an example — it witnesses no API contract. It asserts that generated geometry meets declared asset budgets, recomputed from the finished mesh.

-

What it composes

+

What it composes#

Shipped contentUsed for
skills/mesh-editing-and-bmeshwedge and box construction, chamfer, UVs in one bmesh
skills/procedural-materials-and-shadersashlar, dressed stone and mortar: object-space mottling, speckle, bump, per-block tone
skills/bake-high-to-lowCycles tangent-space normal bake, high onto low
skills/engine-export-presetsUnity glTF (export_yup=True)
skills/depsgraph-and-evaluated-dataevaluated triangle counts for the LOD ratios
snippets/decimate_to_budget.pyLOD1 / LOD2 COLLAPSE chain
snippets/convex_hull_collider.pyconvex collider
snippets/lod_chain.pyLOD naming and ratio pattern
examples/mesh-hygiene-audithygiene combinatorics (copied, not imported)
-

The budget that matters

+

The budget that matters#

An arch can fail invisibly. Nine wedge blocks laid in a row are still nine wedge blocks; the thing that makes them an arch is that their intrados vertices sit on a circle. So the piece recomputes that circle from vertex positions — not from the angles the generator used — and asserts every intrados vertex lands within 4 mm of the declared 0.60 m radius, over an arc spanning at least 168°.

--off-circle is the falsifier built for exactly this. It keeps the angles, the joints, the materials, the triangle count and the bounding box identical, and only wanders the intrados radius by ±18 mm. Every other budget in the piece still passes. Only the circle fit sees it.

-

Measuring it took two corrections

+

Measuring it took two corrections#

The first attempt selected intrados vertices by radius. That swept in the chamfer vertices sitting one bevel-width out on each radial face and reported a 5.1 mm error on a true arch. The fit now runs over the vertices of faces that actually face the springing centre — normal inward in XZ and with a small Y component, which is what excludes the chamfer strips running along the intrados edges.

-

Budgets

+

Budgets#

Declared in the script as named constants, recomputed from the generated mesh. Measured values are from Blender 5.2.1; the cross-version table is at the end.

BudgetBandMeasured
Base triangles950–14501140
LOD1 ratio0.32–0.620.5000
LOD2 ratio0.10–0.350.2140 (5.2) / 0.2193 (4.5, 5.1)
Material slotsexactly 3, distinct3
Dressed faces (keystone, imposts, plinth and head courses)≥ 100182
Ashlar faces≥ 280364
Mortar faces≥ 100108
UV boundsinside 0..1(0.0015, 0.0015)–(0.9985, 0.9985)
UV AABB overlap≤ 1e-50.000000
Outer AABB1.644 × 0.510 × 2.057 m ± 0.0201.6440 × 0.5100 × 2.0568
Clear opening1.200 m ± 0.015, measured between the pier faces1.2033
Collider triangles≤ 260168
Normal bake{'FINISHED'} with image data{'FINISHED'}, has_data=True
glTF exportfile written, non-empty~100 kB
Hygieneall zeroloose 0/0, non-manifold 0, zero-area 0, doubles 0, n-gons 0, coplanar disjoint pairs 0
Grounded AABB\zmin\≤ 1e-40.00000
Pier supports2 piers, each base course zmin ≤ 1e-32 at 0.00000
Keystone proud of the wall face≥ 0.020 m0.03500
Springing jointoverlap, surface gap ≤ 1e-40.00000
Mortar jointsevery adjacent pair in 0.006–0.017 mall eight at 0.00970
Intrados circle fitevery vertex within 0.004 m of R = 0.600.00097
Intrados arc span≥ 168°178.0°
Mortar contact18 joints, each overlapping exactly 2 stones18, all 2

Real-world size: a 1.20 m clear opening under a semicircular head, 1.64 m across the piers and 2.06 m to the top of the keystone — a garden gate arch.

-

Mortar

+

Mortar#

Every joint used to be air. The pier courses stood 7 mm apart and the voussoirs 9.7 mm apart, none of them touching: each pier was a stack of floating blocks, with daylight through every joint in the ground-contact and joint close-ups. The mortar-band budget measured the gaps and passed, because the gaps were the right width. Nothing asked what was in them.

Each joint now holds a mortar shell sized from the two blocks it sits between (add_mortar): beds between courses and under each impost, and wedges between voussoirs. Each is recessed MORTAR_RECESS (chamfer plus 3 mm) behind the stone faces, so it reads as a raked joint, and bites MORTAR_BITE (1.5 mm) into both blocks. Mortar is added after the chamfer pass and left unchamfered, and classify skips mortar shells, so every stone budget — circle fit, joint band, keystone, supports — still measures stone against stone.

mortar_audit asserts that each of the 18 mortar shells BVH-overlaps exactly two stones. --short-mortar stops every joint 1 mm shy of both blocks, so each shell touches none, and the piece exits 18.

-

Surface

+

Surface#

The stone used to read as wood. Its noise-driven colour mix streaked at hero scale into long brown grain, and under neutral light it looked like cream plaster. stone_material now samples object space with isotropic noise: mottling for colour, fine speckle driving roughness, and a small bump for pitting. A seeded BlockTone face attribute (paint_blocks) shades each stone differently. Dressed stone is paler and smoother than ashlar; mortar is a flat grey-beige. The baked normal map is chained under the bump (wire_normal) rather than replacing it.

-

Determinism

+

Determinism#

Fixed seed 23; no unseeded randomness. Every measured value above is byte-identical on 4.5.11, 5.1.2 and 5.2.1 except LOD2, where DECIMATE COLLAPSE produces 244 triangles on 5.2 and 250 on 4.5 and 5.1. That is why the LOD gate is a ratio band (0.10–0.35, measured 0.2140 and 0.2193) and not an exact count.

-

Falsifiers

+

Falsifiers#

Each breaks one pipeline stage so a named budget fails. All eight were run on 4.5.11, 5.1.2 and 5.2.1 and produced the same exit code on all three.

FlagBreaksExit
--skip-decimatedrops the DECIMATE modifiers, LOD1 ratio goes to 1.00009
--stray-vertadds one loose vertex inside the opening, so hygiene catches it rather than the bounding box15
--lift-zlifts the whole mesh 50 mm off the floor16
--float-pierfloats one pier 12 mm; the other still grounds the AABB, so only the named-support budget sees it16
--sink-keystonesinks the keystone to a quarter of its projection (8.75 mm) — the imposts still set the Y envelope, so the bounding box is unchanged17
--wide-mortartriples the joint angle, opening every joint to 29.1 mm18
--off-circlewanders the intrados radius ±18 mm while keeping angles, joints and envelope19
--short-mortarstops every mortar joint 1 mm shy of both stones; each of the 18 touches none, mortar-contact budget18

Three of these needed the model changed, not the budget:

  • --sink-keystone originally removed the projection outright, which shrank the Y bounding box by 70 mm and tripped the AABB gate first. The imposts were added so the envelope no longer depends on the keystone — and an impost course is correct masonry the arch was missing anyway.
  • --off-circle began as --flat-arch, laying the voussoirs as a lintel. That is 0.62 m shorter and fails on the bounding box, proving nothing about the circle fit. Wandering the radius inside the same envelope is the honest version.
  • --float-pier tripped the z-fight budget rather than the support budget, because the impost was pinned to an absolute height while the courses under it rose into it. The impost now rides on its own pier.
-

Findings the budgets forced

+

Findings the budgets forced#

  • The chamfer pass silently repainted the mesh. bmesh.ops.bevel gives every face it creates material_index 0, so chamfering nineteen blocks left 30 faces on the dressed slot and moved 464 to ashlar. The slot count and the distinct-material check both still passed. Only the per-material face floor caught it, which is exactly the class showcase/README.md warns about. Materials are now re-stamped after the bevel, per block, by nearest recorded centroid.
  • The impost landed exactly on the top course. Same footprint, same plane, two coincident face centres — a z-fight. It now sits on its own mortar bed, which is why the pier lays N_COURSE beds rather than N_COURSE - 1.
  • find_nearest is unsigned. A voussoir seated inside the pier head reported a 5.4 mm gap where there was none, because the distance to the host's skin is positive from inside too. Joint gaps now test BVH overlap first and return zero when two blocks interpenetrate.
  • Joint gaps have to be measured both ways. The keystone's radial faces are wider in Y than its neighbours', so every keystone vertex on the joint lies outside the neighbour and read 33 mm instead of the 9.7 mm joint. The metric is the closest approach of the two surfaces, min of both directions.
-

Exit codes

+

Exit codes#

File-local and sequential. 9 is a valid check code; there is no rule against it. 1 is the FATAL wrapper — a crash, never a named check.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build, or has no UV layer
4Base triangle count outside band
5Material slots, or a material's face floor
6UVs outside 0..1
7UV AABB overlap above tolerance
8Outer AABB off declared size
9LOD1 or LOD2 ratio outside band (--skip-decimate)
10Framing gate (examples/gallery_framing.py, render path only)
11Collider triangles above ceiling
12Normal bake failed or produced no image data
13glTF export missing or empty
14--output produced no file
15Mesh hygiene (--stray-vert)
16Grounded zmin, or a pier base floating (--lift-z, --float-pier)
17Keystone projection or springing joint (--sink-keystone)
18Mortar joint outside band (--wide-mortar), or a mortar shell not seated in exactly two stones (--short-mortar)
19Intrados circle fit, or clear opening (--off-circle)
-

Run it

+

Run it#

# Budget check, no render. ~1.1 s on 4.5, ~1.2 s on 5.1, ~1.4 s on 5.2.
 blender --background --python stone_archway.py --
 
@@ -321,16 +380,1440 @@ 

Run it

# Render the gallery still (EEVEE; --engine cycles on a GPU-less host). blender --background --python stone_archway.py -- --output arch.webp

Smoke runs the check-only path. It does not pass --output or any falsifier.

-

Cross-version measurements

+

Cross-version measurements#

Value4.5.115.1.25.2.1
Base triangles114011401140
LOD1 tris / ratio570 / 0.5000570 / 0.5000570 / 0.5000
LOD2 tris / ratio250 / 0.2193250 / 0.2193244 / 0.2140
Face counts (ashlar / dressed / mortar)364 / 182 / 108samesame
Outer AABB1.6440 × 0.5100 × 2.0568samesame
Collider tris168168168
Intrados deviation0.000970.000970.00097
Mortar jointsall 0.00970all 0.00970all 0.00970
Mortar contact18 × 2 stonessamesame
Check wall-clock~1.11 s~1.18 s~1.37 s
-
+

Source

showcase/stone-archway/stone_archway.py - View on GitHub → + 1424 lines · View on GitHub →
-
"""Game-ready stone archway — a showcase piece, not an example.
+      
+
"""Game-ready stone archway — a showcase piece, not an example.
 
 Asserts budget conformance of a procedural masonry arch: two coursed
 piers, nine voussoirs turning a semicircle, and a proud keystone, carried
@@ -1755,7 +3238,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -1775,20 +3292,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/stone-well/index.html b/docs/gallery/stone-well/index.html index d8cf29fa..6e505a47 100644 --- a/docs/gallery/stone-well/index.html +++ b/docs/gallery/stone-well/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

stone-well

A procedural stone well through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

+

Rendered headless by the showcase piece itself — click to zoom.

witnesses Recomputed: 9380 tris, three materials with face floors, UVs in 0..1 with zero AABB overlap, outer AABB 1.312×1.312×1.761 m, LOD ratios in band, convex collider 346 tris, hygiene zero (loose, non-manifold, doubles, n-gons), zero coplanar cross-shell face pairs, all 12 bottom-course stones on the floor, each post tenoned 18 mm into the measured coping and standing under a roof hip corner, the bucket hanging 0.10 m clear of the coping, grounded zmin, non-empty glTF. --stand-posts exits 15 on the z-fight budget; --float-stone exits 16 on the named-support budget; --shallow-tenon and --drop-bucket exit 18; --turn-posts exits 19 on post placement; --skip-decimate exits 9 on the LOD1 ratio budget; --lift-z exits 16 on the grounded budget.
+

tags mesh export showcase

blender --background --python showcase/stone-well/stone_well.py --
@@ -281,16 +340,16 @@

stone-well

A showcase piece, not an example. Procedural round stone well (running-bond bricks, curb, four posts, shingled pyramid roof, windlass, rope, bucket) then the shipped pipeline: unique-cell UVs, Cycles high-to-low normal bake, LOD chain, convex collider, Unity glTF export.

It asserts budget conformance of the generated result. It does not witness an API contract. "It rendered without error" is not a check.

Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package).

-

Budgets

+

Budgets#

Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.

AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles8280–95009380 / 9380 / 9380
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.5000
LOD2 ratio0.10–0.35 of base0.2198 / 0.2198 / 0.2198
Materialsexactly 3 distinct3
Material facesstone ≥ 3000, wood ≥ 600, metal ≥ 100all above
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(1.312, 1.312, 1.761) m ± 0.01(1.3120, 1.3120, 1.7606)
Groundedbbox min Z within 1e-4 of 00.0000 / 0.0000 / 0.0000
Named supportsall 12 bottom-course stones on the floor within 1e-412/12, worst 0.000000
Hygieneloose V/E, non-manifold, zero-area, doubles @1e-5, n-gons: all 00 / 0 / 0 on every axis
Z-fightingcoplanar cross-shell face pairs = 00 / 0 / 0
Post tenoneach post seats 0.015–0.022 m into the measured coping top0.01800 / 0.01800 / 0.01800
Post placementeach post within 0.02 rad, in plan, of a roof hip corner recomputed from the mesh0.00000
Bucket clearancehangs 0.085–0.115 m clear of the measured coping top0.10000
Material-island gapstone↔wood and metal↔wood min distance ≤ 0.008 m0.00000 / 0.00000 / 0.00000
Collider tris≤ 380346
Exportwritten, size > 0, removed after measuring690216 bytes on 5.2.1

DECIMATE COLLAPSE triangle counts are not guaranteed identical across series — the gate is a ratio band, not an exact count. This mesh happened to match on 4.5.11 / 5.1.2 / 5.2.1. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Construction uses no RNG; the per-piece material tone is drawn from a seeded random.Random(TONE_SEED), the same every run. glTF byte size differs by a few hundred bytes across series; the gate is "written and non-empty", not a byte count, and the file is removed once measured.

-

Falsifiers

+

Falsifiers#

Each flag breaks one stage so a named budget fails and the piece exits that code.

FlagTarget budgetExit
--skip-decimateLOD1 ratio band (ratio becomes 1.0)9
--lift-zAABB grounded zmin (whole mesh up 0.05 m)16
--stand-postsCoplanar cross-shell pairs — puts the posts back on the axis and stands them on the coping, which is the pair of choices the shipped piece made (0 → 4 pairs)15
--float-stoneNamed supports — lifts one bottom-course stone 4 mm while the other eleven still ground the AABB16
--shallow-tenonPost tenon band — seats the posts only 4 mm, too shallow to be a joint but not flush, so no faces share a plane18
--drop-bucketBucket clearance — lowers the bucket 0.26 m back down the shaft18
--turn-postsPost placement — rotates the post ring one curb facet off the roof's hip corners19

Two of these had to be re-aimed, which is the point of declaring the target:

  • --stand-posts seats the posts flush and returns them to the axis. Flush alone, at the corrected 45°, leaves no coping top face within COPLANAR_CENTRE_MAX of a foot, so it fell through to the tenon band (18) and witnessed nothing about z-fighting.
  • --turn-posts rotates by exactly one curb facet (30°). Any other angle sets the feet down on a different part of the 12-gon coping and the stone-wood gap gate (17) steals the failure; a full 45° also swings the crank grip past the eave and fails the AABB gate (8) — the same trap stone-archway's --flat-arch fell into. One facet is the only rotation whose local seat geometry is identical by symmetry.
-

Run

+

Run#

blender --background --python stone_well.py --
 blender --background --python stone_well.py -- --skip-decimate
 blender --background --python stone_well.py -- --lift-z
@@ -301,17 +360,1730 @@ 

Run

blender --background --python stone_well.py -- --drop-bucket blender --background --python stone_well.py -- --output well.png

Smoke passes none of the falsifier flags and no --output.

-

Exit codes

+

Exit codes#

File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 3 distinct slots, or a material face floor missed
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Hygiene: loose geometry, non-manifold, zero-area, doubles, n-gons, or coplanar cross-shell face pairs (--stand-posts lands here)
16Bbox min Z not grounded (--lift-z), or a named support off the floor (--float-stone)
17Material-island gap above tolerance (parts meant to touch)
18Post tenon depth outside its band (--shallow-tenon), or bucket clearance outside its band (--drop-bucket)
19A post not under a roof hip corner in plan (--turn-posts lands here)
-
+

Source

showcase/stone-well/stone_well.py - View on GitHub → + 1713 lines · View on GitHub →
-
"""Game-ready stone well — a showcase piece, not an example.
+      
+
"""Game-ready stone well — a showcase piece, not an example.
 
 Asserts budget conformance of a procedural well after composing shipped
 pipeline pieces: bmesh construction, UVs, three materials, high-to-low
@@ -2025,7 +3797,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -2045,20 +3851,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/street-lantern/index.html b/docs/gallery/street-lantern/index.html index 81dd2690..e1d8a37c 100644 --- a/docs/gallery/street-lantern/index.html +++ b/docs/gallery/street-lantern/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

street-lantern

A procedural hanging street lantern through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

+

Rendered headless by the showcase piece itself — click to zoom.

witnesses Recomputed: 3338 tris, three materials with 1530 metal, 24 glass and 209 brass faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.300×0.786×1.287 m, zmin 0, hygiene 0, brace BVH gap 0, brace ends 13 mm into the post and the arm, hanger-roof gap −1 mm, post 0.100×1.120 m plumb 0, LOD ratios in band (5.2 COLLAPSE 2 tris leaner on LOD2), convex collider 78 tris, non-empty glTF. --skip-decimate exits 9; --stray-vert 15; --lift-z 16; --float-brace 17; --sink-arm 18; --rake-post 19; --shallow-brace 20.
+

tags mesh export showcase

blender --background --python showcase/street-lantern/street_lantern.py --
@@ -284,22 +343,22 @@

street-lantern

It asserts budget conformance of the generated result. It does not witness an API contract. "It rendered without error" is not a check.

Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package). Hygiene combinatorics match examples/mesh-hygiene-audit (copied, not imported).

Intended size: 0.30 m square foot, 1.12 m octagonal post (0.10 m across at the base), 0.50 m arm, 0.24 × 0.30 m cage; outer AABB 0.300 × 0.786 × 1.287 m.

-

Budgets

+

Budgets#

Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.

AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles3000–38003338 / 3338 / 3338
LOD1 ratio0.32–0.62 of base0.4997 / 0.4997 / 0.4997
LOD2 ratio0.10–0.35 of base0.2199 / 0.2199 / 0.2193
Materialsexactly 3 distinct; ≥200 metal, ≥8 glass, ≥24 brass3 slots; 1530 / 24 / 209
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(0.300, 0.786, 1.287) m ± 0.01(0.3000, 0.7860, 1.2874), zmin 0
Collider tris≤ 12078
Exportwritten, size > 0, removed after measuring251016 / 251016 / 251000 bytes

Base triangles dropped from 3520 to 3338 in the quality pass: the open 4-gon cone roof became a closed square pyramid, cage crossings stopped sharing vertices, and a pane rebate replaced coplanar glass.

DECIMATE COLLAPSE triangle counts are not identical across series — 5.2.1 is 2 tris leaner on LOD2. The gate is a ratio band, not an exact count. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Construction uses no RNG. Export byte counts differ by 16 B on 5.2.1 (glTF serializer), not a gated axis.

-

Hygiene

+

Hygiene#

Recomputed from the generated mesh, not asserted about the script.

AxisDeclaredMeasured (all three)
Non-manifold edges00
Loose verts / edges0 / 00 / 0
Doubles merged at 1e-500
Zero-area faces00
N-gons00
Coplanar disjoint face pairs00
Grounded: zminwithin 1e-4 of 00.0000
Post plan0.100 m across × 1.120 m high ± (0.02, 0.04)0.1000 × 1.1200
Post plumb (top vs bottom XY)≤ 0.008 m0.00000
-

Joint fit and seat

+

Joint fit and seat#

AxisDeclaredMeasured (all three)
Brace-to-arm BVH gap≤ 0.006 m0.00000
Brace bite: deepest brace vertex inside the post / inside the arm (signed distance to the host shell)≥ 0.004 m each0.01305 / 0.01282
Arm underside vs roof peak≥ 0.008 m clearance0.05300
Hanger-to-roof BVH gap≤ 0.004 m−0.00100
-

Falsifiers

+

Falsifiers#

Each violates one named budget. Every falsifier was run on 4.5.11, 5.1.2 and 5.2.1 and returned the same code on each.

FlagBudget violatedExit
--skip-decimateLOD1 ratio band9
--stray-vertloose vertex count is 015
--lift-zbounding box zmin is 016
--float-bracebrace-to-arm BVH gap17
--sink-armarm-over-roof clearance18
--rake-postpost plumb19
--shallow-bracebrace bite (the first build's ends: −0.00593 m into the post, 0.00000 m into the arm)20

--sink-arm lowers only the arm. Hang-station hang_z stays at post top so the cage does not follow, which is what lets the clearance budget fire.

The brace-bite check runs last, after plumb, so a raked post is reported as plumb (19) rather than as a brace that no longer reaches it.

-

Run

+

Run#

blender --background --python street_lantern.py --
 blender --background --python street_lantern.py -- --skip-decimate
 blender --background --python street_lantern.py -- --stray-vert
@@ -310,17 +369,1471 @@ 

Run

blender --background --python street_lantern.py -- --shallow-brace blender --background --python street_lantern.py -- --output lantern.png

Smoke passes no flags.

-

Exit codes

+

Exit codes#

File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path. 15–19 are the hygiene and joint-fit family; 20 is file-local.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 3 distinct slots, or a face-count floor missed
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight
16Not grounded: bounding box zmin off 0
17Joint fit: brace-to-arm gap (--float-brace)
18Seat: arm-roof clearance or hanger-roof gap (--sink-arm)
19Post plumb or post size off the stated real-world size (--rake-post)
20Brace bite: a brace end not inside the post or the arm (--shallow-brace)
-
+

Source

showcase/street-lantern/street_lantern.py - View on GitHub → + 1454 lines · View on GitHub →
-
"""Game-ready street lantern — a showcase piece, not an example.
+      
+
"""Game-ready street lantern — a showcase piece, not an example.
 
 Asserts budget conformance of a procedural hanging lantern after composing
 shipped pipeline pieces: bmesh construction, UVs, three materials,
@@ -1775,7 +3288,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -1795,20 +3342,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/swatch-grid/index.html b/docs/gallery/swatch-grid/index.html index 1959442d..a5ba1a00 100644 --- a/docs/gallery/swatch-grid/index.html +++ b/docs/gallery/swatch-grid/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

swatch-grid

Procedural Principled materials — metal and dielectric, the emission pattern, and the cross-version set_specular shim.

+

Rendered headless by the example itself — click to zoom.

witnesses EEVEE engine-id mapping: BLENDER_EEVEE on 5.x, BLENDER_EEVEE_NEXT on 4.2–4.5.
+

tags materials rendering

blender --background --python examples/swatch-grid/swatch_grid.py --
@@ -281,7 +340,7 @@

swatch-grid

A runnable example that renders a tiered 3×2 material library — one sphere per material, each seated in a chrome collar on a graphite plinth — to a single PNG. It demonstrates the procedural-materials-and-shaders patterns end to end:

  • Principled BSDF metals (gold, copper: high metallic, low roughness) and dielectrics (red/blue plastic, white rough), configured with string socket lookups and 4-tuple colors.
  • The emission pattern (an emissive orange swatch, its ShaderNodeEmission core mixed toward a dark dielectric shell at the silhouette so it reads as a glowing globe).
  • The cross-version set_specular shim (Specular → Specular IOR Level, renamed in Blender 4.0).

It doubles as a live proof of the EEVEE engine-id behavior: the version-branch helper resolves BLENDER_EEVEE on Blender 5.x and BLENDER_EEVEE_NEXT on 4.2–4.5, and the check witnesses the inversion for real — the other era's id must be rejected by the running build (assignment raises TypeError) and the helper's id accepted — so a regression in that mapping fails the example, not just the docs.

-

Run

+

Run#

# Cheap correctness check (materials + engine-id witness, no render):
 blender --background --python swatch_grid.py --
 
@@ -294,26 +353,490 @@ 

Run

# GPU-less / CI hosts: render the pixels with Cycles (CPU). The EEVEE id is still # asserted; only the final pixels use Cycles. blender --background --python swatch_grid.py -- --output swatch.png --engine cycles --samples 16 --width 960
-

Staging

+

Staging#

The swatches stand as a tiered material library on the default stage (floor, wall at y = 9, warm key, cool fill and rim, warm wedge pooling on the floor behind the display). Each sphere sits in a gunmetal collar on a bevelled graphite plinth; the back row stands on tall plinths so every sphere clears the one in front of it. A 50 mm camera looks down on the display with a TRACK_TO aim, which keeps the floor/wall seam above the back row.

The emissive swatch used to be a bare Emission at strength 1.4: under the Standard view transform its red channel clipped across the whole face and it rendered as a flat orange disk. Now the same ShaderNodeEmission (color and strength asserted) owns the face toward the camera at strength 0.95, below clipping, and a Layer Weight facing ramp hands it over to a dark dielectric shell at the silhouette, so the globe has a limb. A shadowless warm point light at its center stands in for the glow it throws on its own collar, plinth and the floor; the shell is lit from inside only on back faces, so the ball itself still shows only the asserted emission.

The world is a reflection-only sky: glossy rays see a warm overhead gradient above a dark horizon, and camera and diffuse rays see the dark stage. The mirror-finish gold otherwise reflects the near-black world as a black ball.

verify_png samples each swatch where the camera sees it: the sphere center is projected through the render camera (world_to_camera_view) rather than assumed to sit at the center of a third of the frame, so the six-region check follows the layout.

-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Distinct swatch colors ≠ 6 (--same-base lands here); also render not six distinct regions
4--output produced no file
5Wrong-era EEVEE engine id was accepted
10Gallery framing violation

--no-verify was a skip-flag and has been removed. Pixel verification always runs when --output is passed.

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --same-base.

-

Verified

+

Verified#

Runs headless on Blender 4.5.10 LTS and 5.1.1; exercised on both by the blender-smoke workflow on every PR and weekly schedule. The --output render path additionally measures framing against the Layer 1 band via examples/gallery_framing.py (exit 10 on violation) before writing the still.

-
+

Source

examples/swatch-grid/swatch_grid.py - View on GitHub → + 464 lines · View on GitHub →
-
"""Procedural-materials swatch grid -- a runnable BDT example.
+      
+
"""Procedural-materials swatch grid -- a runnable BDT example.
 
 Renders a tiered 3x2 display of material spheres on plinths, one per material, demonstrating the
 `procedural-materials-and-shaders` patterns end to end: Principled BSDF (metal +
@@ -778,7 +1301,41 @@ 

Source

print(f"FATAL: {type(exc).__name__}: {exc}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -798,20 +1355,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/tavern-stool/index.html b/docs/gallery/tavern-stool/index.html index dd9200c3..a69fd397 100644 --- a/docs/gallery/tavern-stool/index.html +++ b/docs/gallery/tavern-stool/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

tavern-stool

A procedural tavern stool with a lathed seat, turned splayed legs, staggered stretchers, and iron ferrules gripping each leg on its own axis over a level tread, through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

+

Rendered headless by the showcase piece itself — click to zoom.

witnesses Recomputed: 3040 tris, two materials with 1168 wood and 576 metal faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.341×0.341×0.470 m, LOD ratios in band, convex collider 468 tris, non-empty glTF. Hygiene all zero, four treads at zmin 0, stretcher-leg gap 0, ferrule grip 1.20 mm on each leg's axis, leg ends 1.1 mm above their treads, bake 22.4 texels per UV cell. --float-stretchers exits 17; --pipe-ferrule 18; --sink-legs 20; --low-bake 21.
+

tags mesh export showcase

blender --background --python showcase/tavern-stool/tavern_stool.py --
@@ -282,22 +341,22 @@

tavern-stool

Legs are one loft per leg, not stacked cones. Each foot is an iron ferrule sleeve on the leg's own axis, its inner wall a named grip (1.2 mm) inside the leg. The sleeve's raked bottom rim is buried in a level iron tread, and the leg ends inside the sleeve, above the tread. The first build ended every leg 3 mm above a vertical ring on the floor, with a 1 mm air gap around the foot. The front-back and side stretchers sit at different heights, so their tenons do not meet inside the leg. Each member has its own wood tone and grain.

It asserts budget conformance of the generated result. It does not witness an API contract. "It rendered without error" is not a check.

Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package).

-

Budgets

+

Budgets#

Declared as named constants. Every gate recomputes its value from the mesh, materials, UVs, evaluated LOD, collider or export file.

AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles2750–33503040 / 3040 / 3040
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.5000
LOD2 ratio0.10–0.35 of base0.2197 / 0.2197 / 0.2197
Materialsexactly 2 distinct; ≥200 wood, ≥48 metal faces2 slots; 1168 / 576
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(0.341, 0.341, 0.470) m ± 0.015(0.3406, 0.3406, 0.4700), zmin 0
Collider tris≤ 640468
Exportwritten, size > 0259676 / 259676 / 259668 bytes

Base triangles rose from 2528 to 3040 in the second quality pass, and the band moved from 2300–3200 to 2750–3350. Each foot gained a tread, and the sleeves are walled on both ends. The bake went from 256 px to 1024 px, and its cage extrusion from 60 mm to 10 mm. See the bake texel budget below.

DECIMATE COLLAPSE triangle counts are not identical across series. The gate is a ratio band, not an exact count. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Construction is closed-form and member tones are seeded, so repeated runs on one binary print identical measurements. Export byte counts may differ by a few bytes across series.

-

Hygiene

+

Hygiene#

Recomputed from the generated mesh, not asserted about the script.

AxisDeclaredMeasured (all three)
Non-manifold edges00
Loose verts / edges0 / 00 / 0
Doubles merged at 1e-500
Zero-area faces00
N-gons00
Coplanar disjoint face pairs00
Grounded: zminwithin 1e-4 of 00.0000
Named supports: 4 treads (plus 4 sleeves)each zmin ≤ 1e-38, cup_z 0.00000
Seat0.340 m dia × 0.470 m high ± 0.020.3400 × 0.4700
-

Joint fit, feet and bake

+

Joint fit, feet and bake#

AxisDeclaredMeasured (all three)
Stretcher-leg BVH gap≤ 0.008 m0.00000
Ferrule grip (exit 18): leg radius where the sleeve wraps it, less the sleeve's inner radius, radially about the leg's own axis; 4 sleeves0.0006–0.0025 m0.00120
Foot stack (exit 20): leg end above its tread's top / sleeve bottom buried below it; 4 legs, 4 treads≥ 0.0005 m / ≥ 0.001 m0.00113 / 0.00943
Bake texel density (exit 21): smallest UV cell in baked texels≥ 12 px22.43 px (1024 px bake)

At 256 px the bake gave each of the 1,744 UV cells about 6 px. The hero then sampled the neighbouring cells' normals across cell borders, which showed as dark slivers on the legs above the ferrules.

-

Falsifiers

+

Falsifiers#

Each violates one named budget. All were run on 4.5.11, 5.1.2 and 5.2.1 and returned the same code on each.

FlagBudget violatedExit
--skip-decimateLOD1 ratio band9
--stray-vertloose vertex count is 015
--lift-zbounding box zmin is 016
--short-legsnamed ferrule supports at Z=016
--float-stretchersstretcher-leg join17
--pipe-ferruleferrule grip (a pipe ring on the tread; measured −0.00011)18
--sink-legsfoot stack (leg ends 10 mm into the tread; measured −0.00887)20
--low-bakebake texel density (256 px bake; measured 5.61 px)21

--pipe-ferrule keeps the treads, so the triangle band and the envelope cannot steal its failure. --sink-legs keeps the leg inside its sleeve, so the grip still passes.

-

Run

+

Run#

blender --background --python tavern_stool.py --
 blender --background --python tavern_stool.py -- --skip-decimate
 blender --background --python tavern_stool.py -- --stray-vert
@@ -309,17 +368,1554 @@ 

Run

blender --background --python tavern_stool.py -- --low-bake blender --background --python tavern_stool.py -- --output stool.png

Smoke passes no flags.

-

Exit codes

+

Exit codes#

File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path. 15–19 are the hygiene and joint-fit family. 20 and 21 are this piece's own.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 2 distinct slots, or a face-count floor missed
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight
16Not grounded: bounding box zmin off 0, or a named ferrule support floats
17Joint fit: stretcher-leg gap
18Ferrule grip out of band, or a sleeve missing (--pipe-ferrule)
19Seat diameter or height off the stated real-world size
20Foot stack: leg end below its tread's top, or sleeve not buried (--sink-legs)
21Bake texel density below floor (--low-bake)
-
+

Source

showcase/tavern-stool/tavern_stool.py - View on GitHub → + 1537 lines · View on GitHub →
-
"""Game-ready tavern stool — a showcase piece, not an example.
+      
+
"""Game-ready tavern stool — a showcase piece, not an example.
 
 Asserts budget conformance of a procedural turned-leg stool after composing
 shipped pipeline pieces: bmesh construction, UVs, two materials, high-to-low
@@ -1857,7 +3453,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -1877,20 +3507,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/temp-override-join/index.html b/docs/gallery/temp-override-join/index.html index 9254afef..d32c80fd 100644 --- a/docs/gallery/temp-override-join/index.html +++ b/docs/gallery/temp-override-join/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

temp-override-join

Join seven lantern parts into one object under bpy.context.temp_override — the supported replacement for the removed context.copy() dict-pass form.

+

Rendered headless by the example itself — click to zoom.

witnesses temp_override actually applies: join consumes all six sources, exactly one mesh remains, no geometry is lost, the five part materials merge into one slot each with every face still on its own material, and local Z spans foot to grip.
+

tags operators context

blender --background --python examples/temp-override-join/temp_override_join.py --
@@ -282,7 +341,7 @@

temp-override-join

The parts are the way a prop artist blocks the lantern out: a red enamel fount, an amber glass globe, an iron wire guard, the two hurricane side air tubes, the bell cap, the wire bail with its wooden grip, and the brass wick knob and filler cap. Each is its own object with its own mesh, materials and transform. The join produces the single Lantern object an engine wants.

What it witnesses: object.join under temp_override actually consumes the sources and merges their material slots. The check asserts that exactly one mesh remains and it is the target, that all six sources are gone, and that no geometry was lost (verts and faces equal the sum over the parts). The joined mesh must carry exactly the five part materials, once each, and every material must still own exactly the faces its parts brought. So the glass is still glass and the grip is still wood. The local Z span must run from the fount's foot (0) to the top of the grip (closed form 2.303), which proves the part transforms were applied. A no-op override (the 5.x failure mode of the old dict-pass path) leaves seven objects.

The render is the joined object. Everything in the still is one mesh object. It shows five materials because the slots merged and the per-face indices were remapped. If they had not been, the lantern would render in the target's red enamel from grip to foot. A shadowless warm point light inside the globe (render-only) stands in for the lit wick.

-

Run

+

Run#

# Cheap correctness check (no render) — the CI check:
 blender --background --python temp_override_join.py --
 
@@ -292,18 +351,592 @@ 

Run

# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts): blender --background --python temp_override_join.py -- --output join.png blender --background --python temp_override_join.py -- --output join.png --engine cycles
-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh object count after join ≠ 1 (--no-override lands here)
4Joined target is not the sole remaining mesh
5Verts / faces ≠ the sum over the seven parts
6Source objects still present
7Local Z span ≠ [0, 2.303]: a part transform was not applied
8Material slots ≠ the five part materials, once each
9Faces per material ≠ what the parts brought (indices not remapped)
10--output framing gate (gallery_framing)
11--output asset-quality floors (gallery_asset_quality)
12--output produced no file

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --no-override.

-
+

Source

examples/temp-override-join/temp_override_join.py - View on GitHub → + 574 lines · View on GitHub →
-
"""Join meshes via bpy.context.temp_override — a runnable example.
+      
+
"""Join meshes via bpy.context.temp_override — a runnable example.
 
 Witnesses the prefer-temp-override-over-context-copy contract: operators that
 need a fabricated active/selection context must run under
@@ -878,7 +1511,23 @@ 

Source

except Exception as e: import traceback; traceback.print_exc(); print(f"FATAL: {e}", file=sys.stderr); sys.exit(1)
+
+ +
@@ -898,20 +1547,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/terrain-scatter/index.html b/docs/gallery/terrain-scatter/index.html index 5a50eaaa..ef909054 100644 --- a/docs/gallery/terrain-scatter/index.html +++ b/docs/gallery/terrain-scatter/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

terrain-scatter

A Geometry Nodes hill tile with embedded, varied stone scatter through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

+

Rendered headless by the showcase piece itself — click to zoom.

witnesses Recomputed: 2398 tris, two materials with 720 stone faces, UVs in 0..1 with zero AABB overlap, outer AABB 1.800x1.800x0.570 m, grounded zmin, 9 stones of >=80 faces each sealed into the ground in 8/8 sectors, footprint ratio 9.8, rim tilt step 33.5 deg, no stone pairs overlapping, LOD ratios in band, convex collider 36 tris, hygiene zero, non-empty glTF. --skip-decimate exits 9; --stray-vert 15; --lift-z 16; --poke-rock 17; --float-rocks 18; --box-rocks 19; --pile-rocks 20; --perch-rocks 21; --uniform-rocks 22; --sharp-rim 23.
+

tags mesh export showcase

blender --background --python showcase/terrain-scatter/terrain_scatter.py --
@@ -283,26 +342,26 @@

terrain-scatter

It asserts budget conformance of the generated result. It does not witness an API contract. "It rendered without error" is not a check.

Composes skills geometry-nodes-python, mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package). Hygiene combinatorics match examples/mesh-hygiene-audit (copied, not imported).

Intended size: 1.80 m square hill tile, ~0.16 m sine amplitude, nine embedded stones from cobble to boulder; outer AABB 1.800 × 1.800 × 0.570 m.

-

Budgets

+

Budgets#

Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.

AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles1400–28002398 / 2398 / 2398
LOD1 ratio0.32–0.62 of base0.4996 / 0.4996 / 0.4996
LOD2 ratio0.10–0.35 of base0.2193 / 0.2193 / 0.2193
Materialsexactly 2 distinct, ≥24 stone faces2 slots, 720 stone
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(1.800, 1.800, 0.570) m ± 0.015(1.8000, 1.8008, 0.5700), zmin 0
Collider tris≤ 12036
Exportwritten, size > 0199496 / 199496 / 199480 bytes

Base triangles rose from 1194 to 1758 in the first quality pass: 9-vert hill became a 21-vert grid, and bevelled cubes became subdiv-2 icospheres. Outer Z dropped from 0.655 m (crate corners swinging through the slab) to 0.552 m of seated stone on the hill.

The second pass took them from 1758 to 2398: the four-segment rim bevel adds 640. The outer Z moved from 0.584 m to 0.570 m, because the stones now sink into the ground rather than perch on it, and the declared size was re-fitted to match. The collider fell from 72 to 36 triangles because it now hulls a coarse, unbevelled 9-vertex tile (COLLIDER_GRID): the chamfer and micro-relief are visual, and on the full-resolution tile they pushed the hull to 178, over its ceiling.

DECIMATE COLLAPSE triangle counts are not guaranteed identical across series — the gate is a ratio band, not an exact count. This mesh matched on 4.5.11 / 5.1.2 / 5.2.1. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Construction uses no RNG: jitter, cleave planes and relaxation are all closed form or fixed iteration. Export byte counts differ by 16 B on 5.2.1 (glTF serializer), not a gated axis.

-

Hygiene

+

Hygiene#

Recomputed from the generated mesh, not asserted about the script.

AxisDeclaredMeasured (all three)
Non-manifold edges00
Loose verts / edges0 / 00 / 0
Doubles merged at 1e-500
Zero-area faces00
N-gons00
Coplanar disjoint face pairs00
Grounded: zminwithin 1e-4 of 00.0000
Stone shells99
Per-stone faces≥ 4080
Stone floor zmin≥ 0.012 m0.06946
Seat offset (zmin − dirt Z)≤ 0.02 m−0.03326
Interpenetrating stone pairs (BVH overlap)00
Sealed sectors per stone (buried vertex in each of 8)8 / 88 / 8
Stone footprint ratio (largest / smallest)≥ 3.09.812
Rim tilt step between adjacent dirt faces≤ 40°33.52°
-

Why the stones are cleaved, relaxed and faceted

+

Why the stones are cleaved, relaxed and faceted#

The committed stones were smooth-shaded, near-white ellipsoids with a gentle sine bump: eggs or marshmallows on a pale clay slab. Two pairs also interpenetrated, because the GN scatter jitters by up to 0.22 m on a 0.575 m grid, and no budget compared stone to stone.

  • Cleaved. Each ellipsoid is cut by N_CLEAVES planes whose normals and depths come from the stone's own centre, closed form. Every vertex beyond a plane is projected onto it, which leaves flat broken faces. Stones are shaded flat, because broken rock is faceted; smooth shading turned the cleaved stones back into eggs.
  • Scaled. Cleaving takes about a third off each stone, and at the old size they read as pebbles, so every semi-axis is ROCK_SCALE = 1.35×.
  • Relaxed. Stone centres are pushed apart to 2 × STONE_R_BOUND + STONE_CLEAR over a fixed number of symmetric passes, and clamped so each stone's bound stays on the tile. STONE_R_BOUND is derived from ROCK_SCALE and the largest semi-axis, bump and tilt, so scaling the stones widens the spacing with them. With today's numbers the unrelaxed scatter would also clear; the relaxation is what keeps that true when the scatter or the sizes change.
  • Materials. Dark mottled soil and grey weathered stone, with roughness and a small bump from fine object-space noise.
-

Why the stones are sunk, sized, and the rim is rounded

+

Why the stones are sunk, sized, and the rim is rounded#

The second pass inspected the tile in clay, wire, six orthos and ground-contact close-ups, and found the tile read as a slice of cake with pebbles dropped on it.

  • Sealed. Stones were seated by sampling the dirt once, at the stone's centre, and biting the lowest point 35 mm below it. On the hill's slopes that left daylight under the downhill side: measured around each stone in 8 azimuth sectors, no stone had a buried vertex in more than 6, two in only 4. Now every vertex samples the ground under itself (a ray down onto the dirt, captured before any stone is added), and each stone sinks until every sector's most-buried vertex is EMBED_SINK below it.
  • Sized. All nine stones were within about 20% of one size, on a relaxed 3×3 grid, and the top view read as planted. STONE_SIZES gives each scatter point a factor from 0.55 to 1.45, boulders among cobbles, and the relaxation spaces each pair by the sum of their own bounds.
  • Rounded rim. The hill met the vertical walls at a knife edge all round. The rim is bevelled in four segments, and the budget bounds the step in normal elevation between adjacent faces. Elevation, not dihedral: at the corners the chamfer strips meet at a right angle in plan, which is a rounded corner.
  • Ground, not a sine. Two closed-form relief terms, 14 mm and 7 mm, ride on the hill in the same GN tree. The hill and rim are smooth-shaded, because flat shading printed the 21×21 grid on the ground; stones and walls stay flat.
  • Materials. Stones are mid-grey (they were near-white where the key hit them), and the dirt darkens toward a subsoil colour below the slab lift, in object space, without adding a material.

stone_overlap_audit BVH-tests every stone pair. --pile-rocks skips the relaxation and draws the scatter in to 40% so neighbours collide: 7 pairs interpenetrate, and the piece exits 20. Skipping the relaxation alone would prove nothing, because the cleaved stones miss each other unrelaxed.

-

Falsifiers

+

Falsifiers#

Each violates one named budget. All were run on 4.5.11, 5.1.2 and 5.2.1 and returned the same code on each.

FlagBudget violatedExit
--skip-decimateLOD1 ratio band9
--stray-vertloose vertex count is 015
--lift-zbounding box zmin is 016
--poke-rockstone floor zmin17
--float-rocksseat offset vs sampled dirt Z18
--box-rocksper-stone face floor19
--pile-rocksstone-to-stone interpenetration is 020
--perch-rocksevery stone sealed in 8/8 sectors (measures 3/8)21
--uniform-rocksfootprint ratio ≥ 3.0 (measures 1.851)22
--sharp-rimrim tilt step ≤ 40° (measures 88.50°)23
-

Run

+

Run#

blender --background --python terrain_scatter.py --
 blender --background --python terrain_scatter.py -- --skip-decimate
 blender --background --python terrain_scatter.py -- --stray-vert
@@ -316,17 +375,1588 @@ 

Run

blender --background --python terrain_scatter.py -- --sharp-rim blender --background --python terrain_scatter.py -- --output terrain.png

Smoke passes no flags.

-

Exit codes

+

Exit codes#

File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path. 15–19 are the hygiene and joint-fit family.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 2 distinct slots, or stone faces missing
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Hygiene (--stray-vert lands here)
16Grounded zmin (--lift-z)
17Stone floor poke (--poke-rock)
18Float above host (--float-rocks)
19Stone shell faces (--box-rocks)
20Stone pairs interpenetrate (--pile-rocks)
21A stone not sealed in every sector (--perch-rocks)
22Stone footprint ratio below floor (--uniform-rocks)
23Rim tilt step above ceiling (--sharp-rim)
-
+

Source

showcase/terrain-scatter/terrain_scatter.py - View on GitHub → + 1571 lines · View on GitHub →
-
"""Game-ready GN terrain scatter — a showcase piece, not an example.
+      
+
"""Game-ready GN terrain scatter — a showcase piece, not an example.
 
 Asserts budget conformance of a Geometry Nodes hill grid with instanced
 rocks after composing shipped pipeline pieces: GN construction,
@@ -1898,7 +3528,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -1918,20 +3582,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/text-version-stamp/index.html b/docs/gallery/text-version-stamp/index.html index 3173aef3..d3854aab 100644 --- a/docs/gallery/text-version-stamp/index.html +++ b/docs/gallery/text-version-stamp/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

text-version-stamp

The TextCurve data API — curves.new(type='FONT'), live body text from bpy.app.version_string, extrude and bevel_depth solids, and evaluated-mesh conversion — so every render self-documents which Blender produced it.

+

Rendered headless by the example itself — click to zoom.

witnesses TextCurve solids are exactly predictable: evaluated z-extent = 2 × (extrude + bevel_depth), bevel widens the outline by 2 × bevel_depth, editing body regenerates geometry — and version_string is not bare semver on LTS ("4.5.11 LTS"), so parse bpy.app.version instead.
+

tags curves depsgraph rendering

blender --background --python examples/text-version-stamp/text_version_stamp.py --
@@ -281,9 +340,9 @@

text-version-stamp

A runnable example that builds a beveled 3D stamp of the running Blender version — a TextCurve (bpy.data.curves.new(type='FONT')) whose body is the live bpy.app.version_string, so every render self-documents which Blender produced it. CI artifacts made this way are self-labeling.

What it witnesses: the TextCurve data-API contract. curves.new(type='FONT') returns a Curve subclass, the built-in font ("Bfont Regular") is always loaded even headless, body is plain assignable text that regenerates geometry on edit (more characters → strictly wider evaluated mesh), and extrude / bevel_depth produce exactly predictable solids: the evaluated mesh z-extent equals 2 × (extrude + bevel_depth) and the round bevel widens the outline in-plane by 2 × bevel_depth. Flat text (no extrude, no bevel) is already filled — faces exist — but strictly planar. The check also witnesses the depsgraph lifetime hazard: after to_mesh_clear() the returned Mesh reference is dead and any access raises ReferenceError.

Version divergence: the string format itself differs — "5.1.2" on 5.x but "4.5.11 LTS" on 4.5 (the suffix is part of version_string). The check therefore asserts the contract that holds on both: version_string starts with the dotted bpy.app.version tuple. Code that parses version_string as a bare semver breaks on every LTS build; branch on the bpy.app.version tuple instead.

-

Staging

+

Staging#

The numerals stand on a dark plinth, with the steel caption set into the plinth's front face. The caption used to float in the air above the stamp, and an emissive bar lay along the bottom edge of the frame. The still renders under the Standard view transform on the house stage. Render path only; the checked text body and its extents are unchanged.

-

Run

+

Run#

# Cheap correctness check (no render) — the CI check:
 blender --background --python text_version_stamp.py --
 
@@ -294,18 +353,343 @@ 

Run

blender --background --python text_version_stamp.py -- --output stamp.png blender --background --python text_version_stamp.py -- --output stamp.png --engine cycles

The render scales the stamp to a constant width, so the frame holds for any version-string length.

-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Not a FONT TextCurve with the built-in Bfont
4body is not the live version_string (--wrong-body lands here)
5Flat text is not a filled planar mesh
6Extrude / bevel closed form failed
7Appending characters did not widen the text
8Mesh survived to_mesh_clear()
9--output produced no file
10Gallery framing violation

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --wrong-body.

-
+

Source

examples/text-version-stamp/text_version_stamp.py - View on GitHub → + 325 lines · View on GitHub →
-
"""A beveled 3D stamp of the running Blender version — a runnable example.
+      
+
"""A beveled 3D stamp of the running Blender version — a runnable example.
 
 Witnesses the TextCurve data-API contract: `bpy.data.curves.new(type='FONT')`
 returns a Curve subclass whose `body` is plain assignable text, whose default
@@ -631,7 +1015,41 @@ 

Source

except Exception as e: import traceback; traceback.print_exc(); print(f"FATAL: {e}", file=sys.stderr); sys.exit(1)
+
+ +
@@ -651,20 +1069,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/treasure-chest/index.html b/docs/gallery/treasure-chest/index.html index 5d86825f..d4f546f0 100644 --- a/docs/gallery/treasure-chest/index.html +++ b/docs/gallery/treasure-chest/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

treasure-chest

A procedural iron-bound chest with a lofted barrel-vault lid, U-wrap bands, and hinge knuckles through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

+

Rendered headless by the showcase piece itself — click to zoom.

witnesses Recomputed: 3852 tris, two materials with 1608 wood and 318 metal faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.764×0.542×0.691 m, LOD ratios in band, convex collider 176 tris, non-empty glTF. Hygiene all zero, four feet at zmin 0, lid-knuckle to hinge barrel 7 mm, band-wall gap 0. --float-hinge exits 17 on the hinge join; --narrow-bands exits 18 on the band seat.
+

tags mesh export showcase

blender --background --python showcase/treasure-chest/treasure_chest.py --
@@ -282,23 +341,23 @@

treasure-chest

The lid is one lofted vault slab, not stacked boxes. Hinge join is measured from local lid knuckles over the barrels — a spanning stile only has verts at the X ends, 8 cm from the knuckles, and reports a false gap.

It asserts budget conformance of the generated result. It does not witness an API contract. "It rendered without error" is not a check.

Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package).

-

Budgets

+

Budgets#

Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.

AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles3600–45003816 / 3816 / 3816
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.5000
LOD2 ratio0.10–0.35 of base0.2196 / 0.2196 / 0.2129
Materialsexactly 2 distinct; ≥200 wood, ≥80 metal faces2 slots; 1608 / 300
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(0.764, 0.524, 0.696) m ± 0.015(0.7640, 0.5237, 0.6958), zmin 0
Lid bands on the vault3 bands spanning the lid; every vertex radius within 1e-4 m of the band's inner/outer radius3; 0.000000
Collider tris≤ 200180
Exportwritten, size > 0285980 / 285980 / 285972 bytes
-

Hygiene

+

Hygiene#

Recomputed from the generated mesh, not asserted about the script.

AxisDeclaredMeasured (all three)
Non-manifold edges00
Loose verts / edges0 / 00 / 0
Doubles merged at 1e-500
Zero-area faces00
N-gons00
Coplanar disjoint face pairs00
Grounded: zminwithin 1e-4 of 00.0000
Named supports: 4 feeteach zmin ≤ 1e-34, foot_z 0.00000
Body plan0.74 × 0.46 m ± 0.040.7400 × 0.4600
-

Joint fit and seat

+

Joint fit and seat#

AxisDeclaredMeasured (all three)
Lid-knuckle to hinge barrel≤ 0.020 m0.00700
Band-wall BVH gap≤ 0.008 m0.00000

DECIMATE COLLAPSE triangle counts are not identical across series — the gate is a ratio band, not an exact count. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Construction uses no RNG. Export byte counts may differ by a few bytes across series.

-

Lid bands

+

Lid bands#

Found on the inspection sheet: the three iron bands on the vaulted lid were each eight boxes rotated about X by +t, with t the arc angle at the segment. At angle t the arc's tangent in (y, z) is (cos t, -sin t), which is a rotation of -t. Every segment sat 2t off the tangent, and in the side orthos the bands fanned out from the lid like feathers. The same sign error tilted the front-edge band caps and the hasp. Those segment tips also set the Y envelope: the bounding box shrank 18 mm when they were fixed, so the declared outer size was re-fitted to (0.764, 0.524, 0.696).

Each band is now one add_arc_slab on the lid's own vault: the same angular span and segment count as the lid timber, inner radius LID_BAND_BITE inside the lid, outer radius BAND_T beyond. Caps and hasp rotate by -t. lid_band_audit un-rotates the lid about its hinge, takes each lid-iron shell's radius from the vault axis, counts the shells spanning at least 60% of the arc, and requires every band vertex inside the band's radial range (exit 18). The builder re-centres the whole mesh after swinging the lid, so the audit recovers that shift from the feet, which are symmetric. The rest of the low body is not: the hasp hangs from the lid down the front. --lift-lid-bands floats the bands 5 mm off the vault (measured 5.000 mm) while the envelope stays inside tolerance.

The timber gets the per-plank tone and grain shader, and the iron gets rust wear (copied from shipping-crate). The temp glTF export is removed after it is measured.

-

Falsifiers

+

Falsifiers#

Each violates one named budget. All eight were run on 4.5.11, 5.1.2 and 5.2.1 and returned the same code on each.

FlagBudget violatedExit
--skip-decimateLOD1 ratio band9
--stray-vertloose vertex count is 015
--lift-zbounding box zmin is 016
--short-legsnamed foot supports at Z=016
--float-hingelid-knuckle to hinge barrel17
--narrow-bandsband-wall seat18
--lift-lid-bandslid bands conform to the vault (floats them 5 mm off it)18
-

Run

+

Run#

blender --background --python treasure_chest.py --
 blender --background --python treasure_chest.py -- --skip-decimate
 blender --background --python treasure_chest.py -- --stray-vert
@@ -309,17 +368,1526 @@ 

Run

blender --background --python treasure_chest.py -- --lift-lid-bands blender --background --python treasure_chest.py -- --output chest.png

Smoke passes no flags.

-

Exit codes

+

Exit codes#

File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path. 15–19 are the hygiene and joint-fit family.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 2 distinct slots, or a face-count floor missed
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight
16Not grounded: bounding box zmin off 0, or a named foot floats
17Joint fit: lid-knuckle to hinge barrel
18Seat: band-wall gap, or lid bands off the vault (--lift-lid-bands)
19Body plan off the stated real-world size
-
+

Source

showcase/treasure-chest/treasure_chest.py - View on GitHub → + 1509 lines · View on GitHub →
-
"""Game-ready treasure chest — a showcase piece, not an example.
+      
+
"""Game-ready treasure chest — a showcase piece, not an example.
 
 Asserts budget conformance of a procedural slatted chest after composing
 shipped pipeline pieces: bmesh construction, UVs, two materials, high-to-low
@@ -1829,7 +3397,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -1849,20 +3451,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/triangulate-tangents/index.html b/docs/gallery/triangulate-tangents/index.html index 463686a6..57b5fc44 100644 --- a/docs/gallery/triangulate-tangents/index.html +++ b/docs/gallery/triangulate-tangents/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

triangulate-tangents

A machined buckler verifying the tangent-space contract a game engine's normal mapping depends on. Deterministic triangulation, unit orthogonal tangent frames, and the edge/UV-delta formula matching mikktspace within welding tolerance.

+

Rendered headless by the example itself — click to zoom.

witnesses calc_tangents aborts on any ngon; planar UVs on a cylindrical wall collapse tangents onto normals; a MeshUVLoopLayer handle held across calc_tangents dangles on 4.5 (471 phantom flips, silent exit 0) while the mikktspace math is byte-identical on both versions.
+

tags mesh uv

blender --background --python examples/triangulate-tangents/triangulate_tangents.py --
@@ -285,7 +344,7 @@

triangulate-tangents

What each check catches on failure: a remeshed asset breaking the triangulation count (probe: dropped profile ring, exit 3, 624 vs 720); a wrong tangent formula in the independent derivation (probe: swapped du/dv, exit 7, weld 1.460); and the stale-handle read (probe: no re-fetch — corrupt measurements on 4.5, correct values on 5.1/5.2 by luck).

Version witness: mikktspace output is identical on Blender 4.5.11 LTS, 5.1.2 and 5.2.1 LTS (same weld deviation, same seam flips). The calc_tangents(uvmap=) signature is stable. The lifetime hazard above is the only behavioral split, and it corrupts measurements rather than raising.

The render shows the buckler turned three-quarter on a walnut stand: a pointed brass boss on a riveted blued-steel flange, a turned-steel dome, and a brass rolled rim ringed with steel rivets. Materials are assigned per lathe ring. The dome's lathe grooves are a bump read straight off UV v (the radius), so they are exact circles, with no procedural distortion. Its anisotropic highlight follows the renderer's own UV-map tangent, so it fans out radially from the boss. This is a numeric-only contract (docs/VISUAL-STYLE.md): the renderer derives its tangents from the UVs, so a break in the calc_tangents() loop data the check reads does not move pixels. The staging is derived from the posed mesh: the stand rests on the floor, the rim's lowest vertex sits 1 cm into its top, and a kickstand strut runs from the buckler's back (found by a local-space ray) to the floor behind it, angled away from the camera.

-

Run

+

Run#

# Cheap correctness check (no render) — the CI check:
 blender --background --python triangulate_tangents.py --
 
@@ -295,18 +354,646 @@ 

Run

# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts): blender --background --python triangulate_tangents.py -- --output buckler.png blender --background --python triangulate_tangents.py -- --output buckler.png --engine cycles
-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper, 11 the shared asset-quality helper.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Loop-triangle count ≠ closed form
4Re-fetched UV layer drifted from the polar field (--zero-uv lands here)
5Tangent basis not orthonormal
6Bitangent ≠ sign × (n × t)
7Tangents deviate from the edge/UV closed form
8Flipped tangent inside a clean triangle
10Gallery framing violation; also --output produced no file
11Asset-quality floor violation (--output only; shared helper)

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --zero-uv.

-
+

Source

examples/triangulate-tangents/triangulate_tangents.py - View on GitHub → + 628 lines · View on GitHub →
-
"""A machined buckler verifying the tangent-space contract — a runnable example.
+      
+
"""A machined buckler verifying the tangent-space contract — a runnable example.
 
 Witnesses the tangent-space contract a game engine's normal mapping depends
 on, computed through the same mikktspace path engines use:
@@ -935,7 +1622,41 @@ 

Source

except Exception as e: import traceback; traceback.print_exc(); print(f"FATAL: {e}", file=sys.stderr); sys.exit(1)
+
+ +
@@ -955,20 +1676,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/turntable/index.html b/docs/gallery/turntable/index.html index e37f985e..c0d849f8 100644 --- a/docs/gallery/turntable/index.html +++ b/docs/gallery/turntable/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

turntable

A slotted-actions Z-rotation turntable keyed through the cross-version channelbag path (get_channelbag_for_slot).

+

Rendered headless by the example itself — click to zoom.

witnesses Slotted-actions boundary: ensure-helper channelbag on 5.x, strip.channelbag on 4.4/4.5.
+

tags animation

blender --background --python examples/turntable/turntable.py --
@@ -280,7 +339,7 @@

turntable

A runnable example that keyframes a Z-rotation turntable through the slotted-actions cross-version channelbag path from the slotted-actions-animation skill, and picks the render engine with the version-branch EEVEE-id helper.

Which fix it witnesses: the slotted-actions cross-version helper. On Blender 5.x the channelbag comes from action_ensure_channelbag_for_slot; on 4.4/4.5 from strip.channelbag(slot, ensure=True) (legacy action.fcurves still works on 4.5, raises AttributeError on 5.x).

-

Run

+

Run#

# Cheap correctness check only (no render) — the CI smoke check:
 blender --background --python turntable.py --
 
@@ -292,18 +351,246 @@ 

Run

blender --background --python turntable.py -- --output turntable.png --engine cycles

By default it runs only the frame-independent correctness check: it inserts the rotation keys, samples the object's Z rotation at frame 1 vs a later frame, and asserts they differ (the keys drive playback). --output additionally renders a still; the full animated loop is a showcase extra, not part of the CI check.

The still's staging is render-only and runs after the check. The head gets a subdivision modifier (the 500-face monkey read as faceted plates) and is seated 10 mm into the platter of a turntable plinth, lifted from its own lowest evaluated vertex. It used to hover just above the floor with nothing under it. The rotation keys, the sampled frames and every exit code are unchanged. This render path does not call gallery_framing.check_framing, because adding it would add exit code 10. The framing was measured with the helper out of band instead: fill y 0.789, minimum margin 0.050, ok.

-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Rotation keys do not drive playback (--no-keys lands here)
4--output produced no file
5Wrong-era EEVEE engine id was accepted

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --no-keys.

-
+

Source

examples/turntable/turntable.py - View on GitHub → + 228 lines · View on GitHub →
-
"""Slotted-actions turntable -- a runnable BDT example.
+      
+
"""Slotted-actions turntable -- a runnable BDT example.
 
 Keyframes a Z-rotation turntable through the slotted-actions cross-version channelbag path
 (`get_channelbag_for_slot`) and selects the engine with the version-branch EEVEE-id helper.
@@ -532,7 +819,32 @@ 

Source

import traceback; traceback.print_exc() print(f"FATAL: {type(exc).__name__}: {exc}", file=sys.stderr); sys.exit(1)
+
+ +
@@ -552,20 +864,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/usd-export-evaluation-mode/index.html b/docs/gallery/usd-export-evaluation-mode/index.html index 4c01f04c..ab6c68cf 100644 --- a/docs/gallery/usd-export-evaluation-mode/index.html +++ b/docs/gallery/usd-export-evaluation-mode/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

usd-export-evaluation-mode

The USD exporter evaluation_mode chooses viewport versus render modifier quality. TESSELLATE makes the split observable; BEST_MATCH writes the cage and the mode is silent.

+

Rendered headless by the example itself — click to zoom.

witnesses TESSELLATE+VIEWPORT writes 26/24; TESSELLATE+RENDER writes 98/96 subdivisionScheme=none; BEST_MATCH writes the 8-vert catmullClark cage. --evaluation-mode VIEWPORT and --subdivision BEST_MATCH exit 4.
+

tags depsgraph export

blender --background --python examples/usd-export-evaluation-mode/usd_export_evaluation_mode.py --
@@ -285,21 +344,380 @@

usd-export-evaluation-mode

  • exit 3 — VIEWPORT TESSELLATE did not write the L1 closed form
  • exit 4 — RENDER TESSELLATE wrote viewport quality or the BEST_MATCH cage (--evaluation-mode VIEWPORT measured 26/24; --subdivision BEST_MATCH measured 8/6 catmullClark)
  • exit 5 — BEST_MATCH stopped writing the cage
  • exit 6 — RENDER and VIEWPORT files are identical

Staging: Catmull-Clark pulls the surface inside the 2 m cage, and level 1 and level 2 shrink by different amounts, so both balls used to hover about 0.16 m over the floor at the cage's z = 1.0. The render path now rests each one on the floor from its own evaluated vertices (measured zmin 0.000000 for both), after the check has run.

Probed on the CI Linux portables Blender 5.2.1 LTS (9e2066aef7ef) and Blender 4.5.13 LTS (daeeeca98fb0): wm.usd_export exists, poll() is true in --background, evaluation_mode is {RENDER, VIEWPORT}. The --output render path measures framing via examples/gallery_framing.py (exit 10 on violation).

-

Run

+

Run#

blender --background --python usd_export_evaluation_mode.py --
 blender --background --python usd_export_evaluation_mode.py -- --output u.png
-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage; also base cube verts ≠ 8
3VIEWPORT TESSELLATE point/face count off closed form
4RENDER TESSELLATE point/face count or scheme off closed form
5BEST_MATCH cage point count or scheme off
6RENDER and VIEWPORT USDA point counts are identical
10Gallery framing violation
12--output produced no file

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output, --evaluation-mode, or --subdivision.

-
+

Source

examples/usd-export-evaluation-mode/usd_export_evaluation_mode.py - View on GitHub → + 359 lines · View on GitHub →
-
"""USD evaluation_mode — a runnable example.
+      
+
"""USD evaluation_mode — a runnable example.
 
 Witnesses ``bpy.ops.wm.usd_export`` ``evaluation_mode='RENDER'`` against
 ``VIEWPORT``, which the snippet names but does not prove. A SUBSURF cube
@@ -659,7 +1077,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -679,20 +1131,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/uv-layer-grid/index.html b/docs/gallery/uv-layer-grid/index.html index e59e0341..cef198d9 100644 --- a/docs/gallery/uv-layer-grid/index.html +++ b/docs/gallery/uv-layer-grid/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

uv-layer-grid

The UV-layer authoring hazard — bmesh.ops.create_grid(..., calc_uvs=True) is a silent no-op unless a UV layer already exists; without one an Image Texture samples texel (0,0) everywhere.

+

Rendered headless by the example itself — click to zoom.

witnesses calc_uvs=True alone leaves 0 UV layers; pre-create + calc_uvs fills loops to the closed-form grid UVs ((x/size+1)/2, (y/size+1)/2) within 1e-6; explicit loop assignment is the calc_uvs-free fallback. The still is self-witnessing: the script re-reads its render and exits non-zero unless the hazard panel is one flat teal and the repaired panel a high-contrast checker.
+

tags mesh bmesh uv materials

blender --background --python examples/uv-layer-grid/uv_layer_grid.py --
@@ -286,7 +345,7 @@

uv-layer-grid

  • Silent no-op — calc_uvs=True starts creating a layer on its own (falsified: expecting n_layers == 0 fails if a layer appears; temporarily pre-creating a layer before the hazard probe exits 3).
  • Topology — create_grid segment/size contract drift (wrong face/vert counts).
  • Closed-form UVs — calc_uvs filling a different parameterization, or a one-texel shift (falsified: adding 0.1 to every U exited 6 with measured error 0.1).
  • Mesh persistence — UV layer or loop data lost across to_mesh.
  • Explicit path — loop assignment or the closed form itself regressing.
  • Pixel witness (with --output) — the saved PNG is read back and probed at each panel's projected center: the hazard panel must be one flat teal (per-channel spread ≤ 0.02, ordering b>g>r), the repaired panel a high-contrast checker (spread ≥ 0.25). Falsified twice: secretly giving the hazard panel UVs exited 11 with measured spread 0.7333; repainting texel (0,0) magenta exited 12 with measured rgb (0.927, 0.118, 0.536).

Version divergence: none probed — the silent no-op and closed-form UV fill assert identically on Blender 4.5 LTS and 5.1. The only gate in the file is the EEVEE engine id for the optional render (BLENDER_EEVEE_NEXT on 4.x, BLENDER_EEVEE on 5.x).

Render: two framed lightbox displays on floor trays (rear kick legs, status LED) sharing one neon checker image, shot at a slight 3/4. Left is the hazard (no UV layer) — flat teal of texel (0, 0). Right is the repair (pre-create + calc_uvs) — full magenta/cyan checker. If the UV contract failed, both panels would read the same — and the still is not just an illustration: the script re-reads its own render and exits non-zero unless the pixels prove the flat-vs-checker split (measured on 5.1.2: hazard spread 0.0078, repair spread 0.7412; identical on 4.5.11).

-

Run

+

Run#

# Cheap correctness check (no render) — the CI check:
 blender --background --python uv_layer_grid.py --
 
@@ -296,18 +355,555 @@ 

Run

# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts): blender --background --python uv_layer_grid.py -- --output uv.png blender --background --python uv_layer_grid.py -- --output uv.png --engine cycles
-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it. This example does not call the gallery framing helper; 10 is the missing-render-file check.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Silent-no-op hazard gone (--precreate-on-hazard lands here)
4Grid topology drifted
5Pre-create + calc_uvs did not persist one UV layer
6calc_uvs closed-form error
7Mesh UV round-trip error
8calc_uvs=False unexpectedly created a UV layer
9Explicit UV assignment error
10--output produced no file
11Broken panel is not flat
12Broken panel is not texel-(0,0) teal
13Repaired panel is not a checker
14Broken and repaired panels render identically

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --precreate-on-hazard.

-
+

Source

examples/uv-layer-grid/uv_layer_grid.py - View on GitHub → + 537 lines · View on GitHub →
-
"""UV-layer authoring hazard — a runnable example.
+      
+
"""UV-layer authoring hazard — a runnable example.
 
 Witnesses that `bmesh.ops.create_grid(..., calc_uvs=True)` silently creates
 *no* UV layer unless one already exists on the BMesh. AI-generated Blender
@@ -845,7 +1441,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -865,20 +1495,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/vertex-color-ao/index.html b/docs/gallery/vertex-color-ao/index.html index 0676f224..fcc07417 100644 --- a/docs/gallery/vertex-color-ao/index.html +++ b/docs/gallery/vertex-color-ao/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

vertex-color-ao

A stone well carrying baked ambient occlusion in a colour attribute, with the bake held to the closed-form hemisphere integral rather than to a captured value.

+

Rendered headless by the example itself — click to zoom.

witnesses Integrator matches 1 - 0.5(1 - 1/sqrt(1+k^2)) to 6.760e-04, unoccluded plate exactly 1.0, FLOAT_COLOR exact vs BYTE_COLOR sRGB-quantised (model agreement 3.189e-07), evaluated deviation 0.0.
+

tags mesh attributes game-pipeline rendering

blender --background --python examples/vertex-color-ao/vertex_color_ao.py --
@@ -286,30 +345,993 @@

vertex-color-ao

Pipeline arc neighbours: attribute domains in attribute-domain-shear and color-attribute-wheel, the second UV set for texture-baked lighting in lightmap-uv-channel, topology gates in mesh-hygiene-audit.

What it witnesses (all closed form or independently re-derived):

  • Analytic AO. The integrator matches 1 − A(H/d) at six probe distances spanning two decades of H/d (60.0 down to 0.60); worst error 6.760e-04 against a 2.5e-03 gate.
  • Unoccluded is exactly 1. An isolated flat plate bakes to 1.000000000 — not approximately, exactly, because no ray can self-hit.
  • Monotone with depth. AO rises strictly across the calibration ruler, 0.508301 → 0.928467.
  • Range and spread. Every baked value on the asset lies in [0, 1] and the bake actually uses the range (measured spread 1.000000); a silently constant bake would pass a bare range check and fails this one.
  • Storage. FLOAT_COLOR round-trips the linear value exactly (0.000e+00). BYTE_COLOR does not — see below.
  • Depsgraph survival. Both attributes read back off the evaluated mesh at deviation exactly 0.0, keeping data_type and domain.
  • Reuse hygiene. Identity scales, Well.Stone.* names, no default datablocks, min z == 0.00e+00, render colour attribute pinned to AO.
-

Hazards found while authoring

+

Hazards found while authoring#

  • BYTE_COLOR is sRGB-encoded 8-bit, not linear 8-bit. Writing linear 0.735 reads back 0.7379107. The round-trip error peaks at 3.782e-03, and the readback matches an independent encode → quantise → decode model to 3.189e-07 — so the encoding is confirmed, not guessed. Darks get more precision than a linear ramp would give and midtones get less. An exporter that hands the raw bytes to an engine expecting linear occlusion ships visibly wrong shadows. Store AO in FLOAT_COLOR unless the target genuinely wants sRGB.
  • bmesh.ops.bevel offsets along cached face normals. Moving a vertex does not refresh them. While the stale normal is within 90° of the true one the bevel merely skews; past 90° it flips sign and grows the solid outward. Measured while authoring, on a ring of bevelled boxes placed around a circle: the boxes at 0/36/72° bevelled correctly and the one at 108° grew by exactly one offset (12 mm) in both z directions, putting geometry below the ground plane. t.normal_update() before every bevel is the fix, and every _box/_prism here calls it; the hygiene check (exit 8) is what caught it.
  • Point-domain AO needs vertices to vary across. An 8-vertex slab bakes to one nearly-constant value per face, so the crevice gradient never reaches the attribute. The first draft rendered as flat pale stone with a correct bake underneath. Parts are subdivided before the bake for this reason.
  • color_attributes enumeration order is not portable — measured BYTE_COLOR first on 4.5.11 and FLOAT_COLOR first on 5.1.2 for the same mesh. Look attributes up by name, never by index.

What each check catches on failure (every one probed, with the measured error): an integrator sampling the full sphere rather than the hemisphere — AO 0.752686 against a closed form of 0.508332, error 2.444e-01 (exit 3); uniform-solid-angle weighting instead of cosine — error 1.202e-02 (exit 3); rays cast with no normal offset so they self-hit the surface they start on — unoccluded plate bakes to 0.160644531 instead of 1.0 (exit 4); a bake collapsed to a constant — spread 0.000000 (exit 5); the sRGB storage model swapped for naive linear 8-bit quantisation — 4.577e-03 disagreement with the real readback (exit 6); a Subdivision modifier resampling the attribute — 3270 evaluated elements against 840 authored (exit 7); a part sunk below the ground plane — -1.200e-02 (exit 8); a default Cube datablock name (exit 8).

The exit-7 probe earned its keep twice: the depsgraph check originally compared zip(src.data, eva.data), which stops at the shorter sequence, so a resampled attribute reported a clean 0.0 deviation and the probe exited 0. The length guard was added because the falsification pass failed to fail.

Version witness: check output is byte-identical on Blender 4.5.11 LTS and 5.1.2 — same 6966 vertices, same closed-form errors to every printed digit, same storage deviations.

Render as proof: the well on the dark stage, lit only by the studio rig, with the baked AO multiplied into base colour — the masonry joints, the shaft mouth, the coping undersides and the apron contact all darken from the attribute, not from the lights. The falsification variant (--falsify) writes the same bake inverted: sky-facing coping and apron tops go grimy while the recesses and post bases glow, occlusion turned inside out.

The studio rig is presentation only. Its fill and rim used to be strongly blue, which lit the whole stage cold (stage luma 0.243, warmth +0.01 against the calibration set). Its wedge grazed floor the camera barely saw and left a hard-edged patch on the back wall. Both are now near neutral, and the wedge pools warm light on the floor just behind the well from the camera's side: luma 0.274, warmth +0.023. The bake, the checks and the exit codes are unchanged.

-

Run

+

Run#

blender --background --python vertex_color_ao.py --
 blender --background --python vertex_color_ao.py -- --output well.png
 blender --background --python vertex_color_ao.py -- --falsify inverted.png

The --output render path additionally gates framing via examples/gallery_framing.py (fill 0.839y, margins 0.241/0.238/0.072/0.089, no edge touched) and the asset floors via examples/gallery_asset_quality.py (11 materials, edge90 0.152, no default names).

-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper. 11 is the shared asset-quality helper.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Calibration AO off closed form
4Unoccluded plate is not 1.0, or AO is not strictly increasing with distance
5Bake empty, out of range, or spread too small
6FLOAT_COLOR / BYTE_COLOR round-trip contract
7Colour attributes missing, retyped, or drifted under the depsgraph
8Asset part count, unapplied scale, namespace, render attribute, or ground plane
9--output produced no file
10Gallery framing violation
11Gallery asset-quality violation

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --falsify.

-
+

Source

examples/vertex-color-ao/vertex_color_ao.py - View on GitHub → + 963 lines · View on GitHub →
-
"""Vertex-colour AO — cheap baked occlusion in a colour attribute.
+      
+
"""Vertex-colour AO — cheap baked occlusion in a colour attribute.
 
 Engines that read vertex colour get their contact shadows for free if the
 asset ships with ambient occlusion baked into a colour attribute. The bake is
@@ -1273,7 +2295,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -1293,20 +2349,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/vertex-weight-limit/index.html b/docs/gallery/vertex-weight-limit/index.html index ba9f2e00..d71e82c3 100644 --- a/docs/gallery/vertex-weight-limit/index.html +++ b/docs/gallery/vertex-weight-limit/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

vertex-weight-limit

A rigged industrial robot arm pruned to the game-engine cap of four bone influences per vertex, through the data API. The check proves no vertex exceeds the cap, weights still sum to one, the pose survives pruning, and the modifier is still exact linear blend skinning.

+

Rendered headless by the example itself — click to zoom.

witnesses Limit-total and normalize without bpy.ops: keep the top four groups, VertexGroup.remove the rest, renormalize. LBS is verified against the weights read back from the mesh's own deform layer, not the authoring function.
+

tags armature mesh

blender --background --python examples/vertex-weight-limit/vertex_weight_limit.py --
@@ -284,10 +343,10 @@

vertex-weight-limit

What it witnesses: the skinning constraint every game engine enforces and AI-generated rigging code most often violates silently.

  • The limit is a data-API operation, not a context operator. Instead of bpy.ops.object.vertex_group_limit_total, the example reads each vertex's groups, keeps the top four by weight, VertexGroup.removes the rest, and renormalizes the survivors. Dropping without renormalizing leaves sums short of one — a mesh that shrinks toward the origin under load (the check's measured failure, 2.647e-03 off unit sum).
  • The armature modifier is still exactly linear blend skinning after the limit: every depsgraph-evaluated vertex equals Σ wᵢ · (pose.matrix @ bone.matrix_local.inverted()) @ rest, with the weights read back from the mesh's own deform layer (v.groups) — the weights on the mesh are the contract, not the weights you meant to write. Measured lbs_err = 1.2e-06.
  • Pruning must not damage the pose. Evaluated positions before and after the limit are held within 0.05 (measured 4.2e-03 over 3108 limited vertices), the plinth stays exactly pinned (Root is unposed), and the pre-limit authoring really carries five influences on the cables — otherwise the witness would be vacuous.

The vertex-group API (v.groups, VertexGroup.add/remove) is stable between Blender 4.5 LTS and 5.2 — the example runs identically on 4.5, 5.1 and 5.2, which is itself the version witness (measured values match to the digit).

-

What the render shows

+

What the render shows#

The render path paints the post-limit weights onto the arm as a BoneBlend colour attribute — Σ wᵢ · colourᵢ per vertex, read back from v.groups after the prune — and the cables, side hatches and trim bands display it through a matte Attribute-node material. Each rigid segment therefore shows its bone's flat colour (blue turret, teal upper arm, violet forearm, magenta wrist, lime gripper), and the cables grade from colour to colour exactly where the skin weights blend across each joint, while the posed arm shows those weights deforming it.

Numeric-only contract (docs/VISUAL-STYLE.md § The render is the proof): the four-influence cap itself has no visual signature. The pruned fifth weight is the small spill (at most 0.0027 on any vertex), so a five-influence arm deforms and paints indistinguishably — the prune preserves the pose by design, and that is what check 3 asserts. The render depicts the subject and where its weights blend; the cap is witnessed by the numbers above and by the --skip-limit falsifier.

-

Run

+

Run#

# Cheap correctness check (no render) — the CI check:
 blender --background --python vertex_weight_limit.py --
 
@@ -297,18 +356,794 @@ 

Run

# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts): blender --background --python vertex_weight_limit.py -- --output arm.png blender --background --python vertex_weight_limit.py -- --output arm.png --engine cycles
-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper; it is also the missing-render code. 11 is the shared asset-quality helper.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Pre-limit max influences ≠ 5
4Vertex over the 4-influence cap (--skip-limit lands here)
5Limit changed nothing
6Weight sums off 1.0 after renormalize
7Pose damaged by pruning, or evaluated vert count changed
8Evaluated mesh off LBS over limited weights
9Root-weighted mount moved
10Gallery framing violation; also --output produced no file
11Asset-quality floor violation (--output only)

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --skip-limit.

-
+

Source

examples/vertex-weight-limit/vertex_weight_limit.py - View on GitHub → + 776 lines · View on GitHub →
-
"""A rigged industrial robot arm pruned to the 4-influence game-engine limit — a runnable example.
+      
+
"""A rigged industrial robot arm pruned to the 4-influence game-engine limit — a runnable example.
 
 Witnesses the skinning constraint every game engine enforces and AI-generated
 rigging code most often violates silently: **no more than four bone influences
@@ -1085,7 +1920,41 @@ 

Source

except Exception as e: import traceback; traceback.print_exc(); print(f"FATAL: {e}", file=sys.stderr); sys.exit(1)
+
+ +
@@ -1105,20 +1974,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/vse-cut-list/index.html b/docs/gallery/vse-cut-list/index.html index a08a550b..f58d5dde 100644 --- a/docs/gallery/vse-cut-list/index.html +++ b/docs/gallery/vse-cut-list/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

vse-cut-list

The sequencer API rename from 4.5 LTS to 5.x — strips (never .sequences), new_effect ending in length= vs frame_end=, and left_handle/right_handle/duration replacing the deprecated frame_final_*. A deterministic cut list — color programs, a clamped GAMMA_CROSS, a scene strip, a text strip — asserted before and after save/reload.

+

Rendered headless by the example itself — click to zoom.

witnesses Each side hard-fails the other's spelling: TypeError on the wrong end kwarg, AttributeError on .sequences. The cross clamps to the source overlap, effect strips consume their inputs only when stacked above them, and a same-scene scene strip renders transparent.
+

tags sequencer rendering

blender --background --python examples/vse-cut-list/vse_cut_list.py --
@@ -288,9 +347,9 @@

vse-cut-list

Version divergence: the whole example is the divergence — gated on the bpy.app.version tuple (>= (5, 0, 0)), never on version_string ("4.5.11 LTS" is not bare semver). Each side asserts its own canonical contract plus the other side's removal/bridge state. Measured values are identical on 4.5.11 and 5.1.2, including the pixel witness. 5.2 LTS COLOR strips bake readonly width/height from the scene render resolution at new_effect time (4.5/5.1 have no intrinsic size — scale is a fraction of the output frame). The pixel check sets 96×54 before building the cut list; creating at factory 1920×1080 then rendering 96×54 makes 0.36-scaled cells larger than the output, C covers the frame, and TL samples amber (0.949, 0.62, 0.102) instead of crimson.

Render: the program wall is the sequencer output at frame 29 (mid cross) — crimson A, teal B, amber long-runner C, and the 50/50 cross blend, over the Stage scene strip showing the dark studio. An off-by-one in end-exclusive span math drops its cell to the dark stage; the caption strip carries the closed form. The hero presents that authentic frame on a reference monitor in a dark-studio editing bay — the pixels on the screen are the genuine sequencer output (evidence); only the bay around them is staged (presentation). Rendered locally with EEVEE (GPU host); the checks and --check-pixels need no GPU (Cycles CPU).

In the bay, each part bites the one it rests on. The monitor's neck rises 0.10 m into the bezel, the stand base sits 5 mm into the desk, and the "PROGRAM" caption stands on the desk's front face. The neck used to butt its top face onto the bezel's bottom on one plane, with half its depth hanging out behind, and the caption lay flat on the desk top 6 mm up, where the camera read it as a smear. None of this touches the sequencer frame or any check.

-

Framing measurement

+

Framing measurement#

The gallery still renders from the dedicated Bay scene (camera BayCam), not bpy.context.scene — a framing measurement with examples/gallery_framing.py must be handed that (scene, camera) pair explicitly.

-

Run

+

Run#

# Cheap correctness check (no render) — the CI check:
 blender --background --python vse_cut_list.py --
 
@@ -303,18 +362,767 @@ 

Run

# Also render the still (EEVEE on a GPU host; --engine cycles on GPU-less): blender --background --python vse_cut_list.py -- --output vse.png
-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is also the shared framing helper.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3.sequences accessor contract for this Blender
4Wrong-era new_effect end kwarg or TRANSFORM type did not TypeError
5Strip span or channel off closed form
6frame_final_* deprecation bridge for this Blender
7GC wiring or strip .type enum (--swap-inputs lands here)
8Scene strip span or Stage source
9Mosaic transform, compositing defaults, text body, or strip color
10Save/reload round-trip re-assert failed; also gallery framing violation
11--output produced no file
12--check-pixels compositing contract

The blender-smoke workflow runs the check and the pixel witness on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --swap-inputs.

-
+

Source

examples/vse-cut-list/vse_cut_list.py - View on GitHub → + 749 lines · View on GitHub →
-
"""VSE cut list — a runnable example.
+      
+
"""VSE cut list — a runnable example.
 
 Witnesses the sequencer API rename between Blender 4.5 LTS and 5.x, the
 single most common way AI-generated VSE code dies on a modern Blender:
@@ -1064,7 +1872,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -1084,20 +1926,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/vse-gamma-cross/index.html b/docs/gallery/vse-gamma-cross/index.html index 9e184562..820f6216 100644 --- a/docs/gallery/vse-gamma-cross/index.html +++ b/docs/gallery/vse-gamma-cross/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

vse-gamma-cross

The GAMMA_CROSS fade is not the naive linear mix: it blends in a gamma-0.5 space, so the mid-cross dips below the sRGB lerp. Tiny per-frame renders are asserted against the closed form per frame.

+

Rendered headless by the example itself — click to zoom.

witnesses out = ((1-t)*sqrt(A) + t*sqrt(B))^2 with t = (frame - start) / duration, never 1 inside the effect. The mid lerp deviation is 0.250; AgX-default sampling poisons the fit (Standard is mandatory).
+

tags sequencer rendering

blender --background --python examples/vse-gamma-cross/vse_gamma_cross.py --
@@ -285,7 +344,7 @@

vse-gamma-cross

Version witness: the blend math is identical on Blender 4.5 LTS, 5.1 and 5.2 LTS — every sample matches to the quantization step. The creation contract from vse-cut-list still gates the timeline: strips (never .sequences), and new_effect ending in length= on 5.x vs frame_end= on 4.5.

The render is the evidence itself, mounted. All 32 frames of the cross are rendered by the sequencer and laid side by side as a filmstrip: the GAMMA_CROSS strip on top, and directly beneath it the same two strips crossed by the sequencer's linear CROSS effect, frame for frame. Both filmstrips are authentic sequencer pixels shown unaltered (closest-texel sampling, emission only, Standard view) on a hooded grading monitor beside a three-ball control surface. The gamma strip sinks into a dark neutral valley at mid-cross where the linear strip passes through violet; three small warm ticks mark the t = 1/2 column. A GAMMA_CROSS that was really a lerp would make the two strips identical, so the still visibly breaks with the contract.

Before shooting, the render path guards its own comparison (exit 9): every frame must fill its tile edge to edge — a 5.2 COLOR strip bakes the scene size into its width/height at creation, so strips built before the tile resolution is set letterbox into black bars (probe: in-frame spread 1.0) — and each strip's mid frame must match its closed form, so the lower strip really is the lerp (probe: wiring GAMMA_CROSS in its place fails at 0.249).

-

Run

+

Run#

# Correctness check (tiny per-frame sample renders) — the CI check:
 blender --background --python vse_gamma_cross.py --
 
@@ -295,18 +354,668 @@ 

Run

# Also render the grading-monitor still (EEVEE on a GPU host; cycles on GPU-less): blender --background --python vse_gamma_cross.py -- --output bench.png blender --background --python vse_gamma_cross.py -- --output bench.png --engine cycles
-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3GC span off closed form
4GC inputs are not T1 → T2 (--swap-inputs lands here)
5Sample t convention drifted
6Cross sample off the gamma-0.5 closed form
7Mid-cross lerp deviation missing (naive mix)
8--output produced no file
9--output filmstrip guard: a frame not uniform, or a strip's mid frame off its closed form
10--output framing violation (gallery_framing)

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --swap-inputs.

-
+

Source

examples/vse-gamma-cross/vse_gamma_cross.py - View on GitHub → + 650 lines · View on GitHub →
-
"""The GAMMA_CROSS blend curve, asserted per frame — a runnable example.
+      
+
"""The GAMMA_CROSS blend curve, asserted per frame — a runnable example.
 
 Witnesses the fade math AI-generated sequencer code most often assumes
 wrong: a GAMMA_CROSS between two strips is NOT the naive linear mix
@@ -957,7 +1666,41 @@ 

Source

except Exception as e: import traceback; traceback.print_exc(); print(f"FATAL: {e}", file=sys.stderr); sys.exit(1)
+
+ +
@@ -977,20 +1720,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/wall-torch/index.html b/docs/gallery/wall-torch/index.html index 4c1e016c..c0a4ec62 100644 --- a/docs/gallery/wall-torch/index.html +++ b/docs/gallery/wall-torch/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

wall-torch

A procedural wall-mounted torch sconce: an iron plate bolted to the stone face, a level arm, an open cup holding a leaning haft with a pitch wrap and upright flames, through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

+

Rendered headless by the showcase piece itself — click to zoom.

witnesses Recomputed: 2004 tris, five materials with 540 stone, 318 metal, 50 wood, 180 flame and 60 pitch faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.304×0.322×0.485 m at zmin 0, LOD ratios in band, convex collider 116 tris, non-empty glTF. Hygiene all zero. Arm level 0.000°, plate 13 mm proud of the stones and 3 mm into them, arm-to-cup gap 0.07 mm, plaque width 0.280 m, bake 27.7 texels per UV cell. --skip-decimate exits 9; --stray-vert 15; --lift-z 16; --float-arm 17; --float-plate 18; --skinny-plaque 19; --droop-arm 20; --sink-plate 21; --low-bake 22.
+

tags mesh export showcase

blender --background --python showcase/wall-torch/wall_torch.py --
@@ -283,22 +342,22 @@

wall-torch

It asserts budget conformance of the generated result. It does not witness an API contract. "It rendered without error" is not a check.

Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, procedural-materials-and-shaders, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package). Hygiene combinatorics match examples/mesh-hygiene-audit (copied, not imported).

Intended size: a 0.28 m wide, 0.40 m tall plaque; the cup centre 0.24 m out from the wall; the flame tip 0.48 m above the floor. Outer AABB 0.304 × 0.322 × 0.485 m.

-

Budgets

+

Budgets#

Declared as named constants. Every gate recomputes its value from the mesh, materials, UVs, evaluated LOD, collider or export file.

AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles1800–22502004 / 2004 / 2004
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.5000
LOD2 ratio0.10–0.35 of base0.2196 / 0.2196 / 0.2196
Materialsexactly 5 distinct; ≥24 stone, ≥24 metal, ≥8 flame, ≥10 pitch5 slots; 540 stone, 318 metal, 50 wood, 180 flame, 60 pitch
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(0.304, 0.322, 0.485) m ± 0.015(0.3040, 0.3218, 0.4845), zmin 0
Collider tris≤ 180116
Exportwritten, size > 0164408 / 164408 / 164384 bytes

Base triangles rose from 1528 to 2004 in the second quality pass, and the band moved from 1400–1700 to 1800–2250. The solid cup became an open, walled cup with a drip boss. The 26 mm haft stub became a 0.20 m haft with a wrap. The three flame cones became lathed teardrops. The envelope grew from 0.293 m to 0.322 m in depth and from 0.424 m to 0.485 m in height, because the torch now stands up out of its cup.

DECIMATE COLLAPSE triangle counts are not identical across series. The gate is a ratio band, not an exact count. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Construction is closed-form and stone tones are seeded, so repeated runs on one binary print identical measurements. Export byte counts differ by 24 B on 5.2.1 (glTF serializer), not a gated axis.

-

Hygiene

+

Hygiene#

Recomputed from the generated mesh, not asserted about the script.

AxisDeclaredMeasured (all three)
Non-manifold edges00
Loose verts / edges0 / 00 / 0
Doubles merged at 1e-500
Zero-area faces00
N-gons00
Coplanar disjoint face pairs00
Grounded: zminwithin 1e-4 of 00.0000
-

Joint fit, seat and bake

+

Joint fit, seat and bake#

AxisDeclaredMeasured (all three)
Arm-to-cup BVH gap≤ 0.008 m0.00007
Plate-to-stone BVH gap≤ 0.008 m0.00057
Plaque width vs WALL_W± 0.04 m0.2800 vs 0.280
Arm level (exit 20): the arm shell's long axis against horizontal≤ 1.0°0.000°
Plate seat (exit 21): plate front proud of the facing stones / plate back into their face≥ 0.008 m / 0.001–0.005 m0.01300 / 0.00300
Bake texel density (exit 22): smallest UV cell in baked texels≥ 12 px27.71 px (1024 px bake)

The plate-to-stone gap is measured to any stone shell. The plate is bolted to the face of the stones, not to the backing slab behind them, as it was in the first build.

-

Falsifiers

+

Falsifiers#

Each violates one named budget. All were run on 4.5.11, 5.1.2 and 5.2.1 and returned the same code on each.

FlagBudget violatedExit
--skip-decimateLOD1 ratio band9
--stray-vertloose vertex count is 015
--lift-zbounding box zmin is 016
--float-armarm-to-cup BVH gap17
--float-plateplate-to-stone BVH gap (measured 0.07700)18
--skinny-plaqueplaque width vs WALL_W19
--droop-armarm level (the old 20 mm drop; measured 9.580°)20
--sink-plateplate seat (the first build's seat; measured proud −0.00400, seat 0.02000)21
--low-bakebake texel density (256 px bake; measured 6.93 px)22

--float-arm shortens the tube to 55 % of the plate-to-cup station vector, and the cup stays put. --float-plate moves only the plate off the stones; the arm root stays at the seated plate, so 17 still passes. --skinny-plaque scales the back plaque and courses to 0.55 · WALL_W.

-

Run

+

Run#

blender --background --python wall_torch.py --
 blender --background --python wall_torch.py -- --skip-decimate
 blender --background --python wall_torch.py -- --stray-vert
@@ -311,17 +370,1476 @@ 

Run

blender --background --python wall_torch.py -- --low-bake blender --background --python wall_torch.py -- --output torch.png

Smoke passes no flags.

-

Exit codes

+

Exit codes#

File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path. 15–19 are the hygiene and joint-fit family. 20–22 are this piece's own.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 5 distinct slots, or stone/metal/flame/pitch faces missing
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Hygiene (--stray-vert lands here)
16Grounded zmin (--lift-z)
17Arm-to-cup gap (--float-arm)
18Plate-to-stone gap (--float-plate)
19Plaque width (--skinny-plaque)
20Arm not level (--droop-arm)
21Plate not seated on the stone face (--sink-plate)
22Bake texel density below floor (--low-bake)
-
+

Source

showcase/wall-torch/wall_torch.py - View on GitHub → + 1459 lines · View on GitHub →
-
"""Game-ready wall torch sconce — a showcase piece, not an example.
+      
+
"""Game-ready wall torch sconce — a showcase piece, not an example.
 
 Asserts budget conformance of a procedural wall-mounted torch after
 composing shipped pipeline pieces: bmesh construction, UVs, five
@@ -1781,7 +3299,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -1801,20 +3353,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/watchtower/index.html b/docs/gallery/watchtower/index.html index b470a917..e2659c8d 100644 --- a/docs/gallery/watchtower/index.html +++ b/docs/gallery/watchtower/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

watchtower

A procedural timber watchtower through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

+

Rendered headless by the showcase piece itself — click to zoom.

witnesses Recomputed: 6840 tris, three materials with 300 metal and 126 roof faces, UVs in 0..1 with zero AABB overlap, outer AABB 1.594×1.594×2.973 m, grounded zmin, 4 iron shoes, rail engage 18 mm, all four side rails tenoned at both ends, both joists bearing 31 mm into deck girts, collar standoff 2 mm, post plumb 0, LOD ratios in band (5.2 COLLAPSE leaner on LOD2), convex collider 150 tris, non-empty glTF. Hygiene all zero. --skip-decimate exits 9; --stray-vert 15; --lift-z/--short-shoes 16; --short-rails/--open-sides/--short-joists 17; --float-band 18; --rake-posts 19.
+

tags mesh export showcase

blender --background --python showcase/watchtower/watchtower.py --
@@ -283,20 +342,20 @@

watchtower

The old roof was a wood U with incomplete shake coverage; rails showed through the eave. The cone plus non-overlapping courses close the pyramid. Shoes are added after the zmin snap so a bevel undershoot cannot lift them.

It asserts budget conformance of the generated result. It does not witness an API contract. "It rendered without error" is not a check.

Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package).

-

Budgets

+

Budgets#

Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.

AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles6200–82006840 / 6840 / 6840
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.5000
LOD2 ratio0.10–0.35 of base0.2199 / 0.2199 / 0.2187
Materialsexactly 3 distinct; ≥80 metal, ≥40 roof faces3 slots; 300 metal, 126 roof
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(1.594, 1.594, 2.973) m ± 0.01(1.5940, 1.5940, 2.9731), zmin 0
Collider tris≤ 180150
Exportwritten, size > 0, removed after measuring494508 / 494508 / 494492 bytes

Base triangles rose from 5220 to 6408 in the first quality pass (a taller eave-true roof and coursed shakes) and to 6840 in the second (four deck girts), not hidden interior faces.

-

Hygiene

+

Hygiene#

Recomputed from the generated mesh, not asserted about the script.

AxisDeclaredMeasured (all three)
Non-manifold edges00
Loose verts / edges0 / 00 / 0
Doubles merged at 1e-500
Zero-area faces00
N-gons00
Coplanar disjoint face pairs00
Grounded: zminwithin 1e-4 of 00.0000
Named supports: 4 iron shoeseach zmin ≤ 1e-34, shoe_z 0.00000
Plan1.56 m eave × 2.97 m height ± 0.08 / ± 0.281.5940 × 2.9731
-

Joint fit and seat

+

Joint fit and seat#

AxisDeclaredMeasured (all three)
Rail-to-post engage (back rails)≥ 0.012 m0.0180
Side-rail engage: 4 side rails, each tenoned into the front and the back post≥ 0.012 m at both ends0.0180
Deck bearing: 4 deck girts; each of 2 joists inside the front and the back girt≥ 0.012 m at both ends0.0315
Girt-collar standoff≤ 0.010 m0.00200
Post plumb (XY drift)≤ 0.010 m0.00000

DECIMATE COLLAPSE triangle counts are not identical across series — 5.2.1 is leaner on LOD2. The gate is a ratio band, not an exact count. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Construction uses no RNG; the per-piece wood tone is drawn from a seeded random.Random(TONE_SEED), the same every run. Export byte counts differ by 16 B on 5.2.1 (glTF serializer), not a gated axis.

-

Falsifiers

+

Falsifiers#

Each violates one named budget. Every falsifier was run on 4.5.11, 5.1.2 and 5.2.1 and returned the same code on each.

FlagBudget violatedExit
--skip-decimateLOD1 ratio band9
--stray-vertloose vertex count is 015
--lift-zbounding box zmin is 016
--short-shoesnamed shoe supports at Z=016
--short-railsrail-to-post joint fit17
--open-sidesside-rail engage (front ends stop 0.30 m short again: −0.2460 m)17
--short-joistsdeck bearing (joists back to the old span, girts kept: −0.1625 m)17
--float-bandiron-collar seat18
--rake-postspost plumb19
-

Run

+

Run#

blender --background --python watchtower.py --
 blender --background --python watchtower.py -- --skip-decimate
 blender --background --python watchtower.py -- --stray-vert
@@ -309,17 +368,1660 @@ 

Run

blender --background --python watchtower.py -- --rake-posts blender --background --python watchtower.py -- --output tower.png

Smoke passes no flags.

-

Exit codes

+

Exit codes#

File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path. 15–19 are the hygiene and joint-fit family.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 3 distinct slots, or metal/roof faces missing
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight
16Not grounded: bounding box zmin off 0, or a named shoe floats
17Joint fit: rail-to-post engage, side-rail engage, or deck bearing
18Seat: girt-collar standoff
19Post plumb or plan off the stated real-world size
-
+

Source

showcase/watchtower/watchtower.py - View on GitHub → + 1643 lines · View on GitHub →
-
"""Game-ready timber watchtower — a showcase piece, not an example.
+      
+
"""Game-ready timber watchtower — a showcase piece, not an example.
 
 Asserts budget conformance of a procedural lookout (posts, braces,
 platform, hatch ladder, coursed shake roof) after composing shipped pipeline
@@ -1963,7 +3665,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -1983,20 +3719,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/water-trough/index.html b/docs/gallery/water-trough/index.html index 11c436f4..18fa13fe 100644 --- a/docs/gallery/water-trough/index.html +++ b/docs/gallery/water-trough/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

water-trough

A procedural watertight water trough through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

+

Rendered headless by the showcase piece itself — click to zoom.

witnesses Recomputed: 3316 tris, three materials with 1482 wood, 156 metal and 29 water faces, UVs in 0..1 with zero AABB overlap, outer AABB 1.093x0.552x0.5055 m, zmin 0, four shoes planted, each end three lapped boards, end rim span 21 mm, strap BVH gap 0, water-hull gap 2.6 mm, water contained on 66 of 66 rays, hull 1.045x0.466 m, LOD ratios in band, convex collider 68 tris, non-empty glTF. --skip-decimate exits 9; --stray-vert 15; --lift-z/--short-legs 16; --box-ends 17; --float-strap/--open-ends 18; --narrow-hull 19; --slab-ends 20.
+

tags mesh export showcase

blender --background --python showcase/water-trough/water_trough.py --
@@ -283,29 +342,29 @@

water-trough

It asserts budget conformance of the generated result. It does not witness an API contract. "It rendered without error" is not a check.

Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package). Hygiene combinatorics match examples/mesh-hygiene-audit (copied, not imported).

Intended size: 1.08 m tray length, 0.44 m across the U, 0.50 m overall height; outer AABB 1.093 × 0.552 × 0.5055 m.

-

Budgets

+

Budgets#

Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.

AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles2800–35003316 / 3316 / 3316
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.5000
LOD2 ratio0.10–0.35 of base0.2195 / 0.2195 / 0.2195
Materialsexactly 3 distinct; ≥24 wood, ≥24 metal, ≥6 water3 slots; 1482 / 156 / 29
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(1.093, 0.552, 0.5055) m ± 0.01(1.0932, 0.5520, 0.5055), zmin 0
Collider tris≤ 8068
Exportwritten, size > 0242240 / 242240 / 242224 bytes

Base triangles rose from 2484 to 3700 in the first quality pass: box end slabs and a single extruded U became ten jittered staves, U end-caps, continuous straps offset from the legs, and stretchers that meet the inner faces. The second pass took them to 3124: the open U-band ends became solid end boards, which close the section with fewer faces. The third pass took them to 3316: each end became three boards.

DECIMATE COLLAPSE triangle counts are not identical across series — the gate is a ratio band, not an exact count. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Stave-width jitter uses closed-form sin(i); plank tone is the only RNG, seeded (TONE_SEED). Export byte counts differ by 16 B on 5.2.1 (glTF serializer), not a gated axis.

The hull-size budget (exit 19) runs before the outer AABB budget (exit 8) so --narrow-hull is not swallowed by a moved bounding box.

-

Hygiene

+

Hygiene#

Recomputed from the generated mesh, not asserted about the script.

AxisDeclaredMeasured (all three)
Non-manifold edges00
Loose verts / edges0 / 00 / 0
Doubles merged at 1e-500
Zero-area faces00
N-gons00
Coplanar disjoint face pairs00
Grounded: zminwithin 1e-4 of 00.0000
Named supports: 4 shoeseach zmin ≤ 0.0014, shoe_z 0.00000
Hull plan1.08 m × 0.44 m ± 0.081.0448 × 0.4661
-

Joint fit and seat

+

Joint fit and seat#

AxisDeclaredMeasured (all three)
End-cap rim Z-span (union of an end's boards)≤ 0.080 m0.0208
Strap-to-hull BVH gap≤ 0.008 m0.00000
Water-to-hull BVH gap≤ 0.008 m0.00260
Water contained by the end boards0 misses over 66 outward rays0 of 66
End boards per end≥ 33
-

Why the ends are solid boards

+

Why the ends are solid boards#

The first pass made each end a U-shaped band the staves tenoned into. That closed the rim and nothing else: the water's own flat end face sat exposed where a board belongs, visible in the end orthos and the end-cap close-ups. Water in that trough would pour straight out, and with the ends open the piece read as a hammock or sling. The strap and water gap budgets passed throughout, because the water did touch the hull; nothing asked whether anything held it in at the ends.

containment_audit asks. From each water end face it casts rays outward along X, from the face's perimeter vertices and from points drawn 50% and 90% of the way in from the centroid, so the middle of the section is tested and not only the rim a band would cover. Every ray must hit timber within 1.5 × END_T. --open-ends restores the U-bands: all 66 rays miss, and the piece exits 18.

-

Why each end is three boards

+

Why each end is three boards#

The third pass found each end was one D-shaped slab: a single board from rim to rim, one tone, no joint, the thing the showcase's "a head or bottom is boards, not a slab" rule exists to prevent (end orthos, end-cap close-ups, the hero). Each end is now three vertical boards of uneven width (END_BOARD_FRACS), each with its own tone and grain, its bottom following the hull arc under its own span. Neighbours lap 1 mm into each other (END_BOARD_LAP), so the joint is tight: a through seam would let the water out and a containment ray through, and the bevel on each board's face edges shows the joint as a groove. The rim span is now read on the union of an end's boards, so the inner edge of a board at a joint is not taken for the end's rim. Each board follows the arc with three samples (END_BOARD_SAMPLES); four cost 3556 triangles, over the 3500 ceiling. --slab-ends restores the one-piece board and exits 20.

-

Surface

+

Surface#

Staves, boards and legs came out of one flat material, so every board was the same board. paint_planks writes a seeded PlankTone and each shell's own long axis as GrainDir, as face attributes. The wood shader stretches its grain along that axis. The water was a 0.08-roughness mirror that went near-white under the key. It is now a dark body with a facing-ratio sky tint toward grazing angles and a faint ripple bump. A light placed to reflect in the surface was tried first and rejected: it either mirrored as a white sheet or showed nothing, and its spill lifted the dark stage. The fill dropped from 68% to 50%, so the inner stave walls show above the waterline; at 68% the surface sat nearly flush with the rim and read as felt in a frame. Straps widened from 28 to 40 mm. The third pass darkened the grazing tint from a pale grey-blue (0.20, 0.27, 0.29) to a deep teal (0.055, 0.105, 0.115): the hero looks across the surface near grazing, where that tint wins, and it read as a painted slab.

-

Falsifiers

+

Falsifiers#

Each violates one named budget. All nine were run on 4.5.11, 5.1.2 and 5.2.1 and returned the same code on each.

FlagBudget violatedExit
--skip-decimateLOD1 ratio band9
--stray-vertloose vertex count is 015
--lift-zbounding box zmin is 016
--short-legsnamed shoe supports at Z=016
--box-endsend-cap rim span17
--float-strapstrap-to-hull gap18
--narrow-hullhull plan vs stated size19
--open-endswater contained by the end boards (restores the open U-band ends)18
--slab-endsend boards per end (restores the one-piece D board; measures 1)20

--short-legs lifts the shoes and stretches each leg's foot down to the floor, so AABB zmin stays 0 (the legs still plant) and the named-support gate is what fires. Lifting the shoes alone used to let the whole piece re-ground on the legs. That shrank the AABB by ~10 mm, 0.4 mm inside the bounding-box tolerance, and a 1.5 mm model change in this pass tipped it to exit 8. With the feet stretched, the envelope moves 1.6 mm.

-

Run

+

Run#

blender --background --python water_trough.py --
 blender --background --python water_trough.py -- --skip-decimate
 blender --background --python water_trough.py -- --stray-vert
@@ -318,17 +377,1634 @@ 

Run

blender --background --python water_trough.py -- --slab-ends blender --background --python water_trough.py -- --output trough.png

Smoke passes no flags.

-

Exit codes

+

Exit codes#

File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path. 15–19 are the hygiene and joint-fit family.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 3 distinct slots, or a face-count floor missed
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight
16Not grounded: bounding box zmin off 0, or a named shoe floats
17Joint fit: end-cap U-rim span (--box-ends)
18Seat: strap or water BVH gap (--float-strap), or water not contained by the end boards (--open-ends)
19Hull length or width off the stated real-world size (--narrow-hull)
20An end is fewer than three boards (--slab-ends)
-
+

Source

showcase/water-trough/water_trough.py - View on GitHub → + 1617 lines · View on GitHub →
-
"""Game-ready wooden water trough — a showcase piece, not an example.
+      
+
"""Game-ready wooden water trough — a showcase piece, not an example.
 
 Asserts budget conformance of a procedural staved trough on a timber
 stand (U-staves and board-built ends from one radius function, contained
@@ -1946,7 +3622,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -1966,20 +3676,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/wave-displace/index.html b/docs/gallery/wave-displace/index.html index cbbabcea..a7f602b2 100644 --- a/docs/gallery/wave-displace/index.html +++ b/docs/gallery/wave-displace/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

wave-displace

Bulk vertex IO at real scale — 9,409 vertices displaced into a standing wave with one foreach_get and one foreach_set, no per-vertex access.

+

Rendered headless by the example itself — click to zoom.

witnesses The bulk path is correct, not just fast: the Z span matches the wave amplitude and every vertex matches the closed-form wave, so a stride bug in the flat buffer cannot hide.
+

tags mesh performance

blender --background --python examples/wave-displace/wave_displace.py --
@@ -280,10 +339,10 @@

wave-displace

A runnable example that displaces a 96×96 grid (9,409 vertices) into a standing wave using one foreach_get and one foreach_set — the bulk-IO pattern from use-foreach-set-for-bulk-data and the mesh-editing-and-bmesh skill — instead of 9,409 individual mesh.vertices[i].co accesses.

What it witnesses: the bulk path is not just faster, it is correct — the check asserts the flat grid gained the expected Z span (the write actually landed) and that every vertex matches the closed-form wave, so a stride or interleave bug in the flat buffer cannot hide behind a lucky probe.

-

Staging

+

Staging#

The still used to be an edge-to-edge field: the sheet cropped at the left, right and bottom edges on a black void, so nothing showed where the surface ended or what it stood on. gallery_framing has no deviation= flag any more, so that framing could not be gated at all.

The render path now stages a copy of the checked grid. Its boundary is extruded straight down to a flat base 0.30 m below the deepest trough, so the wave is the top face of a cast tile standing on the studio floor, framed whole. The checked Wave object is left exactly as asserted and hidden from the render. The copy is render-only and never enters check(). Measured framing on 5.2.1: fill 0.753 x / 0.761 y, every margin ≥ 0.117.

-

Run

+

Run#

# Cheap correctness check (no render) — the CI check:
 blender --background --python wave_displace.py --
 
@@ -293,18 +352,235 @@ 

Run

# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts): blender --background --python wave_displace.py -- --output wave.png blender --background --python wave_displace.py -- --output wave.png --engine cycles
-

Exit codes

+

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
4Z-span not in the closed-form band (--flat lands here)
5A vertex is off the closed-form wave
6--output produced no file

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --flat.

-
+

Source

examples/wave-displace/wave_displace.py - View on GitHub → + 217 lines · View on GitHub →
-
"""Bulk vertex displacement via foreach_get / foreach_set — a runnable example.
+      
+
"""Bulk vertex displacement via foreach_get / foreach_set — a runnable example.
 
 Witnesses the use-foreach-set rule at real scale: a 96x96 grid (9409 verts) is
 displaced into a standing wave by reading every coordinate with one
@@ -522,7 +798,41 @@ 

Source

except Exception as e: import traceback; traceback.print_exc(); print(f"FATAL: {e}", file=sys.stderr); sys.exit(1)
+
+ +
@@ -542,20 +852,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/wheelbarrow/index.html b/docs/gallery/wheelbarrow/index.html index 5e98419d..0f2aea33 100644 --- a/docs/gallery/wheelbarrow/index.html +++ b/docs/gallery/wheelbarrow/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

wheelbarrow

A procedural wooden wheelbarrow through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

+

Rendered headless by the showcase piece itself — click to zoom.

witnesses Recomputed: 2440 tris, two materials with 1085 wood and 214 metal faces, UVs in 0..1 with zero AABB overlap, outer AABB 1.563x0.594x0.577 m, zmin 0, hygiene 0, both shafts one timber from grip to axle, tray flared 15 deg and raked 30 deg, wall-floor seat 8 mm, spoke clearance 52 mm, two boards per side and front wall with 2 mm seams measured in the wall's plane, LOD ratios in band (5.2 COLLAPSE leaner on LOD2), convex collider 80 tris, non-empty glTF. --skip-decimate exits 9 on LOD1; --stray-vert exits 15 on hygiene; --lift-z / --short-legs exit 16 on grounded; --fat-spokes / --float-walls / --wide-seams exit 17 on joint fit; --pipe-rim exits 18 on tread aspect; --split-shafts exits 20 on shaft runs; --box-tray exits 21 on tray flare.
+

tags mesh export showcase

blender --background --python showcase/wheelbarrow/wheelbarrow.py --
@@ -283,28 +342,28 @@

wheelbarrow

It asserts budget conformance of the generated result. It does not witness an API contract. "It rendered without error" is not a check.

Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package).

Intended size: a 1.56 m long barrow, 0.59 m across the tray top, 0.58 m to the top of the raked front board; the tray is 0.94 m long at its top edge and 0.22 m deep.

-

Budgets

+

Budgets#

Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.

AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles2300–28002440 / 2440 / 2440
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.5000
LOD2 ratio0.10–0.35 of base0.2197 / 0.2197 / 0.2057
Materialsexactly 2 distinct; ≥700 wood, ≥180 metal faces2 slots; 1085 / 214
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(1.563, 0.594, 0.577) m ± 0.015(1.5630, 0.5938, 0.5773), zmin 0
Collider tris≤ 22080
Exportwritten, size > 0185652 / 185652 / 185636 bytes
-

Hygiene

+

Hygiene#

Recomputed from the generated mesh, not asserted about the script.

AxisDeclaredMeasured (all three)
Non-manifold edges00
Loose verts / edges0 / 00 / 0
Doubles merged at 1e-500
Zero-area faces00
N-gons00
Coplanar disjoint face pairs00
Grounded: zminwithin 1e-4 of 00.0000
Named supports: 2 shoes + tyreeach zmin ≤ 1e-3shoe_z 0.00000, tyre_z 0.00000
Tray size(0.939, 0.578, 0.225) m ± 0.02(0.9393, 0.5783, 0.2247)
-

Joint fit and seat

+

Joint fit and seat#

AxisDeclaredMeasured (all three)
Spoke clearance (hub − spoke)≥ 0.010 m0.05210
Wall-floor seat (inner bottom edge, lowest board of each wall)≥ 0.005 m0.00800
Metal-wood BVH gap≤ 0.010 m0.00000
Tread aspect (tyre width / radial)≥ 2.55.111
Wall boards2 boards per side wall and in the front wall; every seam 0.001–0.004 m, measured up the wall's own plane2 / 2 / 2; 0.00207–0.00231
-

Form

+

Form#

AxisDeclaredMeasured (all three)
Shaft runs: wood shells unbroken from behind the tray to past its frontexactly 22
Tray flare / rake, from the boards' own facesside ≥ 10°, front ≥ 20°15.00° / 30.00°

DECIMATE COLLAPSE triangle counts are not identical across series, so LOD2 ratios differ slightly. The gate is a ratio band, not an exact count. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Construction is closed-form; the only RNG is the seeded per-board wood tone. Bevel inputs are sorted by edge index, so the face order is the same on every run; a Python set of edges handed to the bevel had made it vary. Export byte counts differ on 5.2.1 (glTF serializer), not a gated axis.

-

Why the frame is two timbers and the tray a hopper

+

Why the frame is two timbers and the tray a hopper#

The second pass found the barrow read as a crate on legs. The frame was three boxes per side: a handle that stopped at the tray's rear corner, a level run hidden exactly under the side wall (both at 0.255 m from the centreline), and a fork that started at the front corner and turned in a 50° V to the hub. From outside, the tray carried everything. The tray itself was a square box, and a spreader between the forks stood inside the wheel, touching neither fork. The iron straps ran 12 mm above the wall tops.

  • Shafts. Each shaft is one mitred sweep through the grip, the tray's rear, its front and the axle. It narrows in plan the whole way (0.27 m at the grips, 0.19 and 0.15 m under the tray, 0.038 m at the hub), so it shows under the tray and reads as one piece of wood. The legs and a stretcher hang from it, and a bearer under the tray front, its ends buried in the shafts, replaces the spreader.
  • Hopper. The tray is built from a bottom outline and a top outline: sides flared 15°, the front raked 30° out over the wheel for tipping, the low rear leaning back 12°. Each wall is the planar band between its bottom and top edges; the end boards are housed half a board into the sides, and the sides run past the ends' outer faces.
  • Measured in the wall's plane. A leaning board's outer face sits lower than its inner one, so AABB tests lie: a Z seam reads negative and a zmin seat reads deep. Seams are measured along each wall's own up direction, and the seat on each wall's lowest board's inner face, both taken from the boards' own faces.
  • Straps follow the flared wall and stop 10 mm below its top.

WALL_SEAT went from 12 mm to 8 mm: a raked board's outer bottom edge sits WALL_T·sin(RAKE) below its inner one, and at 12 mm it would have broken through the underside of the 22 mm floor. The handle-join budget is gone, because the handle no longer joins anything: it is the shaft. The shaft-run budget replaces it. Base triangles went from 2536 to 2440, and the collider from 90 to 80.

-

Walls, wood and iron

+

Walls, wood and iron#

Side and front walls are two boards stacked with a 2 mm seam, held by the straps; the low rear wall stays one board. Wall boards asserts the count and the seam; --wide-seams opens the seams to 8 mm (same boards, same triangles) and exits 17. Faces nobody sees get no bevel: wall bottoms buried in the floor and slat undersides. Wood has a tone and grain per board and shaft; the iron is dark and rusted. The temp .glb is removed after its size is measured.

-

Falsifiers

+

Falsifiers#

Each violates one named budget. All eleven were run on 4.5.11, 5.1.2 and 5.2.1 and returned the same code on each.

FlagBudget violatedExit
--skip-decimateLOD1 ratio band9
--stray-vertloose vertex count is 015
--lift-zbounding box zmin is 016
--short-legsnamed shoe supports at Z=016
--fat-spokesspoke clearance inside the hub17
--float-wallswall-floor seat (measures −0.00691 m)17
--pipe-rimtread aspect (torus on a flat felloe)18
--wide-seamswall boards (seam band)17
--split-shaftstwo unbroken shaft runs (measures 0)20
--box-traytray flare / rake (measures 0° / 0°)21

--box-tray stands every wall vertical on the flared tray's top outline, so the envelope and the tray-size budget stay put and only the flare gate can fail.

-

Run

+

Run#

blender --background --python wheelbarrow.py --
 blender --background --python wheelbarrow.py -- --skip-decimate
 blender --background --python wheelbarrow.py -- --stray-vert
@@ -318,17 +377,1905 @@ 

Run

blender --background --python wheelbarrow.py -- --box-tray blender --background --python wheelbarrow.py -- --output barrow.png

Smoke passes no flags.

-

Exit codes

+

Exit codes#

File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path. 15–19 are the hygiene and joint-fit family.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 2 distinct slots, or a face-count floor missed
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight
16Not grounded: bounding box zmin off 0, or a named shoe/tyre floats
17Joint fit: spoke clearance, wall-floor seat, metal-wood gap, or wall boards and seams
18Seat: tread aspect of the tyre
19Tray size off the stated real-world dimensions
20A shaft is not one timber from grip to axle (--split-shafts)
21Tray flare or front rake under its floor (--box-tray)
-
+

Source

showcase/wheelbarrow/wheelbarrow.py - View on GitHub → + 1888 lines · View on GitHub →
-
"""Game-ready wooden wheelbarrow — a showcase piece, not an example.
+      
+
"""Game-ready wooden wheelbarrow — a showcase piece, not an example.
 
 Asserts budget conformance of a procedural wheelbarrow (two shafts, each
 one timber from grip to axle; a flared hopper tray; a flat-tread spoked
@@ -2217,7 +4164,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -2237,20 +4218,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/wooden-barrel/index.html b/docs/gallery/wooden-barrel/index.html index 81eadc88..58939eb9 100644 --- a/docs/gallery/wooden-barrel/index.html +++ b/docs/gallery/wooden-barrel/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

wooden-barrel

A procedural wine-cask with boarded, croze-seated heads and chord-lofted hoops through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

+

Rendered headless by the showcase piece itself — click to zoom.

witnesses Recomputed: 7312 tris, two materials with 2584 wood and 1440 metal faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.722×0.714×0.880 m, 20 grounded staves, hoop bite 0.003 m, head-croze gap 0.007 m, four boards per head with 1.5 mm seams, LOD ratios in band, convex collider 238 tris, non-empty glTF. --skip-decimate exits 9; --round-band exits 18 on the hoop-seat budget; --one-piece-head exits 17 on the head-board budget.
+

tags mesh export showcase

blender --background --python showcase/wooden-barrel/wooden_barrel.py --
@@ -282,22 +341,22 @@

wooden-barrel

Hoops evaluate the same host_outer_r chord as the stave flats. --round-band is the falsifier: a circle of radius_at(z) instead of the chord, which is what made the old capped cylinder float off every face. Heads follow the inner stave polygon, not a circle, so they fill the croze instead of leaving triangular slots at every joint. Each head is that outline cut across into four boards with a 1.5 mm seam, so the seat is unchanged and the head no longer reads as a lid.

It asserts budget conformance of the generated result. It does not witness an API contract. "It rendered without error" is not a check.

Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package).

-

Budgets

+

Budgets#

Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.

AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles6000–85007312 / 7312 / 7312
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.5000
LOD2 ratio0.10–0.35 of base0.2199 / 0.2199 / 0.2199
Materialsexactly 2 distinct; ≥400 wood, ≥200 metal faces2 slots; 2584 / 1440
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(0.722, 0.714, 0.880) m ± 0.015(0.7220, 0.7142, 0.8800), zmin 0
Collider tris≤ 400238
Exportwritten, size > 0552456 / 552456 / 552440 bytes

Base triangles rose from 4312 to 7216 in the quality pass: 12 identical staves with 5 mm daylight and circular hoops became 20 jittered staves, 8-ring bulge, chord-seated hoops, polygonal croze heads, and a bung. Cutting each head into four boards added 96 more (7312).

DECIMATE COLLAPSE triangle counts are not identical across series — the gate is a ratio band, not an exact count. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Stave-width jitter uses fixed seed 17. Export byte counts differ by 16 B on 5.2.1 (glTF serializer), not a gated axis.

-

Hygiene

+

Hygiene#

Recomputed from the generated mesh, not asserted about the script.

AxisDeclaredMeasured (all three)
Non-manifold edges00
Loose verts / edges0 / 00 / 0
Doubles merged at 1e-500
Zero-area faces00
N-gons00
Coplanar disjoint face pairs00
Grounded: zminwithin 1e-4 of 00.0000
Named supports: 20 staveseach zmin ≤ 0.00120, stave_z 0.00000
Body plan0.714 m dia × 0.880 m high ± 0.040.7142 × 0.8800
-

Joint fit and seat

+

Joint fit and seat#

AxisDeclaredMeasured (all three)
Head-croze rim BVH gap≤ 0.010 m0.00710
Head boards4 per head; every seam 0.0008–0.003 m4 + 4; 0.00150
Hoop bite (host r − inner hoop r)0.0012–0.008 m0.00300
-

Falsifiers

+

Falsifiers#

Each violates one named budget. All seven were run on 4.5.11, 5.1.2 and 5.2.1 and returned the same code on each.

FlagBudget violatedExit
--skip-decimateLOD1 ratio band9
--stray-vertloose vertex count is 015
--lift-zbounding box zmin is 016
--short-stavesnamed stave supports at Z=016
--float-headhead-croze gap17
--round-bandhoop bite band18
--one-piece-headhead boards: 4 per head17
-

Surface

+

Surface#

The staves were one flat tone and the hoops bright polished steel. Each stave and each head board now carries a seeded PlankTone and a GrainDir (its own long axis) as face attributes. wood_material stretches grain along that axis, so it runs up the curved staves and across the head boards. The hoops are dark rusted iron, the same recipe as shipping-crate. The temp .glb is removed once its size is measured.

-

Run

+

Run#

blender --background --python wooden_barrel.py --
 blender --background --python wooden_barrel.py -- --skip-decimate
 blender --background --python wooden_barrel.py -- --stray-vert
@@ -308,17 +367,1435 @@ 

Run

blender --background --python wooden_barrel.py -- --one-piece-head blender --background --python wooden_barrel.py -- --output barrel.png

Smoke passes no flags.

-

Exit codes

+

Exit codes#

File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path. 15–19 are the hygiene and joint-fit family.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 2 distinct slots, or a face-count floor missed
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight
16Not grounded: bounding box zmin off 0, or a named stave floats
17Joint fit: head-croze gap (--float-head), or head boards and seams (--one-piece-head)
18Seat: hoop bite band (--round-band)
19Body diameter or height off the stated real-world size
-
+

Source

showcase/wooden-barrel/wooden_barrel.py - View on GitHub → + 1418 lines · View on GitHub →
-
"""Game-ready wooden barrel — a showcase piece, not an example.
+      
+
"""Game-ready wooden barrel — a showcase piece, not an example.
 
 Asserts budget conformance of a procedural staved barrel (tight staves
 with seeded width jitter, heads seated in a croze, iron hoops lofted on
@@ -1737,7 +3214,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -1757,20 +3268,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/wooden-bucket/index.html b/docs/gallery/wooden-bucket/index.html index b22439fa..d77582da 100644 --- a/docs/gallery/wooden-bucket/index.html +++ b/docs/gallery/wooden-bucket/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

wooden-bucket

A procedural coopered pail with a boarded, croze-seated floor, chord-lofted hoops, and a three-strand rope bail through the ear rings through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

+

Rendered headless by the showcase piece itself — click to zoom.

witnesses Recomputed: 5458 tris, three materials with 1448 wood, 1020 metal, and 486 rope faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.396×0.346×0.534 m, 16 grounded staves, hoop bite 0.0025 m, bail-ear gap 0.0011 m, three bottom boards, no rope vertex inside an ear plate, LOD ratios in band, convex collider 276 tris, non-empty glTF. --skip-decimate exits 9; --float-handle exits 17 on the bail-ear budget; --sunk-rope exits 17 on the rope-clearance budget.
+

tags mesh export showcase

blender --background --python showcase/wooden-bucket/wooden_bucket.py --
@@ -282,25 +341,25 @@

wooden-bucket

Hoops evaluate the same host_outer_r chord as the stave flats. --round-band is the falsifier: a circle of radius_at(z) instead of the chord. The floor follows the inner stave polygon. Ear plates sit on the outer stave face below the rim; the bail is a semicircle whose ends pass through the rings and run on below them. --float-handle shrinks that semicircle and lifts it by what it lost, so the envelope stays put and only the bail-ear gap fails; --float-bottom shrinks the floor out of the croze.

It asserts budget conformance of the generated result. It does not witness an API contract. "It rendered without error" is not a check.

Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package).

-

Budgets

+

Budgets#

Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.

AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles4000–58005458 / 5458 / 5458
LOD1 ratio0.32–0.62 of base0.4998 / 0.4998 / 0.4998
LOD2 ratio0.10–0.35 of base0.2199 / 0.2199 / 0.2191
Materialsexactly 3 distinct; ≥350 wood, ≥180 metal, ≥40 rope3 slots; 1448 / 1020 / 486
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(0.396, 0.346, 0.534) m ± 0.015(0.3957, 0.3462, 0.5338), zmin 0
Collider tris≤ 420276
Exportwritten, size > 0420984 / 420984 / 420976 bytes

Base triangles rose from 4272 to 4872 in the quality pass: overlapping identical staves, circular hoops, and a 12-box bail became 16 jittered staves, chord-seated hoops, a polygonal croze floor, and a round bail through the ear rings. The second pass took it to 5458: the bail became three-strand rope (48 segments of a nine-vertex lobed section, running on through the rings) and the floor three boards.

DECIMATE COLLAPSE triangle counts are not identical across series, so LOD2 ratios differ slightly between versions. The gate is a ratio band, not an exact count. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Stave-width jitter uses fixed seed 17. Export byte counts differ on 5.2.1 (glTF serializer), not a gated axis.

-

Hygiene

+

Hygiene#

Recomputed from the generated mesh, not asserted about the script.

AxisDeclaredMeasured (all three)
Non-manifold edges00
Loose verts / edges0 / 00 / 0
Doubles merged at 1e-500
Zero-area faces00
N-gons00
Coplanar disjoint face pairs00
Grounded: zminwithin 1e-4 of 00.0000
Named supports: 16 staveseach zmin ≤ 0.00116, stave_z 0.00000
Body plan0.335 m dia × 0.360 m high ± 0.040.3349 × 0.3600
-

Joint fit and seat

+

Joint fit and seat#

AxisDeclaredMeasured (all three)
Bail-ear BVH gap≤ 0.006 m0.00107
Floor-croze rim BVH gap≤ 0.008 m0.00474
Bottom boards3; every seam 0.0006–0.0025 m3; 0.00120
Rope clears the ear plates2 plates; 0 rope vertices inside either2; 0
Hoop bite (host r − inner hoop r)0.0010–0.007 m0.00250
-

Falsifiers

+

Falsifiers#

Each violates one named budget. All nine were run on 4.5.11, 5.1.2 and 5.2.1 and returned the same code on each.

FlagBudget violatedExit
--skip-decimateLOD1 ratio band9
--stray-vertloose vertex count is 015
--lift-zbounding box zmin is 016
--short-stavesnamed stave supports at Z=016
--float-handlebail-ear gap17
--float-bottomfloor-croze gap17
--round-bandhoop bite band18
--one-piece-bottombottom boards: 317
--sunk-roperope clears the ear plates17
-

Rope and ears

+

Rope and ears#

The bail was a smooth eight-sided tube in a rope material, and it read as a plastic hose. It is now laid rope: a three-lobed section turned one vertex step per ring, so the lobes wind along the bail as strands. The section's frame is fixed to the bail's plane, so the lay does not jump where the ends turn vertical.

The ring offset from the ear plate was RING_MAJOR * 0.25, which put the rope's axis 3.25 mm off the plate and its body through it (hidden by the ring in the old stills). The offset is now sized from the rope's lobes plus 0.5 mm, and Rope clears the ear plates asserts it. --sunk-rope restores the old offset and puts 19 rope vertices inside a plate. The outer AABB was re-fitted to the honest envelope: 12.5 mm wider across the ears and 6 mm taller.

-

Surface

+

Surface#

Every stave and board carries a seeded PlankTone and a GrainDir (its own long axis) as face attributes, and wood_material stretches grain along that axis. The hoops, plates and rings are dark rusted iron. The temp .glb is removed once its size is measured.

-

Run

+

Run#

blender --background --python wooden_bucket.py --
 blender --background --python wooden_bucket.py -- --skip-decimate
 blender --background --python wooden_bucket.py -- --stray-vert
@@ -313,17 +372,1643 @@ 

Run

blender --background --python wooden_bucket.py -- --sunk-rope blender --background --python wooden_bucket.py -- --output bucket.png

Smoke passes no flags.

-

Exit codes

+

Exit codes#

File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path. 15–19 are the hygiene and joint-fit family.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 3 distinct slots, or a face-count floor missed
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight
16Not grounded: bounding box zmin off 0, or a named stave floats
17Joint fit: bail-ear gap, floor-croze gap, bottom boards and seams, or rope through an ear plate
18Seat: hoop bite band (--round-band)
19Body diameter or height off the stated real-world size
-
+

Source

showcase/wooden-bucket/wooden_bucket.py - View on GitHub → + 1626 lines · View on GitHub →
-
"""Game-ready wooden bucket — a showcase piece, not an example.
+      
+
"""Game-ready wooden bucket — a showcase piece, not an example.
 
 Asserts budget conformance of a procedural coopered bucket (tight staves
 with seeded width jitter, a bottom seated in a croze, iron hoops lofted
@@ -1950,7 +3635,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -1970,20 +3689,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/wooden-ladder/index.html b/docs/gallery/wooden-ladder/index.html index a1d2ea33..08ed12d8 100644 --- a/docs/gallery/wooden-ladder/index.html +++ b/docs/gallery/wooden-ladder/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,11 +325,13 @@

wooden-ladder

A procedural timber ladder with raked tapered stiles, six turned rungs, ferrule caps, and level shoe plates through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

+

Rendered headless by the showcase piece itself — click to zoom.

witnesses Recomputed: 2536 tris, two materials with 612 wood and 736 metal faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.490×0.376×1.487 m, zmin 0, hygiene and coplanar pairs 0, tenon clearance 8 mm, shoe bite 20 mm, rung pitch even to 0.002 m, LOD ratios in band, convex collider 24 tris, non-empty glTF. --skip-decimate exits 9; --stray-vert and --twin-sole exit 15; --lift-z exits 16; --fat-rungs exits 17; --short-stile exits 18; --drift-rungs exits 19.
+

tags mesh export showcase

blender --background --python showcase/wooden-ladder/wooden_ladder.py --
@@ -282,19 +341,19 @@

wooden-ladder

It asserts budget conformance of the generated result. It does not witness an API contract. "It rendered without error" is not a check.

Intended size: stile length 1.50 m, overall about 0.49 × 0.38 × 1.49 m. A household ladder, not a 3 m orchard ladder.

Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package). Hygiene combinatorics match examples/mesh-hygiene-audit (copied, not imported). Coplanar-pair counting matches showcase/signpost.

-

Budgets

+

Budgets#

Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.

AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles1800–28002536 / 2536 / 2536
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.5000
LOD2 ratio0.10–0.35 of base0.2192 / 0.2192 / 0.2192
Materialsexactly 2 distinct, ≥280 wood, ≥180 metal2 slots, 612 wood, 736 metal
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(0.490, 0.376, 1.487) m ± 0.01(0.4897, 0.3760, 1.4870)
Groundedabs(zmin) ≤ 1e-4, each sole at 0, rail end ≥ 0.012 mzmin 0, sole 0, rail 0.02037
Mesh hygienenon-manifold, loose v/e, doubles at 1e-5, zero-area, n-gons, coplanar pairs all 0all 0
Parts16 shells (2 stiles, 6 rungs, 2 sleeves, 2 cap sleeves, 2 plates, 2 soles)16 (2 / 6 / 2 / 2)
Rung depth clearance≥ 0.003 m inside the stile faces0.00800
Tenon engagement≥ 0.008 m past the stile inner face0.01399
Tenon breakout margin≥ 0.008 m short of the stile outer face0.03780
Shoe bite / coverbite ≥ 0.010 m, cover ≥ 0.016 m0.02000 / 0.03800
Rung pitch6 rungs; every rung-to-rung gap within 0.002 m of the mean6; 0.00000
Collider tris≤ 18024
Exportwritten, size > 0187524 / 187524 / 187504 bytes

Base triangles rose from 936 to 2536. The previous mesh was two boxes, six cloned dowels, and four chrome cubes. Turned rungs (tenon and barrel in one shell) plus ferrule collars and chamfered soles added the triangles. Nothing was removed as a hidden face.

The joint budgets are measured per rung against each stile, from vertex positions in the un-raked construction frame, and the reported value is the worst case. Clearance is the tenon, not the barrel: the barrel is thicker only between the stiles.

DECIMATE COLLAPSE triangle counts are not identical across series — the gate is a ratio band, not an exact count. On this mesh the three binaries agreed. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Construction is closed-form; the only RNG is the seeded per-piece wood tone. Bevel inputs are sorted by edge index, so the face order is the same on every run; a Python set of edges handed to the bevel had made it vary. Export byte counts differ on 5.2.1 (glTF serializer), not a gated axis. Euler is 24 (multi-body) and is not gated to 2.

-

Rungs, wood and staging

+

Rungs, wood and staging#

The rung heights were jittered on purpose (up to ±18 mm, RUNG_Z_OFFSETS) and the barrel thickness varied from 0.82× to 1.22×. That is variation put into function: a climber's feet find rungs blind, so an even pitch is part of what a ladder is. The rungs now sit at one pitch, and the turning varies by a few percent. Rung pitch asserts it; --drift-rungs restores the old heights and exits 19 (worst gap 0.03287 m off the mean). Tone and grain now vary per rail and rung instead. The temp .glb is removed after its size is measured.

The hero stood the ladder at its 12° rake with nothing to lean on. The render path now adds a render-only wall section on the side the rails rake toward, with its face through the rail tops, and turns the ladder so that side faces away from the camera.

Each falsifier violates exactly one named budget. Proven on Blender 4.5.11 LTS, 5.1.2, and 5.2.1 LTS (the binaries' own --version):

FalsifierBudget violatedExitMeasured failure
--skip-decimateLOD1 ratio band9ratio 1.0000
--stray-vertMesh hygiene, loose verts15loose_v=1
--twin-soleCoplanar face pairs15zfight=6
--lift-zGrounded zmin16zmin 0.050000
--fat-rungsRung depth clearance17clearance 0
--short-stileShoe bite / cover18bite 0.070, cover −0.012
--drift-rungsRung pitch19worst gap 0.03287 off the mean

Default exit is 0 on all three.

-

Run

+

Run#

blender --background --python wooden_ladder.py --
 blender --background --python wooden_ladder.py -- --skip-decimate
 blender --background --python wooden_ladder.py -- --stray-vert
@@ -305,17 +364,1638 @@ 

Run

blender --background --python wooden_ladder.py -- --drift-rungs blender --background --python wooden_ladder.py -- --output preview.webp --engine cycles

Smoke passes no flags: no --output, no falsifier.

-

Exit codes

+

Exit codes#

File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 2 distinct slots, or wood/metal faces missing
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Mesh hygiene, including coplanar pairs (--stray-vert, --twin-sole)
16Grounded zmin, or a rail end sitting in the tread (--lift-z)
17Part count or rung-to-stile joint fit (--fat-rungs)
18Shoe bite / cover (--short-stile)
19Rung pitch (--drift-rungs)
-
+

Source

showcase/wooden-ladder/wooden_ladder.py - View on GitHub → + 1621 lines · View on GitHub →
-
"""Game-ready wooden ladder — a showcase piece, not an example.
+      
+
"""Game-ready wooden ladder — a showcase piece, not an example.
 
 Asserts budget conformance of a procedural timber ladder after composing
 shipped pipeline pieces: bmesh construction, UVs, two materials,
@@ -1937,7 +3617,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -1957,20 +3671,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/docs/gallery/wooden-yoke/index.html b/docs/gallery/wooden-yoke/index.html index 18f50985..c7a68754 100644 --- a/docs/gallery/wooden-yoke/index.html +++ b/docs/gallery/wooden-yoke/index.html @@ -161,7 +161,7 @@ .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -176,7 +176,7 @@ /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -192,7 +192,7 @@ color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -222,11 +222,68 @@ flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -268,45 +325,47 @@

wooden-yoke

A procedural double ox yoke — a carved oak beam with neck saddles and upturned ends on two bent hickory bows pinned above it, with a staple and a hung iron ring — carried through UVs, bake, LOD, compound collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

+

Rendered headless by the showcase piece itself — click to zoom.

witnesses Recomputed: 5288 tris, three materials with 1088 oak, 720 hickory and 936 iron faces, UVs in 0..1 with zero AABB overlap, outer AABB 1.424×0.140×0.640 m, zmin 0, hygiene 0 including zero coplanar cross-shell pairs, both bows standing on the floor, every leg 52.7 mm proud of the beam, the staple 17.5 mm into it, four pins resting 1.5 mm into its crown, a ring threaded along the staple's bar and hung 1.0 mm onto it, two mirrored bows, and each neck opening 266 mm wide and 371 mm high. --pinch-bows exits 19 at 196 mm while the bows still ground and the envelope is unchanged; --edge-on-ring exits 17.
+

tags mesh export showcase

blender --background --python showcase/wooden-yoke/wooden_yoke.py --

A double ox yoke: a carved oak beam with two neck saddles and upturned ends, two bent-hickory oxbows whose legs pass up through the beam and are pinned above it, and a forged staple under the centre carrying a hung iron ring. It stands on the bottoms of its two bows. A showcase piece, not an example — it witnesses no API contract. It asserts that generated geometry meets declared asset budgets, recomputed from the finished mesh.

-

What it composes

+

What it composes#

Shipped contentUsed for
skills/mesh-editing-and-bmeshlofted beam, swept bows and staple, lathed pins, torus ring, all in one bmesh
skills/custom-propertiesface attributes (PlankTone, GrainDir) read by the wood shaders
skills/procedural-materials-and-shadersoak and hickory with grain along each piece, rusted rough iron
skills/bake-high-to-lowCycles tangent-space normal bake, high onto low
skills/engine-export-presetsUnity glTF (export_yup=True)
skills/depsgraph-and-evaluated-dataevaluated triangle counts for the LOD ratios
snippets/decimate_to_budget.pyLOD1 / LOD2 COLLAPSE chain
snippets/convex_hull_collider.pya hull for the beam and for each bow leg and foot, merged into a compound
snippets/lod_chain.pyLOD naming and ratio pattern
examples/mesh-hygiene-audithygiene combinatorics (copied, not imported)
-

The budget that matters

+

The budget that matters#

A yoke exists to close round two necks. Each bow and the saddle above it make an opening an ox's neck passes through. A bow bent too tight fails its job, and nothing else notices:

  • it still stands on the floor, so zmin and the named bows pass;
  • its legs still pass up through the beam and stand proud of it;
  • its pins still rest on the beam;
  • it still fits the bounding box, because the beam sets the envelope.

The piece measures each opening off the finished mesh:

  • Width is the gap between the inner faces of the bow's two legs, taken over the straight run of the legs. Band 240–300 mm; measured 266 mm.
  • Height runs from the bow's inner bottom up to the saddle's apex on the beam's underside. Band 300–400 mm; measured 370.9 mm.

--pinch-bows is the falsifier built for exactly this. It brings each bow's legs 35 mm closer to its centre, so the opening closes to 196 mm and the run exits 19. Every other budget still passes, and the AABB is unchanged.

-

Budgets

+

Budgets#

Declared in the script as named constants, recomputed from the generated mesh. Measured values are from Blender 5.2.1; every one is byte-identical on 4.5.11 and 5.1.2 (only the glTF file size differs, by 4 bytes, which is exporter metadata and not a budget).

BudgetBandMeasured
Base triangles4800–58005288
LOD1 ratio0.32–0.620.5000
LOD2 ratio0.10–0.350.2197
Material slotsexactly 3, distinct3
Oak / hickory / iron faces≥ 950 / 650 / 7501088 / 720 / 936
UV boundsinside 0..1(0.0100, 0.0100)–(0.9900, 0.9900)
UV AABB overlap≤ 1e-50.000000
Outer AABB1.424 × 0.140 × 0.640 m ± 0.0201.4240 × 0.1400 × 0.6400
Collider triangles≤ 300260 (seven hulls)
Normal bake{'FINISHED'} with image data{'FINISHED'}, has_data=True
glTF exportfile written, non-empty~160 kB
Hygieneall zeroloose 0/0, non-manifold 0, zero-area 0, doubles 0, n-gons 0, coplanar cross-shell pairs 0
Grounded AABB\zmin\≤ 1e-40.00000
Named bows2 bows, each zmin ≤ 1e-42 at 0.00000
Leg protrusion4 legs, each 25–70 mm above the beam's top at that leg52.7 mm
Staple bite8–30 mm into the beam17.5 mm
Threaded ringring's hole axis against the staple's bar, cos ≥ cos 20°1.0000
Pin seat4 pins, shank 0.5–3.0 mm into the beam's top1.50 mm
Hung ringring's inner edge against the bar's top, −3.0 to +0.5 mm−1.00 mm
Neck openingwidth 240–300 mm, height 300–400 mm266.0 / 370.9 mm
Mirrored bowsextents mirrored through x = 0 within 0.1 mm0.000 mm

Real-world size: a 1.42 m beam over two 266 mm openings 0.80 m apart, standing 0.64 m to the tips of its upturned ends. It is a pair yoke for draught oxen.

-

Construction

+

Construction#

  • Beam. Lofted along X through superellipse sections (exponent 3), 16 vertices each. Every shaping term is even in x, so the two halves are mirror images. Each station reads four functions of x:
  • a crowned top;
  • an underside raised by a cosine saddle over each neck;
  • a sixth-power upturn at both ends;
  • a width that tapers toward the ends.

Stations are a uniform run plus one at every leg and bow centre, so the top a pin is measured against, and the saddle apex, are vertices rather than chords. The ends close on a pole a few millimetres past the last ring.

  • Bows. A 44 mm round hickory rod swept along a U:
  • Each leg rises straight up through the beam and ends a named height above it (BOW_PROTRUDE).
  • The bottom is a half-ellipse whose lowest centreline point is one radius off the floor.
  • A section vertex points straight down, so each bow stands on a vertex at exactly z = 0.
  • The section frame is fixed to the bow's plane, so there is no twist.
  • Pins. A forged pin crosses each leg tip, its shank resting PIN_SEAT into the beam's crown. Its head hangs past the beam's edge. Two shank rings sit over the crown, so the seat is measured on vertices where the pin rests.
  • Staple and ring. A round bar bent into a U across Y has its legs driven STAPLE_BITE up into the beam. The ring is a torus whose hole axis runs along the staple's bar (Y). Its inner edge rests on the bar's top, RING_BITE into it, so the bar passes through the ring's hole and the ring hangs clear of the floor.
-

Findings the budgets forced

+

Findings the budgets forced#

  • Pin seat on a span. The pin was first a lathe with rings only at its two ends. No vertex sat over the beam, and the seat audit found nothing to measure. Two shank rings now sit over the crown.
  • A section vertex on the axis. Ten-segment sections have no vertex straight down. That put the pins' seat at 1.21 mm against a designed 1.50 mm, and the staple's bar 0.3 mm off. Pins and staple are now 12-segment.
  • Collider. Hulls over thinned vertex grids still came to 744–766 triangles against a 300 ceiling. The hulls now take every eighth beam ring and every fifth bow ring, with every other section vertex, and the compound is 260 triangles.
  • Bamboo bows (inspection-only). Grain set per face from the path's tangent broke the wood figure at every ring, and the bent bows rendered as bamboo. The bows now take one grain direction, up their legs.
-

Conventions walked

+

Conventions walked#

Every convention in showcase/README.md, and whether it applies here.

ConventionAppliesHow
Deterministic, budgets declared, assertions recomputeyesno RNG; every value above is read off the mesh
Falsifier fails the budget it targetsyestable below, proven on all three binaries
Hygiene incl. cross-shell coplanaryesexit 15
Named supportsyesthe two bows (--float-bow)
Even shaping terms and mirror symmetryyesevery beam term even in x; mirrored bows asserted (--skew-bow)
Plumb and real-world sizeyesthe neck opening is the real-world size that matters (--pinch-bows)
A joint bites; touching is not joiningyeslegs through the beam (--short-bows), staple into it (--short-staple)
Hung ringyestorus hung on the bar, inner edge on its top (--clip-ring)
A ring is threaded across its holeyeshole axis along the bar (--edge-on-ring)
Carried parts bite their bearersyespins resting into the beam's crown (--float-pins)
Rope, masonry, timber boards, roofs, vessels, scatter, paintnothe piece has none of these
A bent bar is one sweepyeseach bow and the staple is one sweep
Shading is part of the modelyesbeam and bows smooth (carved, bent); every edge over 40° hard (sawn beam ends, pin heads)
One substance, one slotyesoak, hickory, iron
Iron is not chromeyesnear-black and rust, roughness 0.55–0.85, metallic 0.65
Identical boards read as CGyesper-piece PlankTone; grain along the beam and up each bow
Edge treatment: no right anglesn/alofted and swept, no box edges; not a separate budget
Sort bmesh operator inputsn/ano set-fed operator is run
The bake cage is narrower than the nearest neighbouryesCAGE_EXTRUSION 0.01 m
Level on the stage; stage 60 myesturned about Z only; 60 m floor and wall
Keep a falsifier's envelope stillyesevery falsifier leaves the AABB unchanged
-

Falsifiers

+

Falsifiers#

Each breaks one pipeline stage so a named budget fails. All eleven were run on 4.5.11, 5.1.2 and 5.2.1 and exited the same declared code on all three.

FlagTarget budgetBreaksExit
--skip-decimateLOD1 ratiodrops the DECIMATE modifiers, LOD1 ratio goes to 1.00009
--stray-vertmesh hygieneadds one loose vertex under the beam15
--lift-zgrounded zminlifts the whole mesh 50 mm16
--float-bownamed bowslifts the left bow 8 mm; the right still grounds the AABB16
--short-bowsleg protrusionstops every leg 10 mm under the beam's top; −2.3 mm at the tip17
--short-staplestaple bitestops the staple 5 mm under the beam; −2.6 mm17
--edge-on-ringthreaded ringturns the ring into the staple's plane; cos 0.000017
--float-pinspin seatlifts every pin 5 mm off the beam; −3.5 mm18
--clip-ringhung ringlifts the ring 6 mm off its bar; +5.0 mm18
--pinch-bowsneck openingbrings each bow's legs 35 mm in; 196 mm wide19
--skew-bowmirrored bowsmoves the right bow 6 mm along the beam; 6.0 mm19
-

Exit codes

+

Exit codes#

File-local and sequential. 9 is a valid check code. 1 is the FATAL wrapper — a crash, never a named check.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build, or has no UV layer
4Base triangle count outside band
5Material slots, or a material's face floor
6UVs outside 0..1
7UV AABB overlap above tolerance
8Outer AABB off declared size
9LOD1 or LOD2 ratio outside band (--skip-decimate)
10Framing gate (examples/gallery_framing.py, render path only)
11Collider triangles above ceiling
12Normal bake failed or produced no image data
13glTF export missing or empty
14--output produced no file
15Mesh hygiene (--stray-vert)
16Grounded zmin, or a bow floating (--lift-z, --float-bow)
17Leg protrusion, staple bite or threaded ring (--short-bows, --short-staple, --edge-on-ring)
18Pin seat or hung ring (--float-pins, --clip-ring)
19Neck opening or mirrored bows (--pinch-bows, --skew-bow)
-

Run it

+

Run it#

# Budget check, no render. ~2.5 s on 4.5, ~4.6 s on 5.2.
 blender --background --python wooden_yoke.py --
 
@@ -319,16 +378,1281 @@ 

Run it

# Render the gallery still (EEVEE; --engine cycles on a GPU-less host). blender --background --python wooden_yoke.py -- --output wooden_yoke.webp

Smoke runs the check-only path. It does not pass --output or any falsifier.

-

Cross-version measurements

+

Cross-version measurements#

Value4.5.115.1.25.2.1
Base triangles528852885288
LOD1 / LOD2 tris2644 / 1162samesame
Face counts (oak / hickory / iron)1088 / 720 / 936samesame
Outer AABB1.4240 × 0.1400 × 0.6400samesame
Collider tris260260260
Neck opening266.0 × 370.9 mmsamesame
glTF bytes159764159764159760
-
+

Source

showcase/wooden-yoke/wooden_yoke.py - View on GitHub → + 1265 lines · View on GitHub →
-
"""Game-ready ox yoke — a showcase piece, not an example.
+      
+
"""Game-ready ox yoke — a showcase piece, not an example.
 
 Asserts budget conformance of a procedural double ox yoke: a carved oak
 beam with two neck saddles and upturned ends, two bent-hickory oxbows
@@ -1594,7 +2918,41 @@ 

Source

print(f"FATAL: {e}", file=sys.stderr) sys.exit(1)
+
+ +
@@ -1614,20 +2972,98 @@

Source

var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); diff --git a/scripts/build_gallery.py b/scripts/build_gallery.py index 4cfe1086..d4a8ffc4 100644 --- a/scripts/build_gallery.py +++ b/scripts/build_gallery.py @@ -36,6 +36,7 @@ import re import sys import tokenize +import urllib.parse from pathlib import Path REPO = Path(__file__).resolve().parent.parent @@ -264,7 +265,7 @@ def check_alts(entries: list) -> None: .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -279,7 +280,7 @@ def check_alts(entries: list) -> None: /* ---- detail page ---- */ .detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; } - .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } + .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -295,7 +296,7 @@ def check_alts(entries: list) -> None: color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer; font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; } .copy-btn:hover { color: var(--select); border-color: var(--select); } - .detail-section { margin-top: 2.25rem; } + .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; } .detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; } @@ -325,11 +326,68 @@ def check_alts(entries: list) -> None: flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; } .src-meta code { font-family: var(--font-mono); font-size: 0.82rem; } + /* ---- detail page: pager, tags, related, anchors, code blocks ---- */ + .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; + margin: 0 0 1.25rem; font-size: 0.85rem; } + .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); } + .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pager a:hover { color: var(--select); text-decoration: none; } + .pager [rel="next"] { text-align: right; } + .pager-pos { text-align: center; } + @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } } + .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border); + border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; } + .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; } + .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); + background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px; + padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; } + .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; } + .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } + @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } } + .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; } + .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; } + .card.mini h3 a { color: var(--text); } + .card.mini h3 a:hover { color: var(--select); text-decoration: none; } + .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .md h1, .md h2, .md h3 { scroll-margin-top: 60px; } + .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0; + transition: opacity 0.15s; } + .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; } + .anchor:hover { color: var(--select); text-decoration: none; } + .codewrap { position: relative; } + .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem; + font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; } + .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; } + @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } } + .copy-btn.done { color: var(--ok); border-color: var(--ok); } + .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); } + .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr); + background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; } + .src .code pre { background: none; border: 0; border-radius: 0; } + .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right; + user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border); + background: var(--bg2); overflow: hidden; } + .src .code.collapsed { max-height: 42rem; position: relative; } + .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem; + background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; } + .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2); + border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); + padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans); + transition: color 0.15s, border-color 0.15s; } + .src-expand:hover { color: var(--select); border-color: var(--select); } + .lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; } - .lightbox[open] { display: flex; align-items: center; justify-content: center; } + /* margin:auto (not align/justify-content) centers without clipping the + top-left of a native-size image that overflows a small viewport. */ + .lightbox[open] { display: flex; overflow: auto; } .lightbox::backdrop { background: transparent; } - .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); } + .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius); + cursor: zoom-in; } + .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; } .lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; } @@ -561,26 +619,104 @@ def check_alts(entries: list) -> None: var box = document.getElementById('lightbox'); if (hero && box) { // .showModal() traps focus, closes on Escape, and returns - // focus to the hero button on close. Any click inside closes it. - hero.addEventListener('click', function () { box.showModal(); }); - box.addEventListener('click', function () { box.close(); }); + // focus to the hero button on close. The dialog fills the viewport, + // so a click whose target is the dialog itself is a backdrop click. + var full = box.querySelector('img'); + hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); }); + box.addEventListener('click', function (e) { if (e.target === box) box.close(); }); + box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); }); + full.addEventListener('click', function () { box.classList.toggle('native'); }); + } + + // Clipboard API first; execCommand for non-secure origins (file://, + // plain-http previews) where navigator.clipboard is undefined. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + if (ok) { resolve(); } else { reject(); } + }); + } + function wireCopy(btn, getText) { + btn.addEventListener('click', function () { + copyText(getText()).then(function () { + btn.textContent = 'Copied'; btn.classList.add('done'); + }, function () { + btn.textContent = 'Copy failed'; btn.classList.add('fail'); + }).then(function () { + setTimeout(function () { + btn.textContent = 'Copy'; btn.classList.remove('done', 'fail'); + }, 1600); + }); + }); } var copy = document.getElementById('copyRun'); if (copy) { - copy.addEventListener('click', function () { - var cmd = document.getElementById('runCmd').textContent; - navigator.clipboard.writeText(cmd).then(function () { - copy.textContent = 'Copied!'; - setTimeout(function () { copy.textContent = 'Copy'; }, 1500); + wireCopy(copy, function () { return document.getElementById('runCmd').textContent; }); + } + Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) { + var wrap = block; + if (block.tagName === 'PRE') { + wrap = document.createElement('div'); + block.parentNode.insertBefore(wrap, block); + wrap.appendChild(block); + } + wrap.classList.add('codewrap'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy'; + var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines'); + btn.setAttribute('aria-label', 'Copy code'); + wireCopy(btn, function () { return src.textContent; }); + wrap.appendChild(btn); + }); + + // Long listings start collapsed; with JS off they render in full. + var code = document.querySelector('.src .code'); + if (code) { + var lines = parseInt(code.getAttribute('data-lines'), 10) || 0; + if (lines > 120) { + code.classList.add('collapsed'); + var more = document.createElement('button'); + more.type = 'button'; more.className = 'src-expand'; + more.setAttribute('aria-expanded', 'false'); + more.textContent = 'Show all ' + lines + ' lines'; + code.parentNode.insertBefore(more, code.nextSibling); + more.addEventListener('click', function () { + var open = code.classList.toggle('collapsed') === false; + more.setAttribute('aria-expanded', open ? 'true' : 'false'); + more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines'; + if (!open) code.scrollIntoView({ block: 'start' }); }); - }); + } } + + // Left/right arrows page between entries of the same kind. + document.addEventListener('keydown', function (e) { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; + if (box && box.open) return; + var ae = document.activeElement; + if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return; + var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : ''; + if (!rel) return; + var a = document.querySelector('.pager a[rel="' + rel + '"]'); + if (a) { location.href = a.href; } + }); })(); """ CARD = """

__NAME__

@@ -590,6 +726,20 @@ def check_alts(entries: list) -> None:
""" +# Compact card for a detail page's Related strip (same visual family as CARD). +MINI_CARD = """ """ + +# Cards above the fold on a typical desktop load eagerly; the rest stay lazy. +EAGER_CARDS = 4 + # --------------------------------------------------------------------------- # Build-time Python syntax highlighting (stdlib tokenize; no Pygments). @@ -705,11 +855,29 @@ def _starts_block(lines: list[str], i: int) -> bool: and bool(_TABLE_SEP.match(lines[i + 1].strip()))) +# Ids the detail page itself uses; a README heading must never collide with them. +RESERVED_IDS = frozenset({"main", "heroZoom", "lightbox", "runCmd", "copyRun", "related", "related-h", "source"}) + + +def slugify(text: str, seen: set[str]) -> str: + """GitHub-style heading slug from raw Markdown heading text, unique within *seen*.""" + plain = re.sub(r"!?\[([^\]]*)\]\([^)]*\)", r"\1", text) # links/images -> label + plain = re.sub(r"[`*_]", "", plain).lower() + slug = re.sub(r"[^a-z0-9]+", "-", plain).strip("-") or "section" + candidate, n = slug, 1 + while candidate in seen or candidate in RESERVED_IDS: + candidate = f"{slug}-{n}" + n += 1 + seen.add(candidate) + return candidate + + def md_to_html(text: str, resolve, skip_first_h1: bool = True) -> str: lines = text.splitlines() out: list[str] = [] i = 0 seen_h1 = False + slugs: set[str] = set() while i < len(lines): line = lines[i] stripped = line.strip() @@ -735,7 +903,10 @@ def md_to_html(text: str, resolve, skip_first_h1: bool = True) -> str: seen_h1 = True i += 1 continue - out.append(f"{render_inline(m.group(2), resolve)}") + hid = slugify(m.group(2), slugs) + out.append(f'{render_inline(m.group(2), resolve)}' + f'#' + f"") i += 1 continue @@ -840,7 +1011,43 @@ def shell(*, title: str, desc: str, canonical: str, og_image: str, .replace("__CONTENT__", content)) -def build_detail(ex: dict, *, base: str, repo_root_url: str, site: str) -> str: +def related_entries(ex: dict, entries: list, limit: int = 3) -> list: + """Entries sharing the most topic tags with *ex*; gallery order breaks ties. + + ``showcase`` is a kind marker, not a topic, so it never counts as shared. + """ + mine = set(ex.get("tags") or []) - {"showcase"} + scored = [] + for i, other in enumerate(entries): + if other["name"] == ex["name"]: + continue + shared = len(mine & (set(other.get("tags") or []) - {"showcase"})) + if shared: + scored.append((-shared, i, other)) + return [o for _, _, o in sorted(scored, key=lambda t: t[:2])[:limit]] + + +def pager_html(prev: dict | None, nxt: dict | None, pos: int, total: int, noun: str, + *, foot: bool = False) -> str: + """Prev/next links between entries of one kind. Never wraps.""" + plural = "showcase pieces" if noun == "showcase piece" else "examples" + def link(e: dict | None, rel: str) -> str: + if e is None: + return "" + n = html.escape(e["name"]) + label = (f' {n}' if rel == "prev" + else f'{n} ') + return (f'{label}') + cls = "pager pager-foot" if foot else "pager" + hint = "" if foot else ' · ←→' + return (f' ') + + +def build_detail(ex: dict, entries: list, *, base: str, repo_root_url: str, site: str) -> str: noun = kind_noun(ex) kind_title = "Showcase" if noun == "showcase piece" else "Examples" name = ex["name"] @@ -848,17 +1055,29 @@ def build_detail(ex: dict, *, base: str, repo_root_url: str, site: str) -> str: script = find_script(ex_dir) hero_file = page_relative(ex["hero"]).split("/")[-1] + peers = [e for e in entries if kind_noun(e) == noun] + idx = next(i for i, e in enumerate(peers) if e["name"] == name) + prev = peers[idx - 1] if idx > 0 else None + nxt = peers[idx + 1] if idx + 1 < len(peers) else None + parts: list[str] = [] parts.append('
') parts.append(f'

{html.escape(name)}

') parts.append(f'

{html.escape(ex["teaches"])}

') parts.append("
") parts.append('
') + parts.append(pager_html(prev, nxt, idx + 1, len(peers), noun)) parts.append(f' ") parts.append(f'

Rendered headless by the {noun} itself — click to zoom.

') parts.append(f'
witnesses {html.escape(ex["witnessesFix"])}
') + tags = ex.get("tags") or [] + if tags: + links = " ".join( + f'{html.escape(t)}' + for t in tags) + parts.append(f'

tags {links}

') if script is not None: cmd = f"blender --background --python {ex['dir']}/{script.name} --" @@ -876,15 +1095,38 @@ def build_detail(ex: dict, *, base: str, repo_root_url: str, site: str) -> str: if script is not None: blob = f"{base}/{ex['dir']}/{script.name}" - parts.append('
') + src = script.read_text(encoding="utf-8") + n_lines = len(src.splitlines()) + gutter = "\n".join(str(n) for n in range(1, n_lines + 1)) + parts.append('
') parts.append("

Source

") parts.append('
') parts.append(f" {html.escape(ex['dir'])}/{html.escape(script.name)}") - parts.append(f' View on GitHub →') + parts.append(f' {n_lines} lines · View on GitHub →') + parts.append("
") + parts.append(f'
') + parts.append(f'' + f'
{highlight_python(src)}
') parts.append("
") - parts.append(f"
{highlight_python(script.read_text(encoding='utf-8'))}
") parts.append("
") + related = related_entries(ex, entries) + if related: + parts.append(' ") + + parts.append(pager_html(prev, nxt, idx + 1, len(peers), noun, foot=True)) parts.append("
") parts.append(' ') parts.append(' ') @@ -961,10 +1203,11 @@ def build_index(data: dict, *, base: str, repo_root_url: str, site: str) -> str: ) cards = [] - for ex in examples: + for i, ex in enumerate(examples): alt = ex["alt"] cards.append( CARD + .replace("__LOADING__", "eager" if i < EAGER_CARDS else "lazy") .replace("__TAGS__", html.escape(" ".join(ex.get("tags", [])), quote=True)) .replace("__HREF__", html.escape(f'{ex["name"]}/', quote=True)) .replace("__HERO__", html.escape(page_relative(ex["hero"]), quote=True)) @@ -1041,7 +1284,7 @@ def main() -> int: page_dir = OUT_DIR / ex["name"] page_dir.mkdir(parents=True, exist_ok=True) (page_dir / "index.html").write_text( - build_detail(ex, base=base, repo_root_url=repo_root_url, site=site), + build_detail(ex, examples, base=base, repo_root_url=repo_root_url, site=site), encoding="utf-8", ) From f495d9f1f047a877de3aadfc5ea07c6d334fe139 Mon Sep 17 00:00:00 2001 From: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Date: Sun, 27 Sep 2026 09:23:07 -0400 Subject: [PATCH 2/2] feat(gallery): browse controls and interaction feel Showcase pieces were reachable only through a "showcase" topic chip, one tag could be active at a time, and the grid had one fixed order. The index now has an All / Examples / Showcase switch, AND-combined topic chips, and a Gallery order / Name A-Z sort, all mirrored in the URL hash (k=, tag=a,b, s=). Legacy #tag=showcase links, the landing page's among them, resolve to the Showcase switch so no inbound link breaks. Search no longer rewrites the hash on every keystroke, card renders fade in once decoded, hover gets a restrained zoom that reduced-motion turns off (replacing a rule that disabled a transform nobody set), focus rings sit inside segmented controls instead of being clipped, README prose is capped at a readable measure, and the sticky bar packs tighter on phones. Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> --- docs/gallery/anvil/index.html | 27 +- docs/gallery/apothecary-shelf/index.html | 27 +- docs/gallery/armature-bend/index.html | 27 +- .../gallery/attribute-domain-shear/index.html | 27 +- .../bake-normal-high-to-low/index.html | 27 +- docs/gallery/bmesh-gear/index.html | 27 +- docs/gallery/book-trolley/index.html | 27 +- docs/gallery/bookshelf/index.html | 27 +- docs/gallery/brazier/index.html | 27 +- docs/gallery/butter-churn/index.html | 27 +- docs/gallery/campfire/index.html | 27 +- docs/gallery/car-mirror-symmetry/index.html | 27 +- docs/gallery/cart/index.html | 27 +- docs/gallery/chopping-block/index.html | 27 +- docs/gallery/collision-hull-proxy/index.html | 27 +- docs/gallery/color-attribute-wheel/index.html | 27 +- docs/gallery/compositor-glare/index.html | 27 +- docs/gallery/crate-stack/index.html | 27 +- .../cross-version-property-delete/index.html | 27 +- docs/gallery/curve-bevel-arc/index.html | 27 +- docs/gallery/custom-normals-shade/index.html | 27 +- docs/gallery/damped-track-aim/index.html | 27 +- docs/gallery/degenerate-bevel-weld/index.html | 27 +- docs/gallery/depsgraph-export/index.html | 27 +- docs/gallery/driver-wave/index.html | 27 +- docs/gallery/export-preset-axis/index.html | 27 +- docs/gallery/fence-kit/index.html | 27 +- docs/gallery/gltf-export-roundtrip/index.html | 27 +- docs/gallery/gltf-skin-roundtrip/index.html | 27 +- docs/gallery/gn-instance-grid/index.html | 27 +- docs/gallery/gn-modifier-inputs/index.html | 27 +- docs/gallery/gn-sdf-remesh/index.html | 27 +- docs/gallery/gn-sim-fountain/index.html | 27 +- docs/gallery/gn-socket-rename/index.html | 27 +- docs/gallery/gn-zone-iterate/index.html | 27 +- docs/gallery/gp-lineart-contour/index.html | 27 +- docs/gallery/grain-sacks/index.html | 27 +- docs/gallery/grease-pencil-rosette/index.html | 27 +- docs/gallery/grindstone/index.html | 27 +- docs/gallery/hand-pump/index.html | 27 +- docs/gallery/hay-bale/index.html | 27 +- docs/gallery/hitching-post/index.html | 27 +- docs/gallery/image-pixels-testcard/index.html | 27 +- docs/gallery/index.html | 414 +++++++++++------- docs/gallery/iron-cauldron/index.html | 27 +- docs/gallery/light-link-studio/index.html | 27 +- docs/gallery/lightmap-uv-channel/index.html | 27 +- docs/gallery/lod-decimate-chain/index.html | 27 +- docs/gallery/market-stall/index.html | 27 +- docs/gallery/mesh-hygiene-audit/index.html | 27 +- docs/gallery/modular-kit-snap/index.html | 27 +- docs/gallery/parent-inverse-orrery/index.html | 27 +- docs/gallery/park-bench/index.html | 27 +- docs/gallery/png-exr-alpha/index.html | 27 +- docs/gallery/prop-origin-transform/index.html | 27 +- docs/gallery/rope-bridge/index.html | 27 +- docs/gallery/shader-node-group/index.html | 27 +- docs/gallery/shape-key-blend/index.html | 27 +- docs/gallery/shipping-crate/index.html | 27 +- docs/gallery/signpost/index.html | 27 +- .../sky-texture-sun-elevation/index.html | 27 +- docs/gallery/soccer-ball-goldberg/index.html | 27 +- docs/gallery/socket-attach-points/index.html | 27 +- docs/gallery/stone-archway/index.html | 27 +- docs/gallery/stone-well/index.html | 27 +- docs/gallery/street-lantern/index.html | 27 +- docs/gallery/swatch-grid/index.html | 27 +- docs/gallery/tavern-stool/index.html | 27 +- docs/gallery/temp-override-join/index.html | 27 +- docs/gallery/terrain-scatter/index.html | 27 +- docs/gallery/text-version-stamp/index.html | 27 +- docs/gallery/treasure-chest/index.html | 27 +- docs/gallery/triangulate-tangents/index.html | 27 +- docs/gallery/turntable/index.html | 27 +- .../usd-export-evaluation-mode/index.html | 27 +- docs/gallery/uv-layer-grid/index.html | 27 +- docs/gallery/vertex-color-ao/index.html | 27 +- docs/gallery/vertex-weight-limit/index.html | 27 +- docs/gallery/vse-cut-list/index.html | 27 +- docs/gallery/vse-gamma-cross/index.html | 27 +- docs/gallery/wall-torch/index.html | 27 +- docs/gallery/watchtower/index.html | 27 +- docs/gallery/water-trough/index.html | 27 +- docs/gallery/wave-displace/index.html | 27 +- docs/gallery/wheelbarrow/index.html | 27 +- docs/gallery/wooden-barrel/index.html | 27 +- docs/gallery/wooden-bucket/index.html | 27 +- docs/gallery/wooden-ladder/index.html | 27 +- docs/gallery/wooden-yoke/index.html | 27 +- examples/gallery.json | 2 +- scripts/build_gallery.py | 166 +++++-- 91 files changed, 2416 insertions(+), 542 deletions(-) diff --git a/docs/gallery/anvil/index.html b/docs/gallery/anvil/index.html index 2a3f191f..0d10560c 100644 --- a/docs/gallery/anvil/index.html +++ b/docs/gallery/anvil/index.html @@ -104,6 +104,12 @@ .density-btn + .density-btn { border-left: 1px solid var(--border); } .density-btn:hover { color: var(--select); } .density-btn.active { background: var(--select); color: #1a1b1e; } + /* Segmented groups clip overflow, so the ring has to sit inside. */ + .density-btn:focus-visible, .chip:focus-visible { outline-offset: -2px; } + .sort select { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); + border-radius: var(--radius); padding: 0.3rem 0.5rem; cursor: pointer; + font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; } + .sort select:hover, .sort select:focus { color: var(--select); border-color: var(--select); outline: none; } .tags-toggle { display: none; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); border-radius: 3px; padding: 0.3rem 0.7rem; cursor: pointer; font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; } @@ -123,6 +129,14 @@ transition: color 0.15s, border-color 0.15s; } .chip:hover { color: var(--select); border-color: var(--select); } .chip.active { color: #1a1b1e; background: var(--select); border-color: var(--select); } + @media (max-width: 559px) { + .controls-inner { padding: 0.45rem 1rem 0.5rem; gap: 0.4rem; } + .controls-row { gap: 0.4rem; } + .searchwrap { flex-basis: 100%; } + .count { position: absolute; width: 1px; height: 1px; overflow: hidden; + clip: rect(0 0 0 0); white-space: nowrap; } + .density-btn { padding: 0.36rem 0.5rem; } + } @media (max-width: 719px) { html.js .tags-toggle { display: inline-block; } html.js .chips { display: none; } @@ -160,8 +174,12 @@ transition: outline-color 0.12s ease, border-color 0.12s ease; } .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } - .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media { display: block; background: var(--bg2); line-height: 0; overflow: hidden; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; + transition: transform 0.35s ease, opacity 0.3s ease; } + .card:hover .card-media img { transform: scale(1.03); } + html.js .grid .card-media img { opacity: 0; } + html.js .grid .card-media img.is-loaded { opacity: 1; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -200,6 +218,7 @@ .md h1, .md h2, .md h3 { letter-spacing: -0.01em; margin: 1.4rem 0 0.6rem; line-height: 1.25; } .md h1 { font-size: 1.35rem; } .md h2 { font-size: 1.15rem; } .md h3 { font-size: 1rem; } .md p { margin: 0.7rem 0; } + .md > p, .md > ul, .md > ol, .md > h2, .md > h3 { max-width: 75ch; } .md ul, .md ol { margin: 0.7rem 0 0.7rem 1.4rem; } .md li { margin: 0.3rem 0; } .md .table-wrap { overflow-x: auto; margin: 0.9rem 0; } @@ -306,8 +325,8 @@ } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - .card { transition: none; } - .card:hover { transform: none; } + .card, .card-media img { transition: none; } + .card:hover .card-media img { transform: none; } .to-top { transition: none; } } diff --git a/docs/gallery/apothecary-shelf/index.html b/docs/gallery/apothecary-shelf/index.html index 00367832..b7c155c3 100644 --- a/docs/gallery/apothecary-shelf/index.html +++ b/docs/gallery/apothecary-shelf/index.html @@ -104,6 +104,12 @@ .density-btn + .density-btn { border-left: 1px solid var(--border); } .density-btn:hover { color: var(--select); } .density-btn.active { background: var(--select); color: #1a1b1e; } + /* Segmented groups clip overflow, so the ring has to sit inside. */ + .density-btn:focus-visible, .chip:focus-visible { outline-offset: -2px; } + .sort select { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); + border-radius: var(--radius); padding: 0.3rem 0.5rem; cursor: pointer; + font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; } + .sort select:hover, .sort select:focus { color: var(--select); border-color: var(--select); outline: none; } .tags-toggle { display: none; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); border-radius: 3px; padding: 0.3rem 0.7rem; cursor: pointer; font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; } @@ -123,6 +129,14 @@ transition: color 0.15s, border-color 0.15s; } .chip:hover { color: var(--select); border-color: var(--select); } .chip.active { color: #1a1b1e; background: var(--select); border-color: var(--select); } + @media (max-width: 559px) { + .controls-inner { padding: 0.45rem 1rem 0.5rem; gap: 0.4rem; } + .controls-row { gap: 0.4rem; } + .searchwrap { flex-basis: 100%; } + .count { position: absolute; width: 1px; height: 1px; overflow: hidden; + clip: rect(0 0 0 0); white-space: nowrap; } + .density-btn { padding: 0.36rem 0.5rem; } + } @media (max-width: 719px) { html.js .tags-toggle { display: inline-block; } html.js .chips { display: none; } @@ -160,8 +174,12 @@ transition: outline-color 0.12s ease, border-color 0.12s ease; } .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } - .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media { display: block; background: var(--bg2); line-height: 0; overflow: hidden; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; + transition: transform 0.35s ease, opacity 0.3s ease; } + .card:hover .card-media img { transform: scale(1.03); } + html.js .grid .card-media img { opacity: 0; } + html.js .grid .card-media img.is-loaded { opacity: 1; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -200,6 +218,7 @@ .md h1, .md h2, .md h3 { letter-spacing: -0.01em; margin: 1.4rem 0 0.6rem; line-height: 1.25; } .md h1 { font-size: 1.35rem; } .md h2 { font-size: 1.15rem; } .md h3 { font-size: 1rem; } .md p { margin: 0.7rem 0; } + .md > p, .md > ul, .md > ol, .md > h2, .md > h3 { max-width: 75ch; } .md ul, .md ol { margin: 0.7rem 0 0.7rem 1.4rem; } .md li { margin: 0.3rem 0; } .md .table-wrap { overflow-x: auto; margin: 0.9rem 0; } @@ -306,8 +325,8 @@ } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - .card { transition: none; } - .card:hover { transform: none; } + .card, .card-media img { transition: none; } + .card:hover .card-media img { transform: none; } .to-top { transition: none; } } diff --git a/docs/gallery/armature-bend/index.html b/docs/gallery/armature-bend/index.html index 9edb1674..f3961581 100644 --- a/docs/gallery/armature-bend/index.html +++ b/docs/gallery/armature-bend/index.html @@ -104,6 +104,12 @@ .density-btn + .density-btn { border-left: 1px solid var(--border); } .density-btn:hover { color: var(--select); } .density-btn.active { background: var(--select); color: #1a1b1e; } + /* Segmented groups clip overflow, so the ring has to sit inside. */ + .density-btn:focus-visible, .chip:focus-visible { outline-offset: -2px; } + .sort select { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); + border-radius: var(--radius); padding: 0.3rem 0.5rem; cursor: pointer; + font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; } + .sort select:hover, .sort select:focus { color: var(--select); border-color: var(--select); outline: none; } .tags-toggle { display: none; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); border-radius: 3px; padding: 0.3rem 0.7rem; cursor: pointer; font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; } @@ -123,6 +129,14 @@ transition: color 0.15s, border-color 0.15s; } .chip:hover { color: var(--select); border-color: var(--select); } .chip.active { color: #1a1b1e; background: var(--select); border-color: var(--select); } + @media (max-width: 559px) { + .controls-inner { padding: 0.45rem 1rem 0.5rem; gap: 0.4rem; } + .controls-row { gap: 0.4rem; } + .searchwrap { flex-basis: 100%; } + .count { position: absolute; width: 1px; height: 1px; overflow: hidden; + clip: rect(0 0 0 0); white-space: nowrap; } + .density-btn { padding: 0.36rem 0.5rem; } + } @media (max-width: 719px) { html.js .tags-toggle { display: inline-block; } html.js .chips { display: none; } @@ -160,8 +174,12 @@ transition: outline-color 0.12s ease, border-color 0.12s ease; } .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } - .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media { display: block; background: var(--bg2); line-height: 0; overflow: hidden; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; + transition: transform 0.35s ease, opacity 0.3s ease; } + .card:hover .card-media img { transform: scale(1.03); } + html.js .grid .card-media img { opacity: 0; } + html.js .grid .card-media img.is-loaded { opacity: 1; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -200,6 +218,7 @@ .md h1, .md h2, .md h3 { letter-spacing: -0.01em; margin: 1.4rem 0 0.6rem; line-height: 1.25; } .md h1 { font-size: 1.35rem; } .md h2 { font-size: 1.15rem; } .md h3 { font-size: 1rem; } .md p { margin: 0.7rem 0; } + .md > p, .md > ul, .md > ol, .md > h2, .md > h3 { max-width: 75ch; } .md ul, .md ol { margin: 0.7rem 0 0.7rem 1.4rem; } .md li { margin: 0.3rem 0; } .md .table-wrap { overflow-x: auto; margin: 0.9rem 0; } @@ -306,8 +325,8 @@ } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - .card { transition: none; } - .card:hover { transform: none; } + .card, .card-media img { transition: none; } + .card:hover .card-media img { transform: none; } .to-top { transition: none; } } diff --git a/docs/gallery/attribute-domain-shear/index.html b/docs/gallery/attribute-domain-shear/index.html index 1c757ec8..cf250513 100644 --- a/docs/gallery/attribute-domain-shear/index.html +++ b/docs/gallery/attribute-domain-shear/index.html @@ -104,6 +104,12 @@ .density-btn + .density-btn { border-left: 1px solid var(--border); } .density-btn:hover { color: var(--select); } .density-btn.active { background: var(--select); color: #1a1b1e; } + /* Segmented groups clip overflow, so the ring has to sit inside. */ + .density-btn:focus-visible, .chip:focus-visible { outline-offset: -2px; } + .sort select { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); + border-radius: var(--radius); padding: 0.3rem 0.5rem; cursor: pointer; + font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; } + .sort select:hover, .sort select:focus { color: var(--select); border-color: var(--select); outline: none; } .tags-toggle { display: none; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); border-radius: 3px; padding: 0.3rem 0.7rem; cursor: pointer; font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; } @@ -123,6 +129,14 @@ transition: color 0.15s, border-color 0.15s; } .chip:hover { color: var(--select); border-color: var(--select); } .chip.active { color: #1a1b1e; background: var(--select); border-color: var(--select); } + @media (max-width: 559px) { + .controls-inner { padding: 0.45rem 1rem 0.5rem; gap: 0.4rem; } + .controls-row { gap: 0.4rem; } + .searchwrap { flex-basis: 100%; } + .count { position: absolute; width: 1px; height: 1px; overflow: hidden; + clip: rect(0 0 0 0); white-space: nowrap; } + .density-btn { padding: 0.36rem 0.5rem; } + } @media (max-width: 719px) { html.js .tags-toggle { display: inline-block; } html.js .chips { display: none; } @@ -160,8 +174,12 @@ transition: outline-color 0.12s ease, border-color 0.12s ease; } .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } - .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media { display: block; background: var(--bg2); line-height: 0; overflow: hidden; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; + transition: transform 0.35s ease, opacity 0.3s ease; } + .card:hover .card-media img { transform: scale(1.03); } + html.js .grid .card-media img { opacity: 0; } + html.js .grid .card-media img.is-loaded { opacity: 1; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -200,6 +218,7 @@ .md h1, .md h2, .md h3 { letter-spacing: -0.01em; margin: 1.4rem 0 0.6rem; line-height: 1.25; } .md h1 { font-size: 1.35rem; } .md h2 { font-size: 1.15rem; } .md h3 { font-size: 1rem; } .md p { margin: 0.7rem 0; } + .md > p, .md > ul, .md > ol, .md > h2, .md > h3 { max-width: 75ch; } .md ul, .md ol { margin: 0.7rem 0 0.7rem 1.4rem; } .md li { margin: 0.3rem 0; } .md .table-wrap { overflow-x: auto; margin: 0.9rem 0; } @@ -306,8 +325,8 @@ } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - .card { transition: none; } - .card:hover { transform: none; } + .card, .card-media img { transition: none; } + .card:hover .card-media img { transform: none; } .to-top { transition: none; } } diff --git a/docs/gallery/bake-normal-high-to-low/index.html b/docs/gallery/bake-normal-high-to-low/index.html index 91768b08..54b43927 100644 --- a/docs/gallery/bake-normal-high-to-low/index.html +++ b/docs/gallery/bake-normal-high-to-low/index.html @@ -104,6 +104,12 @@ .density-btn + .density-btn { border-left: 1px solid var(--border); } .density-btn:hover { color: var(--select); } .density-btn.active { background: var(--select); color: #1a1b1e; } + /* Segmented groups clip overflow, so the ring has to sit inside. */ + .density-btn:focus-visible, .chip:focus-visible { outline-offset: -2px; } + .sort select { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); + border-radius: var(--radius); padding: 0.3rem 0.5rem; cursor: pointer; + font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; } + .sort select:hover, .sort select:focus { color: var(--select); border-color: var(--select); outline: none; } .tags-toggle { display: none; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); border-radius: 3px; padding: 0.3rem 0.7rem; cursor: pointer; font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; } @@ -123,6 +129,14 @@ transition: color 0.15s, border-color 0.15s; } .chip:hover { color: var(--select); border-color: var(--select); } .chip.active { color: #1a1b1e; background: var(--select); border-color: var(--select); } + @media (max-width: 559px) { + .controls-inner { padding: 0.45rem 1rem 0.5rem; gap: 0.4rem; } + .controls-row { gap: 0.4rem; } + .searchwrap { flex-basis: 100%; } + .count { position: absolute; width: 1px; height: 1px; overflow: hidden; + clip: rect(0 0 0 0); white-space: nowrap; } + .density-btn { padding: 0.36rem 0.5rem; } + } @media (max-width: 719px) { html.js .tags-toggle { display: inline-block; } html.js .chips { display: none; } @@ -160,8 +174,12 @@ transition: outline-color 0.12s ease, border-color 0.12s ease; } .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } - .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media { display: block; background: var(--bg2); line-height: 0; overflow: hidden; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; + transition: transform 0.35s ease, opacity 0.3s ease; } + .card:hover .card-media img { transform: scale(1.03); } + html.js .grid .card-media img { opacity: 0; } + html.js .grid .card-media img.is-loaded { opacity: 1; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -200,6 +218,7 @@ .md h1, .md h2, .md h3 { letter-spacing: -0.01em; margin: 1.4rem 0 0.6rem; line-height: 1.25; } .md h1 { font-size: 1.35rem; } .md h2 { font-size: 1.15rem; } .md h3 { font-size: 1rem; } .md p { margin: 0.7rem 0; } + .md > p, .md > ul, .md > ol, .md > h2, .md > h3 { max-width: 75ch; } .md ul, .md ol { margin: 0.7rem 0 0.7rem 1.4rem; } .md li { margin: 0.3rem 0; } .md .table-wrap { overflow-x: auto; margin: 0.9rem 0; } @@ -306,8 +325,8 @@ } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - .card { transition: none; } - .card:hover { transform: none; } + .card, .card-media img { transition: none; } + .card:hover .card-media img { transform: none; } .to-top { transition: none; } } diff --git a/docs/gallery/bmesh-gear/index.html b/docs/gallery/bmesh-gear/index.html index 6bfa1e15..625100a4 100644 --- a/docs/gallery/bmesh-gear/index.html +++ b/docs/gallery/bmesh-gear/index.html @@ -104,6 +104,12 @@ .density-btn + .density-btn { border-left: 1px solid var(--border); } .density-btn:hover { color: var(--select); } .density-btn.active { background: var(--select); color: #1a1b1e; } + /* Segmented groups clip overflow, so the ring has to sit inside. */ + .density-btn:focus-visible, .chip:focus-visible { outline-offset: -2px; } + .sort select { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); + border-radius: var(--radius); padding: 0.3rem 0.5rem; cursor: pointer; + font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; } + .sort select:hover, .sort select:focus { color: var(--select); border-color: var(--select); outline: none; } .tags-toggle { display: none; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); border-radius: 3px; padding: 0.3rem 0.7rem; cursor: pointer; font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; } @@ -123,6 +129,14 @@ transition: color 0.15s, border-color 0.15s; } .chip:hover { color: var(--select); border-color: var(--select); } .chip.active { color: #1a1b1e; background: var(--select); border-color: var(--select); } + @media (max-width: 559px) { + .controls-inner { padding: 0.45rem 1rem 0.5rem; gap: 0.4rem; } + .controls-row { gap: 0.4rem; } + .searchwrap { flex-basis: 100%; } + .count { position: absolute; width: 1px; height: 1px; overflow: hidden; + clip: rect(0 0 0 0); white-space: nowrap; } + .density-btn { padding: 0.36rem 0.5rem; } + } @media (max-width: 719px) { html.js .tags-toggle { display: inline-block; } html.js .chips { display: none; } @@ -160,8 +174,12 @@ transition: outline-color 0.12s ease, border-color 0.12s ease; } .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } - .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media { display: block; background: var(--bg2); line-height: 0; overflow: hidden; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; + transition: transform 0.35s ease, opacity 0.3s ease; } + .card:hover .card-media img { transform: scale(1.03); } + html.js .grid .card-media img { opacity: 0; } + html.js .grid .card-media img.is-loaded { opacity: 1; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -200,6 +218,7 @@ .md h1, .md h2, .md h3 { letter-spacing: -0.01em; margin: 1.4rem 0 0.6rem; line-height: 1.25; } .md h1 { font-size: 1.35rem; } .md h2 { font-size: 1.15rem; } .md h3 { font-size: 1rem; } .md p { margin: 0.7rem 0; } + .md > p, .md > ul, .md > ol, .md > h2, .md > h3 { max-width: 75ch; } .md ul, .md ol { margin: 0.7rem 0 0.7rem 1.4rem; } .md li { margin: 0.3rem 0; } .md .table-wrap { overflow-x: auto; margin: 0.9rem 0; } @@ -306,8 +325,8 @@ } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - .card { transition: none; } - .card:hover { transform: none; } + .card, .card-media img { transition: none; } + .card:hover .card-media img { transform: none; } .to-top { transition: none; } } diff --git a/docs/gallery/book-trolley/index.html b/docs/gallery/book-trolley/index.html index 03074832..8e438b9a 100644 --- a/docs/gallery/book-trolley/index.html +++ b/docs/gallery/book-trolley/index.html @@ -104,6 +104,12 @@ .density-btn + .density-btn { border-left: 1px solid var(--border); } .density-btn:hover { color: var(--select); } .density-btn.active { background: var(--select); color: #1a1b1e; } + /* Segmented groups clip overflow, so the ring has to sit inside. */ + .density-btn:focus-visible, .chip:focus-visible { outline-offset: -2px; } + .sort select { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); + border-radius: var(--radius); padding: 0.3rem 0.5rem; cursor: pointer; + font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; } + .sort select:hover, .sort select:focus { color: var(--select); border-color: var(--select); outline: none; } .tags-toggle { display: none; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); border-radius: 3px; padding: 0.3rem 0.7rem; cursor: pointer; font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; } @@ -123,6 +129,14 @@ transition: color 0.15s, border-color 0.15s; } .chip:hover { color: var(--select); border-color: var(--select); } .chip.active { color: #1a1b1e; background: var(--select); border-color: var(--select); } + @media (max-width: 559px) { + .controls-inner { padding: 0.45rem 1rem 0.5rem; gap: 0.4rem; } + .controls-row { gap: 0.4rem; } + .searchwrap { flex-basis: 100%; } + .count { position: absolute; width: 1px; height: 1px; overflow: hidden; + clip: rect(0 0 0 0); white-space: nowrap; } + .density-btn { padding: 0.36rem 0.5rem; } + } @media (max-width: 719px) { html.js .tags-toggle { display: inline-block; } html.js .chips { display: none; } @@ -160,8 +174,12 @@ transition: outline-color 0.12s ease, border-color 0.12s ease; } .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } - .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media { display: block; background: var(--bg2); line-height: 0; overflow: hidden; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; + transition: transform 0.35s ease, opacity 0.3s ease; } + .card:hover .card-media img { transform: scale(1.03); } + html.js .grid .card-media img { opacity: 0; } + html.js .grid .card-media img.is-loaded { opacity: 1; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -200,6 +218,7 @@ .md h1, .md h2, .md h3 { letter-spacing: -0.01em; margin: 1.4rem 0 0.6rem; line-height: 1.25; } .md h1 { font-size: 1.35rem; } .md h2 { font-size: 1.15rem; } .md h3 { font-size: 1rem; } .md p { margin: 0.7rem 0; } + .md > p, .md > ul, .md > ol, .md > h2, .md > h3 { max-width: 75ch; } .md ul, .md ol { margin: 0.7rem 0 0.7rem 1.4rem; } .md li { margin: 0.3rem 0; } .md .table-wrap { overflow-x: auto; margin: 0.9rem 0; } @@ -306,8 +325,8 @@ } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - .card { transition: none; } - .card:hover { transform: none; } + .card, .card-media img { transition: none; } + .card:hover .card-media img { transform: none; } .to-top { transition: none; } } diff --git a/docs/gallery/bookshelf/index.html b/docs/gallery/bookshelf/index.html index 1dd0ea9d..b9fd8f91 100644 --- a/docs/gallery/bookshelf/index.html +++ b/docs/gallery/bookshelf/index.html @@ -104,6 +104,12 @@ .density-btn + .density-btn { border-left: 1px solid var(--border); } .density-btn:hover { color: var(--select); } .density-btn.active { background: var(--select); color: #1a1b1e; } + /* Segmented groups clip overflow, so the ring has to sit inside. */ + .density-btn:focus-visible, .chip:focus-visible { outline-offset: -2px; } + .sort select { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); + border-radius: var(--radius); padding: 0.3rem 0.5rem; cursor: pointer; + font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; } + .sort select:hover, .sort select:focus { color: var(--select); border-color: var(--select); outline: none; } .tags-toggle { display: none; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); border-radius: 3px; padding: 0.3rem 0.7rem; cursor: pointer; font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; } @@ -123,6 +129,14 @@ transition: color 0.15s, border-color 0.15s; } .chip:hover { color: var(--select); border-color: var(--select); } .chip.active { color: #1a1b1e; background: var(--select); border-color: var(--select); } + @media (max-width: 559px) { + .controls-inner { padding: 0.45rem 1rem 0.5rem; gap: 0.4rem; } + .controls-row { gap: 0.4rem; } + .searchwrap { flex-basis: 100%; } + .count { position: absolute; width: 1px; height: 1px; overflow: hidden; + clip: rect(0 0 0 0); white-space: nowrap; } + .density-btn { padding: 0.36rem 0.5rem; } + } @media (max-width: 719px) { html.js .tags-toggle { display: inline-block; } html.js .chips { display: none; } @@ -160,8 +174,12 @@ transition: outline-color 0.12s ease, border-color 0.12s ease; } .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } - .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media { display: block; background: var(--bg2); line-height: 0; overflow: hidden; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; + transition: transform 0.35s ease, opacity 0.3s ease; } + .card:hover .card-media img { transform: scale(1.03); } + html.js .grid .card-media img { opacity: 0; } + html.js .grid .card-media img.is-loaded { opacity: 1; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -200,6 +218,7 @@ .md h1, .md h2, .md h3 { letter-spacing: -0.01em; margin: 1.4rem 0 0.6rem; line-height: 1.25; } .md h1 { font-size: 1.35rem; } .md h2 { font-size: 1.15rem; } .md h3 { font-size: 1rem; } .md p { margin: 0.7rem 0; } + .md > p, .md > ul, .md > ol, .md > h2, .md > h3 { max-width: 75ch; } .md ul, .md ol { margin: 0.7rem 0 0.7rem 1.4rem; } .md li { margin: 0.3rem 0; } .md .table-wrap { overflow-x: auto; margin: 0.9rem 0; } @@ -306,8 +325,8 @@ } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - .card { transition: none; } - .card:hover { transform: none; } + .card, .card-media img { transition: none; } + .card:hover .card-media img { transform: none; } .to-top { transition: none; } } diff --git a/docs/gallery/brazier/index.html b/docs/gallery/brazier/index.html index 23321a96..f3200315 100644 --- a/docs/gallery/brazier/index.html +++ b/docs/gallery/brazier/index.html @@ -104,6 +104,12 @@ .density-btn + .density-btn { border-left: 1px solid var(--border); } .density-btn:hover { color: var(--select); } .density-btn.active { background: var(--select); color: #1a1b1e; } + /* Segmented groups clip overflow, so the ring has to sit inside. */ + .density-btn:focus-visible, .chip:focus-visible { outline-offset: -2px; } + .sort select { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); + border-radius: var(--radius); padding: 0.3rem 0.5rem; cursor: pointer; + font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; } + .sort select:hover, .sort select:focus { color: var(--select); border-color: var(--select); outline: none; } .tags-toggle { display: none; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); border-radius: 3px; padding: 0.3rem 0.7rem; cursor: pointer; font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; } @@ -123,6 +129,14 @@ transition: color 0.15s, border-color 0.15s; } .chip:hover { color: var(--select); border-color: var(--select); } .chip.active { color: #1a1b1e; background: var(--select); border-color: var(--select); } + @media (max-width: 559px) { + .controls-inner { padding: 0.45rem 1rem 0.5rem; gap: 0.4rem; } + .controls-row { gap: 0.4rem; } + .searchwrap { flex-basis: 100%; } + .count { position: absolute; width: 1px; height: 1px; overflow: hidden; + clip: rect(0 0 0 0); white-space: nowrap; } + .density-btn { padding: 0.36rem 0.5rem; } + } @media (max-width: 719px) { html.js .tags-toggle { display: inline-block; } html.js .chips { display: none; } @@ -160,8 +174,12 @@ transition: outline-color 0.12s ease, border-color 0.12s ease; } .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } - .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media { display: block; background: var(--bg2); line-height: 0; overflow: hidden; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; + transition: transform 0.35s ease, opacity 0.3s ease; } + .card:hover .card-media img { transform: scale(1.03); } + html.js .grid .card-media img { opacity: 0; } + html.js .grid .card-media img.is-loaded { opacity: 1; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -200,6 +218,7 @@ .md h1, .md h2, .md h3 { letter-spacing: -0.01em; margin: 1.4rem 0 0.6rem; line-height: 1.25; } .md h1 { font-size: 1.35rem; } .md h2 { font-size: 1.15rem; } .md h3 { font-size: 1rem; } .md p { margin: 0.7rem 0; } + .md > p, .md > ul, .md > ol, .md > h2, .md > h3 { max-width: 75ch; } .md ul, .md ol { margin: 0.7rem 0 0.7rem 1.4rem; } .md li { margin: 0.3rem 0; } .md .table-wrap { overflow-x: auto; margin: 0.9rem 0; } @@ -306,8 +325,8 @@ } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - .card { transition: none; } - .card:hover { transform: none; } + .card, .card-media img { transition: none; } + .card:hover .card-media img { transform: none; } .to-top { transition: none; } } diff --git a/docs/gallery/butter-churn/index.html b/docs/gallery/butter-churn/index.html index 560f7bf3..09fc2327 100644 --- a/docs/gallery/butter-churn/index.html +++ b/docs/gallery/butter-churn/index.html @@ -104,6 +104,12 @@ .density-btn + .density-btn { border-left: 1px solid var(--border); } .density-btn:hover { color: var(--select); } .density-btn.active { background: var(--select); color: #1a1b1e; } + /* Segmented groups clip overflow, so the ring has to sit inside. */ + .density-btn:focus-visible, .chip:focus-visible { outline-offset: -2px; } + .sort select { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); + border-radius: var(--radius); padding: 0.3rem 0.5rem; cursor: pointer; + font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; } + .sort select:hover, .sort select:focus { color: var(--select); border-color: var(--select); outline: none; } .tags-toggle { display: none; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); border-radius: 3px; padding: 0.3rem 0.7rem; cursor: pointer; font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; } @@ -123,6 +129,14 @@ transition: color 0.15s, border-color 0.15s; } .chip:hover { color: var(--select); border-color: var(--select); } .chip.active { color: #1a1b1e; background: var(--select); border-color: var(--select); } + @media (max-width: 559px) { + .controls-inner { padding: 0.45rem 1rem 0.5rem; gap: 0.4rem; } + .controls-row { gap: 0.4rem; } + .searchwrap { flex-basis: 100%; } + .count { position: absolute; width: 1px; height: 1px; overflow: hidden; + clip: rect(0 0 0 0); white-space: nowrap; } + .density-btn { padding: 0.36rem 0.5rem; } + } @media (max-width: 719px) { html.js .tags-toggle { display: inline-block; } html.js .chips { display: none; } @@ -160,8 +174,12 @@ transition: outline-color 0.12s ease, border-color 0.12s ease; } .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } - .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media { display: block; background: var(--bg2); line-height: 0; overflow: hidden; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; + transition: transform 0.35s ease, opacity 0.3s ease; } + .card:hover .card-media img { transform: scale(1.03); } + html.js .grid .card-media img { opacity: 0; } + html.js .grid .card-media img.is-loaded { opacity: 1; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -200,6 +218,7 @@ .md h1, .md h2, .md h3 { letter-spacing: -0.01em; margin: 1.4rem 0 0.6rem; line-height: 1.25; } .md h1 { font-size: 1.35rem; } .md h2 { font-size: 1.15rem; } .md h3 { font-size: 1rem; } .md p { margin: 0.7rem 0; } + .md > p, .md > ul, .md > ol, .md > h2, .md > h3 { max-width: 75ch; } .md ul, .md ol { margin: 0.7rem 0 0.7rem 1.4rem; } .md li { margin: 0.3rem 0; } .md .table-wrap { overflow-x: auto; margin: 0.9rem 0; } @@ -306,8 +325,8 @@ } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - .card { transition: none; } - .card:hover { transform: none; } + .card, .card-media img { transition: none; } + .card:hover .card-media img { transform: none; } .to-top { transition: none; } } diff --git a/docs/gallery/campfire/index.html b/docs/gallery/campfire/index.html index c298cbce..caec3f6d 100644 --- a/docs/gallery/campfire/index.html +++ b/docs/gallery/campfire/index.html @@ -104,6 +104,12 @@ .density-btn + .density-btn { border-left: 1px solid var(--border); } .density-btn:hover { color: var(--select); } .density-btn.active { background: var(--select); color: #1a1b1e; } + /* Segmented groups clip overflow, so the ring has to sit inside. */ + .density-btn:focus-visible, .chip:focus-visible { outline-offset: -2px; } + .sort select { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); + border-radius: var(--radius); padding: 0.3rem 0.5rem; cursor: pointer; + font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; } + .sort select:hover, .sort select:focus { color: var(--select); border-color: var(--select); outline: none; } .tags-toggle { display: none; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); border-radius: 3px; padding: 0.3rem 0.7rem; cursor: pointer; font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; } @@ -123,6 +129,14 @@ transition: color 0.15s, border-color 0.15s; } .chip:hover { color: var(--select); border-color: var(--select); } .chip.active { color: #1a1b1e; background: var(--select); border-color: var(--select); } + @media (max-width: 559px) { + .controls-inner { padding: 0.45rem 1rem 0.5rem; gap: 0.4rem; } + .controls-row { gap: 0.4rem; } + .searchwrap { flex-basis: 100%; } + .count { position: absolute; width: 1px; height: 1px; overflow: hidden; + clip: rect(0 0 0 0); white-space: nowrap; } + .density-btn { padding: 0.36rem 0.5rem; } + } @media (max-width: 719px) { html.js .tags-toggle { display: inline-block; } html.js .chips { display: none; } @@ -160,8 +174,12 @@ transition: outline-color 0.12s ease, border-color 0.12s ease; } .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } - .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media { display: block; background: var(--bg2); line-height: 0; overflow: hidden; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; + transition: transform 0.35s ease, opacity 0.3s ease; } + .card:hover .card-media img { transform: scale(1.03); } + html.js .grid .card-media img { opacity: 0; } + html.js .grid .card-media img.is-loaded { opacity: 1; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -200,6 +218,7 @@ .md h1, .md h2, .md h3 { letter-spacing: -0.01em; margin: 1.4rem 0 0.6rem; line-height: 1.25; } .md h1 { font-size: 1.35rem; } .md h2 { font-size: 1.15rem; } .md h3 { font-size: 1rem; } .md p { margin: 0.7rem 0; } + .md > p, .md > ul, .md > ol, .md > h2, .md > h3 { max-width: 75ch; } .md ul, .md ol { margin: 0.7rem 0 0.7rem 1.4rem; } .md li { margin: 0.3rem 0; } .md .table-wrap { overflow-x: auto; margin: 0.9rem 0; } @@ -306,8 +325,8 @@ } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - .card { transition: none; } - .card:hover { transform: none; } + .card, .card-media img { transition: none; } + .card:hover .card-media img { transform: none; } .to-top { transition: none; } } diff --git a/docs/gallery/car-mirror-symmetry/index.html b/docs/gallery/car-mirror-symmetry/index.html index e92d364b..d29edb01 100644 --- a/docs/gallery/car-mirror-symmetry/index.html +++ b/docs/gallery/car-mirror-symmetry/index.html @@ -104,6 +104,12 @@ .density-btn + .density-btn { border-left: 1px solid var(--border); } .density-btn:hover { color: var(--select); } .density-btn.active { background: var(--select); color: #1a1b1e; } + /* Segmented groups clip overflow, so the ring has to sit inside. */ + .density-btn:focus-visible, .chip:focus-visible { outline-offset: -2px; } + .sort select { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); + border-radius: var(--radius); padding: 0.3rem 0.5rem; cursor: pointer; + font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; } + .sort select:hover, .sort select:focus { color: var(--select); border-color: var(--select); outline: none; } .tags-toggle { display: none; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); border-radius: 3px; padding: 0.3rem 0.7rem; cursor: pointer; font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; } @@ -123,6 +129,14 @@ transition: color 0.15s, border-color 0.15s; } .chip:hover { color: var(--select); border-color: var(--select); } .chip.active { color: #1a1b1e; background: var(--select); border-color: var(--select); } + @media (max-width: 559px) { + .controls-inner { padding: 0.45rem 1rem 0.5rem; gap: 0.4rem; } + .controls-row { gap: 0.4rem; } + .searchwrap { flex-basis: 100%; } + .count { position: absolute; width: 1px; height: 1px; overflow: hidden; + clip: rect(0 0 0 0); white-space: nowrap; } + .density-btn { padding: 0.36rem 0.5rem; } + } @media (max-width: 719px) { html.js .tags-toggle { display: inline-block; } html.js .chips { display: none; } @@ -160,8 +174,12 @@ transition: outline-color 0.12s ease, border-color 0.12s ease; } .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } - .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media { display: block; background: var(--bg2); line-height: 0; overflow: hidden; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; + transition: transform 0.35s ease, opacity 0.3s ease; } + .card:hover .card-media img { transform: scale(1.03); } + html.js .grid .card-media img { opacity: 0; } + html.js .grid .card-media img.is-loaded { opacity: 1; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -200,6 +218,7 @@ .md h1, .md h2, .md h3 { letter-spacing: -0.01em; margin: 1.4rem 0 0.6rem; line-height: 1.25; } .md h1 { font-size: 1.35rem; } .md h2 { font-size: 1.15rem; } .md h3 { font-size: 1rem; } .md p { margin: 0.7rem 0; } + .md > p, .md > ul, .md > ol, .md > h2, .md > h3 { max-width: 75ch; } .md ul, .md ol { margin: 0.7rem 0 0.7rem 1.4rem; } .md li { margin: 0.3rem 0; } .md .table-wrap { overflow-x: auto; margin: 0.9rem 0; } @@ -306,8 +325,8 @@ } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - .card { transition: none; } - .card:hover { transform: none; } + .card, .card-media img { transition: none; } + .card:hover .card-media img { transform: none; } .to-top { transition: none; } } diff --git a/docs/gallery/cart/index.html b/docs/gallery/cart/index.html index 60ce6f92..e9a13815 100644 --- a/docs/gallery/cart/index.html +++ b/docs/gallery/cart/index.html @@ -104,6 +104,12 @@ .density-btn + .density-btn { border-left: 1px solid var(--border); } .density-btn:hover { color: var(--select); } .density-btn.active { background: var(--select); color: #1a1b1e; } + /* Segmented groups clip overflow, so the ring has to sit inside. */ + .density-btn:focus-visible, .chip:focus-visible { outline-offset: -2px; } + .sort select { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); + border-radius: var(--radius); padding: 0.3rem 0.5rem; cursor: pointer; + font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; } + .sort select:hover, .sort select:focus { color: var(--select); border-color: var(--select); outline: none; } .tags-toggle { display: none; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); border-radius: 3px; padding: 0.3rem 0.7rem; cursor: pointer; font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; } @@ -123,6 +129,14 @@ transition: color 0.15s, border-color 0.15s; } .chip:hover { color: var(--select); border-color: var(--select); } .chip.active { color: #1a1b1e; background: var(--select); border-color: var(--select); } + @media (max-width: 559px) { + .controls-inner { padding: 0.45rem 1rem 0.5rem; gap: 0.4rem; } + .controls-row { gap: 0.4rem; } + .searchwrap { flex-basis: 100%; } + .count { position: absolute; width: 1px; height: 1px; overflow: hidden; + clip: rect(0 0 0 0); white-space: nowrap; } + .density-btn { padding: 0.36rem 0.5rem; } + } @media (max-width: 719px) { html.js .tags-toggle { display: inline-block; } html.js .chips { display: none; } @@ -160,8 +174,12 @@ transition: outline-color 0.12s ease, border-color 0.12s ease; } .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } - .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media { display: block; background: var(--bg2); line-height: 0; overflow: hidden; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; + transition: transform 0.35s ease, opacity 0.3s ease; } + .card:hover .card-media img { transform: scale(1.03); } + html.js .grid .card-media img { opacity: 0; } + html.js .grid .card-media img.is-loaded { opacity: 1; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -200,6 +218,7 @@ .md h1, .md h2, .md h3 { letter-spacing: -0.01em; margin: 1.4rem 0 0.6rem; line-height: 1.25; } .md h1 { font-size: 1.35rem; } .md h2 { font-size: 1.15rem; } .md h3 { font-size: 1rem; } .md p { margin: 0.7rem 0; } + .md > p, .md > ul, .md > ol, .md > h2, .md > h3 { max-width: 75ch; } .md ul, .md ol { margin: 0.7rem 0 0.7rem 1.4rem; } .md li { margin: 0.3rem 0; } .md .table-wrap { overflow-x: auto; margin: 0.9rem 0; } @@ -306,8 +325,8 @@ } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - .card { transition: none; } - .card:hover { transform: none; } + .card, .card-media img { transition: none; } + .card:hover .card-media img { transform: none; } .to-top { transition: none; } } diff --git a/docs/gallery/chopping-block/index.html b/docs/gallery/chopping-block/index.html index 7e950537..c1874772 100644 --- a/docs/gallery/chopping-block/index.html +++ b/docs/gallery/chopping-block/index.html @@ -104,6 +104,12 @@ .density-btn + .density-btn { border-left: 1px solid var(--border); } .density-btn:hover { color: var(--select); } .density-btn.active { background: var(--select); color: #1a1b1e; } + /* Segmented groups clip overflow, so the ring has to sit inside. */ + .density-btn:focus-visible, .chip:focus-visible { outline-offset: -2px; } + .sort select { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); + border-radius: var(--radius); padding: 0.3rem 0.5rem; cursor: pointer; + font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; } + .sort select:hover, .sort select:focus { color: var(--select); border-color: var(--select); outline: none; } .tags-toggle { display: none; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); border-radius: 3px; padding: 0.3rem 0.7rem; cursor: pointer; font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; } @@ -123,6 +129,14 @@ transition: color 0.15s, border-color 0.15s; } .chip:hover { color: var(--select); border-color: var(--select); } .chip.active { color: #1a1b1e; background: var(--select); border-color: var(--select); } + @media (max-width: 559px) { + .controls-inner { padding: 0.45rem 1rem 0.5rem; gap: 0.4rem; } + .controls-row { gap: 0.4rem; } + .searchwrap { flex-basis: 100%; } + .count { position: absolute; width: 1px; height: 1px; overflow: hidden; + clip: rect(0 0 0 0); white-space: nowrap; } + .density-btn { padding: 0.36rem 0.5rem; } + } @media (max-width: 719px) { html.js .tags-toggle { display: inline-block; } html.js .chips { display: none; } @@ -160,8 +174,12 @@ transition: outline-color 0.12s ease, border-color 0.12s ease; } .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } - .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media { display: block; background: var(--bg2); line-height: 0; overflow: hidden; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; + transition: transform 0.35s ease, opacity 0.3s ease; } + .card:hover .card-media img { transform: scale(1.03); } + html.js .grid .card-media img { opacity: 0; } + html.js .grid .card-media img.is-loaded { opacity: 1; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -200,6 +218,7 @@ .md h1, .md h2, .md h3 { letter-spacing: -0.01em; margin: 1.4rem 0 0.6rem; line-height: 1.25; } .md h1 { font-size: 1.35rem; } .md h2 { font-size: 1.15rem; } .md h3 { font-size: 1rem; } .md p { margin: 0.7rem 0; } + .md > p, .md > ul, .md > ol, .md > h2, .md > h3 { max-width: 75ch; } .md ul, .md ol { margin: 0.7rem 0 0.7rem 1.4rem; } .md li { margin: 0.3rem 0; } .md .table-wrap { overflow-x: auto; margin: 0.9rem 0; } @@ -306,8 +325,8 @@ } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - .card { transition: none; } - .card:hover { transform: none; } + .card, .card-media img { transition: none; } + .card:hover .card-media img { transform: none; } .to-top { transition: none; } } diff --git a/docs/gallery/collision-hull-proxy/index.html b/docs/gallery/collision-hull-proxy/index.html index 2351b650..f60bca94 100644 --- a/docs/gallery/collision-hull-proxy/index.html +++ b/docs/gallery/collision-hull-proxy/index.html @@ -104,6 +104,12 @@ .density-btn + .density-btn { border-left: 1px solid var(--border); } .density-btn:hover { color: var(--select); } .density-btn.active { background: var(--select); color: #1a1b1e; } + /* Segmented groups clip overflow, so the ring has to sit inside. */ + .density-btn:focus-visible, .chip:focus-visible { outline-offset: -2px; } + .sort select { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); + border-radius: var(--radius); padding: 0.3rem 0.5rem; cursor: pointer; + font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; } + .sort select:hover, .sort select:focus { color: var(--select); border-color: var(--select); outline: none; } .tags-toggle { display: none; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); border-radius: 3px; padding: 0.3rem 0.7rem; cursor: pointer; font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; } @@ -123,6 +129,14 @@ transition: color 0.15s, border-color 0.15s; } .chip:hover { color: var(--select); border-color: var(--select); } .chip.active { color: #1a1b1e; background: var(--select); border-color: var(--select); } + @media (max-width: 559px) { + .controls-inner { padding: 0.45rem 1rem 0.5rem; gap: 0.4rem; } + .controls-row { gap: 0.4rem; } + .searchwrap { flex-basis: 100%; } + .count { position: absolute; width: 1px; height: 1px; overflow: hidden; + clip: rect(0 0 0 0); white-space: nowrap; } + .density-btn { padding: 0.36rem 0.5rem; } + } @media (max-width: 719px) { html.js .tags-toggle { display: inline-block; } html.js .chips { display: none; } @@ -160,8 +174,12 @@ transition: outline-color 0.12s ease, border-color 0.12s ease; } .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } - .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media { display: block; background: var(--bg2); line-height: 0; overflow: hidden; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; + transition: transform 0.35s ease, opacity 0.3s ease; } + .card:hover .card-media img { transform: scale(1.03); } + html.js .grid .card-media img { opacity: 0; } + html.js .grid .card-media img.is-loaded { opacity: 1; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -200,6 +218,7 @@ .md h1, .md h2, .md h3 { letter-spacing: -0.01em; margin: 1.4rem 0 0.6rem; line-height: 1.25; } .md h1 { font-size: 1.35rem; } .md h2 { font-size: 1.15rem; } .md h3 { font-size: 1rem; } .md p { margin: 0.7rem 0; } + .md > p, .md > ul, .md > ol, .md > h2, .md > h3 { max-width: 75ch; } .md ul, .md ol { margin: 0.7rem 0 0.7rem 1.4rem; } .md li { margin: 0.3rem 0; } .md .table-wrap { overflow-x: auto; margin: 0.9rem 0; } @@ -306,8 +325,8 @@ } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - .card { transition: none; } - .card:hover { transform: none; } + .card, .card-media img { transition: none; } + .card:hover .card-media img { transform: none; } .to-top { transition: none; } } diff --git a/docs/gallery/color-attribute-wheel/index.html b/docs/gallery/color-attribute-wheel/index.html index a9d0e5e0..892777df 100644 --- a/docs/gallery/color-attribute-wheel/index.html +++ b/docs/gallery/color-attribute-wheel/index.html @@ -104,6 +104,12 @@ .density-btn + .density-btn { border-left: 1px solid var(--border); } .density-btn:hover { color: var(--select); } .density-btn.active { background: var(--select); color: #1a1b1e; } + /* Segmented groups clip overflow, so the ring has to sit inside. */ + .density-btn:focus-visible, .chip:focus-visible { outline-offset: -2px; } + .sort select { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); + border-radius: var(--radius); padding: 0.3rem 0.5rem; cursor: pointer; + font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; } + .sort select:hover, .sort select:focus { color: var(--select); border-color: var(--select); outline: none; } .tags-toggle { display: none; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); border-radius: 3px; padding: 0.3rem 0.7rem; cursor: pointer; font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; } @@ -123,6 +129,14 @@ transition: color 0.15s, border-color 0.15s; } .chip:hover { color: var(--select); border-color: var(--select); } .chip.active { color: #1a1b1e; background: var(--select); border-color: var(--select); } + @media (max-width: 559px) { + .controls-inner { padding: 0.45rem 1rem 0.5rem; gap: 0.4rem; } + .controls-row { gap: 0.4rem; } + .searchwrap { flex-basis: 100%; } + .count { position: absolute; width: 1px; height: 1px; overflow: hidden; + clip: rect(0 0 0 0); white-space: nowrap; } + .density-btn { padding: 0.36rem 0.5rem; } + } @media (max-width: 719px) { html.js .tags-toggle { display: inline-block; } html.js .chips { display: none; } @@ -160,8 +174,12 @@ transition: outline-color 0.12s ease, border-color 0.12s ease; } .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } - .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media { display: block; background: var(--bg2); line-height: 0; overflow: hidden; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; + transition: transform 0.35s ease, opacity 0.3s ease; } + .card:hover .card-media img { transform: scale(1.03); } + html.js .grid .card-media img { opacity: 0; } + html.js .grid .card-media img.is-loaded { opacity: 1; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -200,6 +218,7 @@ .md h1, .md h2, .md h3 { letter-spacing: -0.01em; margin: 1.4rem 0 0.6rem; line-height: 1.25; } .md h1 { font-size: 1.35rem; } .md h2 { font-size: 1.15rem; } .md h3 { font-size: 1rem; } .md p { margin: 0.7rem 0; } + .md > p, .md > ul, .md > ol, .md > h2, .md > h3 { max-width: 75ch; } .md ul, .md ol { margin: 0.7rem 0 0.7rem 1.4rem; } .md li { margin: 0.3rem 0; } .md .table-wrap { overflow-x: auto; margin: 0.9rem 0; } @@ -306,8 +325,8 @@ } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - .card { transition: none; } - .card:hover { transform: none; } + .card, .card-media img { transition: none; } + .card:hover .card-media img { transform: none; } .to-top { transition: none; } } diff --git a/docs/gallery/compositor-glare/index.html b/docs/gallery/compositor-glare/index.html index 8cfbbd17..f8def181 100644 --- a/docs/gallery/compositor-glare/index.html +++ b/docs/gallery/compositor-glare/index.html @@ -104,6 +104,12 @@ .density-btn + .density-btn { border-left: 1px solid var(--border); } .density-btn:hover { color: var(--select); } .density-btn.active { background: var(--select); color: #1a1b1e; } + /* Segmented groups clip overflow, so the ring has to sit inside. */ + .density-btn:focus-visible, .chip:focus-visible { outline-offset: -2px; } + .sort select { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); + border-radius: var(--radius); padding: 0.3rem 0.5rem; cursor: pointer; + font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; } + .sort select:hover, .sort select:focus { color: var(--select); border-color: var(--select); outline: none; } .tags-toggle { display: none; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); border-radius: 3px; padding: 0.3rem 0.7rem; cursor: pointer; font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; } @@ -123,6 +129,14 @@ transition: color 0.15s, border-color 0.15s; } .chip:hover { color: var(--select); border-color: var(--select); } .chip.active { color: #1a1b1e; background: var(--select); border-color: var(--select); } + @media (max-width: 559px) { + .controls-inner { padding: 0.45rem 1rem 0.5rem; gap: 0.4rem; } + .controls-row { gap: 0.4rem; } + .searchwrap { flex-basis: 100%; } + .count { position: absolute; width: 1px; height: 1px; overflow: hidden; + clip: rect(0 0 0 0); white-space: nowrap; } + .density-btn { padding: 0.36rem 0.5rem; } + } @media (max-width: 719px) { html.js .tags-toggle { display: inline-block; } html.js .chips { display: none; } @@ -160,8 +174,12 @@ transition: outline-color 0.12s ease, border-color 0.12s ease; } .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } - .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media { display: block; background: var(--bg2); line-height: 0; overflow: hidden; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; + transition: transform 0.35s ease, opacity 0.3s ease; } + .card:hover .card-media img { transform: scale(1.03); } + html.js .grid .card-media img { opacity: 0; } + html.js .grid .card-media img.is-loaded { opacity: 1; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -200,6 +218,7 @@ .md h1, .md h2, .md h3 { letter-spacing: -0.01em; margin: 1.4rem 0 0.6rem; line-height: 1.25; } .md h1 { font-size: 1.35rem; } .md h2 { font-size: 1.15rem; } .md h3 { font-size: 1rem; } .md p { margin: 0.7rem 0; } + .md > p, .md > ul, .md > ol, .md > h2, .md > h3 { max-width: 75ch; } .md ul, .md ol { margin: 0.7rem 0 0.7rem 1.4rem; } .md li { margin: 0.3rem 0; } .md .table-wrap { overflow-x: auto; margin: 0.9rem 0; } @@ -306,8 +325,8 @@ } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - .card { transition: none; } - .card:hover { transform: none; } + .card, .card-media img { transition: none; } + .card:hover .card-media img { transform: none; } .to-top { transition: none; } } diff --git a/docs/gallery/crate-stack/index.html b/docs/gallery/crate-stack/index.html index e26f066c..365687e8 100644 --- a/docs/gallery/crate-stack/index.html +++ b/docs/gallery/crate-stack/index.html @@ -104,6 +104,12 @@ .density-btn + .density-btn { border-left: 1px solid var(--border); } .density-btn:hover { color: var(--select); } .density-btn.active { background: var(--select); color: #1a1b1e; } + /* Segmented groups clip overflow, so the ring has to sit inside. */ + .density-btn:focus-visible, .chip:focus-visible { outline-offset: -2px; } + .sort select { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); + border-radius: var(--radius); padding: 0.3rem 0.5rem; cursor: pointer; + font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; } + .sort select:hover, .sort select:focus { color: var(--select); border-color: var(--select); outline: none; } .tags-toggle { display: none; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); border-radius: 3px; padding: 0.3rem 0.7rem; cursor: pointer; font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; } @@ -123,6 +129,14 @@ transition: color 0.15s, border-color 0.15s; } .chip:hover { color: var(--select); border-color: var(--select); } .chip.active { color: #1a1b1e; background: var(--select); border-color: var(--select); } + @media (max-width: 559px) { + .controls-inner { padding: 0.45rem 1rem 0.5rem; gap: 0.4rem; } + .controls-row { gap: 0.4rem; } + .searchwrap { flex-basis: 100%; } + .count { position: absolute; width: 1px; height: 1px; overflow: hidden; + clip: rect(0 0 0 0); white-space: nowrap; } + .density-btn { padding: 0.36rem 0.5rem; } + } @media (max-width: 719px) { html.js .tags-toggle { display: inline-block; } html.js .chips { display: none; } @@ -160,8 +174,12 @@ transition: outline-color 0.12s ease, border-color 0.12s ease; } .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } - .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media { display: block; background: var(--bg2); line-height: 0; overflow: hidden; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; + transition: transform 0.35s ease, opacity 0.3s ease; } + .card:hover .card-media img { transform: scale(1.03); } + html.js .grid .card-media img { opacity: 0; } + html.js .grid .card-media img.is-loaded { opacity: 1; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -200,6 +218,7 @@ .md h1, .md h2, .md h3 { letter-spacing: -0.01em; margin: 1.4rem 0 0.6rem; line-height: 1.25; } .md h1 { font-size: 1.35rem; } .md h2 { font-size: 1.15rem; } .md h3 { font-size: 1rem; } .md p { margin: 0.7rem 0; } + .md > p, .md > ul, .md > ol, .md > h2, .md > h3 { max-width: 75ch; } .md ul, .md ol { margin: 0.7rem 0 0.7rem 1.4rem; } .md li { margin: 0.3rem 0; } .md .table-wrap { overflow-x: auto; margin: 0.9rem 0; } @@ -306,8 +325,8 @@ } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - .card { transition: none; } - .card:hover { transform: none; } + .card, .card-media img { transition: none; } + .card:hover .card-media img { transform: none; } .to-top { transition: none; } } diff --git a/docs/gallery/cross-version-property-delete/index.html b/docs/gallery/cross-version-property-delete/index.html index 3c5eac1c..1e31314a 100644 --- a/docs/gallery/cross-version-property-delete/index.html +++ b/docs/gallery/cross-version-property-delete/index.html @@ -104,6 +104,12 @@ .density-btn + .density-btn { border-left: 1px solid var(--border); } .density-btn:hover { color: var(--select); } .density-btn.active { background: var(--select); color: #1a1b1e; } + /* Segmented groups clip overflow, so the ring has to sit inside. */ + .density-btn:focus-visible, .chip:focus-visible { outline-offset: -2px; } + .sort select { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); + border-radius: var(--radius); padding: 0.3rem 0.5rem; cursor: pointer; + font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; } + .sort select:hover, .sort select:focus { color: var(--select); border-color: var(--select); outline: none; } .tags-toggle { display: none; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); border-radius: 3px; padding: 0.3rem 0.7rem; cursor: pointer; font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; } @@ -123,6 +129,14 @@ transition: color 0.15s, border-color 0.15s; } .chip:hover { color: var(--select); border-color: var(--select); } .chip.active { color: #1a1b1e; background: var(--select); border-color: var(--select); } + @media (max-width: 559px) { + .controls-inner { padding: 0.45rem 1rem 0.5rem; gap: 0.4rem; } + .controls-row { gap: 0.4rem; } + .searchwrap { flex-basis: 100%; } + .count { position: absolute; width: 1px; height: 1px; overflow: hidden; + clip: rect(0 0 0 0); white-space: nowrap; } + .density-btn { padding: 0.36rem 0.5rem; } + } @media (max-width: 719px) { html.js .tags-toggle { display: inline-block; } html.js .chips { display: none; } @@ -160,8 +174,12 @@ transition: outline-color 0.12s ease, border-color 0.12s ease; } .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } - .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media { display: block; background: var(--bg2); line-height: 0; overflow: hidden; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; + transition: transform 0.35s ease, opacity 0.3s ease; } + .card:hover .card-media img { transform: scale(1.03); } + html.js .grid .card-media img { opacity: 0; } + html.js .grid .card-media img.is-loaded { opacity: 1; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -200,6 +218,7 @@ .md h1, .md h2, .md h3 { letter-spacing: -0.01em; margin: 1.4rem 0 0.6rem; line-height: 1.25; } .md h1 { font-size: 1.35rem; } .md h2 { font-size: 1.15rem; } .md h3 { font-size: 1rem; } .md p { margin: 0.7rem 0; } + .md > p, .md > ul, .md > ol, .md > h2, .md > h3 { max-width: 75ch; } .md ul, .md ol { margin: 0.7rem 0 0.7rem 1.4rem; } .md li { margin: 0.3rem 0; } .md .table-wrap { overflow-x: auto; margin: 0.9rem 0; } @@ -306,8 +325,8 @@ } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - .card { transition: none; } - .card:hover { transform: none; } + .card, .card-media img { transition: none; } + .card:hover .card-media img { transform: none; } .to-top { transition: none; } } diff --git a/docs/gallery/curve-bevel-arc/index.html b/docs/gallery/curve-bevel-arc/index.html index bc620057..80d90710 100644 --- a/docs/gallery/curve-bevel-arc/index.html +++ b/docs/gallery/curve-bevel-arc/index.html @@ -104,6 +104,12 @@ .density-btn + .density-btn { border-left: 1px solid var(--border); } .density-btn:hover { color: var(--select); } .density-btn.active { background: var(--select); color: #1a1b1e; } + /* Segmented groups clip overflow, so the ring has to sit inside. */ + .density-btn:focus-visible, .chip:focus-visible { outline-offset: -2px; } + .sort select { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); + border-radius: var(--radius); padding: 0.3rem 0.5rem; cursor: pointer; + font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; } + .sort select:hover, .sort select:focus { color: var(--select); border-color: var(--select); outline: none; } .tags-toggle { display: none; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); border-radius: 3px; padding: 0.3rem 0.7rem; cursor: pointer; font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; } @@ -123,6 +129,14 @@ transition: color 0.15s, border-color 0.15s; } .chip:hover { color: var(--select); border-color: var(--select); } .chip.active { color: #1a1b1e; background: var(--select); border-color: var(--select); } + @media (max-width: 559px) { + .controls-inner { padding: 0.45rem 1rem 0.5rem; gap: 0.4rem; } + .controls-row { gap: 0.4rem; } + .searchwrap { flex-basis: 100%; } + .count { position: absolute; width: 1px; height: 1px; overflow: hidden; + clip: rect(0 0 0 0); white-space: nowrap; } + .density-btn { padding: 0.36rem 0.5rem; } + } @media (max-width: 719px) { html.js .tags-toggle { display: inline-block; } html.js .chips { display: none; } @@ -160,8 +174,12 @@ transition: outline-color 0.12s ease, border-color 0.12s ease; } .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } - .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media { display: block; background: var(--bg2); line-height: 0; overflow: hidden; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; + transition: transform 0.35s ease, opacity 0.3s ease; } + .card:hover .card-media img { transform: scale(1.03); } + html.js .grid .card-media img { opacity: 0; } + html.js .grid .card-media img.is-loaded { opacity: 1; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -200,6 +218,7 @@ .md h1, .md h2, .md h3 { letter-spacing: -0.01em; margin: 1.4rem 0 0.6rem; line-height: 1.25; } .md h1 { font-size: 1.35rem; } .md h2 { font-size: 1.15rem; } .md h3 { font-size: 1rem; } .md p { margin: 0.7rem 0; } + .md > p, .md > ul, .md > ol, .md > h2, .md > h3 { max-width: 75ch; } .md ul, .md ol { margin: 0.7rem 0 0.7rem 1.4rem; } .md li { margin: 0.3rem 0; } .md .table-wrap { overflow-x: auto; margin: 0.9rem 0; } @@ -306,8 +325,8 @@ } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - .card { transition: none; } - .card:hover { transform: none; } + .card, .card-media img { transition: none; } + .card:hover .card-media img { transform: none; } .to-top { transition: none; } } diff --git a/docs/gallery/custom-normals-shade/index.html b/docs/gallery/custom-normals-shade/index.html index 8f1aa591..b0b97c1d 100644 --- a/docs/gallery/custom-normals-shade/index.html +++ b/docs/gallery/custom-normals-shade/index.html @@ -104,6 +104,12 @@ .density-btn + .density-btn { border-left: 1px solid var(--border); } .density-btn:hover { color: var(--select); } .density-btn.active { background: var(--select); color: #1a1b1e; } + /* Segmented groups clip overflow, so the ring has to sit inside. */ + .density-btn:focus-visible, .chip:focus-visible { outline-offset: -2px; } + .sort select { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); + border-radius: var(--radius); padding: 0.3rem 0.5rem; cursor: pointer; + font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; } + .sort select:hover, .sort select:focus { color: var(--select); border-color: var(--select); outline: none; } .tags-toggle { display: none; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); border-radius: 3px; padding: 0.3rem 0.7rem; cursor: pointer; font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; } @@ -123,6 +129,14 @@ transition: color 0.15s, border-color 0.15s; } .chip:hover { color: var(--select); border-color: var(--select); } .chip.active { color: #1a1b1e; background: var(--select); border-color: var(--select); } + @media (max-width: 559px) { + .controls-inner { padding: 0.45rem 1rem 0.5rem; gap: 0.4rem; } + .controls-row { gap: 0.4rem; } + .searchwrap { flex-basis: 100%; } + .count { position: absolute; width: 1px; height: 1px; overflow: hidden; + clip: rect(0 0 0 0); white-space: nowrap; } + .density-btn { padding: 0.36rem 0.5rem; } + } @media (max-width: 719px) { html.js .tags-toggle { display: inline-block; } html.js .chips { display: none; } @@ -160,8 +174,12 @@ transition: outline-color 0.12s ease, border-color 0.12s ease; } .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } - .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media { display: block; background: var(--bg2); line-height: 0; overflow: hidden; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; + transition: transform 0.35s ease, opacity 0.3s ease; } + .card:hover .card-media img { transform: scale(1.03); } + html.js .grid .card-media img { opacity: 0; } + html.js .grid .card-media img.is-loaded { opacity: 1; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -200,6 +218,7 @@ .md h1, .md h2, .md h3 { letter-spacing: -0.01em; margin: 1.4rem 0 0.6rem; line-height: 1.25; } .md h1 { font-size: 1.35rem; } .md h2 { font-size: 1.15rem; } .md h3 { font-size: 1rem; } .md p { margin: 0.7rem 0; } + .md > p, .md > ul, .md > ol, .md > h2, .md > h3 { max-width: 75ch; } .md ul, .md ol { margin: 0.7rem 0 0.7rem 1.4rem; } .md li { margin: 0.3rem 0; } .md .table-wrap { overflow-x: auto; margin: 0.9rem 0; } @@ -306,8 +325,8 @@ } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - .card { transition: none; } - .card:hover { transform: none; } + .card, .card-media img { transition: none; } + .card:hover .card-media img { transform: none; } .to-top { transition: none; } } diff --git a/docs/gallery/damped-track-aim/index.html b/docs/gallery/damped-track-aim/index.html index 811f1b18..8a06df6a 100644 --- a/docs/gallery/damped-track-aim/index.html +++ b/docs/gallery/damped-track-aim/index.html @@ -104,6 +104,12 @@ .density-btn + .density-btn { border-left: 1px solid var(--border); } .density-btn:hover { color: var(--select); } .density-btn.active { background: var(--select); color: #1a1b1e; } + /* Segmented groups clip overflow, so the ring has to sit inside. */ + .density-btn:focus-visible, .chip:focus-visible { outline-offset: -2px; } + .sort select { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); + border-radius: var(--radius); padding: 0.3rem 0.5rem; cursor: pointer; + font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; } + .sort select:hover, .sort select:focus { color: var(--select); border-color: var(--select); outline: none; } .tags-toggle { display: none; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); border-radius: 3px; padding: 0.3rem 0.7rem; cursor: pointer; font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; } @@ -123,6 +129,14 @@ transition: color 0.15s, border-color 0.15s; } .chip:hover { color: var(--select); border-color: var(--select); } .chip.active { color: #1a1b1e; background: var(--select); border-color: var(--select); } + @media (max-width: 559px) { + .controls-inner { padding: 0.45rem 1rem 0.5rem; gap: 0.4rem; } + .controls-row { gap: 0.4rem; } + .searchwrap { flex-basis: 100%; } + .count { position: absolute; width: 1px; height: 1px; overflow: hidden; + clip: rect(0 0 0 0); white-space: nowrap; } + .density-btn { padding: 0.36rem 0.5rem; } + } @media (max-width: 719px) { html.js .tags-toggle { display: inline-block; } html.js .chips { display: none; } @@ -160,8 +174,12 @@ transition: outline-color 0.12s ease, border-color 0.12s ease; } .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } - .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media { display: block; background: var(--bg2); line-height: 0; overflow: hidden; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; + transition: transform 0.35s ease, opacity 0.3s ease; } + .card:hover .card-media img { transform: scale(1.03); } + html.js .grid .card-media img { opacity: 0; } + html.js .grid .card-media img.is-loaded { opacity: 1; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -200,6 +218,7 @@ .md h1, .md h2, .md h3 { letter-spacing: -0.01em; margin: 1.4rem 0 0.6rem; line-height: 1.25; } .md h1 { font-size: 1.35rem; } .md h2 { font-size: 1.15rem; } .md h3 { font-size: 1rem; } .md p { margin: 0.7rem 0; } + .md > p, .md > ul, .md > ol, .md > h2, .md > h3 { max-width: 75ch; } .md ul, .md ol { margin: 0.7rem 0 0.7rem 1.4rem; } .md li { margin: 0.3rem 0; } .md .table-wrap { overflow-x: auto; margin: 0.9rem 0; } @@ -306,8 +325,8 @@ } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - .card { transition: none; } - .card:hover { transform: none; } + .card, .card-media img { transition: none; } + .card:hover .card-media img { transform: none; } .to-top { transition: none; } } diff --git a/docs/gallery/degenerate-bevel-weld/index.html b/docs/gallery/degenerate-bevel-weld/index.html index d8356726..b9d348d9 100644 --- a/docs/gallery/degenerate-bevel-weld/index.html +++ b/docs/gallery/degenerate-bevel-weld/index.html @@ -104,6 +104,12 @@ .density-btn + .density-btn { border-left: 1px solid var(--border); } .density-btn:hover { color: var(--select); } .density-btn.active { background: var(--select); color: #1a1b1e; } + /* Segmented groups clip overflow, so the ring has to sit inside. */ + .density-btn:focus-visible, .chip:focus-visible { outline-offset: -2px; } + .sort select { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); + border-radius: var(--radius); padding: 0.3rem 0.5rem; cursor: pointer; + font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; } + .sort select:hover, .sort select:focus { color: var(--select); border-color: var(--select); outline: none; } .tags-toggle { display: none; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); border-radius: 3px; padding: 0.3rem 0.7rem; cursor: pointer; font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; } @@ -123,6 +129,14 @@ transition: color 0.15s, border-color 0.15s; } .chip:hover { color: var(--select); border-color: var(--select); } .chip.active { color: #1a1b1e; background: var(--select); border-color: var(--select); } + @media (max-width: 559px) { + .controls-inner { padding: 0.45rem 1rem 0.5rem; gap: 0.4rem; } + .controls-row { gap: 0.4rem; } + .searchwrap { flex-basis: 100%; } + .count { position: absolute; width: 1px; height: 1px; overflow: hidden; + clip: rect(0 0 0 0); white-space: nowrap; } + .density-btn { padding: 0.36rem 0.5rem; } + } @media (max-width: 719px) { html.js .tags-toggle { display: inline-block; } html.js .chips { display: none; } @@ -160,8 +174,12 @@ transition: outline-color 0.12s ease, border-color 0.12s ease; } .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } - .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media { display: block; background: var(--bg2); line-height: 0; overflow: hidden; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; + transition: transform 0.35s ease, opacity 0.3s ease; } + .card:hover .card-media img { transform: scale(1.03); } + html.js .grid .card-media img { opacity: 0; } + html.js .grid .card-media img.is-loaded { opacity: 1; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -200,6 +218,7 @@ .md h1, .md h2, .md h3 { letter-spacing: -0.01em; margin: 1.4rem 0 0.6rem; line-height: 1.25; } .md h1 { font-size: 1.35rem; } .md h2 { font-size: 1.15rem; } .md h3 { font-size: 1rem; } .md p { margin: 0.7rem 0; } + .md > p, .md > ul, .md > ol, .md > h2, .md > h3 { max-width: 75ch; } .md ul, .md ol { margin: 0.7rem 0 0.7rem 1.4rem; } .md li { margin: 0.3rem 0; } .md .table-wrap { overflow-x: auto; margin: 0.9rem 0; } @@ -306,8 +325,8 @@ } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - .card { transition: none; } - .card:hover { transform: none; } + .card, .card-media img { transition: none; } + .card:hover .card-media img { transform: none; } .to-top { transition: none; } } diff --git a/docs/gallery/depsgraph-export/index.html b/docs/gallery/depsgraph-export/index.html index c2178ba8..32e11fb5 100644 --- a/docs/gallery/depsgraph-export/index.html +++ b/docs/gallery/depsgraph-export/index.html @@ -104,6 +104,12 @@ .density-btn + .density-btn { border-left: 1px solid var(--border); } .density-btn:hover { color: var(--select); } .density-btn.active { background: var(--select); color: #1a1b1e; } + /* Segmented groups clip overflow, so the ring has to sit inside. */ + .density-btn:focus-visible, .chip:focus-visible { outline-offset: -2px; } + .sort select { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); + border-radius: var(--radius); padding: 0.3rem 0.5rem; cursor: pointer; + font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; } + .sort select:hover, .sort select:focus { color: var(--select); border-color: var(--select); outline: none; } .tags-toggle { display: none; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); border-radius: 3px; padding: 0.3rem 0.7rem; cursor: pointer; font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; } @@ -123,6 +129,14 @@ transition: color 0.15s, border-color 0.15s; } .chip:hover { color: var(--select); border-color: var(--select); } .chip.active { color: #1a1b1e; background: var(--select); border-color: var(--select); } + @media (max-width: 559px) { + .controls-inner { padding: 0.45rem 1rem 0.5rem; gap: 0.4rem; } + .controls-row { gap: 0.4rem; } + .searchwrap { flex-basis: 100%; } + .count { position: absolute; width: 1px; height: 1px; overflow: hidden; + clip: rect(0 0 0 0); white-space: nowrap; } + .density-btn { padding: 0.36rem 0.5rem; } + } @media (max-width: 719px) { html.js .tags-toggle { display: inline-block; } html.js .chips { display: none; } @@ -160,8 +174,12 @@ transition: outline-color 0.12s ease, border-color 0.12s ease; } .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } - .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media { display: block; background: var(--bg2); line-height: 0; overflow: hidden; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; + transition: transform 0.35s ease, opacity 0.3s ease; } + .card:hover .card-media img { transform: scale(1.03); } + html.js .grid .card-media img { opacity: 0; } + html.js .grid .card-media img.is-loaded { opacity: 1; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -200,6 +218,7 @@ .md h1, .md h2, .md h3 { letter-spacing: -0.01em; margin: 1.4rem 0 0.6rem; line-height: 1.25; } .md h1 { font-size: 1.35rem; } .md h2 { font-size: 1.15rem; } .md h3 { font-size: 1rem; } .md p { margin: 0.7rem 0; } + .md > p, .md > ul, .md > ol, .md > h2, .md > h3 { max-width: 75ch; } .md ul, .md ol { margin: 0.7rem 0 0.7rem 1.4rem; } .md li { margin: 0.3rem 0; } .md .table-wrap { overflow-x: auto; margin: 0.9rem 0; } @@ -306,8 +325,8 @@ } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - .card { transition: none; } - .card:hover { transform: none; } + .card, .card-media img { transition: none; } + .card:hover .card-media img { transform: none; } .to-top { transition: none; } } diff --git a/docs/gallery/driver-wave/index.html b/docs/gallery/driver-wave/index.html index fa50a8e3..ffce4d0e 100644 --- a/docs/gallery/driver-wave/index.html +++ b/docs/gallery/driver-wave/index.html @@ -104,6 +104,12 @@ .density-btn + .density-btn { border-left: 1px solid var(--border); } .density-btn:hover { color: var(--select); } .density-btn.active { background: var(--select); color: #1a1b1e; } + /* Segmented groups clip overflow, so the ring has to sit inside. */ + .density-btn:focus-visible, .chip:focus-visible { outline-offset: -2px; } + .sort select { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); + border-radius: var(--radius); padding: 0.3rem 0.5rem; cursor: pointer; + font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; } + .sort select:hover, .sort select:focus { color: var(--select); border-color: var(--select); outline: none; } .tags-toggle { display: none; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); border-radius: 3px; padding: 0.3rem 0.7rem; cursor: pointer; font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; } @@ -123,6 +129,14 @@ transition: color 0.15s, border-color 0.15s; } .chip:hover { color: var(--select); border-color: var(--select); } .chip.active { color: #1a1b1e; background: var(--select); border-color: var(--select); } + @media (max-width: 559px) { + .controls-inner { padding: 0.45rem 1rem 0.5rem; gap: 0.4rem; } + .controls-row { gap: 0.4rem; } + .searchwrap { flex-basis: 100%; } + .count { position: absolute; width: 1px; height: 1px; overflow: hidden; + clip: rect(0 0 0 0); white-space: nowrap; } + .density-btn { padding: 0.36rem 0.5rem; } + } @media (max-width: 719px) { html.js .tags-toggle { display: inline-block; } html.js .chips { display: none; } @@ -160,8 +174,12 @@ transition: outline-color 0.12s ease, border-color 0.12s ease; } .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } - .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media { display: block; background: var(--bg2); line-height: 0; overflow: hidden; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; + transition: transform 0.35s ease, opacity 0.3s ease; } + .card:hover .card-media img { transform: scale(1.03); } + html.js .grid .card-media img { opacity: 0; } + html.js .grid .card-media img.is-loaded { opacity: 1; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -200,6 +218,7 @@ .md h1, .md h2, .md h3 { letter-spacing: -0.01em; margin: 1.4rem 0 0.6rem; line-height: 1.25; } .md h1 { font-size: 1.35rem; } .md h2 { font-size: 1.15rem; } .md h3 { font-size: 1rem; } .md p { margin: 0.7rem 0; } + .md > p, .md > ul, .md > ol, .md > h2, .md > h3 { max-width: 75ch; } .md ul, .md ol { margin: 0.7rem 0 0.7rem 1.4rem; } .md li { margin: 0.3rem 0; } .md .table-wrap { overflow-x: auto; margin: 0.9rem 0; } @@ -306,8 +325,8 @@ } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - .card { transition: none; } - .card:hover { transform: none; } + .card, .card-media img { transition: none; } + .card:hover .card-media img { transform: none; } .to-top { transition: none; } } diff --git a/docs/gallery/export-preset-axis/index.html b/docs/gallery/export-preset-axis/index.html index 05669891..f27c8415 100644 --- a/docs/gallery/export-preset-axis/index.html +++ b/docs/gallery/export-preset-axis/index.html @@ -104,6 +104,12 @@ .density-btn + .density-btn { border-left: 1px solid var(--border); } .density-btn:hover { color: var(--select); } .density-btn.active { background: var(--select); color: #1a1b1e; } + /* Segmented groups clip overflow, so the ring has to sit inside. */ + .density-btn:focus-visible, .chip:focus-visible { outline-offset: -2px; } + .sort select { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); + border-radius: var(--radius); padding: 0.3rem 0.5rem; cursor: pointer; + font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; } + .sort select:hover, .sort select:focus { color: var(--select); border-color: var(--select); outline: none; } .tags-toggle { display: none; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); border-radius: 3px; padding: 0.3rem 0.7rem; cursor: pointer; font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; } @@ -123,6 +129,14 @@ transition: color 0.15s, border-color 0.15s; } .chip:hover { color: var(--select); border-color: var(--select); } .chip.active { color: #1a1b1e; background: var(--select); border-color: var(--select); } + @media (max-width: 559px) { + .controls-inner { padding: 0.45rem 1rem 0.5rem; gap: 0.4rem; } + .controls-row { gap: 0.4rem; } + .searchwrap { flex-basis: 100%; } + .count { position: absolute; width: 1px; height: 1px; overflow: hidden; + clip: rect(0 0 0 0); white-space: nowrap; } + .density-btn { padding: 0.36rem 0.5rem; } + } @media (max-width: 719px) { html.js .tags-toggle { display: inline-block; } html.js .chips { display: none; } @@ -160,8 +174,12 @@ transition: outline-color 0.12s ease, border-color 0.12s ease; } .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } - .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media { display: block; background: var(--bg2); line-height: 0; overflow: hidden; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; + transition: transform 0.35s ease, opacity 0.3s ease; } + .card:hover .card-media img { transform: scale(1.03); } + html.js .grid .card-media img { opacity: 0; } + html.js .grid .card-media img.is-loaded { opacity: 1; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -200,6 +218,7 @@ .md h1, .md h2, .md h3 { letter-spacing: -0.01em; margin: 1.4rem 0 0.6rem; line-height: 1.25; } .md h1 { font-size: 1.35rem; } .md h2 { font-size: 1.15rem; } .md h3 { font-size: 1rem; } .md p { margin: 0.7rem 0; } + .md > p, .md > ul, .md > ol, .md > h2, .md > h3 { max-width: 75ch; } .md ul, .md ol { margin: 0.7rem 0 0.7rem 1.4rem; } .md li { margin: 0.3rem 0; } .md .table-wrap { overflow-x: auto; margin: 0.9rem 0; } @@ -306,8 +325,8 @@ } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - .card { transition: none; } - .card:hover { transform: none; } + .card, .card-media img { transition: none; } + .card:hover .card-media img { transform: none; } .to-top { transition: none; } } diff --git a/docs/gallery/fence-kit/index.html b/docs/gallery/fence-kit/index.html index 25f84280..aa5a9054 100644 --- a/docs/gallery/fence-kit/index.html +++ b/docs/gallery/fence-kit/index.html @@ -104,6 +104,12 @@ .density-btn + .density-btn { border-left: 1px solid var(--border); } .density-btn:hover { color: var(--select); } .density-btn.active { background: var(--select); color: #1a1b1e; } + /* Segmented groups clip overflow, so the ring has to sit inside. */ + .density-btn:focus-visible, .chip:focus-visible { outline-offset: -2px; } + .sort select { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); + border-radius: var(--radius); padding: 0.3rem 0.5rem; cursor: pointer; + font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; } + .sort select:hover, .sort select:focus { color: var(--select); border-color: var(--select); outline: none; } .tags-toggle { display: none; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); border-radius: 3px; padding: 0.3rem 0.7rem; cursor: pointer; font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; } @@ -123,6 +129,14 @@ transition: color 0.15s, border-color 0.15s; } .chip:hover { color: var(--select); border-color: var(--select); } .chip.active { color: #1a1b1e; background: var(--select); border-color: var(--select); } + @media (max-width: 559px) { + .controls-inner { padding: 0.45rem 1rem 0.5rem; gap: 0.4rem; } + .controls-row { gap: 0.4rem; } + .searchwrap { flex-basis: 100%; } + .count { position: absolute; width: 1px; height: 1px; overflow: hidden; + clip: rect(0 0 0 0); white-space: nowrap; } + .density-btn { padding: 0.36rem 0.5rem; } + } @media (max-width: 719px) { html.js .tags-toggle { display: inline-block; } html.js .chips { display: none; } @@ -160,8 +174,12 @@ transition: outline-color 0.12s ease, border-color 0.12s ease; } .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } - .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media { display: block; background: var(--bg2); line-height: 0; overflow: hidden; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; + transition: transform 0.35s ease, opacity 0.3s ease; } + .card:hover .card-media img { transform: scale(1.03); } + html.js .grid .card-media img { opacity: 0; } + html.js .grid .card-media img.is-loaded { opacity: 1; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -200,6 +218,7 @@ .md h1, .md h2, .md h3 { letter-spacing: -0.01em; margin: 1.4rem 0 0.6rem; line-height: 1.25; } .md h1 { font-size: 1.35rem; } .md h2 { font-size: 1.15rem; } .md h3 { font-size: 1rem; } .md p { margin: 0.7rem 0; } + .md > p, .md > ul, .md > ol, .md > h2, .md > h3 { max-width: 75ch; } .md ul, .md ol { margin: 0.7rem 0 0.7rem 1.4rem; } .md li { margin: 0.3rem 0; } .md .table-wrap { overflow-x: auto; margin: 0.9rem 0; } @@ -306,8 +325,8 @@ } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - .card { transition: none; } - .card:hover { transform: none; } + .card, .card-media img { transition: none; } + .card:hover .card-media img { transform: none; } .to-top { transition: none; } } diff --git a/docs/gallery/gltf-export-roundtrip/index.html b/docs/gallery/gltf-export-roundtrip/index.html index 82f09013..cd58c38e 100644 --- a/docs/gallery/gltf-export-roundtrip/index.html +++ b/docs/gallery/gltf-export-roundtrip/index.html @@ -104,6 +104,12 @@ .density-btn + .density-btn { border-left: 1px solid var(--border); } .density-btn:hover { color: var(--select); } .density-btn.active { background: var(--select); color: #1a1b1e; } + /* Segmented groups clip overflow, so the ring has to sit inside. */ + .density-btn:focus-visible, .chip:focus-visible { outline-offset: -2px; } + .sort select { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); + border-radius: var(--radius); padding: 0.3rem 0.5rem; cursor: pointer; + font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; } + .sort select:hover, .sort select:focus { color: var(--select); border-color: var(--select); outline: none; } .tags-toggle { display: none; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); border-radius: 3px; padding: 0.3rem 0.7rem; cursor: pointer; font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; } @@ -123,6 +129,14 @@ transition: color 0.15s, border-color 0.15s; } .chip:hover { color: var(--select); border-color: var(--select); } .chip.active { color: #1a1b1e; background: var(--select); border-color: var(--select); } + @media (max-width: 559px) { + .controls-inner { padding: 0.45rem 1rem 0.5rem; gap: 0.4rem; } + .controls-row { gap: 0.4rem; } + .searchwrap { flex-basis: 100%; } + .count { position: absolute; width: 1px; height: 1px; overflow: hidden; + clip: rect(0 0 0 0); white-space: nowrap; } + .density-btn { padding: 0.36rem 0.5rem; } + } @media (max-width: 719px) { html.js .tags-toggle { display: inline-block; } html.js .chips { display: none; } @@ -160,8 +174,12 @@ transition: outline-color 0.12s ease, border-color 0.12s ease; } .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } - .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media { display: block; background: var(--bg2); line-height: 0; overflow: hidden; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; + transition: transform 0.35s ease, opacity 0.3s ease; } + .card:hover .card-media img { transform: scale(1.03); } + html.js .grid .card-media img { opacity: 0; } + html.js .grid .card-media img.is-loaded { opacity: 1; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -200,6 +218,7 @@ .md h1, .md h2, .md h3 { letter-spacing: -0.01em; margin: 1.4rem 0 0.6rem; line-height: 1.25; } .md h1 { font-size: 1.35rem; } .md h2 { font-size: 1.15rem; } .md h3 { font-size: 1rem; } .md p { margin: 0.7rem 0; } + .md > p, .md > ul, .md > ol, .md > h2, .md > h3 { max-width: 75ch; } .md ul, .md ol { margin: 0.7rem 0 0.7rem 1.4rem; } .md li { margin: 0.3rem 0; } .md .table-wrap { overflow-x: auto; margin: 0.9rem 0; } @@ -306,8 +325,8 @@ } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - .card { transition: none; } - .card:hover { transform: none; } + .card, .card-media img { transition: none; } + .card:hover .card-media img { transform: none; } .to-top { transition: none; } } diff --git a/docs/gallery/gltf-skin-roundtrip/index.html b/docs/gallery/gltf-skin-roundtrip/index.html index 298f9b65..d2449ed5 100644 --- a/docs/gallery/gltf-skin-roundtrip/index.html +++ b/docs/gallery/gltf-skin-roundtrip/index.html @@ -104,6 +104,12 @@ .density-btn + .density-btn { border-left: 1px solid var(--border); } .density-btn:hover { color: var(--select); } .density-btn.active { background: var(--select); color: #1a1b1e; } + /* Segmented groups clip overflow, so the ring has to sit inside. */ + .density-btn:focus-visible, .chip:focus-visible { outline-offset: -2px; } + .sort select { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); + border-radius: var(--radius); padding: 0.3rem 0.5rem; cursor: pointer; + font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; } + .sort select:hover, .sort select:focus { color: var(--select); border-color: var(--select); outline: none; } .tags-toggle { display: none; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); border-radius: 3px; padding: 0.3rem 0.7rem; cursor: pointer; font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; } @@ -123,6 +129,14 @@ transition: color 0.15s, border-color 0.15s; } .chip:hover { color: var(--select); border-color: var(--select); } .chip.active { color: #1a1b1e; background: var(--select); border-color: var(--select); } + @media (max-width: 559px) { + .controls-inner { padding: 0.45rem 1rem 0.5rem; gap: 0.4rem; } + .controls-row { gap: 0.4rem; } + .searchwrap { flex-basis: 100%; } + .count { position: absolute; width: 1px; height: 1px; overflow: hidden; + clip: rect(0 0 0 0); white-space: nowrap; } + .density-btn { padding: 0.36rem 0.5rem; } + } @media (max-width: 719px) { html.js .tags-toggle { display: inline-block; } html.js .chips { display: none; } @@ -160,8 +174,12 @@ transition: outline-color 0.12s ease, border-color 0.12s ease; } .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } - .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media { display: block; background: var(--bg2); line-height: 0; overflow: hidden; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; + transition: transform 0.35s ease, opacity 0.3s ease; } + .card:hover .card-media img { transform: scale(1.03); } + html.js .grid .card-media img { opacity: 0; } + html.js .grid .card-media img.is-loaded { opacity: 1; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -200,6 +218,7 @@ .md h1, .md h2, .md h3 { letter-spacing: -0.01em; margin: 1.4rem 0 0.6rem; line-height: 1.25; } .md h1 { font-size: 1.35rem; } .md h2 { font-size: 1.15rem; } .md h3 { font-size: 1rem; } .md p { margin: 0.7rem 0; } + .md > p, .md > ul, .md > ol, .md > h2, .md > h3 { max-width: 75ch; } .md ul, .md ol { margin: 0.7rem 0 0.7rem 1.4rem; } .md li { margin: 0.3rem 0; } .md .table-wrap { overflow-x: auto; margin: 0.9rem 0; } @@ -306,8 +325,8 @@ } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - .card { transition: none; } - .card:hover { transform: none; } + .card, .card-media img { transition: none; } + .card:hover .card-media img { transform: none; } .to-top { transition: none; } } diff --git a/docs/gallery/gn-instance-grid/index.html b/docs/gallery/gn-instance-grid/index.html index d9cd73c9..556c629b 100644 --- a/docs/gallery/gn-instance-grid/index.html +++ b/docs/gallery/gn-instance-grid/index.html @@ -104,6 +104,12 @@ .density-btn + .density-btn { border-left: 1px solid var(--border); } .density-btn:hover { color: var(--select); } .density-btn.active { background: var(--select); color: #1a1b1e; } + /* Segmented groups clip overflow, so the ring has to sit inside. */ + .density-btn:focus-visible, .chip:focus-visible { outline-offset: -2px; } + .sort select { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); + border-radius: var(--radius); padding: 0.3rem 0.5rem; cursor: pointer; + font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; } + .sort select:hover, .sort select:focus { color: var(--select); border-color: var(--select); outline: none; } .tags-toggle { display: none; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); border-radius: 3px; padding: 0.3rem 0.7rem; cursor: pointer; font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; } @@ -123,6 +129,14 @@ transition: color 0.15s, border-color 0.15s; } .chip:hover { color: var(--select); border-color: var(--select); } .chip.active { color: #1a1b1e; background: var(--select); border-color: var(--select); } + @media (max-width: 559px) { + .controls-inner { padding: 0.45rem 1rem 0.5rem; gap: 0.4rem; } + .controls-row { gap: 0.4rem; } + .searchwrap { flex-basis: 100%; } + .count { position: absolute; width: 1px; height: 1px; overflow: hidden; + clip: rect(0 0 0 0); white-space: nowrap; } + .density-btn { padding: 0.36rem 0.5rem; } + } @media (max-width: 719px) { html.js .tags-toggle { display: inline-block; } html.js .chips { display: none; } @@ -160,8 +174,12 @@ transition: outline-color 0.12s ease, border-color 0.12s ease; } .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } - .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media { display: block; background: var(--bg2); line-height: 0; overflow: hidden; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; + transition: transform 0.35s ease, opacity 0.3s ease; } + .card:hover .card-media img { transform: scale(1.03); } + html.js .grid .card-media img { opacity: 0; } + html.js .grid .card-media img.is-loaded { opacity: 1; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -200,6 +218,7 @@ .md h1, .md h2, .md h3 { letter-spacing: -0.01em; margin: 1.4rem 0 0.6rem; line-height: 1.25; } .md h1 { font-size: 1.35rem; } .md h2 { font-size: 1.15rem; } .md h3 { font-size: 1rem; } .md p { margin: 0.7rem 0; } + .md > p, .md > ul, .md > ol, .md > h2, .md > h3 { max-width: 75ch; } .md ul, .md ol { margin: 0.7rem 0 0.7rem 1.4rem; } .md li { margin: 0.3rem 0; } .md .table-wrap { overflow-x: auto; margin: 0.9rem 0; } @@ -306,8 +325,8 @@ } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - .card { transition: none; } - .card:hover { transform: none; } + .card, .card-media img { transition: none; } + .card:hover .card-media img { transform: none; } .to-top { transition: none; } } diff --git a/docs/gallery/gn-modifier-inputs/index.html b/docs/gallery/gn-modifier-inputs/index.html index 0987dc16..6a998932 100644 --- a/docs/gallery/gn-modifier-inputs/index.html +++ b/docs/gallery/gn-modifier-inputs/index.html @@ -104,6 +104,12 @@ .density-btn + .density-btn { border-left: 1px solid var(--border); } .density-btn:hover { color: var(--select); } .density-btn.active { background: var(--select); color: #1a1b1e; } + /* Segmented groups clip overflow, so the ring has to sit inside. */ + .density-btn:focus-visible, .chip:focus-visible { outline-offset: -2px; } + .sort select { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); + border-radius: var(--radius); padding: 0.3rem 0.5rem; cursor: pointer; + font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; } + .sort select:hover, .sort select:focus { color: var(--select); border-color: var(--select); outline: none; } .tags-toggle { display: none; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); border-radius: 3px; padding: 0.3rem 0.7rem; cursor: pointer; font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; } @@ -123,6 +129,14 @@ transition: color 0.15s, border-color 0.15s; } .chip:hover { color: var(--select); border-color: var(--select); } .chip.active { color: #1a1b1e; background: var(--select); border-color: var(--select); } + @media (max-width: 559px) { + .controls-inner { padding: 0.45rem 1rem 0.5rem; gap: 0.4rem; } + .controls-row { gap: 0.4rem; } + .searchwrap { flex-basis: 100%; } + .count { position: absolute; width: 1px; height: 1px; overflow: hidden; + clip: rect(0 0 0 0); white-space: nowrap; } + .density-btn { padding: 0.36rem 0.5rem; } + } @media (max-width: 719px) { html.js .tags-toggle { display: inline-block; } html.js .chips { display: none; } @@ -160,8 +174,12 @@ transition: outline-color 0.12s ease, border-color 0.12s ease; } .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } - .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media { display: block; background: var(--bg2); line-height: 0; overflow: hidden; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; + transition: transform 0.35s ease, opacity 0.3s ease; } + .card:hover .card-media img { transform: scale(1.03); } + html.js .grid .card-media img { opacity: 0; } + html.js .grid .card-media img.is-loaded { opacity: 1; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -200,6 +218,7 @@ .md h1, .md h2, .md h3 { letter-spacing: -0.01em; margin: 1.4rem 0 0.6rem; line-height: 1.25; } .md h1 { font-size: 1.35rem; } .md h2 { font-size: 1.15rem; } .md h3 { font-size: 1rem; } .md p { margin: 0.7rem 0; } + .md > p, .md > ul, .md > ol, .md > h2, .md > h3 { max-width: 75ch; } .md ul, .md ol { margin: 0.7rem 0 0.7rem 1.4rem; } .md li { margin: 0.3rem 0; } .md .table-wrap { overflow-x: auto; margin: 0.9rem 0; } @@ -306,8 +325,8 @@ } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - .card { transition: none; } - .card:hover { transform: none; } + .card, .card-media img { transition: none; } + .card:hover .card-media img { transform: none; } .to-top { transition: none; } } diff --git a/docs/gallery/gn-sdf-remesh/index.html b/docs/gallery/gn-sdf-remesh/index.html index 0b6334fd..44dbb819 100644 --- a/docs/gallery/gn-sdf-remesh/index.html +++ b/docs/gallery/gn-sdf-remesh/index.html @@ -104,6 +104,12 @@ .density-btn + .density-btn { border-left: 1px solid var(--border); } .density-btn:hover { color: var(--select); } .density-btn.active { background: var(--select); color: #1a1b1e; } + /* Segmented groups clip overflow, so the ring has to sit inside. */ + .density-btn:focus-visible, .chip:focus-visible { outline-offset: -2px; } + .sort select { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); + border-radius: var(--radius); padding: 0.3rem 0.5rem; cursor: pointer; + font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; } + .sort select:hover, .sort select:focus { color: var(--select); border-color: var(--select); outline: none; } .tags-toggle { display: none; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); border-radius: 3px; padding: 0.3rem 0.7rem; cursor: pointer; font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; } @@ -123,6 +129,14 @@ transition: color 0.15s, border-color 0.15s; } .chip:hover { color: var(--select); border-color: var(--select); } .chip.active { color: #1a1b1e; background: var(--select); border-color: var(--select); } + @media (max-width: 559px) { + .controls-inner { padding: 0.45rem 1rem 0.5rem; gap: 0.4rem; } + .controls-row { gap: 0.4rem; } + .searchwrap { flex-basis: 100%; } + .count { position: absolute; width: 1px; height: 1px; overflow: hidden; + clip: rect(0 0 0 0); white-space: nowrap; } + .density-btn { padding: 0.36rem 0.5rem; } + } @media (max-width: 719px) { html.js .tags-toggle { display: inline-block; } html.js .chips { display: none; } @@ -160,8 +174,12 @@ transition: outline-color 0.12s ease, border-color 0.12s ease; } .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } - .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media { display: block; background: var(--bg2); line-height: 0; overflow: hidden; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; + transition: transform 0.35s ease, opacity 0.3s ease; } + .card:hover .card-media img { transform: scale(1.03); } + html.js .grid .card-media img { opacity: 0; } + html.js .grid .card-media img.is-loaded { opacity: 1; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -200,6 +218,7 @@ .md h1, .md h2, .md h3 { letter-spacing: -0.01em; margin: 1.4rem 0 0.6rem; line-height: 1.25; } .md h1 { font-size: 1.35rem; } .md h2 { font-size: 1.15rem; } .md h3 { font-size: 1rem; } .md p { margin: 0.7rem 0; } + .md > p, .md > ul, .md > ol, .md > h2, .md > h3 { max-width: 75ch; } .md ul, .md ol { margin: 0.7rem 0 0.7rem 1.4rem; } .md li { margin: 0.3rem 0; } .md .table-wrap { overflow-x: auto; margin: 0.9rem 0; } @@ -306,8 +325,8 @@ } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - .card { transition: none; } - .card:hover { transform: none; } + .card, .card-media img { transition: none; } + .card:hover .card-media img { transform: none; } .to-top { transition: none; } } diff --git a/docs/gallery/gn-sim-fountain/index.html b/docs/gallery/gn-sim-fountain/index.html index 07517d9d..a1d9590c 100644 --- a/docs/gallery/gn-sim-fountain/index.html +++ b/docs/gallery/gn-sim-fountain/index.html @@ -104,6 +104,12 @@ .density-btn + .density-btn { border-left: 1px solid var(--border); } .density-btn:hover { color: var(--select); } .density-btn.active { background: var(--select); color: #1a1b1e; } + /* Segmented groups clip overflow, so the ring has to sit inside. */ + .density-btn:focus-visible, .chip:focus-visible { outline-offset: -2px; } + .sort select { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); + border-radius: var(--radius); padding: 0.3rem 0.5rem; cursor: pointer; + font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; } + .sort select:hover, .sort select:focus { color: var(--select); border-color: var(--select); outline: none; } .tags-toggle { display: none; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); border-radius: 3px; padding: 0.3rem 0.7rem; cursor: pointer; font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; } @@ -123,6 +129,14 @@ transition: color 0.15s, border-color 0.15s; } .chip:hover { color: var(--select); border-color: var(--select); } .chip.active { color: #1a1b1e; background: var(--select); border-color: var(--select); } + @media (max-width: 559px) { + .controls-inner { padding: 0.45rem 1rem 0.5rem; gap: 0.4rem; } + .controls-row { gap: 0.4rem; } + .searchwrap { flex-basis: 100%; } + .count { position: absolute; width: 1px; height: 1px; overflow: hidden; + clip: rect(0 0 0 0); white-space: nowrap; } + .density-btn { padding: 0.36rem 0.5rem; } + } @media (max-width: 719px) { html.js .tags-toggle { display: inline-block; } html.js .chips { display: none; } @@ -160,8 +174,12 @@ transition: outline-color 0.12s ease, border-color 0.12s ease; } .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } - .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media { display: block; background: var(--bg2); line-height: 0; overflow: hidden; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; + transition: transform 0.35s ease, opacity 0.3s ease; } + .card:hover .card-media img { transform: scale(1.03); } + html.js .grid .card-media img { opacity: 0; } + html.js .grid .card-media img.is-loaded { opacity: 1; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -200,6 +218,7 @@ .md h1, .md h2, .md h3 { letter-spacing: -0.01em; margin: 1.4rem 0 0.6rem; line-height: 1.25; } .md h1 { font-size: 1.35rem; } .md h2 { font-size: 1.15rem; } .md h3 { font-size: 1rem; } .md p { margin: 0.7rem 0; } + .md > p, .md > ul, .md > ol, .md > h2, .md > h3 { max-width: 75ch; } .md ul, .md ol { margin: 0.7rem 0 0.7rem 1.4rem; } .md li { margin: 0.3rem 0; } .md .table-wrap { overflow-x: auto; margin: 0.9rem 0; } @@ -306,8 +325,8 @@ } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - .card { transition: none; } - .card:hover { transform: none; } + .card, .card-media img { transition: none; } + .card:hover .card-media img { transform: none; } .to-top { transition: none; } } diff --git a/docs/gallery/gn-socket-rename/index.html b/docs/gallery/gn-socket-rename/index.html index 56a830c4..108a3db2 100644 --- a/docs/gallery/gn-socket-rename/index.html +++ b/docs/gallery/gn-socket-rename/index.html @@ -104,6 +104,12 @@ .density-btn + .density-btn { border-left: 1px solid var(--border); } .density-btn:hover { color: var(--select); } .density-btn.active { background: var(--select); color: #1a1b1e; } + /* Segmented groups clip overflow, so the ring has to sit inside. */ + .density-btn:focus-visible, .chip:focus-visible { outline-offset: -2px; } + .sort select { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); + border-radius: var(--radius); padding: 0.3rem 0.5rem; cursor: pointer; + font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; } + .sort select:hover, .sort select:focus { color: var(--select); border-color: var(--select); outline: none; } .tags-toggle { display: none; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); border-radius: 3px; padding: 0.3rem 0.7rem; cursor: pointer; font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; } @@ -123,6 +129,14 @@ transition: color 0.15s, border-color 0.15s; } .chip:hover { color: var(--select); border-color: var(--select); } .chip.active { color: #1a1b1e; background: var(--select); border-color: var(--select); } + @media (max-width: 559px) { + .controls-inner { padding: 0.45rem 1rem 0.5rem; gap: 0.4rem; } + .controls-row { gap: 0.4rem; } + .searchwrap { flex-basis: 100%; } + .count { position: absolute; width: 1px; height: 1px; overflow: hidden; + clip: rect(0 0 0 0); white-space: nowrap; } + .density-btn { padding: 0.36rem 0.5rem; } + } @media (max-width: 719px) { html.js .tags-toggle { display: inline-block; } html.js .chips { display: none; } @@ -160,8 +174,12 @@ transition: outline-color 0.12s ease, border-color 0.12s ease; } .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } - .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media { display: block; background: var(--bg2); line-height: 0; overflow: hidden; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; + transition: transform 0.35s ease, opacity 0.3s ease; } + .card:hover .card-media img { transform: scale(1.03); } + html.js .grid .card-media img { opacity: 0; } + html.js .grid .card-media img.is-loaded { opacity: 1; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -200,6 +218,7 @@ .md h1, .md h2, .md h3 { letter-spacing: -0.01em; margin: 1.4rem 0 0.6rem; line-height: 1.25; } .md h1 { font-size: 1.35rem; } .md h2 { font-size: 1.15rem; } .md h3 { font-size: 1rem; } .md p { margin: 0.7rem 0; } + .md > p, .md > ul, .md > ol, .md > h2, .md > h3 { max-width: 75ch; } .md ul, .md ol { margin: 0.7rem 0 0.7rem 1.4rem; } .md li { margin: 0.3rem 0; } .md .table-wrap { overflow-x: auto; margin: 0.9rem 0; } @@ -306,8 +325,8 @@ } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - .card { transition: none; } - .card:hover { transform: none; } + .card, .card-media img { transition: none; } + .card:hover .card-media img { transform: none; } .to-top { transition: none; } } diff --git a/docs/gallery/gn-zone-iterate/index.html b/docs/gallery/gn-zone-iterate/index.html index f8299322..00544673 100644 --- a/docs/gallery/gn-zone-iterate/index.html +++ b/docs/gallery/gn-zone-iterate/index.html @@ -104,6 +104,12 @@ .density-btn + .density-btn { border-left: 1px solid var(--border); } .density-btn:hover { color: var(--select); } .density-btn.active { background: var(--select); color: #1a1b1e; } + /* Segmented groups clip overflow, so the ring has to sit inside. */ + .density-btn:focus-visible, .chip:focus-visible { outline-offset: -2px; } + .sort select { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); + border-radius: var(--radius); padding: 0.3rem 0.5rem; cursor: pointer; + font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; } + .sort select:hover, .sort select:focus { color: var(--select); border-color: var(--select); outline: none; } .tags-toggle { display: none; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); border-radius: 3px; padding: 0.3rem 0.7rem; cursor: pointer; font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; } @@ -123,6 +129,14 @@ transition: color 0.15s, border-color 0.15s; } .chip:hover { color: var(--select); border-color: var(--select); } .chip.active { color: #1a1b1e; background: var(--select); border-color: var(--select); } + @media (max-width: 559px) { + .controls-inner { padding: 0.45rem 1rem 0.5rem; gap: 0.4rem; } + .controls-row { gap: 0.4rem; } + .searchwrap { flex-basis: 100%; } + .count { position: absolute; width: 1px; height: 1px; overflow: hidden; + clip: rect(0 0 0 0); white-space: nowrap; } + .density-btn { padding: 0.36rem 0.5rem; } + } @media (max-width: 719px) { html.js .tags-toggle { display: inline-block; } html.js .chips { display: none; } @@ -160,8 +174,12 @@ transition: outline-color 0.12s ease, border-color 0.12s ease; } .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } - .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media { display: block; background: var(--bg2); line-height: 0; overflow: hidden; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; + transition: transform 0.35s ease, opacity 0.3s ease; } + .card:hover .card-media img { transform: scale(1.03); } + html.js .grid .card-media img { opacity: 0; } + html.js .grid .card-media img.is-loaded { opacity: 1; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -200,6 +218,7 @@ .md h1, .md h2, .md h3 { letter-spacing: -0.01em; margin: 1.4rem 0 0.6rem; line-height: 1.25; } .md h1 { font-size: 1.35rem; } .md h2 { font-size: 1.15rem; } .md h3 { font-size: 1rem; } .md p { margin: 0.7rem 0; } + .md > p, .md > ul, .md > ol, .md > h2, .md > h3 { max-width: 75ch; } .md ul, .md ol { margin: 0.7rem 0 0.7rem 1.4rem; } .md li { margin: 0.3rem 0; } .md .table-wrap { overflow-x: auto; margin: 0.9rem 0; } @@ -306,8 +325,8 @@ } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - .card { transition: none; } - .card:hover { transform: none; } + .card, .card-media img { transition: none; } + .card:hover .card-media img { transform: none; } .to-top { transition: none; } } diff --git a/docs/gallery/gp-lineart-contour/index.html b/docs/gallery/gp-lineart-contour/index.html index 28ca493b..58ce1693 100644 --- a/docs/gallery/gp-lineart-contour/index.html +++ b/docs/gallery/gp-lineart-contour/index.html @@ -104,6 +104,12 @@ .density-btn + .density-btn { border-left: 1px solid var(--border); } .density-btn:hover { color: var(--select); } .density-btn.active { background: var(--select); color: #1a1b1e; } + /* Segmented groups clip overflow, so the ring has to sit inside. */ + .density-btn:focus-visible, .chip:focus-visible { outline-offset: -2px; } + .sort select { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); + border-radius: var(--radius); padding: 0.3rem 0.5rem; cursor: pointer; + font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; } + .sort select:hover, .sort select:focus { color: var(--select); border-color: var(--select); outline: none; } .tags-toggle { display: none; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); border-radius: 3px; padding: 0.3rem 0.7rem; cursor: pointer; font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; } @@ -123,6 +129,14 @@ transition: color 0.15s, border-color 0.15s; } .chip:hover { color: var(--select); border-color: var(--select); } .chip.active { color: #1a1b1e; background: var(--select); border-color: var(--select); } + @media (max-width: 559px) { + .controls-inner { padding: 0.45rem 1rem 0.5rem; gap: 0.4rem; } + .controls-row { gap: 0.4rem; } + .searchwrap { flex-basis: 100%; } + .count { position: absolute; width: 1px; height: 1px; overflow: hidden; + clip: rect(0 0 0 0); white-space: nowrap; } + .density-btn { padding: 0.36rem 0.5rem; } + } @media (max-width: 719px) { html.js .tags-toggle { display: inline-block; } html.js .chips { display: none; } @@ -160,8 +174,12 @@ transition: outline-color 0.12s ease, border-color 0.12s ease; } .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } - .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media { display: block; background: var(--bg2); line-height: 0; overflow: hidden; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; + transition: transform 0.35s ease, opacity 0.3s ease; } + .card:hover .card-media img { transform: scale(1.03); } + html.js .grid .card-media img { opacity: 0; } + html.js .grid .card-media img.is-loaded { opacity: 1; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -200,6 +218,7 @@ .md h1, .md h2, .md h3 { letter-spacing: -0.01em; margin: 1.4rem 0 0.6rem; line-height: 1.25; } .md h1 { font-size: 1.35rem; } .md h2 { font-size: 1.15rem; } .md h3 { font-size: 1rem; } .md p { margin: 0.7rem 0; } + .md > p, .md > ul, .md > ol, .md > h2, .md > h3 { max-width: 75ch; } .md ul, .md ol { margin: 0.7rem 0 0.7rem 1.4rem; } .md li { margin: 0.3rem 0; } .md .table-wrap { overflow-x: auto; margin: 0.9rem 0; } @@ -306,8 +325,8 @@ } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - .card { transition: none; } - .card:hover { transform: none; } + .card, .card-media img { transition: none; } + .card:hover .card-media img { transform: none; } .to-top { transition: none; } } diff --git a/docs/gallery/grain-sacks/index.html b/docs/gallery/grain-sacks/index.html index 7a39e755..15a16c04 100644 --- a/docs/gallery/grain-sacks/index.html +++ b/docs/gallery/grain-sacks/index.html @@ -104,6 +104,12 @@ .density-btn + .density-btn { border-left: 1px solid var(--border); } .density-btn:hover { color: var(--select); } .density-btn.active { background: var(--select); color: #1a1b1e; } + /* Segmented groups clip overflow, so the ring has to sit inside. */ + .density-btn:focus-visible, .chip:focus-visible { outline-offset: -2px; } + .sort select { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); + border-radius: var(--radius); padding: 0.3rem 0.5rem; cursor: pointer; + font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; } + .sort select:hover, .sort select:focus { color: var(--select); border-color: var(--select); outline: none; } .tags-toggle { display: none; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); border-radius: 3px; padding: 0.3rem 0.7rem; cursor: pointer; font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; } @@ -123,6 +129,14 @@ transition: color 0.15s, border-color 0.15s; } .chip:hover { color: var(--select); border-color: var(--select); } .chip.active { color: #1a1b1e; background: var(--select); border-color: var(--select); } + @media (max-width: 559px) { + .controls-inner { padding: 0.45rem 1rem 0.5rem; gap: 0.4rem; } + .controls-row { gap: 0.4rem; } + .searchwrap { flex-basis: 100%; } + .count { position: absolute; width: 1px; height: 1px; overflow: hidden; + clip: rect(0 0 0 0); white-space: nowrap; } + .density-btn { padding: 0.36rem 0.5rem; } + } @media (max-width: 719px) { html.js .tags-toggle { display: inline-block; } html.js .chips { display: none; } @@ -160,8 +174,12 @@ transition: outline-color 0.12s ease, border-color 0.12s ease; } .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } - .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media { display: block; background: var(--bg2); line-height: 0; overflow: hidden; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; + transition: transform 0.35s ease, opacity 0.3s ease; } + .card:hover .card-media img { transform: scale(1.03); } + html.js .grid .card-media img { opacity: 0; } + html.js .grid .card-media img.is-loaded { opacity: 1; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -200,6 +218,7 @@ .md h1, .md h2, .md h3 { letter-spacing: -0.01em; margin: 1.4rem 0 0.6rem; line-height: 1.25; } .md h1 { font-size: 1.35rem; } .md h2 { font-size: 1.15rem; } .md h3 { font-size: 1rem; } .md p { margin: 0.7rem 0; } + .md > p, .md > ul, .md > ol, .md > h2, .md > h3 { max-width: 75ch; } .md ul, .md ol { margin: 0.7rem 0 0.7rem 1.4rem; } .md li { margin: 0.3rem 0; } .md .table-wrap { overflow-x: auto; margin: 0.9rem 0; } @@ -306,8 +325,8 @@ } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - .card { transition: none; } - .card:hover { transform: none; } + .card, .card-media img { transition: none; } + .card:hover .card-media img { transform: none; } .to-top { transition: none; } } diff --git a/docs/gallery/grease-pencil-rosette/index.html b/docs/gallery/grease-pencil-rosette/index.html index 6518c554..1e593d5a 100644 --- a/docs/gallery/grease-pencil-rosette/index.html +++ b/docs/gallery/grease-pencil-rosette/index.html @@ -104,6 +104,12 @@ .density-btn + .density-btn { border-left: 1px solid var(--border); } .density-btn:hover { color: var(--select); } .density-btn.active { background: var(--select); color: #1a1b1e; } + /* Segmented groups clip overflow, so the ring has to sit inside. */ + .density-btn:focus-visible, .chip:focus-visible { outline-offset: -2px; } + .sort select { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); + border-radius: var(--radius); padding: 0.3rem 0.5rem; cursor: pointer; + font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; } + .sort select:hover, .sort select:focus { color: var(--select); border-color: var(--select); outline: none; } .tags-toggle { display: none; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); border-radius: 3px; padding: 0.3rem 0.7rem; cursor: pointer; font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; } @@ -123,6 +129,14 @@ transition: color 0.15s, border-color 0.15s; } .chip:hover { color: var(--select); border-color: var(--select); } .chip.active { color: #1a1b1e; background: var(--select); border-color: var(--select); } + @media (max-width: 559px) { + .controls-inner { padding: 0.45rem 1rem 0.5rem; gap: 0.4rem; } + .controls-row { gap: 0.4rem; } + .searchwrap { flex-basis: 100%; } + .count { position: absolute; width: 1px; height: 1px; overflow: hidden; + clip: rect(0 0 0 0); white-space: nowrap; } + .density-btn { padding: 0.36rem 0.5rem; } + } @media (max-width: 719px) { html.js .tags-toggle { display: inline-block; } html.js .chips { display: none; } @@ -160,8 +174,12 @@ transition: outline-color 0.12s ease, border-color 0.12s ease; } .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } - .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media { display: block; background: var(--bg2); line-height: 0; overflow: hidden; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; + transition: transform 0.35s ease, opacity 0.3s ease; } + .card:hover .card-media img { transform: scale(1.03); } + html.js .grid .card-media img { opacity: 0; } + html.js .grid .card-media img.is-loaded { opacity: 1; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -200,6 +218,7 @@ .md h1, .md h2, .md h3 { letter-spacing: -0.01em; margin: 1.4rem 0 0.6rem; line-height: 1.25; } .md h1 { font-size: 1.35rem; } .md h2 { font-size: 1.15rem; } .md h3 { font-size: 1rem; } .md p { margin: 0.7rem 0; } + .md > p, .md > ul, .md > ol, .md > h2, .md > h3 { max-width: 75ch; } .md ul, .md ol { margin: 0.7rem 0 0.7rem 1.4rem; } .md li { margin: 0.3rem 0; } .md .table-wrap { overflow-x: auto; margin: 0.9rem 0; } @@ -306,8 +325,8 @@ } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - .card { transition: none; } - .card:hover { transform: none; } + .card, .card-media img { transition: none; } + .card:hover .card-media img { transform: none; } .to-top { transition: none; } } diff --git a/docs/gallery/grindstone/index.html b/docs/gallery/grindstone/index.html index 2c498acd..4757da2f 100644 --- a/docs/gallery/grindstone/index.html +++ b/docs/gallery/grindstone/index.html @@ -104,6 +104,12 @@ .density-btn + .density-btn { border-left: 1px solid var(--border); } .density-btn:hover { color: var(--select); } .density-btn.active { background: var(--select); color: #1a1b1e; } + /* Segmented groups clip overflow, so the ring has to sit inside. */ + .density-btn:focus-visible, .chip:focus-visible { outline-offset: -2px; } + .sort select { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); + border-radius: var(--radius); padding: 0.3rem 0.5rem; cursor: pointer; + font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; } + .sort select:hover, .sort select:focus { color: var(--select); border-color: var(--select); outline: none; } .tags-toggle { display: none; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); border-radius: 3px; padding: 0.3rem 0.7rem; cursor: pointer; font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; } @@ -123,6 +129,14 @@ transition: color 0.15s, border-color 0.15s; } .chip:hover { color: var(--select); border-color: var(--select); } .chip.active { color: #1a1b1e; background: var(--select); border-color: var(--select); } + @media (max-width: 559px) { + .controls-inner { padding: 0.45rem 1rem 0.5rem; gap: 0.4rem; } + .controls-row { gap: 0.4rem; } + .searchwrap { flex-basis: 100%; } + .count { position: absolute; width: 1px; height: 1px; overflow: hidden; + clip: rect(0 0 0 0); white-space: nowrap; } + .density-btn { padding: 0.36rem 0.5rem; } + } @media (max-width: 719px) { html.js .tags-toggle { display: inline-block; } html.js .chips { display: none; } @@ -160,8 +174,12 @@ transition: outline-color 0.12s ease, border-color 0.12s ease; } .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } - .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media { display: block; background: var(--bg2); line-height: 0; overflow: hidden; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; + transition: transform 0.35s ease, opacity 0.3s ease; } + .card:hover .card-media img { transform: scale(1.03); } + html.js .grid .card-media img { opacity: 0; } + html.js .grid .card-media img.is-loaded { opacity: 1; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -200,6 +218,7 @@ .md h1, .md h2, .md h3 { letter-spacing: -0.01em; margin: 1.4rem 0 0.6rem; line-height: 1.25; } .md h1 { font-size: 1.35rem; } .md h2 { font-size: 1.15rem; } .md h3 { font-size: 1rem; } .md p { margin: 0.7rem 0; } + .md > p, .md > ul, .md > ol, .md > h2, .md > h3 { max-width: 75ch; } .md ul, .md ol { margin: 0.7rem 0 0.7rem 1.4rem; } .md li { margin: 0.3rem 0; } .md .table-wrap { overflow-x: auto; margin: 0.9rem 0; } @@ -306,8 +325,8 @@ } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - .card { transition: none; } - .card:hover { transform: none; } + .card, .card-media img { transition: none; } + .card:hover .card-media img { transform: none; } .to-top { transition: none; } } diff --git a/docs/gallery/hand-pump/index.html b/docs/gallery/hand-pump/index.html index a52f948e..168d385f 100644 --- a/docs/gallery/hand-pump/index.html +++ b/docs/gallery/hand-pump/index.html @@ -104,6 +104,12 @@ .density-btn + .density-btn { border-left: 1px solid var(--border); } .density-btn:hover { color: var(--select); } .density-btn.active { background: var(--select); color: #1a1b1e; } + /* Segmented groups clip overflow, so the ring has to sit inside. */ + .density-btn:focus-visible, .chip:focus-visible { outline-offset: -2px; } + .sort select { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); + border-radius: var(--radius); padding: 0.3rem 0.5rem; cursor: pointer; + font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; } + .sort select:hover, .sort select:focus { color: var(--select); border-color: var(--select); outline: none; } .tags-toggle { display: none; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); border-radius: 3px; padding: 0.3rem 0.7rem; cursor: pointer; font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; } @@ -123,6 +129,14 @@ transition: color 0.15s, border-color 0.15s; } .chip:hover { color: var(--select); border-color: var(--select); } .chip.active { color: #1a1b1e; background: var(--select); border-color: var(--select); } + @media (max-width: 559px) { + .controls-inner { padding: 0.45rem 1rem 0.5rem; gap: 0.4rem; } + .controls-row { gap: 0.4rem; } + .searchwrap { flex-basis: 100%; } + .count { position: absolute; width: 1px; height: 1px; overflow: hidden; + clip: rect(0 0 0 0); white-space: nowrap; } + .density-btn { padding: 0.36rem 0.5rem; } + } @media (max-width: 719px) { html.js .tags-toggle { display: inline-block; } html.js .chips { display: none; } @@ -160,8 +174,12 @@ transition: outline-color 0.12s ease, border-color 0.12s ease; } .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } - .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media { display: block; background: var(--bg2); line-height: 0; overflow: hidden; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; + transition: transform 0.35s ease, opacity 0.3s ease; } + .card:hover .card-media img { transform: scale(1.03); } + html.js .grid .card-media img { opacity: 0; } + html.js .grid .card-media img.is-loaded { opacity: 1; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -200,6 +218,7 @@ .md h1, .md h2, .md h3 { letter-spacing: -0.01em; margin: 1.4rem 0 0.6rem; line-height: 1.25; } .md h1 { font-size: 1.35rem; } .md h2 { font-size: 1.15rem; } .md h3 { font-size: 1rem; } .md p { margin: 0.7rem 0; } + .md > p, .md > ul, .md > ol, .md > h2, .md > h3 { max-width: 75ch; } .md ul, .md ol { margin: 0.7rem 0 0.7rem 1.4rem; } .md li { margin: 0.3rem 0; } .md .table-wrap { overflow-x: auto; margin: 0.9rem 0; } @@ -306,8 +325,8 @@ } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - .card { transition: none; } - .card:hover { transform: none; } + .card, .card-media img { transition: none; } + .card:hover .card-media img { transform: none; } .to-top { transition: none; } } diff --git a/docs/gallery/hay-bale/index.html b/docs/gallery/hay-bale/index.html index 5cb771d2..7a314be1 100644 --- a/docs/gallery/hay-bale/index.html +++ b/docs/gallery/hay-bale/index.html @@ -104,6 +104,12 @@ .density-btn + .density-btn { border-left: 1px solid var(--border); } .density-btn:hover { color: var(--select); } .density-btn.active { background: var(--select); color: #1a1b1e; } + /* Segmented groups clip overflow, so the ring has to sit inside. */ + .density-btn:focus-visible, .chip:focus-visible { outline-offset: -2px; } + .sort select { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); + border-radius: var(--radius); padding: 0.3rem 0.5rem; cursor: pointer; + font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; } + .sort select:hover, .sort select:focus { color: var(--select); border-color: var(--select); outline: none; } .tags-toggle { display: none; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); border-radius: 3px; padding: 0.3rem 0.7rem; cursor: pointer; font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; } @@ -123,6 +129,14 @@ transition: color 0.15s, border-color 0.15s; } .chip:hover { color: var(--select); border-color: var(--select); } .chip.active { color: #1a1b1e; background: var(--select); border-color: var(--select); } + @media (max-width: 559px) { + .controls-inner { padding: 0.45rem 1rem 0.5rem; gap: 0.4rem; } + .controls-row { gap: 0.4rem; } + .searchwrap { flex-basis: 100%; } + .count { position: absolute; width: 1px; height: 1px; overflow: hidden; + clip: rect(0 0 0 0); white-space: nowrap; } + .density-btn { padding: 0.36rem 0.5rem; } + } @media (max-width: 719px) { html.js .tags-toggle { display: inline-block; } html.js .chips { display: none; } @@ -160,8 +174,12 @@ transition: outline-color 0.12s ease, border-color 0.12s ease; } .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } - .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media { display: block; background: var(--bg2); line-height: 0; overflow: hidden; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; + transition: transform 0.35s ease, opacity 0.3s ease; } + .card:hover .card-media img { transform: scale(1.03); } + html.js .grid .card-media img { opacity: 0; } + html.js .grid .card-media img.is-loaded { opacity: 1; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -200,6 +218,7 @@ .md h1, .md h2, .md h3 { letter-spacing: -0.01em; margin: 1.4rem 0 0.6rem; line-height: 1.25; } .md h1 { font-size: 1.35rem; } .md h2 { font-size: 1.15rem; } .md h3 { font-size: 1rem; } .md p { margin: 0.7rem 0; } + .md > p, .md > ul, .md > ol, .md > h2, .md > h3 { max-width: 75ch; } .md ul, .md ol { margin: 0.7rem 0 0.7rem 1.4rem; } .md li { margin: 0.3rem 0; } .md .table-wrap { overflow-x: auto; margin: 0.9rem 0; } @@ -306,8 +325,8 @@ } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - .card { transition: none; } - .card:hover { transform: none; } + .card, .card-media img { transition: none; } + .card:hover .card-media img { transform: none; } .to-top { transition: none; } } diff --git a/docs/gallery/hitching-post/index.html b/docs/gallery/hitching-post/index.html index df09b089..0311ac4b 100644 --- a/docs/gallery/hitching-post/index.html +++ b/docs/gallery/hitching-post/index.html @@ -104,6 +104,12 @@ .density-btn + .density-btn { border-left: 1px solid var(--border); } .density-btn:hover { color: var(--select); } .density-btn.active { background: var(--select); color: #1a1b1e; } + /* Segmented groups clip overflow, so the ring has to sit inside. */ + .density-btn:focus-visible, .chip:focus-visible { outline-offset: -2px; } + .sort select { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); + border-radius: var(--radius); padding: 0.3rem 0.5rem; cursor: pointer; + font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; } + .sort select:hover, .sort select:focus { color: var(--select); border-color: var(--select); outline: none; } .tags-toggle { display: none; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); border-radius: 3px; padding: 0.3rem 0.7rem; cursor: pointer; font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; } @@ -123,6 +129,14 @@ transition: color 0.15s, border-color 0.15s; } .chip:hover { color: var(--select); border-color: var(--select); } .chip.active { color: #1a1b1e; background: var(--select); border-color: var(--select); } + @media (max-width: 559px) { + .controls-inner { padding: 0.45rem 1rem 0.5rem; gap: 0.4rem; } + .controls-row { gap: 0.4rem; } + .searchwrap { flex-basis: 100%; } + .count { position: absolute; width: 1px; height: 1px; overflow: hidden; + clip: rect(0 0 0 0); white-space: nowrap; } + .density-btn { padding: 0.36rem 0.5rem; } + } @media (max-width: 719px) { html.js .tags-toggle { display: inline-block; } html.js .chips { display: none; } @@ -160,8 +174,12 @@ transition: outline-color 0.12s ease, border-color 0.12s ease; } .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } - .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media { display: block; background: var(--bg2); line-height: 0; overflow: hidden; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; + transition: transform 0.35s ease, opacity 0.3s ease; } + .card:hover .card-media img { transform: scale(1.03); } + html.js .grid .card-media img { opacity: 0; } + html.js .grid .card-media img.is-loaded { opacity: 1; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -200,6 +218,7 @@ .md h1, .md h2, .md h3 { letter-spacing: -0.01em; margin: 1.4rem 0 0.6rem; line-height: 1.25; } .md h1 { font-size: 1.35rem; } .md h2 { font-size: 1.15rem; } .md h3 { font-size: 1rem; } .md p { margin: 0.7rem 0; } + .md > p, .md > ul, .md > ol, .md > h2, .md > h3 { max-width: 75ch; } .md ul, .md ol { margin: 0.7rem 0 0.7rem 1.4rem; } .md li { margin: 0.3rem 0; } .md .table-wrap { overflow-x: auto; margin: 0.9rem 0; } @@ -306,8 +325,8 @@ } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - .card { transition: none; } - .card:hover { transform: none; } + .card, .card-media img { transition: none; } + .card:hover .card-media img { transform: none; } .to-top { transition: none; } } diff --git a/docs/gallery/image-pixels-testcard/index.html b/docs/gallery/image-pixels-testcard/index.html index 9005d7ff..266f1c02 100644 --- a/docs/gallery/image-pixels-testcard/index.html +++ b/docs/gallery/image-pixels-testcard/index.html @@ -104,6 +104,12 @@ .density-btn + .density-btn { border-left: 1px solid var(--border); } .density-btn:hover { color: var(--select); } .density-btn.active { background: var(--select); color: #1a1b1e; } + /* Segmented groups clip overflow, so the ring has to sit inside. */ + .density-btn:focus-visible, .chip:focus-visible { outline-offset: -2px; } + .sort select { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); + border-radius: var(--radius); padding: 0.3rem 0.5rem; cursor: pointer; + font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; } + .sort select:hover, .sort select:focus { color: var(--select); border-color: var(--select); outline: none; } .tags-toggle { display: none; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); border-radius: 3px; padding: 0.3rem 0.7rem; cursor: pointer; font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; } @@ -123,6 +129,14 @@ transition: color 0.15s, border-color 0.15s; } .chip:hover { color: var(--select); border-color: var(--select); } .chip.active { color: #1a1b1e; background: var(--select); border-color: var(--select); } + @media (max-width: 559px) { + .controls-inner { padding: 0.45rem 1rem 0.5rem; gap: 0.4rem; } + .controls-row { gap: 0.4rem; } + .searchwrap { flex-basis: 100%; } + .count { position: absolute; width: 1px; height: 1px; overflow: hidden; + clip: rect(0 0 0 0); white-space: nowrap; } + .density-btn { padding: 0.36rem 0.5rem; } + } @media (max-width: 719px) { html.js .tags-toggle { display: inline-block; } html.js .chips { display: none; } @@ -160,8 +174,12 @@ transition: outline-color 0.12s ease, border-color 0.12s ease; } .card:hover { border-color: var(--select); outline-color: var(--select); } .card.hidden { display: none; } - .card-media { display: block; background: var(--bg2); line-height: 0; } - .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } + .card-media { display: block; background: var(--bg2); line-height: 0; overflow: hidden; } + .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; + transition: transform 0.35s ease, opacity 0.3s ease; } + .card:hover .card-media img { transform: scale(1.03); } + html.js .grid .card-media img { opacity: 0; } + html.js .grid .card-media img.is-loaded { opacity: 1; } .card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; } .card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; } .card-body h2 a { color: var(--text); } @@ -200,6 +218,7 @@ .md h1, .md h2, .md h3 { letter-spacing: -0.01em; margin: 1.4rem 0 0.6rem; line-height: 1.25; } .md h1 { font-size: 1.35rem; } .md h2 { font-size: 1.15rem; } .md h3 { font-size: 1rem; } .md p { margin: 0.7rem 0; } + .md > p, .md > ul, .md > ol, .md > h2, .md > h3 { max-width: 75ch; } .md ul, .md ol { margin: 0.7rem 0 0.7rem 1.4rem; } .md li { margin: 0.3rem 0; } .md .table-wrap { overflow-x: auto; margin: 0.9rem 0; } @@ -306,8 +325,8 @@ } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - .card { transition: none; } - .card:hover { transform: none; } + .card, .card-media img { transition: none; } + .card:hover .card-media img { transform: none; } .to-top { transition: none; } } diff --git a/docs/gallery/index.html b/docs/gallery/index.html index 59c0754e..554b568e 100644 --- a/docs/gallery/index.html +++ b/docs/gallery/index.html @@ -4,14 +4,14 @@ Examples and Showcase — Blender Developer Tools - + - + @@ -20,7 +20,7 @@ - + @@ -338,7 +357,7 @@

Examples and Showcase

-

Runnable, smoke-gated Blender Python examples — each executed headless on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron), so every render reflects code that actually runs. Showcase pieces share the grid and carry the 'showcase' tag; they are budget-conformance props, not examples.

+

Runnable, smoke-gated Blender Python examples — each executed headless on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron), so every render reflects code that actually runs. Showcase pieces share the grid (use the Showcase filter to see only them); they are budget-conformance props, not examples.

@@ -349,56 +368,66 @@

Examples and Showcase

52 examples, 36 showcase pieces +
+ + + +
+
-

-
-
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+ +