From ec6996676aab5382f1c1fca3252973f537ae579e Mon Sep 17 00:00:00 2001 From: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Date: Sun, 4 Oct 2026 20:22:59 -0400 Subject: [PATCH] fix(site): visible focus on selected filters, arrow keys stay in code blocks, pager polish - #397: an orange focus ring on an orange selected button was invisible (14 changed pixels). Selected kind, category and chip buttons now get a 2px dark inset ring (--on-select on --select, about 7.8:1). - #398: clicking a wide code block did not move focus, so ArrowRight navigated to the next gallery page.
 blocks are now focusable
  (tabindex=0, which also gives keyboard scrolling), and the pager keys
  stand down while the last click is inside a block that scrolls sideways.
- #399: the pager's next arrow was clipped with the truncated name at
  375px; the name now truncates in its own span. The first gallery card
  (the LCP image) gets fetchpriority=high; touch devices drop the "/" hint
  from the search placeholder; the gallery search and the landing filter
  inputs get name/id attributes.

Closes #397
Closes #398
Closes #399

Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 
---
 docs/gallery/anvil/index.html                 | 20 +++++++---
 docs/gallery/apothecary-shelf/index.html      | 20 +++++++---
 docs/gallery/archery-target/index.html        | 20 +++++++---
 docs/gallery/armature-bend/index.html         | 20 +++++++---
 .../gallery/attribute-domain-shear/index.html | 20 +++++++---
 .../bake-normal-high-to-low/index.html        | 20 +++++++---
 docs/gallery/bamboo-clump/index.html          | 20 +++++++---
 docs/gallery/basketball-hoop/index.html       | 20 +++++++---
 docs/gallery/bmesh-gear/index.html            | 20 +++++++---
 docs/gallery/book-trolley/index.html          | 20 +++++++---
 docs/gallery/bookshelf/index.html             | 20 +++++++---
 docs/gallery/boolean-exact-volume/index.html  | 20 +++++++---
 docs/gallery/boulder-cluster/index.html       | 20 +++++++---
 docs/gallery/bowling-pins/index.html          | 20 +++++++---
 docs/gallery/brazier/index.html               | 20 +++++++---
 docs/gallery/broadleaf-oak/index.html         | 20 +++++++---
 docs/gallery/butter-churn/index.html          | 20 +++++++---
 docs/gallery/cactus-garden/index.html         | 20 +++++++---
 docs/gallery/campfire/index.html              | 20 +++++++---
 docs/gallery/car-mirror-symmetry/index.html   | 20 +++++++---
 docs/gallery/cargo-loader/index.html          | 20 +++++++---
 docs/gallery/cart/index.html                  | 20 +++++++---
 docs/gallery/chopping-block/index.html        | 20 +++++++---
 docs/gallery/collision-hull-proxy/index.html  | 20 +++++++---
 docs/gallery/color-attribute-wheel/index.html | 20 +++++++---
 docs/gallery/compositor-glare/index.html      | 20 +++++++---
 docs/gallery/crate-stack/index.html           | 20 +++++++---
 docs/gallery/cricket-wicket/index.html        | 20 +++++++---
 .../cross-version-property-delete/index.html  | 20 +++++++---
 docs/gallery/curve-bevel-arc/index.html       | 20 +++++++---
 docs/gallery/custom-normals-shade/index.html  | 20 +++++++---
 docs/gallery/damped-track-aim/index.html      | 20 +++++++---
 docs/gallery/degenerate-bevel-weld/index.html | 20 +++++++---
 docs/gallery/depsgraph-export/index.html      | 20 +++++++---
 docs/gallery/desk-lamp/index.html             | 20 +++++++---
 docs/gallery/driver-wave/index.html           | 20 +++++++---
 docs/gallery/espresso-machine/index.html      | 20 +++++++---
 docs/gallery/export-preset-axis/index.html    | 20 +++++++---
 docs/gallery/fallen-log/index.html            | 20 +++++++---
 docs/gallery/farm-tractor/index.html          | 20 +++++++---
 docs/gallery/fence-kit/index.html             | 20 +++++++---
 docs/gallery/fern-mossy-rock/index.html       | 20 +++++++---
 docs/gallery/floor-fan/index.html             | 20 +++++++---
 docs/gallery/gallery.css                      | 10 ++++-
 docs/gallery/garden-gate/index.html           | 20 +++++++---
 docs/gallery/gltf-export-roundtrip/index.html | 20 +++++++---
 docs/gallery/gltf-skin-roundtrip/index.html   | 20 +++++++---
 docs/gallery/gn-instance-grid/index.html      | 20 +++++++---
 docs/gallery/gn-modifier-inputs/index.html    | 20 +++++++---
 docs/gallery/gn-sdf-remesh/index.html         | 20 +++++++---
 docs/gallery/gn-sim-fountain/index.html       | 22 +++++++----
 docs/gallery/gn-socket-rename/index.html      | 20 +++++++---
 docs/gallery/gn-zone-iterate/index.html       | 20 +++++++---
 docs/gallery/go-kart/index.html               | 20 +++++++---
 docs/gallery/gp-lineart-contour/index.html    | 20 +++++++---
 docs/gallery/grain-sacks/index.html           | 20 +++++++---
 docs/gallery/grease-pencil-rosette/index.html | 20 +++++++---
 docs/gallery/grindstone/index.html            | 20 +++++++---
 docs/gallery/hand-pump/index.html             | 20 +++++++---
 docs/gallery/hay-bale/index.html              | 20 +++++++---
 docs/gallery/hitching-post/index.html         | 20 +++++++---
 docs/gallery/hover-bike/index.html            | 20 +++++++---
 docs/gallery/image-pixels-testcard/index.html | 20 +++++++---
 docs/gallery/index.html                       | 10 +++--
 docs/gallery/iron-cauldron/index.html         | 20 +++++++---
 docs/gallery/lattice-deform/index.html        | 20 +++++++---
 docs/gallery/light-link-studio/index.html     | 20 +++++++---
 docs/gallery/lightmap-uv-channel/index.html   | 20 +++++++---
 docs/gallery/lod-decimate-chain/index.html    | 20 +++++++---
 docs/gallery/market-stall/index.html          | 20 +++++++---
 docs/gallery/mesh-hygiene-audit/index.html    | 20 +++++++---
 docs/gallery/mine-cart/index.html             | 20 +++++++---
 docs/gallery/modular-kit-snap/index.html      | 20 +++++++---
 docs/gallery/moka-pot/index.html              | 20 +++++++---
 docs/gallery/motor-scooter/index.html         | 20 +++++++---
 docs/gallery/mushroom-stump/index.html        | 20 +++++++---
 docs/gallery/office-chair/index.html          | 20 +++++++---
 docs/gallery/palm-tree/index.html             | 20 +++++++---
 docs/gallery/parent-inverse-orrery/index.html | 22 +++++++----
 docs/gallery/park-bench/index.html            | 20 +++++++---
 docs/gallery/pine-tree/index.html             | 20 +++++++---
 docs/gallery/planet-rover/index.html          | 20 +++++++---
 docs/gallery/png-exr-alpha/index.html         | 20 +++++++---
 docs/gallery/pond-edge/index.html             | 20 +++++++---
 docs/gallery/prop-origin-transform/index.html | 20 +++++++---
 docs/gallery/quad-drone/index.html            | 20 +++++++---
 docs/gallery/ray-cast-space/index.html        | 20 +++++++---
 docs/gallery/road-bicycle/index.html          | 20 +++++++---
 docs/gallery/rope-bridge/index.html           | 20 +++++++---
 docs/gallery/sawhorse-plank/index.html        | 20 +++++++---
 docs/gallery/sea-stack-arch/index.html        | 20 +++++++---
 docs/gallery/shader-node-group/index.html     | 20 +++++++---
 docs/gallery/shape-key-blend/index.html       | 20 +++++++---
 docs/gallery/shipping-crate/index.html        | 20 +++++++---
 docs/gallery/ships-wheel/index.html           | 20 +++++++---
 docs/gallery/signpost/index.html              | 20 +++++++---
 docs/gallery/skate-ramp/index.html            | 20 +++++++---
 .../sky-texture-sun-elevation/index.html      | 20 +++++++---
 docs/gallery/soccer-ball-goldberg/index.html  | 20 +++++++---
 docs/gallery/soccer-goal/index.html           | 20 +++++++---
 docs/gallery/socket-attach-points/index.html  | 20 +++++++---
 .../solidify-even-thickness/index.html        | 20 +++++++---
 docs/gallery/stand-mixer/index.html           | 20 +++++++---
 docs/gallery/stone-archway/index.html         | 20 +++++++---
 docs/gallery/stone-well/index.html            | 20 +++++++---
 docs/gallery/street-lantern/index.html        | 20 +++++++---
 docs/gallery/sundial/index.html               | 20 +++++++---
 docs/gallery/swatch-grid/index.html           | 20 +++++++---
 docs/gallery/tavern-stool/index.html          | 20 +++++++---
 docs/gallery/temp-override-join/index.html    | 20 +++++++---
 docs/gallery/terrain-scatter/index.html       | 20 +++++++---
 docs/gallery/text-version-stamp/index.html    | 20 +++++++---
 docs/gallery/toboggan/index.html              | 20 +++++++---
 docs/gallery/traffic-cones/index.html         | 20 +++++++---
 docs/gallery/treasure-chest/index.html        | 20 +++++++---
 docs/gallery/triangulate-tangents/index.html  | 20 +++++++---
 docs/gallery/turntable/index.html             | 20 +++++++---
 .../usd-export-evaluation-mode/index.html     | 20 +++++++---
 docs/gallery/uv-layer-grid/index.html         | 20 +++++++---
 docs/gallery/vertex-color-ao/index.html       | 22 +++++++----
 docs/gallery/vertex-weight-limit/index.html   | 20 +++++++---
 docs/gallery/vse-cut-list/index.html          | 20 +++++++---
 docs/gallery/vse-gamma-cross/index.html       | 20 +++++++---
 docs/gallery/wall-torch/index.html            | 20 +++++++---
 docs/gallery/watchtower/index.html            | 20 +++++++---
 docs/gallery/water-trough/index.html          | 20 +++++++---
 docs/gallery/wave-displace/index.html         | 20 +++++++---
 docs/gallery/weight-rack/index.html           | 20 +++++++---
 docs/gallery/wheelbarrow/index.html           | 20 +++++++---
 docs/gallery/wingback-armchair/index.html     | 20 +++++++---
 docs/gallery/wooden-barrel/index.html         | 20 +++++++---
 docs/gallery/wooden-bucket/index.html         | 20 +++++++---
 docs/gallery/wooden-ladder/index.html         | 20 +++++++---
 docs/gallery/wooden-yoke/index.html           | 20 +++++++---
 scripts/build_gallery.py                      | 38 ++++++++++++++-----
 scripts/site/template.html.j2                 |  4 +-
 136 files changed, 1897 insertions(+), 811 deletions(-)

diff --git a/docs/gallery/anvil/index.html b/docs/gallery/anvil/index.html
index 84bd3ff2..894e5549 100644
--- a/docs/gallery/anvil/index.html
+++ b/docs/gallery/anvil/index.html
@@ -27,7 +27,7 @@
   
   
   
-  
+  
   
 
 
@@ -48,7 +48,7 @@ 

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.

- + @@ -57,7 +57,7 @@

Anvil

category Village

tags mesh export showcase

-
blender --background --python showcase/anvil/anvil.py --
+
blender --background --python showcase/anvil/anvil.py --
@@ -86,7 +86,7 @@

Falsifiers
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#

-
blender --background --python anvil.py --
+
blender --background --python anvil.py --
 blender --background --python anvil.py -- --skip-decimate
 blender --background --python anvil.py -- --stray-vert
 blender --background --python anvil.py -- --lift-z
@@ -139,7 +139,7 @@ 

Wooden Barrel

- +

@@ -291,10 +291,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/apothecary-shelf/index.html b/docs/gallery/apothecary-shelf/index.html index ebb59f43..393c7df3 100644 --- a/docs/gallery/apothecary-shelf/index.html +++ b/docs/gallery/apothecary-shelf/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -57,7 +57,7 @@

Apothecary Shelf

category Village

tags mesh export showcase

-
blender --background --python showcase/apothecary-shelf/apothecary_shelf.py --
+
blender --background --python showcase/apothecary-shelf/apothecary_shelf.py --
@@ -90,7 +90,7 @@

Exit codes
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)
24Gallery asset-quality violation (render path; the floors' 11 is remapped here)

Run it#

-
# Budget check, no render. ~4.5 s on 5.1 and 5.2.
+
# Budget check, no render. ~4.5 s on 5.1 and 5.2.
 blender --background --python apothecary_shelf.py --
 
 # Falsifier: the middle-shelf flask runs into the shelf above. Must exit 20.
@@ -144,7 +144,7 @@ 

Wooden Barrel

- +

@@ -296,10 +296,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/archery-target/index.html b/docs/gallery/archery-target/index.html index 068dff30..e7c60fcb 100644 --- a/docs/gallery/archery-target/index.html +++ b/docs/gallery/archery-target/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Archery Target

A procedural archery range target on a turf patch — a 1.10 m compressed-straw boss coiled from rope courses that show on its back and round its edge, bound with doubled jute twine drawn tight over the crests; an 80 cm ten-zone World Archery face in aged inks with ring lines, an X ring and a printed X, held by four target pins; a timber easel leaning it back 12° on toe skids with knee braces, a ledge with a stop lip on a bolted rail and gussets, a numbered butt board, a strap hinge pinned to a rear leg in a steel ferrule and foot, and a splay chain between eye bolts; five carbon arrows in the face with cresting, nocks and three vanes, one glancing into the straw edge, one snapped with its fletched half on the grass, and a leather quiver of arrows lying among grass tufts — through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

- + @@ -57,7 +57,7 @@

Archery Target

category Sports

tags mesh export showcase

-
blender --background --python showcase/archery-target/archery_target.py --
+
blender --background --python showcase/archery-target/archery_target.py --
@@ -86,7 +86,7 @@

Falsifiers
FlagBudget violatedExit
--skip-decimateLOD1 ratio band (measured 1.0000)9
--stray-vertloose vertex count is 0 (measured 1, placed inside the envelope)15
--lift-zbounding box zmin is 0 (measured 0.05000)16
--float-footevery support bedded in the turf (rear tread and ferrule 6 mm up: bite −0.004 m)16
--offset-hingehinge pin coaxial with its knuckles (pin 2.5 mm up the face: all three 0.0025 m off)17
--lift-bossboss resting on its ledge (boss and all it carries 6 mm up the legs: seat −0.0035 m, lean unchanged)18
--high-bossregulation gold height (boss, ledge, rail and gussets 80 mm up the legs: 1.3783 m)19
--shallow-arrowevery point buried (third arrow in 30 mm: 0.0287 m)20
--short-arrowevery fletching clear of the face (second arrow a 0.40 m shaft: a vane 0.1420 m from the straw)21
--skew-vanevanes at 120° (one vane of the first arrow turned 15°: 15.000° off)22
--wide-goldring radii of the scoring table (gold/red line printed at 0.09 m)23
--short-skidsforward tip angle (skid toes cut back to 50 mm ahead of the legs: 15.20°, mass centre still 0.289 m inside)24
--loose-pinone connected assembly (one target pin 75 mm out of the face: 2 components)25

--lift-boss moves the boss along the legs, so it still leans on both. --high-boss moves the ledge with the boss, so the seat holds and only the height fails. --shallow-arrow pushes the fletching further from the face. --short-arrow keeps the point's depth. --short-skids keeps both skids bedded and the mass centre inside the supports.

Run#

-
blender --background --python archery_target.py --
+
blender --background --python archery_target.py --
 blender --background --python archery_target.py -- --skip-decimate
 blender --background --python archery_target.py -- --stray-vert
 blender --background --python archery_target.py -- --lift-z
@@ -146,7 +146,7 @@ 

Wooden Barrel

- +

@@ -298,10 +298,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/armature-bend/index.html b/docs/gallery/armature-bend/index.html index 97557e99..b8abbe8e 100644 --- a/docs/gallery/armature-bend/index.html +++ b/docs/gallery/armature-bend/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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 ribbed bellows hose through rest, half, and full curl.

- + @@ -56,7 +56,7 @@

Armature Bend

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 --
+
blender --background --python examples/armature-bend/armature_bend.py --
@@ -67,7 +67,7 @@

Armature Bend

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. The hose is lathed from an (r, z) profile — brass collars at both ends, a ribbed rubber section between — and every weight is a function of z alone, so the ribs change nothing the LBS check asserts; they make the deformation legible, fanning open on the outside of each bend and crowding on the inside. Per-bone weight bands are visualized through a BoneTint color attribute on the rib crests (a Crest attribute darkens the valleys) — the smooth color blends at the joints are the same weights the check asserts. Each hose stands on a render-only steel foot flange; the render path gates framing through examples/gallery_framing.py (exit 10) before writing the still.

Run#

-
# Cheap correctness check (no render) — the CI check:
+
# Cheap correctness check (no render) — the CI check:
 blender --background --python armature_bend.py --
 
 # Falsifier: rest pose. Must exit non-zero (tip deflection).
@@ -120,7 +120,7 @@ 

LOD Decimate Chain

- +
@@ -272,10 +272,18 @@

LOD Decimate Chain

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/attribute-domain-shear/index.html b/docs/gallery/attribute-domain-shear/index.html index 33d08475..38505abf 100644 --- a/docs/gallery/attribute-domain-shear/index.html +++ b/docs/gallery/attribute-domain-shear/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -56,7 +56,7 @@

Attribute Domain Shear

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 --
+
blender --background --python examples/attribute-domain-shear/attribute_domain_shear.py --
@@ -71,7 +71,7 @@

Attribute Domain Shear

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. The pair stands on a terracotta paver patio (a render-only 10 x 5 deck of beveled pavers with grout joints, two tones alternated by a fixed index hash, passed to the framing gate as stage like the floor), and each base has a slate placard on a brass post in front of it, the domain name inlaid in cream enamel. The render path runs the framing gate (exit 10) and the asset-quality floors on one parasol (exit 11).

Run#

-
blender --background --python attribute_domain_shear.py --
+
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
@@ -119,7 +119,7 @@

Custom Normals Shade< - +

@@ -271,10 +271,18 @@

Custom Normals Shade< // Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/bake-normal-high-to-low/index.html b/docs/gallery/bake-normal-high-to-low/index.html index 5d801378..4be3b256 100644 --- a/docs/gallery/bake-normal-high-to-low/index.html +++ b/docs/gallery/bake-normal-high-to-low/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Bake Normal High To Low

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

- + @@ -56,7 +56,7 @@

Bake Normal High To Low

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 --
+
blender --background --python examples/bake-normal-high-to-low/bake_normal_high_to_low.py --
@@ -70,7 +70,7 @@

Bake Normal High To Low

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 LOD's 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#

-
# Cheap correctness check (no render) - the CI check:
+
# Cheap correctness check (no render) - the CI check:
 blender --background --python bake_normal_high_to_low.py --
 
 # Falsifier: undisplaced high. Must exit non-zero (detail frac gate).
@@ -123,7 +123,7 @@ 

Shape Key Blend

- +
@@ -275,10 +275,18 @@

Shape Key Blend

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/bamboo-clump/index.html b/docs/gallery/bamboo-clump/index.html index 9a0f9841..b284dafe 100644 --- a/docs/gallery/bamboo-clump/index.html +++ b/docs/gallery/bamboo-clump/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Bamboo Clump

A procedural clump of seven clumping-bamboo culms rising from a 0.6 m rhizome mass on a lobed hummock of leaf-litter soil, five jointed rhizome knuckles breaking the soil at the outer culms' feet; each culm splayed out from the soil, then leaning and arching to a fine whip, ringed with 99 raised nodes at a pitch short at the foot, long through the middle and short again at the tip, its lowest three nodes still in papery sheaths and a fresh culm white-waxed below every node; 107 arching branches, one or two to a node, carrying 815 slim leaves with a droop of their own; three young shoots and litter off the hummock, through UVs, bake, LOD, collider and Unity glTF, asserting recomputed budgets rather than an API contract.

- + @@ -57,7 +57,7 @@

Bamboo Clump

category Nature

tags mesh export showcase

-
blender --background --python showcase/bamboo-clump/bamboo_clump.py --
+
blender --background --python showcase/bamboo-clump/bamboo_clump.py --
@@ -91,7 +91,7 @@

FalsifiersRun#

-
blender --background --python bamboo_clump.py --
+
blender --background --python bamboo_clump.py --
 blender --background --python bamboo_clump.py -- --skip-decimate
 blender --background --python bamboo_clump.py -- --stray-vert
 blender --background --python bamboo_clump.py -- --lift-z
@@ -150,7 +150,7 @@ 

Wooden Barrel

- +
@@ -302,10 +302,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/basketball-hoop/index.html b/docs/gallery/basketball-hoop/index.html index 5e562a2d..97000f04 100644 --- a/docs/gallery/basketball-hoop/index.html +++ b/docs/gallery/basketball-hoop/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Basketball Hoop

A procedural in-ground basketball goal on a patch of driveway court — a square gooseneck pole swept in one piece with welded bend webs, its plate sunk and bolted into a two-pour concrete pad with a sealed expansion joint, a braced board in an aluminium frame, a breakaway regulation rim, a knotted twelve-loop net hung on the rim hooks, faded court paint set into the slabs and two pebbled basketballs resting on the concrete — through UVs, bake, LOD, convex collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

- + @@ -57,7 +57,7 @@

Basketball Hoop

category Sports

tags mesh export showcase

-
blender --background --python showcase/basketball-hoop/basketball_hoop.py --
+
blender --background --python showcase/basketball-hoop/basketball_hoop.py --
@@ -88,7 +88,7 @@

Falsifiers
FlagBudget violatedExit
--skip-decimateLOD1 ratio band (measured 1.0000)9
--stray-vertloose vertex count is 0 (measured 1)15
--lift-zbounding box zmin is 0 (measured 0.05000)16
--short-bracebrace bite (braces stop 60 mm short of the rail: −0.02150 m)17
--drop-netnet seat (net lowered 25 mm: worst eye-to-loop 0.03200 m)18
--tilt-rimrim level (rim assembly tilted 3° about the flange: 3.000°)19
--float-ballball seat (both balls lifted 20 mm: sink −0.01800 m, 3 components)21
--short-boltsanchor bolts through the plate into the slab (feet stop in the plate: embed −0.0125 m)22
--float-paintcourt paint set into the slab (paint lifted 6 mm: sink −0.00460 to −0.00300 m, top 0.00700–0.00820 m)23
--loose-padone connected assembly (pad bore 6 mm clear of the pole: 2 components)20

--drop-net and --tilt-rim move whole sub-assemblies. The net drops without its hooks, and the rim tilts with its hooks and net about the flange, so only the budget each one targets can see the change. --drop-net, --float-ball and --float-paint also split the assembly, so the net-seat, ball-seat and paint checks all run before the component check (20). --short-bolts keeps each bolt in the plate, so it stays connected and only the embedment fails. --loose-pad needs a part held by one joint only: the pad (with its flap) touches nothing but the pole.

Run#

-
blender --background --python basketball_hoop.py --
+
blender --background --python basketball_hoop.py --
 blender --background --python basketball_hoop.py -- --skip-decimate
 blender --background --python basketball_hoop.py -- --stray-vert
 blender --background --python basketball_hoop.py -- --lift-z
@@ -145,7 +145,7 @@ 

Wooden Barrel

- +

@@ -297,10 +297,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/bmesh-gear/index.html b/docs/gallery/bmesh-gear/index.html index e5794fd6..532d7940 100644 --- a/docs/gallery/bmesh-gear/index.html +++ b/docs/gallery/bmesh-gear/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -56,7 +56,7 @@

BMesh Gear

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 --
+
blender --background --python examples/bmesh-gear/bmesh_gear.py --
@@ -66,7 +66,7 @@

BMesh Gear

The still mounts the checked gear in a small gear train on a painted steel backplate over a walnut plinth: a blued 8-tooth pinion above right and a 22-tooth gunmetal wheel with a spoked web below left, each on a hub boss, steel shaft, washer and hex nut. The companions are render-only meshes built from the same four-verts-per-tooth profile, pitched to the checked gear (pitch radius midway between root and tip, so all three share one circular pitch), and each is spun so a gap sits on the line of centres facing a tooth of the checked gear. A render-only Bevel modifier chamfers the edges after the check has run, so the mesh the check counts is untouched.

The checked gear keeps its machined finish: lathe-faced caps whose turning marks run concentric about the gear's own axis (object coordinates), far finer than a pixel, so they read only as a satin sheen, and rougher hobbed tooth flanks. A softbox above and left of the camera gives the brass face one warm reflection. There is no bore in the checked gear: the closed-form topology check counts exactly two rings and two caps, so the hub boss and nut sit on its face rather than faking a hole.

Run#

-
# Cheap correctness check (no render) — the CI check:
+
# Cheap correctness check (no render) — the CI check:
 blender --background --python bmesh_gear.py --
 
 # Falsifier: skip the extrude. Must exit non-zero (topology).
@@ -119,7 +119,7 @@ 

Mesh Hygiene Audit

- +
@@ -271,10 +271,18 @@

Mesh Hygiene Audit

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/book-trolley/index.html b/docs/gallery/book-trolley/index.html index 2c6bdcb0..a5db2fb3 100644 --- a/docs/gallery/book-trolley/index.html +++ b/docs/gallery/book-trolley/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -57,7 +57,7 @@

Book Trolley

category Village

tags mesh export showcase

-
blender --background --python showcase/book-trolley/book_trolley.py --
+
blender --background --python showcase/book-trolley/book_trolley.py --
@@ -89,7 +89,7 @@

Exit codes
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)
24Gallery asset-quality violation (render path; the floors' 11 is remapped here)

Run it#

-
# Budget check, no render.
+
# Budget check, no render.
 blender --background --python book_trolley.py --
 
 # Falsifier: every trough book stands off its backrest. Must exit 21.
@@ -144,7 +144,7 @@ 

Wooden Barrel

- +

@@ -296,10 +296,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/bookshelf/index.html b/docs/gallery/bookshelf/index.html index 445f585b..a8457cd9 100644 --- a/docs/gallery/bookshelf/index.html +++ b/docs/gallery/bookshelf/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -57,7 +57,7 @@

Bookshelf

category Village

tags mesh export showcase

-
blender --background --python showcase/bookshelf/bookshelf.py --
+
blender --background --python showcase/bookshelf/bookshelf.py --
@@ -89,7 +89,7 @@

Exit codes
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)
25Gallery asset-quality violation (render path; the floors' 11 is remapped here)

Run it#

-
# Budget check, no render.
+
# Budget check, no render.
 blender --background --python bookshelf.py --
 
 # Falsifier: each leaning book stops short of its neighbour. Must exit 22.
@@ -144,7 +144,7 @@ 

Wooden Barrel

- +

@@ -296,10 +296,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/boolean-exact-volume/index.html b/docs/gallery/boolean-exact-volume/index.html index bba6210d..e50f66e9 100644 --- a/docs/gallery/boolean-exact-volume/index.html +++ b/docs/gallery/boolean-exact-volume/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Boolean Exact Volume

The Boolean modifier on the EXACT solver: union, difference and intersection of a cube and a slab whose top face is coplanar with the cube's, read back through the depsgraph and measured by the divergence theorem.

- + @@ -56,7 +56,7 @@

Boolean Exact Volume

witnesses EXACT booleans are closed-form even on coplanar faces: through translated, 18°-turned operands the union, difference and intersection measure 9, 7 and 1 m³ (8.999999 / 6.999999 / 1.000000, float32) and are closed 2-manifolds with no loose geometry; the floating-point solver ('FAST' on 4.5, 'FLOAT' on 5.x) gives a 7.708 m³ union and --float-solver exits 3.

tags mesh modifiers depsgraph

-
blender --background --python examples/boolean-exact-volume/boolean_exact_volume.py --
+
blender --background --python examples/boolean-exact-volume/boolean_exact_volume.py --
@@ -68,7 +68,7 @@

Boolean Exact Volume

The still shows the three operations side by side on a walnut plinth, each named by a brass plaque in front of it. On the left, the union in teal glaze, with the slab's free end grown out of the cube. In the middle, the difference in machined bronze, with the notch cut down through the top face and the removed slab drawn as frosted amber glass. On the right, the intersection: the 1 m³ overlap cube in orange glaze, inside frosted ghosts of both operands. Thin rods trace the operands' edges (steel for the cube, amber for the coplanar slab).

Everything shown is render-only and is built after the check has run. The results on display are frozen copies of the checked meshes with a 12 mm chamfer; the measured modifier objects and the cutter objects never render. The ghosts are grown 2 mm and 4 mm so none of their faces is coplanar with a result or with each other.

Run#

-
# Cheap correctness check (no render) — the CI check:
+
# Cheap correctness check (no render) — the CI check:
 blender --background --python boolean_exact_volume.py --
 
 # Falsifier: the floating-point solver (FLOAT / FAST) on the coplanar case. Must exit 3.
@@ -121,7 +121,7 @@ 

LOD Decimate Chain

- +
@@ -273,10 +273,18 @@

LOD Decimate Chain

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/boulder-cluster/index.html b/docs/gallery/boulder-cluster/index.html index 5f00a386..c84889c0 100644 --- a/docs/gallery/boulder-cluster/index.html +++ b/docs/gallery/boulder-cluster/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Boulder Cluster

A procedural glacial boulder cluster — four boulders cut from ellipsoids by closed-form cleavage planes with weathered bevels, a speckled granite block, a banded sandstone boulder split in two with matching crack faces and bedding, a sandstone slab resting flat on a smaller granite boulder, lichen crusts and moss cushions on the tops and north faces — sunk into a soil mound with grass tufts, pebbles and wildflowers, through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

- + @@ -57,7 +57,7 @@

Boulder Cluster

category Nature

tags mesh export showcase

-
blender --background --python showcase/boulder-cluster/boulder_cluster.py --
+
blender --background --python showcase/boulder-cluster/boulder_cluster.py --
@@ -88,7 +88,7 @@

Falsifiers
FlagBudget violatedExit
--skip-decimateLOD1 ratio band (measured 1.0000)9
--stray-vertloose vertex count is 0 (measured 1, placed inside the envelope)15
--lift-zbounding box zmin is 0 (measured 0.05000)16
--perch-boulderembedding (the front boulder raised 90 mm off its bed: shallowest sector −0.0151 m, deepest vertex 0.0086 m)17
--float-slabslab seat (the slab lifted 20 mm off the front boulder: deepest vertex −0.0100 m)18
--perch-slabslab balance (the slab slid 0.36 m south across the boulder's top and re-seated: centroid 0.1170 m outside its 41-vertex footprint)19
--skew-halfsplit halves matching (the east half turned 6° about Z at its crack: faces 6.65° apart)20
--sunny-lichengrowth on up- and shade-facing surfaces (every crust turned by one rotation onto south faces: 0.6243)21
--float-coverground cover rooted (every blade, pebble and flower lifted 30 mm: 475 of 497 shells loose)22

--perch-boulder carries the slab with the boulder, so the slab still seats 10 mm deep and balances; only the embedding fails. --float-slab keeps the slab's footprint (read relative to its lowest gap), so balance still passes. --perch-slab re-seats the slab at the same 10 mm bite, so the seat passes and only the balance fails. --skew-half turns the half about its crack's own centre, so the gap (0.0779 m) and slide (0.0102 m) stay in band and the half stays bedded. --sunny-lichen moves the crusts only: a moss cushion on the block's crown is the top of the envelope, and moving it would move the box. It uses one rigid rotation for every crust, so crusts that were apart stay apart. --float-cover leaves 22 of the largest pebbles still touching, sunk deeper than 30 mm.

Run#

-
blender --background --python boulder_cluster.py --
+
blender --background --python boulder_cluster.py --
 blender --background --python boulder_cluster.py -- --skip-decimate
 blender --background --python boulder_cluster.py -- --stray-vert
 blender --background --python boulder_cluster.py -- --lift-z
@@ -144,7 +144,7 @@ 

Wooden Barrel

- +

@@ -296,10 +296,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/bowling-pins/index.html b/docs/gallery/bowling-pins/index.html index af04504d..11fd696c 100644 --- a/docs/gallery/bowling-pins/index.html +++ b/docs/gallery/bowling-pins/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Bowling Pins

A procedural pin deck end of a ten-pin bowling lane — one laminated slab of 39 maple boards, 41.5 in across, each board's top edges chamfered into a V seam and crossed by the joint to a dark pin deck, ten pin spots inlaid on a 12 in equilateral triangle 34 3/16 in from the pit edge, a chevron of targeting arrows and a row of range dots, channel-profile gutters, cappings, kickbacks with sloped noses under mitred aluminium caps and phenolic kick plates, a steel pit-edge bullnose and five sleepers; ten pins lathe-turned from the regulation profile table (15 in tall, 4.766 in belly) with two red neck stripes and a crown band of red teeth; and an 8.5 in marbled resin ball drilled by an exact Boolean with a countersunk thumb hole and two parallel finger holes — through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

- + @@ -57,7 +57,7 @@

Bowling Pins

category Sports

tags mesh export showcase

-
blender --background --python showcase/bowling-pins/bowling_pins.py --
+
blender --background --python showcase/bowling-pins/bowling_pins.py --
@@ -87,7 +87,7 @@

FalsifiersRun#

-
blender --background --python bowling_pins.py --
+
blender --background --python bowling_pins.py --
 blender --background --python bowling_pins.py -- --skip-decimate
 blender --background --python bowling_pins.py -- --stray-vert
 blender --background --python bowling_pins.py -- --lift-z
@@ -147,7 +147,7 @@ 

Wooden Barrel

- +
@@ -299,10 +299,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/brazier/index.html b/docs/gallery/brazier/index.html index 8f29ffc3..b98d3736 100644 --- a/docs/gallery/brazier/index.html +++ b/docs/gallery/brazier/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -57,7 +57,7 @@

Brazier

category Village

tags mesh export showcase

-
blender --background --python showcase/brazier/brazier.py --
+
blender --background --python showcase/brazier/brazier.py --
@@ -88,7 +88,7 @@

Exit codes
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)
22Gallery asset-quality violation (render path; the floors' 11 is remapped here)

Run it#

-
# Budget check, no render. ~2.2 s on 4.5, ~2.0 s on 5.1, ~2.4 s on 5.2.
+
# 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 --
 
 # Falsifier: the legs tuck in under the bowl. Must exit 19.
@@ -143,7 +143,7 @@ 

Wooden Barrel

- +

@@ -295,10 +295,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/broadleaf-oak/index.html b/docs/gallery/broadleaf-oak/index.html index f3a5a004..4357efd0 100644 --- a/docs/gallery/broadleaf-oak/index.html +++ b/docs/gallery/broadleaf-oak/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Broadleaf Oak

A procedural mature, open-grown English oak on a grassy mound — a massive, short trunk with broad flutes and deep-furrowed bark plates flaring through seven buttresses into surface roots bedded in the soil, a low fork into five zig-zag scaffold limbs (the low ones reaching out sideways nearly level) that divide three more times, every branch a tapered tube seated inside its parent and narrowing past each junction by its child's area (da Vinci's rule), a domed crown of 295 leaf-mass cushions — a hidden core on each carrier, faceted leaf clusters centred in it shaded as one billow by custom normals, small lobed oak leaves on the outer clusters — dead twigs, acorns (scaly cup and glossy nut) hanging on stalks and lying among leaf litter, twigs and tufts — through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

- + @@ -57,7 +57,7 @@

Broadleaf Oak

category Nature

tags mesh export showcase

-
blender --background --python showcase/broadleaf-oak/broadleaf_oak.py --
+
blender --background --python showcase/broadleaf-oak/broadleaf_oak.py --
@@ -93,7 +93,7 @@

Falsifiers
FlagBudget violatedExit
--skip-decimateLOD1 ratio band (measured 1.0000)9
--stray-vertloose vertex count is 0 (measured 1)15
--lift-zbounding box zmin is 0 (measured 0.05000)16
--float-branchesbranch seat (every tube starts 1.3 parent radii plus its own radius along its path, or 0.85 of its first segment: shallowest −1.5494)17
--pop-nutsnut seat (hanging nuts pushed 35 mm out along the cup's axis: −0.0248 m)18
--lean-crowntrunk plumb (bole bent x += 0.040 (z − 0.55)² up to 1.95 m above the soil, the crown carried over rigidly: lean 2.908°)19
--fat-twigstaper (every twig 1.8× thicker: junction ratio up to 1.7240, a child 1.0413 of its parent)20
--perch-trunktrunk sealed (foot rings set from the soil at the axis alone: 0 of 8 sectors)21
--arch-rootsroots bedded (each root's middle lifted clear of the soil: shallowest station −0.1555 m)21
--orphan-clumpcushion on its carrier (the cushion on the live twig nearest the crown's centre dropped 0.55 m off it: −0.1063 m)22
--scatter-clusterscluster in its cushion (the clusters of cushions inside crown_q 0.70 pushed 0.25 m out along their bearing: −0.1894 m)22
--shed-leavesleaf in its cluster (leaves inside crown_q 0.85 slid 0.9 m in toward the crown's centre: −0.8680 m)22
--drop-acornshanging acorns (cups and nuts lowered 60 mm off their pedicels: pedicel in cup −0.0491 m)23
--float-coverground cover (tufts, fallen leaves, fallen acorns and sticks lifted 60 mm: 100 shells not joined, tufts 0.0010–0.0047 m)24

--float-branches keeps each branch's path, so its tip and everything carried on it stay put; only the tube's first ring moves. --fat-twigs also bursts twigs out of their parents, so the taper (20) is checked before the seat (17) and names the cause. --lean-crown bends only the bole under the fork and carries the crown over rigidly, so the envelope keeps its size. The three foliage falsifiers move only interior cushions, clusters or leaves (and carry what hangs on them), so nothing at the envelope moves; --shed-leaves slides leaves inward because pushed outward the outermost ones grew the envelope 0.11 m and would have exited 8.

Run#

-
blender --background --python broadleaf_oak.py --
+
blender --background --python broadleaf_oak.py --
 blender --background --python broadleaf_oak.py -- --skip-decimate
 blender --background --python broadleaf_oak.py -- --stray-vert
 blender --background --python broadleaf_oak.py -- --lift-z
@@ -154,7 +154,7 @@ 

Wooden Barrel

- +

@@ -306,10 +306,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/butter-churn/index.html b/docs/gallery/butter-churn/index.html index 4d52bd86..f9f07686 100644 --- a/docs/gallery/butter-churn/index.html +++ b/docs/gallery/butter-churn/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -57,7 +57,7 @@

Butter Churn

category Village

tags mesh export showcase

-
blender --background --python showcase/butter-churn/butter_churn.py --
+
blender --background --python showcase/butter-churn/butter_churn.py --
@@ -93,7 +93,7 @@

Exit codes
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)
21Gallery asset-quality violation (render path; the floors' 11 is remapped here)

Run it#

-
# Budget check, no render. ~1.8 s on 4.5 and 5.2.
+
# Budget check, no render. ~1.8 s on 4.5 and 5.2.
 blender --background --python butter_churn.py --
 
 # Falsifier: the plunger jams partway up the narrowing body. Must exit 20.
@@ -147,7 +147,7 @@ 

Wooden Barrel

- +

@@ -299,10 +299,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/cactus-garden/index.html b/docs/gallery/cactus-garden/index.html index 47994bbf..a324bc88 100644 --- a/docs/gallery/cactus-garden/index.html +++ b/docs/gallery/cactus-garden/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Cactus Garden

A procedural Sonoran cactus garden on a raised sand-and-gravel disc — a 2.2 m saguaro, a column of fourteen pleated ribs with rounded crests, three arms that leave the trunk through a smooth collar, run out and turn up, a corky boot and scars, cream flowers on its crown, and a row of felt areoles down every rib crest, each with a radiating spine cluster; a fishhook barrel cactus of eighteen ribs with a red hooked central spine at every areole and a ring of yellow fruit round its crown; a prickly pear of twelve broad sage paddles grown pad on pad in four tiers, each fused into the rim of the one below, dotted with glochids and carrying magenta fruit; a blue agave rosette, cleaved sandstone rocks, a sun-bleached branch, two fallen saguaro ribs, dry bunchgrass and gravel — through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

- + @@ -57,7 +57,7 @@

Cactus Garden

category Nature

tags mesh export showcase

-
blender --background --python showcase/cactus-garden/cactus_garden.py --
+
blender --background --python showcase/cactus-garden/cactus_garden.py --
@@ -87,7 +87,7 @@

Falsifiers
FlagBudget violatedExit
--skip-decimateLOD1 ratio band (measured 1.0000)9
--stray-vertloose vertex count is 0 (measured 1, placed inside the envelope)15
--lift-zbounding box zmin is 0 (measured 0.05000)16
--short-armarm joints (every arm drawn 70 mm out along its bearing, its collar still in the trunk: 0.0465–0.0545 m)17
--shallow-padpad joints (the top right pad slid 22 mm out of its parent's rim, still inside it: seat 0.0093 m)18
--lean-saguarosaguaro plumb and mass over its foot (the whole saguaro tipped 3.5° about its foot: lean 3.702°, mass centre 0.0606 m off)19
--skew-ribsribs at equal stations (one trunk rib turned 7° off its station: spread 13.908°)20
--float-spinesspines rooted (every cluster, hook and glochid lifted 3 mm off the skin, still touching it: 0.00096 m)21
--perch-barrelbedding (the barrel raised 35 mm, its foot still in the sand: shallowest sector 0.0200 m)22
--float-covercover joined (every pebble, grass blade, branch and fallen rib lifted 50 mm: 109 of 1118 shells loose)23

Each falsifier moves only what its budget measures, and none is sized past the point where a second budget would see it. --short-arm moves each arm rigidly, so its spines stay rooted, and its areoles are chosen by arc length along the arm, not by distance from the trunk, so the count does not change. --shallow-pad slides a pad that carries no pad, so no other seat moves. The lifts in --float-spines and --perch-barrel leave every part still touching its host, so the cover stays joined. --lean-saguaro passes the rib budget because that budget measures in the trunk's own frame.

Run#

-
blender --background --python cactus_garden.py --
+
blender --background --python cactus_garden.py --
 blender --background --python cactus_garden.py -- --skip-decimate
 blender --background --python cactus_garden.py -- --stray-vert
 blender --background --python cactus_garden.py -- --lift-z
@@ -144,7 +144,7 @@ 

Wooden Barrel

- +

@@ -296,10 +296,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/campfire/index.html b/docs/gallery/campfire/index.html index 40bc5ac3..27ddaf6c 100644 --- a/docs/gallery/campfire/index.html +++ b/docs/gallery/campfire/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Campfire

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

- + @@ -57,7 +57,7 @@

Campfire

category Village

tags mesh export showcase

-
blender --background --python showcase/campfire/campfire.py --
+
blender --background --python showcase/campfire/campfire.py --
@@ -85,7 +85,7 @@

Falsifiers
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#

-
blender --background --python campfire.py --
+
blender --background --python campfire.py --
 blender --background --python campfire.py -- --skip-decimate
 blender --background --python campfire.py -- --stray-vert
 blender --background --python campfire.py -- --lift-z
@@ -138,7 +138,7 @@ 

Wooden Barrel

- +

@@ -290,10 +290,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/car-mirror-symmetry/index.html b/docs/gallery/car-mirror-symmetry/index.html index 659d696f..23847aeb 100644 --- a/docs/gallery/car-mirror-symmetry/index.html +++ b/docs/gallery/car-mirror-symmetry/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -56,7 +56,7 @@

Car Mirror Symmetry

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 --
+
blender --background --python examples/car-mirror-symmetry/car_mirror_symmetry.py --
@@ -69,7 +69,7 @@

Car Mirror Symmetry

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 13-point loft reads faceted at gallery size, so the --output path — after check() has asserted every closed-form count on the Mirror-only stack — stacks a Subdivision Surface modifier (level 2) after MirrorHalf. Order matters: subdividing the half first would pull the centerline off the plane; subdividing the welded whole keeps it exact. Panel breaks stay crisp through a crease_edge attribute: every material border (glass, pillars, arch cladding, the chrome window-sill strip) creases fully and the fascia cap outlines half-crease, so the nose keeps its shape. The body is smooth-shaded with hard shading only at material borders; the separate parts keep a 40° dihedral split, and their boundary edges on the plane take the angle to their own mirror image, acos(1 − 2nₓ²), so centerlines shade smooth. The paint is candy red over a faint metallic base under a softened clear coat; the rim light is large and dim and set steep — a small bright rim mirrors in the roof as a clipped white slab, and a shallow one prints its own terminator line across the floor. Tires carry a 72-groove tread bump masked to the outward-facing band, and the rims are a half-metallic alloy (a full metal mirrors the black stage and the spokes vanish). 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#

-
# Cheap correctness check (no render) — the CI check:
+
# Cheap correctness check (no render) — the CI check:
 blender --background --python car_mirror_symmetry.py --
 
 # Falsifier: Mirror X off. Must exit non-zero (evaluated verts stay at n).
@@ -122,7 +122,7 @@ 

Boolean Exact Volume< - +

@@ -274,10 +274,18 @@

Boolean Exact Volume< // Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/cargo-loader/index.html b/docs/gallery/cargo-loader/index.html index 826e949c..eea40f53 100644 --- a/docs/gallery/cargo-loader/index.html +++ b/docs/gallery/cargo-loader/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Cargo Loader

A procedural sci-fi industrial cargo loader, a bipedal powered-lift exoframe standing in a crouch and holding a corrugated cargo crate off the ground on two forged L-tines — broad flat feet with rubber soles, grip cleats and toe bumpers; hydraulic legs of lofted box-section shins and thighs pinned at the ankle, knee and hip; arms hung from shoulder yokes, each joint a clevis of two lug bushings straddling an eye bushing with a chrome pin through all three; eight rams (hip and knee, shoulder and elbow), each a collared steel cylinder with a chromed rod, a pinned clevis bracket at each end and a hose from its port; fork carriages with hazard-striped faces; an open roll-cage cockpit with a bucket seat, four-point harness, joystick consoles, amber beacon and work lights; a power pack with a slatted grille, cooling fins, hazard bands, exhaust stacks and cable looms — through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

- + @@ -57,7 +57,7 @@

Cargo Loader

category Vehicles

tags mesh export showcase

-
blender --background --python showcase/cargo-loader/cargo_loader.py --
+
blender --background --python showcase/cargo-loader/cargo_loader.py --
@@ -88,7 +88,7 @@

Falsifiers
FlagBudget violatedExit
--skip-decimateLOD1 ratio band (measured 1.0000)9
--stray-vertloose vertex count is 0 (measured 1, placed inside the envelope)15
--lift-zbounding box zmin is 0 (measured 0.05000)16
--float-footboth soles on the ground (left sole 5 mm up into its foot: zmin 0.00500, the right 0)16
--offset-pinclevis pins coaxial with their bushings (left knee pin 2.5 mm forward: 0.002500 m off)17
--lift-cratecrate resting on both tines (crate 6 mm up: bites −0.0040 m on both)18
--tilt-solesoles level (left sole turned 0.6° about its toe edge: spread 0.012545 m, tilt 0.600°)19
--skew-rodrams coaxial (left hip ram's rod turned 1° about its eye: tilt 1.0000°, 0.002184 m off)20
--bottom-ramram stroke (left knee ram's barrel run up to 40 mm short of the rod eye: exposed 0.0440 m)21
--overloadstance (the crate 14 times as dense: 6401.9 kg of cargo, margin −0.3853 m)22
--loose-lightone connected assembly (left work light 60 mm out along its aim, off its bracket: 2 components)23

--float-foot lifts only the left sole, into its foot, so the foot stays on its sole and the right sole grounds the box. --tilt-sole turns the sole about its own front bottom edge, so the toe stays on the ground and only the heel lifts; its contact shrinks to that edge, which drops the stance margin to 0.2097, but the level budget is checked first. --offset-pin moves one pin across its three bushings, inside them, and leaves them where they are. --lift-crate moves the whole crate, so every station along both tines sees the same gap; the crate also loses contact with the assembly, which is why the contact budget is checked before connectivity. --skew-rod turns the rod about its own eye, so the eye end stays pinned and only the barrel end wanders. --bottom-ram lengthens one barrel, not the ram: both pins, the rod and the envelope stay put. --overload changes a density, not the mesh, which is the point: the machine that stands with 457 kg in its forks tips with 6.4 t. --loose-light moves the lamp head 60 mm, well clear of the bracket's end, and the head is held by that one bracket only.

Run#

-
blender --background --python cargo_loader.py --
+
blender --background --python cargo_loader.py --
 blender --background --python cargo_loader.py -- --skip-decimate
 blender --background --python cargo_loader.py -- --stray-vert
 blender --background --python cargo_loader.py -- --lift-z
@@ -146,7 +146,7 @@ 

Wooden Barrel

- +

@@ -298,10 +298,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/cart/index.html b/docs/gallery/cart/index.html index c8f3a6bb..4069e11a 100644 --- a/docs/gallery/cart/index.html +++ b/docs/gallery/cart/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Cart

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

- + @@ -57,7 +57,7 @@

Cart

category Village

tags mesh export showcase

-
blender --background --python showcase/cart/cart.py --
+
blender --background --python showcase/cart/cart.py --
@@ -79,7 +79,7 @@

Falsifiers
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#

-
blender --background --python cart.py --
+
blender --background --python cart.py --
 blender --background --python cart.py -- --skip-decimate
 blender --background --python cart.py -- --flush-tyre
 blender --background --python cart.py -- --lift-z
@@ -136,7 +136,7 @@ 

Wooden Barrel

- +

@@ -288,10 +288,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/chopping-block/index.html b/docs/gallery/chopping-block/index.html index 9ca68af2..ebb6b702 100644 --- a/docs/gallery/chopping-block/index.html +++ b/docs/gallery/chopping-block/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -57,7 +57,7 @@

Chopping Block

category Village

tags mesh export showcase

-
blender --background --python showcase/chopping-block/chopping_block.py --
+
blender --background --python showcase/chopping-block/chopping_block.py --
@@ -89,7 +89,7 @@

Falsifiers
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#

-
blender --background --python chopping_block.py --
+
blender --background --python chopping_block.py --
 blender --background --python chopping_block.py -- --skip-decimate
 blender --background --python chopping_block.py -- --stray-vert
 blender --background --python chopping_block.py -- --lift-z
@@ -142,7 +142,7 @@ 

Wooden Barrel

- +

@@ -294,10 +294,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/collision-hull-proxy/index.html b/docs/gallery/collision-hull-proxy/index.html index 81921390..e6bee0bb 100644 --- a/docs/gallery/collision-hull-proxy/index.html +++ b/docs/gallery/collision-hull-proxy/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -56,7 +56,7 @@

Collision Hull Proxy

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 --
+
blender --background --python examples/collision-hull-proxy/collision_hull_proxy.py --
@@ -70,7 +70,7 @@

Collision Hull Proxy

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#

-
# Cheap correctness check (no render) — the CI check:
+
# Cheap correctness check (no render) — the CI check:
 blender --background --python collision_hull_proxy.py --
 
 # Falsifier: shrink hull vertices. Must exit non-zero (containment).
@@ -123,7 +123,7 @@ 

Mesh Hygiene Audit

- +
@@ -275,10 +275,18 @@

Mesh Hygiene Audit

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/color-attribute-wheel/index.html b/docs/gallery/color-attribute-wheel/index.html index af4007ff..619a5efb 100644 --- a/docs/gallery/color-attribute-wheel/index.html +++ b/docs/gallery/color-attribute-wheel/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -56,7 +56,7 @@

Color Attribute Wheel

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 --
+
blender --background --python examples/color-attribute-wheel/color_attribute_wheel.py --
@@ -66,7 +66,7 @@

Color Attribute Wheel

Staging#

The checked disc is flat and zero-thickness. The render path — only after check(), so the verified mesh is untouched until then — domes it into a shallow glazed plate (one foreach_get/foreach_set over vertex positions; the CORNER colours ride the same loops, so the colours on the plate are the colours the check verified), gives it a Solidify body, rings it with a turned brass bezel over a dark backing disc, and stands it at a 14° lean on a walnut studio easel. The glaze is a satin base under a thin clear coat; the dome curves the coat's reflection into a soft sheen instead of the hard horizon line a flat glaze draws. The path gates framing through examples/gallery_framing.py (exit 10): fill 0.739 y, every margin ≥ 0.050.

Run#

-
# Cheap correctness check (no render) — the CI check:
+
# Cheap correctness check (no render) — the CI check:
 blender --background --python color_attribute_wheel.py --
 
 # Falsifier: POINT-domain attribute. Must exit non-zero.
@@ -119,7 +119,7 @@ 

Attribute Domain Shear< - +

@@ -271,10 +271,18 @@

Attribute Domain Shear< // Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/compositor-glare/index.html b/docs/gallery/compositor-glare/index.html index 3b5a5a39..1b54b253 100644 --- a/docs/gallery/compositor-glare/index.html +++ b/docs/gallery/compositor-glare/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -56,7 +56,7 @@

Compositor Glare

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 --
+
blender --background --python examples/compositor-glare/compositor_glare.py --
@@ -65,7 +65,7 @@

Compositor Glare

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#

-
# Cheap correctness check (two tiny renders) — the CI check:
+
# Cheap correctness check (two tiny renders) — the CI check:
 blender --background --python compositor_glare.py --
 
 # Falsifier: Threshold=10.0. Must exit non-zero (Threshold==1.0).
@@ -118,7 +118,7 @@ 

Image Pixels Testcard - +

@@ -270,10 +270,18 @@

Image Pixels Testcard scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/crate-stack/index.html b/docs/gallery/crate-stack/index.html index 15454ece..604300b6 100644 --- a/docs/gallery/crate-stack/index.html +++ b/docs/gallery/crate-stack/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -57,7 +57,7 @@

Crate Stack

category Village

tags mesh export showcase

-
blender --background --python showcase/crate-stack/crate_stack.py --
+
blender --background --python showcase/crate-stack/crate_stack.py --
@@ -96,7 +96,7 @@

Exit codes
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)
22Gallery asset-quality violation (render path; the floors' 11 is remapped here)

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#

-
# Budget check, no render. ~1.4-1.7 s on 4.5 / 5.1 / 5.2.
+
# Budget check, no render. ~1.4-1.7 s on 4.5 / 5.1 / 5.2.
 blender --background --python crate_stack.py --
 
 # Falsifier: the three crates become copies. Must exit 20.
@@ -150,7 +150,7 @@ 

Wooden Barrel

- +

@@ -302,10 +302,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/cricket-wicket/index.html b/docs/gallery/cricket-wicket/index.html index c3c1099d..bb142d29 100644 --- a/docs/gallery/cricket-wicket/index.html +++ b/docs/gallery/cricket-wicket/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Cricket Wicket

A procedural cricket wicket — three lacquered ash stumps with grooved domed crowns and red ball scuffs, two turned and collared bails lying in the grooves, a turf strip worn bare along a chalked crease and a red leather ball with a raised seam — carried through UVs, bake, LOD, compound collider, and Unity glTF, asserting Laws-of-Cricket sizes and bail seats recomputed from the mesh rather than an API contract.

- + @@ -57,7 +57,7 @@

Cricket Wicket

category Sports

tags mesh export showcase

-
blender --background --python showcase/cricket-wicket/cricket_wicket.py --
+
blender --background --python showcase/cricket-wicket/cricket_wicket.py --
@@ -90,7 +90,7 @@

Exit codes
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, or asset-quality gate (render path only)
12Normal bake failed or produced no image data
13glTF export missing or empty
14--output produced no file
15Mesh hygiene (--stray-vert)
16Grounded AABB zmin, or a stump floating (--lift-z, --float-stump)
17Bails not located, spigot tip off its neck, barrel against a stump, or bails touching (--shift-bails)
18Spigot seat, chalk line, ball rest or seam (--lift-bails, --sink-chalk, --float-ball, --sink-seam)
19Stump diameter, height, width or gaps, mirrored stumps, bail projection, ball diameter (--thin-stumps, --skew-stump, --fat-bails, --small-ball)

Run it#

-
# Budget check, no render.
+
# Budget check, no render.
 blender --background --python cricket_wicket.py --
 
 # Falsifier: the bails float over the grooves. Must exit 18.
@@ -145,7 +145,7 @@ 

Wooden Barrel

- +

@@ -297,10 +297,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/cross-version-property-delete/index.html b/docs/gallery/cross-version-property-delete/index.html index 160f27ad..cf612a7a 100644 --- a/docs/gallery/cross-version-property-delete/index.html +++ b/docs/gallery/cross-version-property-delete/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -56,7 +56,7 @@

Cross Version Property Delete

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 --
+
blender --background --python examples/cross-version-property-delete/cross_version_property_delete.py --
@@ -72,7 +72,7 @@

Cross Version Property Delete

Staging#

Both lamps hang in U-yokes from one T-stand. The render path reads each housing's keys() after the check has run, so the still is driven by the checked IDs, not staged from the expected outcome. A lamp that still carries the property gets an emissive lens and a real SPOT light along its beam axis, which pools warm light on the placard below it. A lamp without the property keeps a dark lens and casts nothing. Each placard is set in the bundled DejaVu Sans Mono and shows the line of code that produced its lamp's state. Only render-side geometry (stand, yokes, lenses, placards) is added in the render path; the check reads ID properties, never geometry.

Run#

-
blender --background --python cross_version_property_delete.py --
+
blender --background --python cross_version_property_delete.py --
 blender --background --python cross_version_property_delete.py -- --output lamps.png

--skip-delete and --unset-instead are falsification switches: both leave the Clear lamp 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.

@@ -102,7 +102,7 @@

Temp Override Join

- +
@@ -254,10 +254,18 @@

Temp Override Join

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/curve-bevel-arc/index.html b/docs/gallery/curve-bevel-arc/index.html index 7f86977f..c2531782 100644 --- a/docs/gallery/curve-bevel-arc/index.html +++ b/docs/gallery/curve-bevel-arc/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -56,7 +56,7 @@

Curve Bevel Arc

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 --
+
blender --background --python examples/curve-bevel-arc/curve_bevel_arc.py --
@@ -65,7 +65,7 @@

Curve Bevel Arc

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#

-
# Cheap correctness check (no render) — the CI check:
+
# Cheap correctness check (no render) — the CI check:
 blender --background --python curve_bevel_arc.py --
 
 # Falsifier: uncapped tube. Must exit non-zero (use_fill_caps).
@@ -100,7 +100,7 @@ 

Text Version Stamp

- +
@@ -252,10 +252,18 @@

Text Version Stamp

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/custom-normals-shade/index.html b/docs/gallery/custom-normals-shade/index.html index ef0ba9c7..7934efa8 100644 --- a/docs/gallery/custom-normals-shade/index.html +++ b/docs/gallery/custom-normals-shade/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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).

- + @@ -56,7 +56,7 @@

Custom Normals Shade

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 --
+
blender --background --python examples/custom-normals-shade/custom_normals_shade.py --
@@ -70,7 +70,7 @@

Custom Normals Shade

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 amber drafting lines (low emission, no bloom halo), so the viewer sees exactly which edges the data marked hard. A low strip light makes the highlight shapes diverge between the three.

Run#

-
# Cheap correctness check (no render) — the CI check:
+
# Cheap correctness check (no render) — the CI check:
 blender --background --python custom_normals_shade.py --
 
 # Falsifier: mark sharp at 20° while auditing 30°. Must exit non-zero.
@@ -123,7 +123,7 @@ 

Vertex Color AO

- +
@@ -275,10 +275,18 @@

Vertex Color AO

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/damped-track-aim/index.html b/docs/gallery/damped-track-aim/index.html index b8d42161..35e534b3 100644 --- a/docs/gallery/damped-track-aim/index.html +++ b/docs/gallery/damped-track-aim/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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: twelve spotlight heads on stands, all swung onto one glowing orb.

- + @@ -56,7 +56,7 @@

Damped Track Aim

witnesses Every lamp head has one unmuted DAMPED_TRACK on the orb; evaluated local +Z (the lens) aligns toward the orb (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 --
+
blender --background --python examples/damped-track-aim/damped_track_aim.py --
@@ -66,7 +66,7 @@

Damped Track Aim

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

The stands, ball pivots and the orb's pedestal are render-only staging, built after the check. The ring opens toward the camera so no head hides the orb. The render path gates framing through examples/gallery_framing.py (exit 10) before writing the still.

Run#

-
# Cheap correctness check (no render) — the CI check:
+
# Cheap correctness check (no render) — the CI check:
 blender --background --python damped_track_aim.py --
 
 # Falsifier: mute every constraint. Must exit non-zero.
@@ -110,7 +110,7 @@ 

Driver Wave

- +
@@ -262,10 +262,18 @@

Driver Wave

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/degenerate-bevel-weld/index.html b/docs/gallery/degenerate-bevel-weld/index.html index ea410db5..809231cc 100644 --- a/docs/gallery/degenerate-bevel-weld/index.html +++ b/docs/gallery/degenerate-bevel-weld/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -56,7 +56,7 @@

Degenerate Bevel Weld

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 --
+
blender --background --python examples/degenerate-bevel-weld/degenerate_bevel_weld.py --
@@ -69,7 +69,7 @@

Degenerate Bevel Weld

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, steel draw latches with wire bails, rubber corner guards, 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 as a thin red-orange ink line (low emission, no neon halo) from live mesh data (every edge of every face thinner than 1e-6), with a small amber pin 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#

-
blender --background --python degenerate_bevel_weld.py --
+
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
@@ -117,7 +117,7 @@

BMesh Gear

- +
@@ -269,10 +269,18 @@

BMesh Gear

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/depsgraph-export/index.html b/docs/gallery/depsgraph-export/index.html index 93fa2b7c..8a7b857b 100644 --- a/docs/gallery/depsgraph-export/index.html +++ b/docs/gallery/depsgraph-export/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Depsgraph Export

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

- + @@ -56,7 +56,7 @@

Depsgraph Export

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 --
+
blender --background --python examples/depsgraph-export/depsgraph_export.py --
@@ -68,7 +68,7 @@

The renderRun#

-
# Cheap correctness check (writes an OBJ to a temp path, asserts the counts) — the CI check:
+
# Cheap correctness check (writes an OBJ to a temp path, asserts the counts) — the CI check:
 blender --background --python depsgraph_export.py --
 
 # Falsifier: apply_modifiers=False. Must exit non-zero (export ≠ evaluated).
@@ -126,7 +126,7 @@ 

Text Version Stamp

- +
@@ -278,10 +278,18 @@

Text Version Stamp

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/desk-lamp/index.html b/docs/gallery/desk-lamp/index.html index d8d399e3..896c19b5 100644 --- a/docs/gallery/desk-lamp/index.html +++ b/docs/gallery/desk-lamp/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Desk Lamp

A procedural balanced-arm desk lamp — stepped cast base, swivel turret, two parallelogram arm sections pinned through knuckle bosses with knurled tension knobs, three extension springs hung on cross bars, a domed enamel shade with a rolled rim, reflector and glowing lamp, a draped flex and a cable to a plug — through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

- + @@ -57,7 +57,7 @@

Desk Lamp

category Household

tags mesh export showcase

-
blender --background --python showcase/desk-lamp/desk_lamp.py --
+
blender --background --python showcase/desk-lamp/desk_lamp.py --
@@ -88,7 +88,7 @@

Falsifiers
FlagBudget violatedExit
--skip-decimateLOD1 ratio band (measured 1.0000)9
--stray-vertloose vertex count is 0 (measured 1, placed inside the envelope)15
--lift-zbounding box zmin is 0 (measured 0.05000)16
--offset-pinjoint pins coaxial (elbow pin moved 2.5 mm off its eyes: offset 0.00250 m)17
--unhook-springspring seat (one loop 10 mm short of its bar: 0.01000 m)18
--hollow-basestance (base pressed from 1.5 mm steel: 3.22 kg, centre at x 0.184, margin −0.094 m)19
--unscrew-bulbone connected assembly (lamp backed 5 mm out of its socket: 2 components)20

--offset-pin still leaves the pin inside every eye it passes through, and the arm lengths it moves (0.3208, 0.3575 m) stay inside their tolerance, so only the coaxial budget sees it. --unhook-spring leaves the spring hanging from its lower bar, so the assembly stays connected. --hollow-base keeps the outer loft, so the envelope and footprint are unchanged and only the mass moves.

Run#

-
blender --background --python desk_lamp.py --
+
blender --background --python desk_lamp.py --
 blender --background --python desk_lamp.py -- --skip-decimate
 blender --background --python desk_lamp.py -- --stray-vert
 blender --background --python desk_lamp.py -- --lift-z
@@ -142,7 +142,7 @@ 

Wooden Barrel

- +

@@ -294,10 +294,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/driver-wave/index.html b/docs/gallery/driver-wave/index.html index 3934d88d..f10e0711 100644 --- a/docs/gallery/driver-wave/index.html +++ b/docs/gallery/driver-wave/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Driver Wave

A driver_namespace function driving sixteen organ-pipe heights through SCRIPTED drivers — the sine skyline of the pipe tops is entirely driver-evaluated.

- + @@ -56,7 +56,7 @@

Driver Wave

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 --
+
blender --background --python examples/driver-wave/driver_wave.py --
@@ -67,7 +67,7 @@

Driver Wave

Staging#

The sixteen driven objects are the speaking pipes of a small organ facade. They share one open-tube body mesh of unit height (z 0..1), so the driven Z scale is each pipe's speaking length and the pipe tops trace wave_scale directly; the rim annulus is horizontal and stays crisp under any Z scale. Everything else — the walnut windchest and case back, the side towers with brass finials, a brass foot cone and a mouth under each pipe — is render-only staging built around the driven bodies. The case back sits behind the pipes so their tops read as a wave against wood rather than fading into the stage. The render path gates framing through examples/gallery_framing.py (exit 10) before writing the still.

Run#

-
# Cheap correctness check (no render) — the CI check:
+
# Cheap correctness check (no render) — the CI check:
 blender --background --python driver_wave.py --
 
 # Falsifier: constant 1.0 expression. Must exit non-zero.
@@ -111,7 +111,7 @@ 

Damped Track Aim

- +
@@ -263,10 +263,18 @@

Damped Track Aim

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/espresso-machine/index.html b/docs/gallery/espresso-machine/index.html index e37156d8..4e0c2e17 100644 --- a/docs/gallery/espresso-machine/index.html +++ b/docs/gallery/espresso-machine/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Espresso Machine

A procedural prosumer E61 semi-automatic espresso machine on a honed limestone countertop section — a black chassis behind a brushed stainless front panel, two side panels folded round the front and back edges, each one sheet with louvred vent slots cut through it, a warmer tray with a rail bent from one bar and four upturned cups, on adjustable feet; the chromed E61 group with its neck, thermosiphon pipe, gasket, mushroom cap and a lever pinned through two bosses; a spouted portafilter with a walnut handle locked against the gasket; steam and hot-water valves with fluted bakelite knobs and wands on ball joints; twin gauges with printed dials in chrome bezels; a toggle and pilot lamp; a drip tray whose slotted grate carries a shot cup of espresso; and a knock box, tamper and milk pitcher — with a studio carried in the metals' material so stainless reads as metal on a dark stage, through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

- + @@ -57,7 +57,7 @@

Espresso Machine

category Household

tags mesh export showcase

-
blender --background --python showcase/espresso-machine/espresso_machine.py --
+
blender --background --python showcase/espresso-machine/espresso_machine.py --
@@ -89,7 +89,7 @@

Falsifiers
FlagBudget violatedExit
--skip-decimateLOD1 ratio band (measured 1.0000)9
--stray-vertloose vertex count is 0 (measured 1, placed inside the envelope)15
--lift-zbounding box zmin is 0 (measured 0.05000)16
--float-footfeet on the counter (one pad 3 mm up: seat −0.00230 m)16
--offset-pinlever pin coaxial through its bosses and hub (pin 1.5 mm up: 0.001500 m)17
--sink-cupcups seated on their host (the cappuccino cup 3 mm into the tray: 0.00356 m)18
--narrow-bodysize (side panels drawn in: width 0.28300 m)19
--offset-pfportafilter coaxial with the group (slid 1.2 mm: 0.001200 m)20
--drop-pfportafilter seated to the gasket (2 mm low: seat −0.00160 m)21
--proud-gaugegauge faces behind their bezels (glass, dial and needle 4.5 mm out: recess −0.0011 m)22
--shift-traydrip tray inside the body's footprint (30 mm right: margin −0.0050 m)23
--bunch-feetstance (feet under the back: margin −0.1613 m)24
--loose-knobone connected assembly (steam knob 5 mm off its valve: 2 components)25

--float-foot leaves the pad's stem still inside it, so the assembly holds. --drop-pf and --offset-pf leave the portafilter inside the group's skirt, so it stays joined and only the seat or the axis moves. --sink-cup sinks, rather than floats, the cup, so the cup stays joined to the tray. --proud-gauge leaves the glass and dial inside the bezel's bore. --shift-tray keeps the tray tucked into the chassis, and the shot cup on the grate. --bunch-feet keeps every pad on the counter and under the chassis, so only the mass centre leaves the support polygon.

Run#

-
blender --background --python espresso_machine.py --
+
blender --background --python espresso_machine.py --
 blender --background --python espresso_machine.py -- --skip-decimate
 blender --background --python espresso_machine.py -- --stray-vert
 blender --background --python espresso_machine.py -- --lift-z
@@ -149,7 +149,7 @@ 

Wooden Barrel

- +

@@ -301,10 +301,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/export-preset-axis/index.html b/docs/gallery/export-preset-axis/index.html index 02c7d27f..978baa0e 100644 --- a/docs/gallery/export-preset-axis/index.html +++ b/docs/gallery/export-preset-axis/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Export Preset Axis

A radio mast exported once with the engine glTF preset (export_yup=True, shared by Unity, Godot and Unreal) and once naively Z-up, then re-imported: the Z-up file lies on its back

- + @@ -56,7 +56,7 @@

Export Preset Axis

witnesses The engine preset (export_yup=True) round-trips standing (x,y,z); a naive export_yup=False file reimports lying along -Y as (x,-z,y), as it would in any glTF engine; --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 --
+
blender --background --python examples/export-preset-axis/export_preset_axis.py --
@@ -68,7 +68,7 @@

Export Preset Axis

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 preset copy standing on the left, the Z-up 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 preset gizmo comes out as the identity, blue Z up. The Z-up 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 Z-up file stores Z-up coordinates, and the importer reads them as Y-up, as every glTF consumer does.

Run#

-
# Cheap correctness check (no render) - the CI check:
+
# Cheap correctness check (no render) - the CI check:
 blender --background --python export_preset_axis.py --
 
 # Falsifier: both exports Y-up. Must exit non-zero.
@@ -121,7 +121,7 @@ 

glTF Skin Roundtrip - +

@@ -273,10 +273,18 @@

glTF Skin Roundtrip scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/fallen-log/index.html b/docs/gallery/fallen-log/index.html index 729a0d78..03793bf3 100644 --- a/docs/gallery/fallen-log/index.html +++ b/docs/gallery/fallen-log/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Fallen Log

A procedural fallen forest log — one furrowed lathe along a bowed axis with bark peeled to galleried sapwood, a rotted hollow saw-cut butt, a splintered snapped top, broken branch stubs, moss cushions on its top and shaded side and three tiers of bracket fungi rooted in the bark — settled into a soil mound with toadstools, ferns and fallen leaves, through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

- + @@ -57,7 +57,7 @@

Fallen Log

category Nature

tags mesh export showcase

-
blender --background --python showcase/fallen-log/fallen_log.py --
+
blender --background --python showcase/fallen-log/fallen_log.py --
@@ -87,7 +87,7 @@

Falsifiers
FlagBudget violatedExit
--skip-decimateLOD1 ratio band (measured 1.0000)9
--stray-vertloose vertex count is 0 (measured 1, placed inside the envelope)15
--lift-zbounding box zmin is 0 (measured 0.05000)16
--hump-groundcontact patch (the bed sinks 90 mm under the log but for a hump under its mass centre: 0.0833 of the stations bedded)17
--float-fungirooted brackets (every shelf moved 35 mm off the bark: deepest vertex −0.0133 to −0.0027 m, outside)18
--tilt-groundbalance (the bed falls away under the south half and banks up on the north: centroid 0.0316 m outside the contact hull)19
--sunny-mossmoss on up- and shade-facing surfaces (every cushion turned half round the log: 0.1146)20
--float-litterground cover rooted (every leaf lifted 25 mm off the soil: 130 of 436 shells loose)21

--hump-ground keeps the log where it is and changes only the soil, so the log balances on one point: the balance budget still passes with the hump under the mass centre (0.1015 m inside), and only the contact budget fails. --tilt-ground is the converse: the log still lies bedded along its length (0.8611 of the stations) but only by its north flank, so it would roll. Both edit the soil within 0.50 m of the log's axis, which keeps the fern clumps where they are. --float-fungi leaves each shelf's shape and swing alone. --sunny-moss moves the cushions onto the underside and south flank with the same outlines, so the moss face count is unchanged.

Run#

-
blender --background --python fallen_log.py --
+
blender --background --python fallen_log.py --
 blender --background --python fallen_log.py -- --skip-decimate
 blender --background --python fallen_log.py -- --stray-vert
 blender --background --python fallen_log.py -- --lift-z
@@ -142,7 +142,7 @@ 

Wooden Barrel

- +

@@ -294,10 +294,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/farm-tractor/index.html b/docs/gallery/farm-tractor/index.html index 688f6ba8..37bcd724 100644 --- a/docs/gallery/farm-tractor/index.html +++ b/docs/gallery/farm-tractor/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Farm Tractor

A procedural mid-century utility tractor standing in the ruts of a muddy farmyard with a puddle between its wheels — a cast-iron backbone (engine, bell housing, gearbox, rear axle housing and trumpets); a front axle oscillating on a pivot pin through the front support's lugs and the beam's two bushings, kingpins, spindles and a tie rod; 1.240 m rear tyres with 44 chevron bars rooted in the carcass, handed for forward travel and wrapped round the shoulder, mud packed between them, on dished rims with clamp lugs and cast-iron half-moon weights; ribbed front tyres; a grille shell with radiator and headlamps, bonnet, fuel tank and dash; a vertical exhaust stack with a hinged rain cap and an oil-bath pre-cleaner bowl; a raked steering column, a pan seat on a leaf spring, pedals and footplates; mudguards with a tail lamp and a work lamp; a three-point linkage with lift arms, lift rods, lower links, a stowed top link and drawbar, and a PTO stub under its shield; worn paint chipped to primer at its edges, rust-streaked and mud-splashed — through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

- + @@ -57,7 +57,7 @@

Farm Tractor

category Vehicles

tags mesh export showcase

-
blender --background --python showcase/farm-tractor/farm_tractor.py --
+
blender --background --python showcase/farm-tractor/farm_tractor.py --
@@ -84,7 +84,7 @@

Falsifiers
FlagBudget violatedExit
--skip-decimateLOD1 ratio band (measured 1.0000)9
--stray-vertloose vertex count is 0 (measured 1, placed inside the envelope)15
--lift-zbounding box zmin is 0 (measured 0.05000)16
--float-tyreevery tyre pressed into the mud (the left front lifted 40 mm: −9.889 mm)16
--cock-hubhubs coaxial (the left rear hub 1.5 mm off its half-shaft: 1.5 mm)17
--cant-pinpivot pin coaxial with its bushings (the pin turned 1.5° in plan: 1.894 mm, 1.5°)17
--unpin-linklink eyes on their pins (the top link 8 mm short: its rear eye 8.0 mm off the stowage pin)17
--sink-tyretyre bead seat (the right rear bead 3 mm into its seat: 2.762 mm)18
--float-lugsbars rooted in the carcass (the left rear bars lifted 7 mm off the carcass along its normal: −3.056 mm)18
--skew-wheelrear wheels mirrored (the left rear wheel 6 mm aft of its hub: 6.000 mm)19
--wide-trackrear track at its stated size (each rear wheel 4 mm out: 1.3280 m)19
--tall-lugsrear tyre diameter at its stated size (bars 8 mm taller: 1.256 m)19
--odd-fenderbody mirror symmetry (the left mudguard bowed 3 mm out: 3.000 mm)19
--bunch-lugseven bar pitch (every fourth bar of one right rear half turned 2°: 2.000°)20
--reverse-lugschevron handing (the right rear tread built about its outboard axis: −10.886°)21
--offset-pivotstance (the pivot pin and bushings 0.30 m to the right: 0.3100 m)22
--loose-lampone connected assembly (the tail lamp 4 mm off its mudguard: 2 components, 4 and 501 shells)23

--float-tyre lifts the carcass alone while the rest of the tractor and the slab still ground the box. --float-lugs also makes the tyre 14 mm taller, but the bars' roots are read (18) before the diameter (19); --tall-lugs keeps the roots and changes only the crowns. --skew-wheel and --wide-track move the wheel (tyre, bars, rim, disc, weights) and leave the hub on its half-shaft, so the joint budget stays green. --cant-pin first turned the bushings with the pin, which stayed coaxial and exited 0; it now turns the pin alone. --loose-lamp first exited 0 because the lamp's bezel rested on the skirt; the lamp now stands clear on its bracket, which is its only contact.

Run#

-
blender --background --python farm_tractor.py --
+
blender --background --python farm_tractor.py --
 blender --background --python farm_tractor.py -- --skip-decimate
 blender --background --python farm_tractor.py -- --stray-vert
 blender --background --python farm_tractor.py -- --lift-z
@@ -150,7 +150,7 @@ 

Wooden Barrel

- +

@@ -302,10 +302,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/fence-kit/index.html b/docs/gallery/fence-kit/index.html index cffa3e70..286edf04 100644 --- a/docs/gallery/fence-kit/index.html +++ b/docs/gallery/fence-kit/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -57,7 +57,7 @@

Fence Kit

category Village

tags mesh export showcase

-
blender --background --python showcase/fence-kit/fence_kit.py --
+
blender --background --python showcase/fence-kit/fence_kit.py --
@@ -84,7 +84,7 @@

Falsifiers
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#

-
blender --background --python fence_kit.py --
+
blender --background --python fence_kit.py --
 blender --background --python fence_kit.py -- --skip-decimate
 blender --background --python fence_kit.py -- --stray-vert
 blender --background --python fence_kit.py -- --lift-z
@@ -139,7 +139,7 @@ 

Wooden Barrel

- +

@@ -291,10 +291,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/fern-mossy-rock/index.html b/docs/gallery/fern-mossy-rock/index.html index a70f3680..8e538939 100644 --- a/docs/gallery/fern-mossy-rock/index.html +++ b/docs/gallery/fern-mossy-rock/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Fern Mossy Rock

A procedural shaded woodland corner on a sloping soil bank — a gritstone boulder cut by deep cleavage planes into broad flat fracture faces meeting at crisp, weathered arrises, pitted, lichened and damp at its foot, split down its front by a deep crack and sunk into the bank all round; lobed moss cushions and satellite tufts laid on it as conforming shells, deepest in its hollows and feathering to nothing at the rim, tucked over the crack's lip, with bare patches; a male fern's shuttlecock crown of bipinnate fronds (alternate pinnae cut into pinnules, tapering to the tip) with fiddleheads unrolling as true spirals from a scaly rootstock bedded in the soil, a small fern growing out of the crack, beech litter, pebbles, a twig and wood sorrel — through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

- + @@ -57,7 +57,7 @@

Fern Mossy Rock

category Nature

tags mesh export showcase

-
blender --background --python showcase/fern-mossy-rock/fern_mossy_rock.py --
+
blender --background --python showcase/fern-mossy-rock/fern_mossy_rock.py --
@@ -90,7 +90,7 @@

Falsifiers
FlagBudget violatedExit
--skip-decimateLOD1 ratio band (measured 1.0000)9
--stray-vertloose vertex count is 0 (measured 1)15
--lift-zbounding box zmin is 0 (measured 0.05000)16
--smooth-rockcleaved (the shipped recipe: shallow cuts through broad bevels, full waves on the faces, the ellipsoid drawn 0.90 smaller to keep its size: 0.2892 of the turning in sharp arrises)27
--float-frondsrooted (every frond's stipe starts 60 mm out along its path, which is otherwise unchanged: shallowest base −0.0503 m)17
--lift-crownrooted (the rootstock alone raised 50 mm: worst sector −0.0180 m, 1 of 8)17
--float-mossmoss seat (every cushion lifted 40 mm along the stone's normal: top up to 0.0858 m, rim −0.0380 m)18
--slab-mossmoss rim feathers (the shipped profile, near full height to the last ring: 0.0189 m)28
--perch-rockboulder sealed (bedded against the soil height at its centre alone: worst sector −0.0321 m, 7 of 8)20
--sunny-mossmoss facing (the main cushion laid on the boulder's sunny back face: 0.5986)21
--perch-crack-ferncrack fern (slid 100 mm sideways out of the cleft into the wall: centre not in the cleft, walls 0.1576 m)22
--flat-taperpinnae taper (one frond's pinnae all 0.80 of its longest: tip 0.996)23
--opposite-pinnaepinnae alternate (one frond's lower-side pinnae moved level with the upper side's: gaps 0.0013–0.9987)23
--open-crozierfiddleheads (each coil a circular arc of the spiral's length: 0.932 turns, ratio 1.128)24
--float-coverground cover (leaves, pebbles, twig and sorrel lifted 50 mm: −0.046 m, 164 shells loose)25

The crack fern is the top of the envelope, so --float-moss and --perch-rock (which lifts the boulder with its moss) cannot move the box; the soil disc sets the left and back, frond tips the right and front, so --smooth-rock (a different stone, re-bedded, cutting its own flat foot) leaves it too. --slab-moss keeps every cushion's outline, thickness and windows' places and changes only the height profile. --flat-taper and --opposite-pinnae reshape only the crown frond held furthest inside the envelope. --perch-crack-fern moves the small fern level, never up. --lift-crown moves the rootstock alone; the stalks stay inside it.

Run#

-
blender --background --python fern_mossy_rock.py --
+
blender --background --python fern_mossy_rock.py --
 blender --background --python fern_mossy_rock.py -- --skip-decimate
 blender --background --python fern_mossy_rock.py -- --stray-vert
 blender --background --python fern_mossy_rock.py -- --lift-z
@@ -152,7 +152,7 @@ 

Wooden Barrel

- +

@@ -304,10 +304,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/floor-fan/index.html b/docs/gallery/floor-fan/index.html index 17f2e808..ae7e6079 100644 --- a/docs/gallery/floor-fan/index.html +++ b/docs/gallery/floor-fan/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Floor Fan

A procedural 1950s oscillating pedestal fan — a hollow two-tier cast base on a rubber gasket with a rotary speed switch and badge, a telescoping enamel and chrome column with a knurled height-lock collar, a strap yoke with a knurled tilt knob, a slotted motor housing with a trim band and an oscillation knob, a two-half wire guard of radial spokes and concentric rings welded to clipped rim rings with a centre badge, four swept brass blades on a hub with a hex spinner nut, and a cord to a plug — through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

- + @@ -57,7 +57,7 @@

Floor Fan

category Household

tags mesh export showcase

-
blender --background --python showcase/floor-fan/floor_fan.py --
+
blender --background --python showcase/floor-fan/floor_fan.py --
@@ -91,7 +91,7 @@

Falsifiers
FlagBudget violatedExit
--skip-decimateLOD1 ratio band (measured 1.0000)9
--stray-vertloose vertex count is 0 (measured 1, placed inside the envelope)15
--lift-zbounding box zmin is 0 (measured 0.05000)16
--offset-hubrotor coaxial with the guard (hub, spinner and blades moved 3 mm off the axis: 0.003000 m)17
--short-spokeguard wires seated (one front spoke stopped 12 mm short of its rim ring: 0.014906 m)18
--lean-columncolumn plumb and coaxial (lower tube leaning 1° on its foot: 1.0000°, 0.001501 m off the base's axis)19
--long-bladesblade-tip clearance band (tips 12 mm longer: 0.00745 m)20
--skew-bladeequal blade spacing (one blade turned 5°: 4.9998°)21
--hollow-basestance (base pressed from 1.2 mm sheet: 18.64 kg, base 2.87 kg, centre 0.752 m up: 12.39°)22
--loose-spinnerone connected assembly (spinner nut backed 6 mm off the hub: 2 components)23

--offset-hub leaves the tip clearance in band (0.01648 m) and the blades evenly spaced about their own hub, so only the coaxial budget sees it. --long-blades still leaves every tip 7.4 mm clear of the nearest wire, so the band's floor is what fails. --lean-column tilts only the lower tube; the upper tube, the collar and the head stay put, and the tube stays inside the base's boss. --hollow-base keeps the outer casting, so the envelope and footprint are unchanged and only the mass moves.

Run#

-
blender --background --python floor_fan.py --
+
blender --background --python floor_fan.py --
 blender --background --python floor_fan.py -- --skip-decimate
 blender --background --python floor_fan.py -- --stray-vert
 blender --background --python floor_fan.py -- --lift-z
@@ -148,7 +148,7 @@ 

Wooden Barrel

- +

@@ -300,10 +300,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/gallery.css b/docs/gallery/gallery.css index ef0f507f..b79bd09e 100644 --- a/docs/gallery/gallery.css +++ b/docs/gallery/gallery.css @@ -156,6 +156,10 @@ .view-mode .density-btn.active { background: var(--border); color: var(--text); } /* Segmented groups clip overflow, so the ring has to sit inside. */ .density-btn:focus-visible, .chip:focus-visible { outline-offset: -2px; } + /* On a selected (orange) button an orange ring vanishes into the fill + (#397): draw a dark inset ring instead, ~7.8:1 against --select. */ + .density-btn.active:focus-visible, .cat-btn.active:focus-visible, + .chip.active:focus-visible { outline: 2px solid var(--on-select); outline-offset: -4px; } .sort select { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); padding: 0.4rem 0.6rem; min-height: 36px; cursor: pointer; font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.04em; text-transform: uppercase; } @@ -353,9 +357,11 @@ 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; } + display: flex; align-items: baseline; gap: 0.35em; min-width: 0; white-space: nowrap; } + /* Only the name truncates; the arrow is its own box so it never clips (#399). */ + .pager-label { overflow: hidden; text-overflow: ellipsis; min-width: 0; } .pager a:hover { color: var(--select); text-decoration: none; } - .pager [rel="next"] { text-align: right; } + .pager [rel="next"] { justify-content: flex-end; } .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); diff --git a/docs/gallery/garden-gate/index.html b/docs/gallery/garden-gate/index.html index 73abf16c..9f9b4a02 100644 --- a/docs/gallery/garden-gate/index.html +++ b/docs/gallery/garden-gate/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Garden Gate

A procedural ledged-and-braced garden gate on hook-and-band hinges through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets — every knuckle on its pin, both pins on one plumb axis, and the swing clearing the latch post — rather than an API contract.

- + @@ -57,7 +57,7 @@

Garden Gate

category Village

tags mesh export showcase

-
blender --background --python showcase/garden-gate/garden_gate.py --
+
blender --background --python showcase/garden-gate/garden_gate.py --
@@ -90,7 +90,7 @@

Exit codes
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build, has no UV layer, or a part count is wrong
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 (--lift-z)
17Knuckle off its pin or bore clearance out of band (--offset-knuckle)
18Knuckle seat on its hook (--lift-gate)
19Pin plumb, shared hinge axis or real-world size (--tilt-pintle)
20Swing clearance at the latch post (--tight-post)
21A brace does not rise from the hinge side (--reversed-braces)
24Asset-quality floors (examples/gallery_asset_quality.py, render path only)

Run it#

-
# Budget check, no render. A few seconds warm.
+
# Budget check, no render. A few seconds warm.
 blender --background --python garden_gate.py --
 
 # Falsifier: the latch post 14 mm closer. Must exit 20.
@@ -148,7 +148,7 @@ 

Wooden Barrel

- +

@@ -300,10 +300,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/gltf-export-roundtrip/index.html b/docs/gallery/gltf-export-roundtrip/index.html index 2c79a052..98edec49 100644 --- a/docs/gallery/gltf-export-roundtrip/index.html +++ b/docs/gallery/gltf-export-roundtrip/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -56,7 +56,7 @@

glTF Export Roundtrip

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 --
+
blender --background --python examples/gltf-export-roundtrip/gltf_export_roundtrip.py --
@@ -71,7 +71,7 @@

glTF Export Roundtrip

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#

-
# Cheap correctness check (no render) — the CI check:
+
# Cheap correctness check (no render) — the CI check:
 blender --background --python gltf_export_roundtrip.py --
 
 # Falsifier: export_yup=False. Must exit non-zero (bbox is Z-up on disk).
@@ -124,7 +124,7 @@ 

Export Preset Axis

- +
@@ -276,10 +276,18 @@

Export Preset Axis

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/gltf-skin-roundtrip/index.html b/docs/gallery/gltf-skin-roundtrip/index.html index bbb45301..2f6f8f2b 100644 --- a/docs/gallery/gltf-skin-roundtrip/index.html +++ b/docs/gallery/gltf-skin-roundtrip/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -56,7 +56,7 @@

glTF Skin Roundtrip

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 --
+
blender --background --python examples/gltf-skin-roundtrip/gltf_skin_roundtrip.py --
@@ -71,7 +71,7 @@

glTF Skin Roundtrip

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#

-
# Cheap correctness check (no render) — the CI check:
+
# Cheap correctness check (no render) — the CI check:
 blender --background --python gltf_skin_roundtrip.py --
 
 # Falsifier: export_skins=False. Must exit non-zero.
@@ -125,7 +125,7 @@ 

glTF Export Roundtrip - +

@@ -277,10 +277,18 @@

glTF Export Roundtrip scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/gn-instance-grid/index.html b/docs/gallery/gn-instance-grid/index.html index 8a8ad4ea..337018da 100644 --- a/docs/gallery/gn-instance-grid/index.html +++ b/docs/gallery/gn-instance-grid/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -56,7 +56,7 @@

GN Instance Grid

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 --
+
blender --background --python examples/gn-instance-grid/gn_instance_grid.py --
@@ -65,7 +65,7 @@

GN Instance Grid

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#

-
# Cheap correctness check (no render) — the CI check:
+
# Cheap correctness check (no render) — the CI check:
 blender --background --python gn_instance_grid.py --
 
 # Falsifier: 1×1 grid. Must exit non-zero (4: eight keycaps missing).
@@ -118,7 +118,7 @@ 

Modular Kit Snap

- +
@@ -270,10 +270,18 @@

Modular Kit Snap

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/gn-modifier-inputs/index.html b/docs/gallery/gn-modifier-inputs/index.html index 36a4ea67..153209fe 100644 --- a/docs/gallery/gn-modifier-inputs/index.html +++ b/docs/gallery/gn-modifier-inputs/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -56,7 +56,7 @@

GN Modifier Inputs

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 --
+
blender --background --python examples/gn-modifier-inputs/gn_modifier_inputs.py --
@@ -67,7 +67,7 @@

GN Modifier Inputs

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#

-
# Cheap correctness check (no render) — the CI check:
+
# Cheap correctness check (no render) — the CI check:
 blender --background --python gn_modifier_inputs.py --
 
 # Portable falsifier: write 1.0 to every modifier. Must exit non-zero.
@@ -127,7 +127,7 @@ 

GP Lineart Contour

- +
@@ -279,10 +279,18 @@

GP Lineart Contour

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/gn-sdf-remesh/index.html b/docs/gallery/gn-sdf-remesh/index.html index 0d699502..7f0f9b28 100644 --- a/docs/gallery/gn-sdf-remesh/index.html +++ b/docs/gallery/gn-sdf-remesh/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -56,7 +56,7 @@

GN SDF Remesh

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 --
+
blender --background --python examples/gn-sdf-remesh/gn_sdf_remesh.py --
@@ -67,7 +67,7 @@

GN SDF Remesh

Render#

The check measures a torus; the still proves the same build_remesh_via_sdf tree on a subject where remeshing is visible. A vase is kitbashed from overlapping primitives (foot, belly, neck, lip, two handles), each in its own clay tone under a thin cage of its own edges, so the interior faces and hard intersection seams read. Beside it the identical kit runs through the tree (finer 0.018 voxels, cobalt glaze carried in by Set Material) and comes out one fused, watertight shell with the handles filleted into the belly. The torus leaves the stage before the render; the render path gates framing through examples/gallery_framing.py (exit 10).

Run#

-
# Cheap correctness check only (no render) — the CI smoke check:
+
# Cheap correctness check only (no render) — the CI smoke check:
 blender --background --python gn_sdf_remesh.py --
 
 # Falsifier: no SDF modifier. Must exit non-zero (evaluated == base).
@@ -120,7 +120,7 @@ 

GN Instance Grid

- +
@@ -272,10 +272,18 @@

GN Instance Grid

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/gn-sim-fountain/index.html b/docs/gallery/gn-sim-fountain/index.html index ab5c0f5b..15c5aa1a 100644 --- a/docs/gallery/gn-sim-fountain/index.html +++ b/docs/gallery/gn-sim-fountain/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -56,14 +56,14 @@

GN Sim Fountain

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 --
+
blender --background --python examples/gn-sim-fountain/gn_sim_fountain.py --

A two-tier stone fountain whose twenty jets are integrated by a Simulation Zone, witnessing the geometry-nodes-python frame contract: a Simulation Zone only advances when the scene steps one frame at a time. The evaluated state at frame N is not a function of N. AI code that calls scene.frame_set(N) and reads positions gets a silently wrong answer.

Each droplet is a point with a vel attribute. Per step the zone applies the constant-gravity update that is exact for any dt:

-
p += v*dt - 0.5*g*dt^2 * z
+
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#

@@ -75,7 +75,7 @@

Measured behaviorRun#

-
blender --background --python gn_sim_fountain.py --
+
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#

@@ -125,7 +125,7 @@

GN Modifier Inputs

- +
@@ -277,10 +277,18 @@

GN Modifier Inputs

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/gn-socket-rename/index.html b/docs/gallery/gn-socket-rename/index.html index 67659e55..858fc9e3 100644 --- a/docs/gallery/gn-socket-rename/index.html +++ b/docs/gallery/gn-socket-rename/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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

- + @@ -56,7 +56,7 @@

GN Socket Rename

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 --
+
blender --background --python examples/gn-socket-rename/gn_socket_rename.py --
@@ -70,7 +70,7 @@

GN Socket Rename

--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#

-
blender --background --python gn_socket_rename.py --
+
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.

@@ -121,7 +121,7 @@

GN Modifier Inputs

- +
@@ -273,10 +273,18 @@

GN Modifier Inputs

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/gn-zone-iterate/index.html b/docs/gallery/gn-zone-iterate/index.html index 530d52ff..f2b6ec6d 100644 --- a/docs/gallery/gn-zone-iterate/index.html +++ b/docs/gallery/gn-zone-iterate/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -56,7 +56,7 @@

GN Zone Iterate

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 --
+
blender --background --python examples/gn-zone-iterate/gn_zone_iterate.py --
@@ -70,7 +70,7 @@

GN Zone Iterate

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#

-
blender --background --python gn_zone_iterate.py --
+
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).

Staging. Everything presentational runs after check() has passed on the raw evaluated zones. Each zone's blocks are graded along the axis the zone iterates on, using Object-space coordinates (amber to red along X for the four Repeat blocks, deep teal to mint along Z for the six For Each blocks), so the colour steps count the iterations. A render-only Bevel modifier after the GN modifier chamfers the cubes into machined blocks. The Repeat row sits on a walnut plinth; the For Each tower is threaded on a steel spindle over a round walnut base, so its 0.18 m gaps read as spacing on a rod rather than floating cubes.

@@ -118,7 +118,7 @@

GN Modifier Inputs

- +
@@ -270,10 +270,18 @@

GN Modifier Inputs

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/go-kart/index.html b/docs/gallery/go-kart/index.html index bc5ad65a..557854db 100644 --- a/docs/gallery/go-kart/index.html +++ b/docs/gallery/go-kart/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Go Kart

A procedural rental go-kart on a patch of kart-track asphalt against a red and white kerb — a 30 mm tube chassis whose main loop is one bar swept round every bend, cross tubes ending inside the rails with welded gussets, bearing hangers and kingpin C-brackets; spindles on kingpin bolts with 14° caster, a raked column in a bushing, a steering plate and two tie rods on rod-end eyes; blue moulded bodywork — a nose cone with a raised hump and lipped shoulders, pods with a crest, a ribbed deck and a groove, a front panel leaning back from the hump with a white number plate — on ten strap brackets welded to the tubes and bolted to the mouldings; a moulded seat on stays, a floor tray, pedals, a brake master cylinder with its hose to the caliper, a throttle cable, a strapped fuel tank and a lead ballast block; a finned four-stroke clamped to the rail driving the live rear axle through a clutch, an 88-link chain solved to a whole number of pitches and a 60-tooth sprocket, bearing carriers, a brake disc and caliper; and four loaded slicks on split rims with valve stems, the fronts narrower than the rears — through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

- + @@ -57,7 +57,7 @@

Go Kart

category Vehicles

tags mesh export showcase

-
blender --background --python showcase/go-kart/go_kart.py --
+
blender --background --python showcase/go-kart/go_kart.py --
@@ -84,7 +84,7 @@

Falsifiers
FlagBudget violatedExit
--skip-decimateLOD1 ratio band (measured 1.0000)9
--stray-vertloose vertex count is 0 (measured 1, placed inside the envelope)15
--lift-zbounding box zmin is 0 (measured 0.05000)16
--float-tyreevery tyre pressed into the asphalt in one plane (the left front lifted 4 mm: −2.5 mm, the rest 1.5 mm)16
--cock-hubhubs coaxial (the left rear hub 1.5 mm off the axle: 1.5 mm)17
--drop-bearingaxle through both bearings (the right bearing and carrier 2 mm low: 2.0 mm)17
--short-tierodtie-rod eyes on their pins (the left rod 6 mm short: 6.0 mm)17
--sink-tyretyre bead seat (the right rear bead 3 mm into its seat: 3.000 mm)18
--lift-chainchain seated on its sprockets (the chain 4 mm off both: −2.089 and −2.060 mm)18
--toe-wheelwheels mirrored (the left front corner turned 1.5°: 4.539 mm)19
--wide-trackrear track at its stated size (each rear wheel 4 mm out: 1.2080 m)19
--odd-framechassis mirror symmetry (the left rail bowed 3 mm out: 1.819 mm)19
--no-castercaster band (kingpins vertical: 0.0°)19
--aft-ballaststance (the 8 kg ballast moved behind the axle: 0.2887 m)20
--loose-ballastone connected assembly (the ballast lifted 3 mm off the tray: 2 components, 5 and 325 shells)21
--float-nosebodywork brackets (the nose and its bolts slid 3 mm forward: both nose brackets' tabs −2.0 mm)23

--float-tyre lifts the tyre alone, after the patch is flattened, while the slab still grounds the box. --toe-wheel turns the whole corner (spindle, hub, rim and tyre) about the vertical through the kingpin, and the tie rod is rebuilt to the moved arm, so the hubs stay coaxial and the eye stays on its pin; a turn about the raked kingpin itself would have dropped the patch out of its band first. --lift-chain moves the chain alone, keeping the tooth phase of the seated chain: phasing the teeth to the lifted pins, and lifts of 1.7, 2.0, 2.5, 3.0 and 3.5 mm, each put a chain face within 0.1 mm of a tooth flat's plane and exited 15. The assembly falsifier first lifted a side pod, which broke the bodywork's mirror and exited 19; the ballast stack is measured by nothing else. --float-nose moves the nose along X only, so the mirror holds, the nose stays on its bumper bars and on the panel's foot (one assembly), and the slab still sets the envelope.

Run#

-
blender --background --python go_kart.py --
+
blender --background --python go_kart.py --
 blender --background --python go_kart.py -- --skip-decimate
 blender --background --python go_kart.py -- --stray-vert
 blender --background --python go_kart.py -- --lift-z
@@ -150,7 +150,7 @@ 

Wooden Barrel

- +

@@ -302,10 +302,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/gp-lineart-contour/index.html b/docs/gallery/gp-lineart-contour/index.html index 15562798..6a522685 100644 --- a/docs/gallery/gp-lineart-contour/index.html +++ b/docs/gallery/gp-lineart-contour/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -56,7 +56,7 @@

GP Lineart Contour

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 --
+
blender --background --python examples/gp-lineart-contour/gp_lineart_contour.py --
@@ -70,7 +70,7 @@

GP Lineart Contour

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#

-
# Depsgraph contour check — the CI check:
+
# Depsgraph contour check — the CI check:
 blender --background --python gp_lineart_contour.py --
 
 # Falsifier: use_contour off. Must exit non-zero.
@@ -122,7 +122,7 @@ 

Car Mirror Symmetry - +

@@ -274,10 +274,18 @@

Car Mirror Symmetry scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/grain-sacks/index.html b/docs/gallery/grain-sacks/index.html index ed3a4b4b..9d03ed34 100644 --- a/docs/gallery/grain-sacks/index.html +++ b/docs/gallery/grain-sacks/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -57,7 +57,7 @@

Grain Sacks

category Village

tags mesh export showcase

-
blender --background --python showcase/grain-sacks/grain_sacks.py --
+
blender --background --python showcase/grain-sacks/grain_sacks.py --
@@ -86,7 +86,7 @@

Exit codes
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)
20Gallery asset-quality violation (render path; the floors' 11 is remapped here)

Run it#

-
# Budget check, no render. ~1.9 s on 4.5, ~1.7 s on 5.1, ~2.1 s on 5.2.
+
# 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 --
 
 # Falsifier: the sacks stop settling onto flat bases. Must exit 19.
@@ -141,7 +141,7 @@ 

Wooden Barrel

- +

@@ -293,10 +293,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/grease-pencil-rosette/index.html b/docs/gallery/grease-pencil-rosette/index.html index c86ee51b..741cbf5b 100644 --- a/docs/gallery/grease-pencil-rosette/index.html +++ b/docs/gallery/grease-pencil-rosette/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -56,7 +56,7 @@

Grease Pencil Rosette

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 --
+
blender --background --python examples/grease-pencil-rosette/grease_pencil_rosette.py --
@@ -68,7 +68,7 @@

Grease Pencil Rosette

Render#

The strokes stay unlit (layer.use_lights = False) and act as the neon tubes of a sign: they hang a few centimetres proud of a satin black-lacquer backboard in a brass frame with brass standoff caps and a maker's plate, mounted on the studio wall. Grease Pencil does not light its surroundings, so the spill is staged: one small shadowless disk light per sample along each ring, placed on that ring's rose_point and tinted with its ring_color, washes the board behind each tube. The spill reads the same closed-form curve the check asserts. The render path gates framing through examples/gallery_framing.py (exit 10).

Run#

-
# Cheap correctness check (no render) — the CI check:
+
# Cheap correctness check (no render) — the CI check:
 blender --background --python grease_pencil_rosette.py --
 
 # Falsifier: open strokes. Must exit non-zero (cyclic).
@@ -121,7 +121,7 @@ 

GP Lineart Contour

- +
@@ -273,10 +273,18 @@

GP Lineart Contour

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/grindstone/index.html b/docs/gallery/grindstone/index.html index 70fcd81d..d5f3bfa6 100644 --- a/docs/gallery/grindstone/index.html +++ b/docs/gallery/grindstone/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Grindstone

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

- + @@ -57,7 +57,7 @@

Grindstone

category Village

tags mesh export showcase

-
blender --background --python showcase/grindstone/grindstone.py --
+
blender --background --python showcase/grindstone/grindstone.py --
@@ -83,7 +83,7 @@

Falsifiers
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#

-
blender --background --python grindstone.py --
+
blender --background --python grindstone.py --
 blender --background --python grindstone.py -- --skip-decimate
 blender --background --python grindstone.py -- --stray-vert
 blender --background --python grindstone.py -- --flush-shoes
@@ -141,7 +141,7 @@ 

Wooden Barrel

- +

@@ -293,10 +293,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/hand-pump/index.html b/docs/gallery/hand-pump/index.html index b68e0216..8ed75d80 100644 --- a/docs/gallery/hand-pump/index.html +++ b/docs/gallery/hand-pump/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -57,7 +57,7 @@

Hand Pump

category Village

tags mesh export showcase

-
blender --background --python showcase/hand-pump/hand_pump.py --
+
blender --background --python showcase/hand-pump/hand_pump.py --
@@ -84,7 +84,7 @@

Falsifiers
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
--shift-bucketbucket under the spout20

Run#

-
blender --background --python hand_pump.py --
+
blender --background --python hand_pump.py --
 blender --background --python hand_pump.py -- --skip-decimate
 blender --background --python hand_pump.py -- --stray-vert
 blender --background --python hand_pump.py -- --lift-z
@@ -137,7 +137,7 @@ 

Wooden Barrel

- +

@@ -289,10 +289,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/hay-bale/index.html b/docs/gallery/hay-bale/index.html index 39859418..1cb87ef1 100644 --- a/docs/gallery/hay-bale/index.html +++ b/docs/gallery/hay-bale/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -57,7 +57,7 @@

Hay Bale

category Village

tags mesh export showcase

-
blender --background --python showcase/hay-bale/hay_bale.py --
+
blender --background --python showcase/hay-bale/hay_bale.py --
@@ -87,7 +87,7 @@

Falsifiers
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#

-
blender --background --python hay_bale.py --
+
blender --background --python hay_bale.py --
 blender --background --python hay_bale.py -- --skip-decimate
 blender --background --python hay_bale.py -- --stray-vert
 blender --background --python hay_bale.py -- --lift-z
@@ -148,7 +148,7 @@ 

Wooden Barrel

- +

@@ -300,10 +300,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/hitching-post/index.html b/docs/gallery/hitching-post/index.html index 24ee8251..038d409c 100644 --- a/docs/gallery/hitching-post/index.html +++ b/docs/gallery/hitching-post/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Hitching Post

A procedural hitching post (eaved cap, one rail, cup shoe, rings hung through eyes, a face-nailed horseshoe) through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

- + @@ -57,7 +57,7 @@

Hitching Post

category Village

tags mesh export showcase

-
blender --background --python showcase/hitching-post/hitching_post.py --
+
blender --background --python showcase/hitching-post/hitching_post.py --
@@ -83,7 +83,7 @@

Bands, wood and stage
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
--float-horseshoeHorseshoe seat18bite −0.0025
--float-bracesBrace seat18brace_arm 0

Default exit is 0 on all three. --clip-ring also overlaps the rail; the centerline gate is the one that fires.

Run#

-
blender --background --python hitching_post.py --
+
blender --background --python hitching_post.py --
 blender --background --python hitching_post.py -- --skip-decimate
 blender --background --python hitching_post.py -- --stray-vert
 blender --background --python hitching_post.py -- --twin-sole
@@ -138,7 +138,7 @@ 

Wooden Barrel

- +

@@ -290,10 +290,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/hover-bike/index.html b/docs/gallery/hover-bike/index.html index 7a63e6c5..7115e3d9 100644 --- a/docs/gallery/hover-bike/index.html +++ b/docs/gallery/hover-bike/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Hover Bike

A procedural sci-fi hover bike — a sculpted two-tone fuselage with panel seams, gasketed access panels with hex fasteners, a belly service plate, decal plate, hinged charge-port door, heat sink, intake grilles, grab rail and tail light, a stitched pleated saddle, a twin-stanchion fork through sculpted triple clamps with clip-ons, switch pods, pivoted levers and a visored instrument cluster, two glowing exhaust nozzles, two ducted fans with canted stator vanes and handed seven-blade rotors, aerofoil pylons flared into the shrouds with fastener rows and a conduit, and landing gear of twin-plate pressed links and oleo shocks on sixteen pinned joints carrying ski-section skids on wear strips — through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

- + @@ -57,7 +57,7 @@

Hover Bike

category Vehicles

tags mesh export showcase

-
blender --background --python showcase/hover-bike/hover_bike.py --
+
blender --background --python showcase/hover-bike/hover_bike.py --
@@ -86,7 +86,7 @@

Falsifiers
FlagBudget violatedExit
--skip-decimateLOD1 ratio band (measured 1.0000)9
--stray-vertloose vertex count is 0 (measured 1, placed inside the envelope)15
--lift-zbounding box zmin is 0 (measured 0.05000)16
--float-skidboth skids on the ground (the y+ shoe, strip, caps and bolts 4 mm up: its zmin 0.004, the other 0)16
--offset-hubfans coaxial (rear rotor 2.5 mm off its shroud's axis: 0.00250 m)17
--long-bladesblade-tip clearance band (rear blades 12 mm longer: clearance −0.0022 m)18
--odd-hullhull mirror symmetry (a 4 mm sideways bow, zero at both ends: deviation 0.00799 m)19
--skew-bladeblade spacing (one rear blade turned 4°: worst gap error 4.000°)20
--narrow-skidsstance (shoes drawn in to ±0.150 m: margin 0.1715 m)21
--pop-lensone connected assembly (headlight lens 20 mm out of its housing: 2 components)22
--offset-pinjoints (the near front link's foot pin 1.5 mm along x: 0.001500 m off its bushings' axis)24
--skew-rodshocks (the near rear rod 1.5 mm across its axis: 0.001500 m off its barrel's)25
--short-rodshocks (the near rear rod's inner end drawn out 45 mm: engaged 0.020 m, floor 0.045)25

The rear fan is the one --offset-hub, --long-blades and --skew-blade change, because nothing inside a shroud sets the envelope. --offset-hub leaves the tip clearance inside its band and the blade gaps exact, so only coaxiality sees it. --long-blades pushes the tips 2.2 mm into the wall, and every blade still sits in its hub. --odd-hull bows the fuselage by cos(πx / 2·0.515), so both ends stay inside their shroud walls, and it bows everything laid on the skin with it: when only the hull bowed, a panel met a hull facet at a shared plane and the run stopped at hygiene (15) instead. --float-skid lifts one skid's own parts while the other grounds the box; its first draft lifted the skid lug's pin too, which slid a pin face onto a boss's plane and also stopped at 15, so the lug and its pin now stay with the gear. --pop-lens first stopped at 15 because the lens (36 segments) and its housing (32) had faces within 0.6° of parallel; both are now 32 segments turned half a step apart. --narrow-skids keeps both strips on the ground and the envelope unchanged; only the contact polygon shrinks. --offset-pin moves a pin and no bushing; --skew-rod and --short-rod move a rod and no pin, so each fails its own budget alone.

Run#

-
blender --background --python hover_bike.py --
+
blender --background --python hover_bike.py --
 blender --background --python hover_bike.py -- --skip-decimate
 blender --background --python hover_bike.py -- --stray-vert
 blender --background --python hover_bike.py -- --lift-z
@@ -146,7 +146,7 @@ 

Wooden Barrel

- +

@@ -298,10 +298,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/image-pixels-testcard/index.html b/docs/gallery/image-pixels-testcard/index.html index 8b6f2871..f5ae25ce 100644 --- a/docs/gallery/image-pixels-testcard/index.html +++ b/docs/gallery/image-pixels-testcard/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -56,7 +56,7 @@

Image Pixels Testcard

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 --
+
blender --background --python examples/image-pixels-testcard/image_pixels_testcard.py --
@@ -69,7 +69,7 @@

Image Pixels Testcard

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#

-
# Cheap correctness check (no render) — the CI check:
+
# Cheap correctness check (no render) — the CI check:
 blender --background --python image_pixels_testcard.py --
 
 # Falsifier: write the card top-down. Must exit non-zero (byte round-trip).
@@ -123,7 +123,7 @@ 

Wave Displace

- +
@@ -275,10 +275,18 @@

Wave Displace

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/index.html b/docs/gallery/index.html index bad3a63b..6a0f68f7 100644 --- a/docs/gallery/index.html +++ b/docs/gallery/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -57,7 +57,7 @@

Iron Cauldron

category Village

tags mesh export showcase

-
blender --background --python showcase/iron-cauldron/iron_cauldron.py --
+
blender --background --python showcase/iron-cauldron/iron_cauldron.py --
@@ -84,7 +84,7 @@

Falsifiers
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#

-
blender --background --python iron_cauldron.py --
+
blender --background --python iron_cauldron.py --
 blender --background --python iron_cauldron.py -- --skip-decimate
 blender --background --python iron_cauldron.py -- --stray-vert
 blender --background --python iron_cauldron.py -- --lift-z
@@ -138,7 +138,7 @@ 

Wooden Barrel

- +

@@ -290,10 +290,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/lattice-deform/index.html b/docs/gallery/lattice-deform/index.html index 30cd5371..9b146ab5 100644 --- a/docs/gallery/lattice-deform/index.html +++ b/docs/gallery/lattice-deform/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Lattice Deform

A 2x2x2 Lattice modifier set to KEY_LINEAR on all three axes, with two top control points moved through LatticePoint.co_deform, read back through the depsgraph (evaluated_get, to_mesh, to_mesh_clear).

- + @@ -56,7 +56,7 @@

Lattice Deform

witnesses Linear lattice deform is exact trilinear interpolation: all 602 evaluated verts land on the closed-form blend of the eight co_deform corners (max error 5.6e-7 through a translated, non-uniformly scaled lattice), the undeformed lattice is the identity, and --bspline (the default interpolation) misses by 0.2145 and exits 4.

tags mesh modifiers depsgraph

-
blender --background --python examples/lattice-deform/lattice_deform.py --
+
blender --background --python examples/lattice-deform/lattice_deform.py --
@@ -66,7 +66,7 @@

Lattice Deform

The trap it exposes: a new lattice defaults to 'KEY_BSPLINE'. B-spline weights smooth over the control points instead of interpolating them, so a script that moves a corner and expects the mesh to follow it linearly gets a softer, smaller pull. On a 2×2×2 lattice the rest state is still the identity, so nothing looks wrong until a point moves. --bspline keeps the default and fails check 4 with the measured error.

The still shows the rule as a before-and-after on a walnut plinth: on the left a render-only twin of the column, undeformed, inside its rest cage; on the right the checked column inside the deformed cage, with the two moved corners in selection orange and a short orange rod from each back to its rest position. The column carries a glazed two-tone checker in Generated (rest) coordinates, so the squares are square before the lattice acts, and their shear and taper in the still are the deformation. Lattices do not render, so both cages are drawn as thin steel rods built from the corner positions after the check has run.

Run#

-
# Cheap correctness check (no render) — the CI check:
+
# Cheap correctness check (no render) — the CI check:
 blender --background --python lattice_deform.py --
 
 # Falsifier: keep the default KEY_BSPLINE interpolation. Must exit 4.
@@ -119,7 +119,7 @@ 

LOD Decimate Chain

- +
@@ -271,10 +271,18 @@

LOD Decimate Chain

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/light-link-studio/index.html b/docs/gallery/light-link-studio/index.html index 54fbadac..56812a95 100644 --- a/docs/gallery/light-link-studio/index.html +++ b/docs/gallery/light-link-studio/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -56,7 +56,7 @@

Light Link Studio

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 --
+
blender --background --python examples/light-link-studio/light_link_studio.py --
@@ -68,7 +68,7 @@

Light Link Studio

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, on designed subjects: after the check passes on the spheres, the still hides them and stands two identical lathed porcelain chess kings (gilt collar and cross, one shared material) in the hero and decoy collections. The only difference between the kings is the linked key, recoloured amber for the still: the LINKED king glows warm over its placard and the warm floor inlay, the UNLINKED king stays cool grey. The shared fill and rim are pulled down for the still only, because glossy porcelain picks up far more of them than the matte check spheres do. The still's render path gates framing through examples/gallery_framing.py (exit 10) before writing.

Run#

-
# Two-render correctness check (tiny Cycles CPU renders) — the CI check:
+
# Two-render correctness check (tiny Cycles CPU renders) — the CI check:
 blender --background --python light_link_studio.py --
 
 # Falsifier: receiver_collection unset. Must exit non-zero (assignment).
@@ -120,7 +120,7 @@ 

Parent Inverse Orrery - +

@@ -272,10 +272,18 @@

Parent Inverse Orrery scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/lightmap-uv-channel/index.html b/docs/gallery/lightmap-uv-channel/index.html index 3a86af17..91da30b6 100644 --- a/docs/gallery/lightmap-uv-channel/index.html +++ b/docs/gallery/lightmap-uv-channel/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -56,7 +56,7 @@

Lightmap UV Channel

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 --
+
blender --background --python examples/lightmap-uv-channel/lightmap_uv_channel.py --
@@ -71,7 +71,7 @@

Lightmap UV Channel

Version witness: check output is byte-identical on Blender 4.5.11 LTS and 5.2.1 (24 parts, 3428 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 display — a slate face in a walnut frame on a plinth with a brass UV1 LIGHTMAP plate, a faint quarter grid marking the [0,1] square the bounds check holds — 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): 24 named parts, 5 materials, right-angle share 0.138. Wood, canvas and iron carry procedural grain, weave and pitting over Object coordinates; no check reads a material.

Run#

-
blender --background --python lightmap_uv_channel.py --
+
blender --background --python lightmap_uv_channel.py --
 blender --background --python lightmap_uv_channel.py -- --overlap-islands   # falsifier: overlap scan reports the dragged island; exits 7
 blender --background --python lightmap_uv_channel.py -- --output atlas.png
 blender --background --python lightmap_uv_channel.py -- --falsify overlap.png
@@ -119,7 +119,7 @@

Compositor Glare

- +
@@ -271,10 +271,18 @@

Compositor Glare

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/lod-decimate-chain/index.html b/docs/gallery/lod-decimate-chain/index.html index f4a89346..91c54d9d 100644 --- a/docs/gallery/lod-decimate-chain/index.html +++ b/docs/gallery/lod-decimate-chain/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -56,7 +56,7 @@

LOD Decimate Chain

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 --
+
blender --background --python examples/lod-decimate-chain/lod_decimate_chain.py --
@@ -68,7 +68,7 @@

LOD Decimate Chain

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#

-
# Cheap correctness check (no render) — the CI check:
+
# Cheap correctness check (no render) — the CI check:
 blender --background --python lod_decimate_chain.py --
 
 # Falsifier: no Decimate on the LOD copies. Must exit non-zero (no reduction).
@@ -121,7 +121,7 @@ 

Boolean Exact Volume< - +

@@ -273,10 +273,18 @@

Boolean Exact Volume< // Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/market-stall/index.html b/docs/gallery/market-stall/index.html index f14e57a0..f178257a 100644 --- a/docs/gallery/market-stall/index.html +++ b/docs/gallery/market-stall/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Market Stall

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

- + @@ -57,7 +57,7 @@

Market Stall

category Village

tags mesh export showcase

-
blender --background --python showcase/market-stall/market_stall.py --
+
blender --background --python showcase/market-stall/market_stall.py --
@@ -80,7 +80,7 @@

Falsifiers
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#

-
blender --background --python market_stall.py --
+
blender --background --python market_stall.py --
 blender --background --python market_stall.py -- --skip-decimate
 blender --background --python market_stall.py -- --stray-vert
 blender --background --python market_stall.py -- --lift-z
@@ -134,7 +134,7 @@ 

Wooden Barrel

- +

@@ -286,10 +286,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/mesh-hygiene-audit/index.html b/docs/gallery/mesh-hygiene-audit/index.html index fd7d0eae..2248193e 100644 --- a/docs/gallery/mesh-hygiene-audit/index.html +++ b/docs/gallery/mesh-hygiene-audit/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -56,7 +56,7 @@

Mesh Hygiene Audit

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 --
+
blender --background --python examples/mesh-hygiene-audit/mesh_hygiene_audit.py --
@@ -71,7 +71,7 @@

Mesh Hygiene Audit

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#

-
blender --background --python mesh_hygiene_audit.py --
+
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
@@ -120,7 +120,7 @@ 

BMesh Gear

- +
@@ -272,10 +272,18 @@

BMesh Gear

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/mine-cart/index.html b/docs/gallery/mine-cart/index.html index d336d6fe..67c00eb8 100644 --- a/docs/gallery/mine-cart/index.html +++ b/docs/gallery/mine-cart/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Mine Cart

A procedural narrow-gauge mine cart on a rail segment — riveted steel tub with a rolled lip heaped with ore, timber frame, four flanged wheels on two axles, two 30 lb rails spiked to sleepers in ballast — through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

- + @@ -57,7 +57,7 @@

Mine Cart

category Village

tags mesh export showcase

-
blender --background --python showcase/mine-cart/mine_cart.py --
+
blender --background --python showcase/mine-cart/mine_cart.py --
@@ -91,7 +91,7 @@

Exit codes
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build, has no UV layer, or a part count is wrong
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 (--lift-z)
18Wheel seat or flange depth (--float-wheel)
19Axle squareness, wheelbase, gauge, rail height or wheel size (--skew-axle)
20Flange clearance to the railhead (--wide-gauge)
21Asset-quality floors (examples/gallery_asset_quality.py, render path only; its 11 is this piece's collider code)

Run it#

-
# Budget check, no render. A few seconds warm.
+
# Budget check, no render. A few seconds warm.
 blender --background --python mine_cart.py --
 
 # Falsifier: one wheel hovers 6 mm over its railhead. Must exit 18.
@@ -146,7 +146,7 @@ 

Wooden Barrel

- +

@@ -298,10 +298,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/modular-kit-snap/index.html b/docs/gallery/modular-kit-snap/index.html index cc7f24c9..ae5b22eb 100644 --- a/docs/gallery/modular-kit-snap/index.html +++ b/docs/gallery/modular-kit-snap/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -56,7 +56,7 @@

Modular Kit Snap

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 --
+
blender --background --python examples/modular-kit-snap/modular_kit_snap.py --
@@ -71,7 +71,7 @@

Modular Kit Snap

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. The surface language is procedural and reads Object coordinates: every part's origin is the segment pivot, so painted-panel wear, the walkway's diamond-plate bump and the diagonal hazard stripes on the trim rails are functions of segment-local position — identical in every instance and continuous across a snapped joint, exactly like the geometry.

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#

-
blender --background --python modular_kit_snap.py --
+
blender --background --python modular_kit_snap.py --
 blender --background --python modular_kit_snap.py -- --seam-gap   # falsifier: tiled duplicate 120 mm past the joint; exits 5
 blender --background --python modular_kit_snap.py -- --output corridor.png
 blender --background --python modular_kit_snap.py -- --falsify seams.png
@@ -120,7 +120,7 @@ 

Socket Attach Points< - +

@@ -272,10 +272,18 @@

Socket Attach Points< // Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/moka-pot/index.html b/docs/gallery/moka-pot/index.html index fec02fc8..abf0cee0 100644 --- a/docs/gallery/moka-pot/index.html +++ b/docs/gallery/moka-pot/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Moka Pot

A procedural three-cup Moka Express-pattern pot — an eight-flat boiler and collector flaring out from an hourglass waist with a proud grip band, a V beak pinched from the rim, a hinged faceted lid and bakelite knob, a two-root bakelite D handle on a riveted fin, and a brass valve — carried through UVs, bake, LOD, compound collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

- + @@ -57,7 +57,7 @@

Moka Pot

category Household

tags mesh export showcase

-
blender --background --python showcase/moka-pot/moka_pot.py --
+
blender --background --python showcase/moka-pot/moka_pot.py --
@@ -89,7 +89,7 @@

Exit codes
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, --flush-joint)
16Grounded zmin (--lift-z)
17Shell count, collector seat, or handle bite (--short-handle)
18Knob, lid, spout, fin, hinge or fastener seat (--float-knob, --float-spout, --float-valve)
19Real-world size, plumb, or eight-fold spread (--lean-pot, --odd-facet)
21Asset-quality gate (examples/gallery_asset_quality.py, render path only)

Run it#

-
# Budget check, no render. ~1 s on a warm 5.2.
+
# Budget check, no render. ~1 s on a warm 5.2.
 blender --background --python moka_pot.py --
 
 # Falsifier: the pot leans at the lid. Must exit 19.
@@ -144,7 +144,7 @@ 

Wooden Barrel

- +

@@ -296,10 +296,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/motor-scooter/index.html b/docs/gallery/motor-scooter/index.html index 84cd6e49..352af538 100644 --- a/docs/gallery/motor-scooter/index.html +++ b/docs/gallery/motor-scooter/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Motor Scooter

A procedural 1960s Italian step-through motor scooter — a pressed-steel tail lofted from superellipse sections with two bulbous side cowls (pressed louvres on the engine side, chrome belt trims), a floorboard with rubber runner strips and chrome edge trims pressed up into the tail and into a leg shield convex in plan, arched up into the headset and rimmed in a rolled chrome trim, with a horn cast and grille, a single-sided front end (a column raked 26 degrees, crown, fork leg, trailing link and coil spring) under a crested mudguard, a headset carrying the headlamp, speedometer, ribbed grips, levers and two mirrors, a two-tone dual saddle with piping and a grab strap, 10-inch split rims with five nuts and finned drums in block-tread tyres, an alloy engine case with a finned cylinder, header, silencer and kick-start, a luggage rack, tail lamp and plate, and a centre stand with rubber feet — through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

- + @@ -57,7 +57,7 @@

Motor Scooter

category Vehicles

tags mesh export showcase

-
blender --background --python showcase/motor-scooter/motor_scooter.py --
+
blender --background --python showcase/motor-scooter/motor_scooter.py --
@@ -88,7 +88,7 @@

Falsifiers
FlagBudget violatedExit
--skip-decimateLOD1 ratio band (measured 1.0000)9
--stray-vertloose vertex count is 0 (measured 1, placed inside the envelope)15
--lift-zbounding box zmin is 0 (measured 0.05000)16
--float-tyreboth tyres and both stand feet on the ground (rear wheel 4 mm up: its zmin 0.00400, the rest 0)16
--toe-wheelwheel alignment (front wheel steered 2.5° about its own vertical: toe 2.5000°)17
--steep-headsteering trail (column turned 6° steeper about its crown: rake 20.000°, trail 0.01683 m)18
--odd-bodybody mirror symmetry (a 3.5 mm sideways bow in the tail, zero at both ends: deviation 0.00695 m)19
--short-wheelbasewheelbase band (rear wheel 15 mm forward: 1.18500 m)20
--narrow-standstance (stand feet drawn in to ±0.060 m: margin 0.0424 m)21
--pop-speedoone connected assembly (speedometer 30 mm out of the headset: 2 components)22
--lift-shieldshield seam (the shield's foot stood 4 mm above the floor's top: −0.00400 m)23

--float-tyre lifts the rear wheel while the front tyre and the stand ground the box. --toe-wheel turns the front wheel about the vertical through its axle, so its contact ring stays where it was and the trail does not move; the rim, drum and nuts turn with the tyre and stay in the link. --steep-head turns only the column, about its crown, so the headset and horn cast stay put. --odd-body bows the tail by sin²(π s) along its length, so both ends stay seated in the floorboard and under the tail lamp; its first draft, a plain sin bow with the luggage rack's rails riding past the tail's shoulder, laid one rail face in the plane of a bowed tail face and exited 15. The rails now follow the crown inside the shoulder, and the falsifier reaches 19. --narrow-stand keeps both feet on the ground; the first draft drew them in to ±0.035 m, where the two soles' faces came within the coplanar range of each other and exited 15, so it stops at ±0.060 m. --short-wheelbase moves the rear wheel inside the envelope; the plate and the mudguard still set the length. --pop-speedo lifts a part held by one joint only: the speedometer is seated in the headset and nothing else. Its first draft moved it 15 mm, which left its base still inside the headset's crown; it moves 30 mm. --lift-shield raises only the shield's foot, its top held, so the fillets follow it and the assembly stays one piece; only the seam budget sees the gap.

Run#

-
blender --background --python motor_scooter.py --
+
blender --background --python motor_scooter.py --
 blender --background --python motor_scooter.py -- --skip-decimate
 blender --background --python motor_scooter.py -- --stray-vert
 blender --background --python motor_scooter.py -- --lift-z
@@ -146,7 +146,7 @@ 

Wooden Barrel

- +

@@ -298,10 +298,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/mushroom-stump/index.html b/docs/gallery/mushroom-stump/index.html index ec66c257..936ac892 100644 --- a/docs/gallery/mushroom-stump/index.html +++ b/docs/gallery/mushroom-stump/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Mushroom Stump

A procedural felled-tree stump on a sloping forest-floor soil disc — one furrowed lathe about a plumb axis flaring through five buttresses into the ground, its bark peeled in two patches to grey sapwood a bark's thickness under a ragged torn lip with curling flakes, a felling top with weathered growth rings, radial drying checks, the undercut floor a step under the back cut and a low band of torn hinge fibres along the step, five surface roots bedded along their run, thin moss cushions laid on the furrowed bark with tufted rims, two tiers of turkey-tail brackets rooted in the bark, three fly agarics with warted caps, pleated gills, volva and skirt, and two clusters of ringed honey fungus out of a root and the stump's foot — with fallen leaves, bipinnate ferns, twigs and pebbles, through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

- + @@ -57,7 +57,7 @@

Mushroom Stump

category Nature

tags mesh export showcase

-
blender --background --python showcase/mushroom-stump/mushroom_stump.py --
+
blender --background --python showcase/mushroom-stump/mushroom_stump.py --
@@ -88,7 +88,7 @@

Falsifiers
FlagBudget violatedExit
--skip-decimateLOD1 ratio band (measured 1.0000)9
--stray-vertloose vertex count is 0 (measured 1, placed inside the envelope)15
--lift-zbounding box zmin is 0 (measured 0.05000)16
--perch-stumpstump sealed all round (the foot set from the soil at the axis alone: sealed in 6/8 sectors)17
--float-capscaps seated on their stipes (every cap moved 24 mm up its axis: stipe bite −0.0156 to −0.0100 m)18
--tilt-cutsaw cut level (the cut plane turned 5° about the hinge chord: measured 4.9584°)19
--fat-stumpstated size (every stump radius × 1.08: 0.6706 m across)19
--arch-rootsroots bedded along their run (each root's middle raised 120 mm: shallowest station −0.0524 m)20
--float-mushroomsstipes rooted in their host (every mushroom moved 50 mm off its host: −0.0249 to −0.0032 m)21
--float-bracketsbrackets rooted in the bark (every shelf moved 35 mm off the bark: −0.0164 to −0.0131 m)22
--sunny-mossmoss on up- and shade-facing surfaces (every cushion and tuft turned half round the stump: 0.4270)23
--float-coverground cover joined (every leaf lifted 25 mm off the soil: 77 of 339 shells loose)24
--flat-fellingfelling step (the undercut floor sawn level with the back cut: step 0.0006 m)25
--tall-hingefelling hinge band low (every torn fibre × 1.25: tallest 0.0358 m; the AABB's top rises 7.2 mm, inside its 10 mm tolerance)25
--flush-peeltorn bark over the sapwood (no bark thickness and no lip: −0.0015 and −0.0011 m)26
--float-flakesflakes rooted in the bark (every flake moved 20 mm off the bark along its normal: −0.0108 to 0.0011 m)26
--slab-mossmoss conforms (every cushion the old 26 mm dome over the plate crests: 0.0360 m)27

--tilt-cut turns the cut about the hinge chord, so the holding wood and the stump's height at its axis stay put and only the tilt fails. --fat-stump scales the stump and leaves the soil, the roots and the holding wood's height alone, so only the diameter fails. --float-caps at 30 mm put one honey cap's crown on a neighbour's gill plane and exited 15; 24 mm keeps it on 18. --float-mushrooms moves soil- and root-grown mushrooms straight up and stump-grown ones out along the bark's normal: lifted, those would have stayed inside the flaring foot. --tall-hinge is sized to the tolerance: the tallest fibre sets the top of the envelope, and × 1.25 clears the 0.034 m band by 1.8 mm while the top moves 7.2 mm of its 10 mm. --arch-roots went from 90 to 120 mm when the roots were shortened for the smaller disc and bedded deeper: at 90 mm the shallowest station still lay 3.4 mm under the soil, a fail by only 2.6 mm.

Run#

-
blender --background --python mushroom_stump.py --
+
blender --background --python mushroom_stump.py --
 blender --background --python mushroom_stump.py -- --skip-decimate
 blender --background --python mushroom_stump.py -- --stray-vert
 blender --background --python mushroom_stump.py -- --lift-z
@@ -152,7 +152,7 @@ 

Wooden Barrel

- +

@@ -304,10 +304,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/office-chair/index.html b/docs/gallery/office-chair/index.html index 7479b9cd..68e00217 100644 --- a/docs/gallery/office-chair/index.html +++ b/docs/gallery/office-chair/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Office Chair

A procedural ergonomic task chair on a chair mat at a desk corner — a polished aluminium five-star base with ribbed spokes and five swivelled hooded twin-wheel casters pressed into a smoked polycarbonate mat, a chrome gas lift in a telescoping cover under a synchro-tilt mechanism with tension knob and paddles, a moulded shell cupping a two-tone sculpted cushion with thigh channels, a waterfall front and two piped welts, an elastomeric mesh back on a graphite frame with an S-curved lumbar pad on sliders, a mesh headrest, 4D armrests, and a snake plant in a stone planter — through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

- + @@ -57,7 +57,7 @@

Office Chair

category Household

tags mesh export showcase

-
blender --background --python showcase/office-chair/office_chair.py --
+
blender --background --python showcase/office-chair/office_chair.py --
@@ -86,7 +86,7 @@

Falsifiers
FlagBudget violatedExit
--skip-decimateLOD1 ratio band (measured 1.0000)9
--stray-vertloose vertex count is 0 (measured 1, placed inside the envelope)15
--lift-zbounding box zmin is 0 (measured 0.05000)16
--float-casterevery wheel pressed into the mat (caster 3's body lifted 5 mm up its stem: its wheels 4.4 mm over the sheet, the other eight still pressed 0.6 mm in)16
--curl-matmat flat on the floor (its back-left corner curled 6 mm up: underside rises to 0.00600 m, clear of every wheel)16
--skew-spokefive-star geometry (spoke 0 and its caster turned 3°: worst gap off 72° by 3.000°)17
--offset-columngas lift coaxial (column moved 3 mm in X: 0.00300 m off both the hub's and the socket's axes)18
--uneven-armsarmrests mirrored and level (right post, buttons, plate and pad raised 10 mm in the cover: 0.01000 m)19
--loose-wheelone connected chair (caster 0's outer wheel slid 24 mm off its axle end: 2 components)20
--float-lumbarlumbar sliders clamped on the frame (pad, straps and sliders floated 6 mm forward: gap 0.07–11.0 mm)21
--float-leavesleaves rooted in the soil (every blade lifted 40 mm: depth −15.5 to −8.4 mm)22

--float-caster lifts the caster body, not the stem, so the stem only rides deeper into the web and the star budget still passes. --curl-mat curls only the corner where −x + y is largest, which no wheel reaches, so the wheels stay seated and the mat-flat budget is the one that fires. --offset-column leaves the column seated 52 mm and 28 mm deep and plumb, so only the coaxial budget sees it. --uneven-arms slides the arm up its own cover, as the height adjuster does, so the chair stays connected and the envelope holds; the seat and armrest heights stay in band. --loose-wheel slides a wheel along its own axle, so it still presses into the mat and the named-supports budget passes; it is attached by one joint, the axle, which is why it is the part that splits. --float-lumbar moves the lumbar unit less than a slider's wall is deep past the frame, so the sliders still bite the frame and the chair stays one component; only the clamp band sees it. --float-leaves lifts the blades inside the envelope: the headrest, not the plant, sets the top.

Run#

-
blender --background --python office_chair.py --
+
blender --background --python office_chair.py --
 blender --background --python office_chair.py -- --skip-decimate
 blender --background --python office_chair.py -- --stray-vert
 blender --background --python office_chair.py -- --lift-z
@@ -144,7 +144,7 @@ 

Wooden Barrel

- +

@@ -296,10 +296,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/palm-tree/index.html b/docs/gallery/palm-tree/index.html index c673ce36..3d2b080a 100644 --- a/docs/gallery/palm-tree/index.html +++ b/docs/gallery/palm-tree/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Palm Tree

A procedural coconut palm on a raised disc of rippled beach sand — a trunk that rises from a swollen, lobed bole with a mat of roots flaring out of it into the sand, leans away along a curve and comes back upright under the crown, tapering, ringed with leaf scars at an even pitch; a fibrous boss of leaf bases carrying eighteen pinnate fronds in a golden-angle spiral, each a curved, drooping rachis with 36 pairs of folded leaflets hanging below it in a V, the young fronds high and rising, the old ones low and hanging; two dead brown fronds against the trunk, two unopened spear leaves, and bunches of green and ripening coconuts under the frond bases; two fallen coconuts, a fallen frond, a driftwood branch, seashells and sea grass — through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

- + @@ -57,7 +57,7 @@

Palm Tree

category Nature

tags mesh export showcase

-
blender --background --python showcase/palm-tree/palm_tree.py --
+
blender --background --python showcase/palm-tree/palm_tree.py --
@@ -88,7 +88,7 @@

Falsifiers
FlagBudget violatedExit
--skip-decimateLOD1 ratio band (measured 1.0000)9
--stray-vertloose vertex count is 0 (measured 1, placed inside the envelope)15
--lift-zbounding box zmin is 0 (measured 0.05000)16
--short-petiolesfronds seated (every rachis and spear starts 80 mm nearer the boss's skin, still inside it: 0.0175–0.0422 m)17
--drop-coconutcoconuts attached (one nut slid out along its axis until 6 mm of it is in the boss, still touching: 0.0037 m)18
--short-rootstip-over margin (every root dives in 0.26 m from the axis: the plate reaches 0.2059–0.3502 m and the mass centre is 0.3507 m outside it)19
--bunch-ringsrings at an even pitch (three rings slid 45 mm down the trunk, still on the bark: spread 0.08992 m)20
--perch-trunkbedding (the trunk's foot 12 mm under the sand, the roots unchanged: shallowest sector 0.0123 m)21
--float-nutsresting (the fallen coconuts raised 16 mm, still touching the sand: 0.0038–0.0047 m)22
--float-covercover joined (shells, driftwood, sea grass and the fallen frond lifted 50 mm: 120 of 1566 shells loose)23

Each falsifier moves only what its budget measures, and none is sized past the point where a second budget would see it. --short-petioles trims each rachis at its base and leaves the rest of the frond where it was, so the envelope and the leaflets do not move. --drop-coconut leaves the nut touching the boss and --float-nuts leaves the nuts touching the sand, so the cover stays joined. --short-roots keeps every root seated in the bole and bedded at its tip. --perch-trunk raises only the bottom of the bole: the axis above the sand, the rings, the roots and the crown do not move.

Run#

-
blender --background --python palm_tree.py --
+
blender --background --python palm_tree.py --
 blender --background --python palm_tree.py -- --skip-decimate
 blender --background --python palm_tree.py -- --stray-vert
 blender --background --python palm_tree.py -- --lift-z
@@ -145,7 +145,7 @@ 

Wooden Barrel

- +

@@ -297,10 +297,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/parent-inverse-orrery/index.html b/docs/gallery/parent-inverse-orrery/index.html index 9f6f2b56..0a3e54d0 100644 --- a/docs/gallery/parent-inverse-orrery/index.html +++ b/docs/gallery/parent-inverse-orrery/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -56,17 +56,17 @@

Parent Inverse Orrery

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 --
+
blender --background --python examples/parent-inverse-orrery/parent_inverse_orrery.py --

A runnable example that assembles a brass orrery — sun, three planets on pivot arms, one moon — entirely through the data API, witnessing the parenting contract that generated Blender code gets wrong most often. child.parent = pivot alone re-interprets the child's local matrix in the pivot's space and the child visibly teleports; keeping the world transform takes the two-line idiom:

-
child.parent = pivot
+
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#

-
# Cheap correctness check (no render) — the CI check:
+
# Cheap correctness check (no render) — the CI check:
 blender --background --python parent_inverse_orrery.py --
 
 # Falsifier: parent without MPI. Must exit non-zero (orbit closed form).
@@ -121,7 +121,7 @@ 

Prop Origin Transform

- +
@@ -273,10 +273,18 @@

Prop Origin Transform scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/park-bench/index.html b/docs/gallery/park-bench/index.html index a0c25683..20e19821 100644 --- a/docs/gallery/park-bench/index.html +++ b/docs/gallery/park-bench/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -57,7 +57,7 @@

Park Bench

category Village

tags mesh export showcase

-
blender --background --python showcase/park-bench/park_bench.py --
+
blender --background --python showcase/park-bench/park_bench.py --
@@ -83,7 +83,7 @@

Falsifiers
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#

-
blender --background --python park_bench.py --
+
blender --background --python park_bench.py --
 blender --background --python park_bench.py -- --skip-decimate
 blender --background --python park_bench.py -- --stray-vert
 blender --background --python park_bench.py -- --lift-z
@@ -139,7 +139,7 @@ 

Wooden Barrel

- +

@@ -291,10 +291,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/pine-tree/index.html b/docs/gallery/pine-tree/index.html index 199e2219..4206772f 100644 --- a/docs/gallery/pine-tree/index.html +++ b/docs/gallery/pine-tree/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Pine Tree

A procedural mature Scots pine — a buttressed trunk with plated lower bark bedded in a needle-litter soil disc, eight seeded whorls of heavy limbs seated in the bark, 113 overlapping brushes of long thin needle strands each on its own twig, dead limbs, hanging cones, fallen cones, mossy stones and sticks — through UVs, bake, LOD, trunk collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

- + @@ -57,7 +57,7 @@

Pine Tree

category Nature

tags mesh export showcase

-
blender --background --python showcase/pine-tree/pine_tree.py --
+
blender --background --python showcase/pine-tree/pine_tree.py --
@@ -90,7 +90,7 @@

Falsifiers
FlagBudget violatedExit
--skip-decimateLOD1 ratio band (measured 1.0000)9
--stray-vertloose vertex count is 0 (measured 1)15
--lift-zbounding box zmin is 0 (measured 0.05000)16
--float-branchesbranch seat (every limb's tube starts at 1.2 × the trunk radius, outside the bark: worst 1.2447)17
--lean-crowntrunk plumb (bole bent x += 0.030 (z − 1)² up to 2.95 m, the crown carried over rigidly: lean 1.205°)19
--bunch-whorlswhorl tier spacing (whorl 5 lifted 0.24 m into whorl 6: 7 tiers, gaps up to 0.7121 m)20
--short-twigsclump seat (every carrier stops where it has entered 0.36 of the way into its pad's ellipsoid, short of the core: shallowest −0.1955 m)22
--float-litterground cover bedded in the soil (cones, stones and sticks lifted 0.08 m: cones −0.0705, stones −0.0352, sticks −0.0703)23
--drop-conesone connected assembly (hanging cones lowered 50 mm off their limbs: 10 components)21

--float-branches keeps each limb's path, so the tip and everything hung on it stay put. Only the tube's first ring moves out of the bark. --lean-crown bends only the bole under the lowest whorl and carries the crown over rigidly, so the envelope keeps its size to the tenth of a millimetre and the plumb budget is what fails. An earlier draft bent the whole axis quadratically, which grew the X extent 17.7 mm and exited 8 on the envelope instead; the bend was moved, the band was not widened. --bunch-whorls lifts one mid-crown whorl, whose limbs are not the extremes of the envelope. --short-twigs keeps the point count of every carrier, so the face budgets hold; it also splits the tree into 2 components, but the clump seat is checked first and names the cause. --float-litter likewise detaches the lifted pieces (9 components), and the bedding budget is checked before the component count. --drop-cones leaves the rest of the tree whole: 9 cones float free and the tree is the 10th component.

Run#

-
blender --background --python pine_tree.py --
+
blender --background --python pine_tree.py --
 blender --background --python pine_tree.py -- --skip-decimate
 blender --background --python pine_tree.py -- --stray-vert
 blender --background --python pine_tree.py -- --lift-z
@@ -146,7 +146,7 @@ 

Wooden Barrel

- +

@@ -298,10 +298,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/planet-rover/index.html b/docs/gallery/planet-rover/index.html index 6d16de2b..4877767c 100644 --- a/docs/gallery/planet-rover/index.html +++ b/docs/gallery/planet-rover/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Planet Rover

A procedural six-wheeled planetary science rover in the rocker-bogie tradition on a patch of rocky red-grey regolith — a cream warm-electronics body with gold-foil insulation blankets quilted into puffed cells, anodised corner posts and a trimmed deck carrying instrument boxes, sample inlets, a calibration target, a UHF can and a whip; a finned radioisotope power unit on struts between two radiator fin banks; on each side a rocker pivoting on a boss in the body side through its eye and pin, and a bogie pinned in a clevis on its front end, the two rockers linked through cranks and links to a differential bar on the deck so they turn equal and opposite; six 0.50 m aluminium drum wheels with 24 chevron grousers, six curved titanium flexure spokes and a drive actuator each, the four corners hanging from steering actuators through C-brackets; a camera mast with a laser telescope window, two camera barrels and navigation cameras; a high-gain dish on a two-axis gimbal; a 5-DOF arm whose turret drills a boulder; cable harnesses along every leg, the mast and the arm; ruts trailing behind both wheel lines and 19 broken rocks, the regolith fitted so every wheel sinks into it — through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

- + @@ -57,7 +57,7 @@

Planet Rover

category Vehicles

tags mesh export showcase

-
blender --background --python showcase/planet-rover/planet_rover.py --
+
blender --background --python showcase/planet-rover/planet_rover.py --
@@ -86,7 +86,7 @@

Falsifiers
FlagBudget violatedExit
--skip-decimateLOD1 ratio band (measured 1.0000)9
--stray-vertloose vertex count is 0 (measured 1)15
--lift-zbounding box zmin is 0 (measured 0.05000)16
--float-wheelevery wheel sunk into the regolith (the right middle wheel lifted 30 mm off its actuator: sink −0.0184 m)16
--offset-pinpivot pins coaxial (the right bogie pin 4 mm forward: 0.00400 m off its bushings' axis)17
--sink-grousersgrousers seated (the right front wheel's grousers pressed 3 mm into the drum: root 0.00400 m in, crown 0.00450 m proud)18
--lean-mastmast plumb and size (the mast turned 1.5° about its foot: tilt 1.5000°, height 2.1034 m still in band)19
--jam-rockerdifferential (the right rocker turned 1.5° past the bar: deflections +4.0°, −2.5°, residual 1.5000°, link eye 0.00785 m off its crank pin)20
--offset-steersteering axes (the right front steering actuator 10 mm forward: 0.01000 m off the wheel's centre)21
--camber-wheelaxles level and lateral (the right front wheel tilted 1° about its centre: 1.0000°)22
--bunch-grousersgrouser pitch (one right front grouser turned 3° about the axle: pitch off by 3.0148°)23
--overload-turretstance (the turret 40 times its mass, 296.7 kg: centre at x 0.467, margin 0.6645 m < 0.7470 m)24
--loose-dishone connected assembly (the dish, hub, feed and struts lifted 0.12 m along the boresight off the elevation axle: 2 components, 6 shells and 435)25

--float-wheel lifts the middle wheel because its contact patch lies on the support polygon's edge between the front and rear wheels' patches: its first draft lifted the right front wheel, which also removed a corner of the polygon and failed the stance budget with it. --sink-grousers replaced a first draft that floated the grousers 3 mm off the skin; that also split them from the drum and failed the assembly budget. Pressed in, they stay attached and only the seat band sees them; the wheel still sinks in band (the tips are 3 mm higher). --jam-rocker keeps the regolith fitted to the jammed stance (the wheels still sink in band) and builds the differential for the nominal one, so only the differential sees it; the height moves 3 mm, inside its band. --loose-dish's first draft moved the dish 60 mm and left the hub still round the axle (exit 0); the dish hangs on the gimbal by that one joint, and at 0.12 m it comes away alone. --camber-wheel widens the track 4.3 mm, inside its 6 mm tolerance, and --lean-mast raises the envelope 2.9 mm, inside BBOX_TOL.

Run#

-
blender --background --python planet_rover.py --
+
blender --background --python planet_rover.py --
 blender --background --python planet_rover.py -- --skip-decimate
 blender --background --python planet_rover.py -- --stray-vert
 blender --background --python planet_rover.py -- --lift-z
@@ -146,7 +146,7 @@ 

Wooden Barrel

- +

@@ -298,10 +298,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/png-exr-alpha/index.html b/docs/gallery/png-exr-alpha/index.html index c24164a7..95928b7e 100644 --- a/docs/gallery/png-exr-alpha/index.html +++ b/docs/gallery/png-exr-alpha/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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).

- + @@ -56,7 +56,7 @@

PNG EXR Alpha

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 --
+
blender --background --python examples/png-exr-alpha/png_exr_alpha.py --
@@ -70,7 +70,7 @@

PNG EXR Alpha

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 thin-bezel monitors on machined stands, sharing a walnut-topped console — the left screen bakes the closed-form PNG mangling (dark rows flash to white at low-alpha columns), the right screen shows the authored straight buffer (EXR-clean); brass desk plates in front read FLOAT → PNG and FLOAT → EXR. If the contract failed, both screens would read the same. The render path gates framing through examples/gallery_framing.py (exit 10) before writing the still.

Run#

-
# Cheap correctness check (no render) — the CI check:
+
# Cheap correctness check (no render) — the CI check:
 blender --background --python png_exr_alpha.py --
 
 # Falsifier: opaque authored alpha. Must exit non-zero (float→PNG error floor).
@@ -123,7 +123,7 @@ 

Compositor Glare

- +
@@ -275,10 +275,18 @@

Compositor Glare

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/pond-edge/index.html b/docs/gallery/pond-edge/index.html index 36124e90..57870747 100644 --- a/docs/gallery/pond-edge/index.html +++ b/docs/gallery/pond-edge/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Pond Edge

A procedural pond margin on a raised soil disc — a shallow pond whose water is its own mesh at a declared level, rippled in low rings round the reeds and the branch, its rim run on under a sloped mud bank; a clump of cattails with arching strap leaves and five stems, each threaded through a brown seed head with a bare spike above it; soft rush and turf tufts on the bank; five notched lily pads floating on the water with two layered white-and-pink water lilies; wet stones at the waterline, a half-submerged branch, pebbles and fallen leaves — through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

- + @@ -57,7 +57,7 @@

Pond Edge

category Nature

tags mesh export showcase

-
blender --background --python showcase/pond-edge/pond_edge.py --
+
blender --background --python showcase/pond-edge/pond_edge.py --
@@ -87,7 +87,7 @@

Falsifiers
FlagBudget violatedExit
--skip-decimateLOD1 ratio band (measured 1.0000)9
--stray-vertloose vertex count is 0 (measured 1, placed inside the envelope)15
--lift-zbounding box zmin is 0 (measured 0.05000)16
--float-reedsrooted reeds (every leaf and stem base raised 70 mm, fading to 0 at the tip: lowest vertex −0.0303 to −0.0255 m, above the mud)17
--sink-padfloating pads (the first pad pushed 4 mm under, its rim still awash: freeboard −0.00251 m, draft 0.00550 m)18
--flat-waterwater level and ripple band (the sheet laid flat: ripple 0.00000 m)19
--slip-headsthreaded heads (every head slid 8 mm off its stem's axis, still pierced by it: 0.0079–0.0080 m off)20
--short-waterenclosed water (the rim stopped 60 mm short of the shoreline: the bank 0.03736 m below it)21
--float-covercover joined (every rush blade, pebble and bank leaf lifted 30 mm: 108 of 598 shells loose)22

--float-reeds holds each tip where it was, so the envelope, set by a spike tip, does not move; the heads stay threaded (1.0 mm off-axis). The stems and leaves still cross the water, so the cover budget still joins them. --sink-pad pushes the pad 4 mm, not more: pushed 12 mm it drowned whole and came loose from the water, which the cover budget saw too. --slip-heads slides 8 mm for the same reason: at 25 mm the heads left their stems and came loose. --flat-water changes no draw, so the pads float exactly as before. --short-water changes only the water; the ripples, the pads and the cover are unchanged. --stray-vert and --lift-z necessarily move the loose-shell count and the water's absolute level with them; they exit on their own, earlier codes.

Run#

-
blender --background --python pond_edge.py --
+
blender --background --python pond_edge.py --
 blender --background --python pond_edge.py -- --skip-decimate
 blender --background --python pond_edge.py -- --stray-vert
 blender --background --python pond_edge.py -- --lift-z
@@ -143,7 +143,7 @@ 

Wooden Barrel

- +

@@ -295,10 +295,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/prop-origin-transform/index.html b/docs/gallery/prop-origin-transform/index.html index fafc72aa..f1f3a2e5 100644 --- a/docs/gallery/prop-origin-transform/index.html +++ b/docs/gallery/prop-origin-transform/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -56,7 +56,7 @@

Prop Origin Transform

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 --
+
blender --background --python examples/prop-origin-transform/prop_origin_transform.py --
@@ -71,7 +71,7 @@

Prop Origin Transform

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#

-
blender --background --python prop_origin_transform.py --
+
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
@@ -119,7 +119,7 @@

Mesh Hygiene Audit

- +
@@ -271,10 +271,18 @@

Mesh Hygiene Audit

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/quad-drone/index.html b/docs/gallery/quad-drone/index.html index ce56715d..85d9fdbb 100644 --- a/docs/gallery/quad-drone/index.html +++ b/docs/gallery/quad-drone/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Quad Drone

A procedural camera quadcopter — a two-tone moulded shell with panel lines, grilles, a sensor visor, a battery pack and a GPS puck, four folding arms hinged on pinned clevises with lock rings, slotted brushless motors carrying twisted, swept two-blade propellers, rubber-footed skids and a three-axis gimbal camera — through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

- + @@ -57,7 +57,7 @@

Quad Drone

category Vehicles

tags mesh export showcase

-
blender --background --python showcase/quad-drone/quad_drone.py --
+
blender --background --python showcase/quad-drone/quad_drone.py --
@@ -87,7 +87,7 @@

Falsifiers
FlagBudget violatedExit
--skip-decimateLOD1 ratio band (measured 1.0000)9
--stray-vertloose vertex count is 0 (measured 1, placed inside the envelope)15
--lift-zbounding box zmin is 0 (measured 0.05000)16
--float-footevery foot on the ground (front-left foot 3 mm up: its zmin 0.00300, the other three 0)16
--unlock-armmotor axes on an exact X (rear-left arm 2.5° short of locked open: worst gap 1.491° off 90°, motor centroid 1.69 mm off centre)17
--long-bladespropeller clearance (rear-left blades 14 mm longer: disc 0.1795 m, gap to both neighbours 0.0226 m)18
--narrow-skidsstance (skids drawn in to ±0.075 m: margin 0.0776 m)19
--drop-lensone connected assembly (glass element 12 mm out of its barrel: 2 components)20

The rear-left arm is the one both --unlock-arm and --long-blades move because its prop is parked across its arm, so neither its tips nor its motor ever set the envelope. --unlock-arm leaves the prop gaps inside their band (0.0317 and 0.0412 m), so only the layout budget sees it. --long-blades leaves every motor where it was. --float-foot lifts one foot while the other three still ground the box. --narrow-skids keeps the feet on the ground and the envelope unchanged; only the footprint shrinks. It stops at ±0.075 m on purpose: drawn in to ±0.044 m, the front struts ran through the gimbal's roll arm and the run exited 15 on 69 coplanar pairs instead of 19. The stance footprint is each foot's sole (its lowest ring) rather than only the vertices touching the floor. Otherwise --float-foot also collapsed the footprint to a triangle (margin 0.0039 m) and broke two budgets at once.

Run#

-
blender --background --python quad_drone.py --
+
blender --background --python quad_drone.py --
 blender --background --python quad_drone.py -- --skip-decimate
 blender --background --python quad_drone.py -- --stray-vert
 blender --background --python quad_drone.py -- --lift-z
@@ -142,7 +142,7 @@ 

Wooden Barrel

- +

@@ -294,10 +294,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/ray-cast-space/index.html b/docs/gallery/ray-cast-space/index.html index f8d1164c..63add8e4 100644 --- a/docs/gallery/ray-cast-space/index.html +++ b/docs/gallery/ray-cast-space/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Ray Cast Space

Object.ray_cast is object-local while Scene.ray_cast is world space: rays mapped through matrix_world.inverted() (directions by its 3x3 only) hit the same points both ways, and raw world coords handed to Object.ray_cast miss.

- + @@ -56,7 +56,7 @@

Ray Cast Space

witnesses Five closed-form rays on a translated, Z-rotated, non-uniformly scaled block: Scene.ray_cast hits each world point and normal exactly; Object.ray_cast fed matrix_world.inverted() coordinates hits the same polygons, and mapped back lands within 5e-7 m; world coords fed to Object.ray_cast land 0.287 m or more off. --world-to-object exits 4 (1.125 m error).

tags objects transforms depsgraph

-
blender --background --python examples/ray-cast-space/ray_cast_space.py --
+
blender --background --python examples/ray-cast-space/ray_cast_space.py --
@@ -67,7 +67,7 @@

Ray Cast Space

Both 5.2 LTS and 4.5 LTS give identical results: every hit exact to under 1e-6 m, and the world-coordinate trap's closest landing 0.287 m away.

The still shows the block glazed in a teal checker laid out in object coordinates. The cells are square in local space, so the non-uniform scale stretches them into the block's own grid. The block stands on a walnut plinth. Five orange rays run from brass emitter balls to orange hit rings that sit exactly on three faces: the correct casts. One red ray is the first front-face ray's world origin and direction read by Object.ray_cast as if local. In the world that is the ray matrix_world @ origin along matrix_world.to_3x3() @ direction. It starts somewhere else entirely and strikes the far side of the block. A render-only Bevel modifier rounds the block's edges after the check has run.

Run#

-
# Cheap correctness check (no render) — the CI check:
+
# Cheap correctness check (no render) — the CI check:
 blender --background --python ray_cast_space.py --
 
 # Falsifier: feed WORLD coords to Object.ray_cast in the checked path. Must exit 4.
@@ -120,7 +120,7 @@ 

Armature Bend

- +
@@ -272,10 +272,18 @@

Armature Bend

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/road-bicycle/index.html b/docs/gallery/road-bicycle/index.html index 98965f0f..5fd2f93b 100644 --- a/docs/gallery/road-bicycle/index.html +++ b/docs/gallery/road-bicycle/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Road Bicycle

A procedural 1970s lugged-steel road bicycle standing on its side kickstand — a 56 cm diamond frame of round tubes joined in chromed, spear-pointed lugs at the head tube, seat cluster and bottom bracket, tapered stays in chromed tips, a fork with a chromed sloping crown and socks; two 700c wheels, each a box-section rim with 36 spokes laced three-cross from both flanges of a small-flange hub to nipples through the rim, quick-release skewers and tan-wall tyres with a black file-tread crown; a 52/42 crankset with quill pedals, toe clips and straps, a six-speed freewheel, a roller chain of 112 individual links solved round the ring, the cog and both jockey wheels, front and rear derailleurs, down-tube shifters, side-pull calipers with pads a millimetre off the rims, levers under gum hoods on cotton-taped drop bars, a quill stem, a leather saddle on rails and a bottle in a bolted wire cage — leaned 5° onto its stand, through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

- + @@ -57,7 +57,7 @@

Road Bicycle

category Vehicles

tags mesh export showcase

-
blender --background --python showcase/road-bicycle/road_bicycle.py --
+
blender --background --python showcase/road-bicycle/road_bicycle.py --
@@ -86,7 +86,7 @@

Falsifiers
FlagBudget violatedExit
--skip-decimateLOD1 ratio band (measured 1.0000)9
--stray-vertloose vertex count is 0 (measured 1, placed inside the envelope)15
--lift-zbounding box zmin is 0 (measured 0.05000)16
--float-tyreboth tyres and the stand foot on the ground (rear tyre 3 mm up: its zmin 0.00300, the rest 0)16
--slip-wheelhubs coaxial with their dropouts (front wheel 2 mm forward in its dropouts: 0.00200 m)17
--short-spokespoke seats (one front spoke 9 mm short at the rim: its end 1.5 mm outside its nipple, −0.00150 m)18
--dish-wheelwheels in the frame's plane (rear rim, tyre and nipples 3 mm to the drive side, spokes re-aimed from their flanges: 0.00299 m)19
--steep-headsteering trail (head tube turned 6° steeper about its middle: trail −0.02926 m)20
--bunch-spokeslacing (one front spoke and nipple turned 4° about the axle: pitch off by 4.000°)21
--lift-chainchain seated (the chain wrapped 5 mm high on ring and cog: seated up to 8.11 mm above the root)22
--shift-rollerschain line (every roller pushed 1.5 mm inboard along its pin: 0.00150 m)23
--tuck-standstance (the stand's foot drawn in to 40 mm off the tyres' line: margin −0.0062 m)24
--pop-bottleone connected assembly (the bottle lifted 80 mm straight off the tube, clear of its cage: 2 components, bottle and cap)25

--float-tyre lifts only the rear tyre, so the rim, spokes and chain stay seated and only the support budget sees it. --slip-wheel moves the whole front wheel (tyre, rim, spokes, nipples, hub, skewer) along its dropouts, so every spoke still seats and the trail stays in band (53.4 mm); only the hub-to-eye distance sees it. Its first draft moved it 3 mm, which set a hub face on a dropout eye's plane and exited 15. --short-spoke pulls one spoke's rim end back along the spoke, which barely turns it about the axle, so the lacing still passes. --dish-wheel moves the rim horizontally in world space rather than along the leaned axle, which would have lifted the rear tyre off the ground and exited 16. --steep-head turns only the head tube, in the frame's plane, so the frame-plane budget before it is unchanged. --shift-rollers replaced a first draft that bent the chain's rear half inboard: a gradient of 2 mm over the chain stay moved each plate 0.12 mm per two links, exactly one stagger level, and 4528 plate faces landed on each other's planes (exit 15). Moving the rollers along their own pins leaves every plate face where it was. The chain audit counts a roller as engaged only when its body straddles the sprocket's plane; with contact alone the shifted rollers' pin tips touched the 19 and the audit chose it as the driven cog (exit 22). --pop-bottle lifts a part held by one joint only, and it caught a real defect: its first draft moved the bottle 30 mm sideways and split off seven shells, not two — the wire cage had been hanging on the bottle, its spines never reaching the bosses. The cage now has a crossbar and bolt at each boss, and the bottle, lifted 80 mm clear of the hoops, comes away alone with its cap. --float-tyre's first draft lifted the tyre 4 mm and set a face on a brake pad's plane (exit 15); it lifts 3 mm.

Run#

-
blender --background --python road_bicycle.py --
+
blender --background --python road_bicycle.py --
 blender --background --python road_bicycle.py -- --skip-decimate
 blender --background --python road_bicycle.py -- --stray-vert
 blender --background --python road_bicycle.py -- --lift-z
@@ -146,7 +146,7 @@ 

Wooden Barrel

- +

@@ -298,10 +298,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/rope-bridge/index.html b/docs/gallery/rope-bridge/index.html index ee1e7596..06d22ab3 100644 --- a/docs/gallery/rope-bridge/index.html +++ b/docs/gallery/rope-bridge/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -57,7 +57,7 @@

Rope Bridge

category Village

tags mesh export showcase

-
blender --background --python showcase/rope-bridge/rope_bridge.py --
+
blender --background --python showcase/rope-bridge/rope_bridge.py --
@@ -86,7 +86,7 @@

Exit codes
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)
22Gallery asset-quality violation (render path; the floors' 11 is remapped here)

Run it#

-
# Budget check, no render. ~2.2 s on 4.5, ~2.0 s on 5.1, ~2.2 s on 5.2.
+
# 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 --
 
 # Falsifier: the deck stops hanging as a parabola. Must exit 19.
@@ -141,7 +141,7 @@ 

Wooden Barrel

- +

@@ -293,10 +293,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/sawhorse-plank/index.html b/docs/gallery/sawhorse-plank/index.html index 855cbf79..cceab403 100644 --- a/docs/gallery/sawhorse-plank/index.html +++ b/docs/gallery/sawhorse-plank/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Sawhorses and Plank

A procedural pair of pine sawhorses carrying a fir plank, with a handsaw leaning against one — tapered legs splayed and housed into each beam, nailed end aprons, eased arrises baked from a rounded high mesh, a toothed blade in a closed apple handle with brass split nuts — through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

- + @@ -57,7 +57,7 @@

Sawhorses and Plank

category Village

tags mesh export showcase

-
blender --background --python showcase/sawhorse-plank/sawhorse_plank.py --
+
blender --background --python showcase/sawhorse-plank/sawhorse_plank.py --
@@ -89,7 +89,7 @@

Exit codes
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build, has no UV layer, or a part count is wrong
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 (--lift-z)
17Leg housing into its beam (--loose-leg)
18A foot off the floor (--short-leg)
19Leg splay or real-world size (--uneven-splay)
20Plank seat on a saddle (--low-horse)
24Asset-quality floors (examples/gallery_asset_quality.py, render path only)

Run it#

-
# Budget check, no render. A few seconds warm.
+
# Budget check, no render. A few seconds warm.
 blender --background --python sawhorse_plank.py --
 
 # Falsifier: the second horse 6 mm low under the plank. Must exit 20.
@@ -144,7 +144,7 @@ 

Wooden Barrel

- +

@@ -296,10 +296,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/sea-stack-arch/index.html b/docs/gallery/sea-stack-arch/index.html index f3b5d4ef..ce879185 100644 --- a/docs/gallery/sea-stack-arch/index.html +++ b/docs/gallery/sea-stack-arch/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Sea Stack Arch

A procedural coastal diorama tile — a fragment of a bedded sandstone headland at the sea's edge as one eroded mass: eighteen level hard and soft beds running on from the legs into the roof and the stack, jointed faces leaning back and battered and flared at the foot, one weathering field read on the face through every bed, fallen-out blocks and open joints, soft beds undercut to a wave-cut notch; two legs curving in on an ellipse to meet at the crown under a roof that steps down seaward; a tapering stack standing in the sea; the sea filling the tile to its rim with foam at the shore and round the feet, a wave-cut platform, tide pools and a shingle cove, fallen blocks, boulders and pebbles, and a draped turf mat with sea grass and thrift cushions — through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

- + @@ -57,7 +57,7 @@

Sea Stack Arch

category Nature

tags mesh export showcase

-
blender --background --python showcase/sea-stack-arch/sea_stack_arch.py --
+
blender --background --python showcase/sea-stack-arch/sea_stack_arch.py --
@@ -88,7 +88,7 @@

Falsifiers
FlagBudget violatedExit
--skip-decimateLOD1 ratio band (measured 1.0000)9
--stray-vertloose vertex count is 0 (measured 1)15
--lift-zbounding box zmin is 0 (measured 0.05000)16
--gap-lintelbeds bite (the left leg's top bed stopped 15 mm under the roof: −0.0150 m)17
--lean-stackstack plumb (the stack sheared 0.50 m per metre of height toward the sea: 0.0571 m)19
--cut-pillarfeet sealed (the right leg's foot stopped 20 mm over the seabed: worst sector −0.0406 m, 0 of 8)20
--close-archaperture (the right leg's side of the opening narrowed to a tenth: width 0.2080 m)21
--tilt-bedsbedding level (the interior bed boundaries turned 2.5° about a line across the fin: 2.530°)22
--flush-bedsundercut (every soft bed's middle rings 2 mm behind the hard beds, its bites unchanged: 0.0020–0.0065 m on the failing beds)23
--flat-seawater level and ripple (the sheet laid flat: ripple 0.00000 m)24
--short-seawater contained (the rim stopped 50 mm short of the shoreline: −0.01925 m)25
--lift-foamfoam seated (the foam raised 20 mm: 0.02430 m off the water, land edge −0.01465 m)26
--perch-talusloose rock sealed (each piece seated against the ground at its centre alone: −0.0420 m)27
--pile-talusloose rock apart (three blocks drawn together after the relaxation: 1 pair overlaps)28
--float-covercover rooted (every blade, stalk and cushion base raised 50 mm, its tip held: −0.0335 m, all 398 pieces loose)29
--tidy-rockragged (every bed squared to a box on its column's axes, leaning and battered as before but without weathering, fallen blocks, joints or the curved arch: 0.8314)31

The roof's grass and thrift are the top of the envelope, so --float-cover holds every tip where it was and moves the bases alone. --lean-stack shears the stack horizontally about its foot, which keeps each bed's footprint over the one below at every height, so the bites do not steal it; --close-arch narrows only the right leg's side of the hole, so the legs' faces never come to share a plane; --tilt-beds turns the interior boundaries only, leaving the foot and the top. --tidy-rock keeps the triangle count (every ring keeps its rays) and spreads the cover over the weathered top's outline, so it changes the rock alone.

Run#

-
blender --background --python sea_stack_arch.py --
+
blender --background --python sea_stack_arch.py --
 blender --background --python sea_stack_arch.py -- --skip-decimate
 blender --background --python sea_stack_arch.py -- --stray-vert
 blender --background --python sea_stack_arch.py -- --lift-z
@@ -151,7 +151,7 @@ 

Wooden Barrel

- +

@@ -303,10 +303,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/shader-node-group/index.html b/docs/gallery/shader-node-group/index.html index fc60c9f2..5dffae43 100644 --- a/docs/gallery/shader-node-group/index.html +++ b/docs/gallery/shader-node-group/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -56,7 +56,7 @@

Shader Node Group

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 --
+
blender --background --python examples/shader-node-group/shader_node_group.py --
@@ -67,7 +67,7 @@

Shader Node Group

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#

-
# Cheap correctness check (no render) — the CI check:
+
# Cheap correctness check (no render) — the CI check:
 blender --background --python shader_node_group.py --
 
 # Falsifier: identical instance Tints. Must exit non-zero.
@@ -120,7 +120,7 @@ 

Color Attribute Wheel - +

@@ -272,10 +272,18 @@

Color Attribute Wheel scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/shape-key-blend/index.html b/docs/gallery/shape-key-blend/index.html index 18c3fa35..444393f8 100644 --- a/docs/gallery/shape-key-blend/index.html +++ b/docs/gallery/shape-key-blend/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -56,7 +56,7 @@

Shape Key Blend

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 --
+
blender --background --python examples/shape-key-blend/shape_key_blend.py --
@@ -66,7 +66,7 @@

Shape Key Blend

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#

-
# Cheap correctness check (no render) — the CI check:
+
# Cheap correctness check (no render) — the CI check:
 blender --background --python shape_key_blend.py --
 
 # Falsifier: Tall.value = 0. Must exit non-zero.
@@ -119,7 +119,7 @@ 

Color Attribute Wheel - +

@@ -271,10 +271,18 @@

Color Attribute Wheel scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/shipping-crate/index.html b/docs/gallery/shipping-crate/index.html index bd85ecd3..874b8519 100644 --- a/docs/gallery/shipping-crate/index.html +++ b/docs/gallery/shipping-crate/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Shipping Crate

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

- + @@ -57,7 +57,7 @@

Shipping Crate

category Village

tags mesh export showcase

-
blender --background --python showcase/shipping-crate/shipping_crate.py --
+
blender --background --python showcase/shipping-crate/shipping_crate.py --
@@ -87,7 +87,7 @@

Falsifiers
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#

-
blender --background --python shipping_crate.py --
+
blender --background --python shipping_crate.py --
 blender --background --python shipping_crate.py -- --skip-decimate
 blender --background --python shipping_crate.py -- --stray-vert
 blender --background --python shipping_crate.py -- --lift-z
@@ -141,7 +141,7 @@ 

Campfire

- +

@@ -293,10 +293,18 @@

Campfire

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/ships-wheel/index.html b/docs/gallery/ships-wheel/index.html index 4f912ecf..dc42f80b 100644 --- a/docs/gallery/ships-wheel/index.html +++ b/docs/gallery/ships-wheel/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Ship's Wheel

A procedural eight-spoke ship's wheel on its pedestal — turned teak spokes and handles, a laminated rim with a brass inlay, a brass nave on an iron shaft in a bolted bearing collar, a helm stand with a moulded plinth and base, panelled column, brass band, coved capital and cap — through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

- + @@ -57,7 +57,7 @@

Ship's Wheel

category Village

tags mesh export showcase

-
blender --background --python showcase/ships-wheel/ships_wheel.py --
+
blender --background --python showcase/ships-wheel/ships_wheel.py --
@@ -89,7 +89,7 @@

Exit codes
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build, has no UV layer, or a part count is wrong
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 (--lift-z)
17Nave off the rim's centre, shaft off its axis, or bore clearance out of band (--offset-hub)
18A spoke or handle not seated in the rim (--short-spoke)
19Spoke spacing, a handle off its spoke's axis, or real-world size (--skew-spoke)
21Asset-quality floors (examples/gallery_asset_quality.py, render path only)

Run it#

-
# Budget check, no render. A few seconds warm.
+
# Budget check, no render. A few seconds warm.
 blender --background --python ships_wheel.py --
 
 # Falsifier: one spoke turned 2 degrees. Must exit 19.
@@ -144,7 +144,7 @@ 

Wooden Barrel

- +

@@ -296,10 +296,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/signpost/index.html b/docs/gallery/signpost/index.html index 9b4d08d4..3437e7ec 100644 --- a/docs/gallery/signpost/index.html +++ b/docs/gallery/signpost/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Signpost

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

- + @@ -57,7 +57,7 @@

Signpost

category Village

tags mesh export showcase

-
blender --background --python showcase/signpost/signpost.py --
+
blender --background --python showcase/signpost/signpost.py --
@@ -85,7 +85,7 @@

Falsifiers
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#

-
blender --background --python signpost.py --
+
blender --background --python signpost.py --
 blender --background --python signpost.py -- --skip-decimate
 blender --background --python signpost.py -- --stray-vert
 blender --background --python signpost.py -- --lift-z
@@ -138,7 +138,7 @@ 

Wooden Barrel

- +

@@ -290,10 +290,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/skate-ramp/index.html b/docs/gallery/skate-ramp/index.html index 0a730c64..89fe9af2 100644 --- a/docs/gallery/skate-ramp/index.html +++ b/docs/gallery/skate-ramp/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Skate Ramp

A procedural backyard quarter-pipe, 1.2 m high on a 1.8 m transition and 2.44 m wide, with a skateboard on its deck — five plywood transition templates (curved bands with a solid toe and an arm under the deck) on 2x4 sills with studs and a back post sistered to their faces, nine 2x4 stringers under a two-layer 12 mm plywood skin whose joints are offset between layers and land on stringers, 143 screws in rows over the stringers, a 60 mm steel coping pipe standing proud of the transition on five welded, bolted tabs, a steel kicker plate ground to a lip at the toe on a timber toe block, a worn stencilled roundel and wheel marks on the riding face; the skateboard a seven-ply maple deck with concave and kicktails under grip tape, two trucks (baseplate, raked kingpin, bushings, hanger, pivot arm, axle) and four urethane wheels on bearings — through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

- + @@ -57,7 +57,7 @@

Skate Ramp

category Sports

tags mesh export showcase

-
blender --background --python showcase/skate-ramp/skate_ramp.py --
+
blender --background --python showcase/skate-ramp/skate_ramp.py --
@@ -84,7 +84,7 @@

Falsifiers
FlagBudget violatedExit
--skip-decimateLOD1 ratio band (measured 1.0000)9
--stray-vertloose vertex count is 0 (measured 1, placed inside the envelope)15
--lift-zbounding box zmin is 0 (measured 0.05000)16
--float-sillevery sill on the floor (the middle sill 3 mm up: its zmin 0.003, the rest 0)16
--short-ribevery template seated under the skin (the middle template's curved edge 4 mm short: bite −3.10 mm)17
--small-wheelevery wheel on the deck (one wheel 2 mm smaller: bite −1.50 mm)18
--sag-skintransition on its circle (the middle top sheet 4 mm down mid-span: radius 1.77073, deviation 1.55 mm, lowest point 5.65 mm)19
--sink-copingcoping reveal (the pipe 6 mm deeper into the ramp: reveal −1.00 mm)20
--proud-platekicker plate flush (its top 3 mm proud at the joint, ramped to 0 at the toe: step 2.99 mm, toe still 0.80 mm)21
--stack-seamsseams staggered (the inner joint moved 10 mm from a top joint: stagger 0.010 m, still over its stringer)22
--miss-screwsscrews over stringers (the row at 1.44 m moved 60 mm along the arc: 20.6 mm past a stringer's edge)23
--skew-wheelwheels coaxial (one wheel 2 mm along the deck: 2.00 mm off its axle)24
--lift-gripone connected assembly (grip tape and its eight bolt heads lifted 1 mm: 2 components)25

--stray-vert also leaves the stray vertex as a second component and --lift-z also moves the fitted arc and the plate's toe off the ground; both fail their target first. --float-sill lifts only the sill: its template, still standing where it was, sits 5 mm into it, inside the seat band. --sag-skin sags the sheet as sin², level with its neighbours at both joints; the screws follow the sheet they hold down, so they stay seated. --stack-seams moves the inner joint rather than a top one: moving a top joint moved sheet corners onto the collider's side faces and tripped its ceiling (exit 11) first. --small-wheel shrinks one wheel's tread and keeps its bore, so it stays on its bearings and coaxial; --skew-wheel slides one along the deck and keeps its lowest point, so it stays on the deck.

Run#

-
blender --background --python skate_ramp.py --
+
blender --background --python skate_ramp.py --
 blender --background --python skate_ramp.py -- --skip-decimate
 blender --background --python skate_ramp.py -- --stray-vert
 blender --background --python skate_ramp.py -- --lift-z
@@ -144,7 +144,7 @@ 

Wooden Barrel

- +

@@ -296,10 +296,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/sky-texture-sun-elevation/index.html b/docs/gallery/sky-texture-sun-elevation/index.html index 87683fbc..0823a192 100644 --- a/docs/gallery/sky-texture-sun-elevation/index.html +++ b/docs/gallery/sky-texture-sun-elevation/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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).

- + @@ -56,7 +56,7 @@

Sky Texture Sun Elevation

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 --
+
blender --background --python examples/sky-texture-sun-elevation/sky_texture_sun_elevation.py --
@@ -74,7 +74,7 @@

Stage deviationFraming#

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#

-
# Zenith-luminance correctness check (tiny Cycles CPU EXR probes) — the CI check:
+
# Zenith-luminance correctness check (tiny Cycles CPU EXR probes) — the CI check:
 blender --background --python sky_texture_sun_elevation.py --
 
 # Falsifier: Sky→Background unlinked. Must exit non-zero (world links).
@@ -126,7 +126,7 @@ 

Text Version Stamp

- +
@@ -278,10 +278,18 @@

Text Version Stamp

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/soccer-ball-goldberg/index.html b/docs/gallery/soccer-ball-goldberg/index.html index 3da1f507..f765391e 100644 --- a/docs/gallery/soccer-ball-goldberg/index.html +++ b/docs/gallery/soccer-ball-goldberg/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -56,7 +56,7 @@

Soccer Ball Goldberg

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 --
+
blender --background --python examples/soccer-ball-goldberg/soccer_ball_goldberg.py --
@@ -74,7 +74,7 @@

Staging (render path only)Stage deviation#

The floor is dark mown turf rather than the near-black studio floor: a ball alone on a black stage read as an object in a void, and the pitch is the context that makes it a match ball. The back wall and warm wedge are the default stage; measured stage luminance 0.217 sits inside the calibration set.

Run#

-
# Cheap correctness check (no render) — the CI check:
+
# Cheap correctness check (no render) — the CI check:
 blender --background --python soccer_ball_goldberg.py --
 
 # Falsifier: swap pentagon/hexagon slots. Must exit non-zero.
@@ -127,7 +127,7 @@ 

Collision Hull Proxy< - +

@@ -279,10 +279,18 @@

Collision Hull Proxy< // Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/soccer-goal/index.html b/docs/gallery/soccer-goal/index.html index f1d0e0b3..fb2719b6 100644 --- a/docs/gallery/soccer-goal/index.html +++ b/docs/gallery/soccer-goal/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Soccer Goal

A procedural freestanding 5 × 2 m youth soccer goal — posts and crossbar in one oval aluminium extrusion with a net channel, mitred and welded at the corners, cast corner brackets, foot connectors and rear hubs, sloped supports and a steel ground frame pinned by staples and spikes, and a sagging diamond-mesh net whose red head rope runs through 53 nylon clips — on a turf patch with a painted goal line, through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

- + @@ -57,7 +57,7 @@

Soccer Goal

category Sports

tags mesh export showcase

-
blender --background --python showcase/soccer-goal/soccer_goal.py --
+
blender --background --python showcase/soccer-goal/soccer_goal.py --
@@ -87,7 +87,7 @@

Falsifiers
FlagBudget violatedExit
--skip-decimateLOD1 ratio band (measured 1.0000)9
--stray-vertloose vertex count is 0 (measured 1, placed inside the envelope)15
--lift-zbounding box zmin is 0 (measured 0.05000)16
--float-barevery ground bar bedded in the turf (back bar lifted 8 mm: bed −0.0055 m, the two side bars 0.0025)16
--short-supportsupports seated in their sockets (left support stops 90 mm short of its hub: insertion 0.000 m at that end, 0.080 at the bracket)17
--unclip-nethead rope on its clips (crossbar rope dropped 20 mm out of its loops: axis offset 0.0200 m)18
--wide-mouthmouth size (posts 15 mm further out each: clear width 5.03019 m)19
--taut-netnet sag (no sag: deepest cord 0.00772 m below the chord)20

--float-bar lifts the back bar inside its hub sockets, so the frame stays whole and the turf still grounds the box. --short-support leaves a 20 mm gap between the tube's end and the hub socket, in the air, where a support that has slipped shows. --unclip-net moves only the crossbar rope; the net's strands still end at the old rope line and the sag, read against the dropped rope, stays in band (0.13771 m). --wide-mouth rebuilds the whole goal 30 mm wider, net and clips included, so every other budget still holds, and the turf still sets the envelope.

Run#

-
blender --background --python soccer_goal.py --
+
blender --background --python soccer_goal.py --
 blender --background --python soccer_goal.py -- --skip-decimate
 blender --background --python soccer_goal.py -- --stray-vert
 blender --background --python soccer_goal.py -- --lift-z
@@ -142,7 +142,7 @@ 

Wooden Barrel

- +

@@ -294,10 +294,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/socket-attach-points/index.html b/docs/gallery/socket-attach-points/index.html index 7c6183e1..37a9234b 100644 --- a/docs/gallery/socket-attach-points/index.html +++ b/docs/gallery/socket-attach-points/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -56,7 +56,7 @@

Socket Attach Points

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 --
+
blender --background --python examples/socket-attach-points/socket_attach_points.py --
@@ -76,7 +76,7 @@

Hazards found while authoringRun#

-
blender --background --python socket_attach_points.py --
+
blender --background --python socket_attach_points.py --
 blender --background --python socket_attach_points.py -- --skip-mpi   # falsifier: sockets parented without matrix_parent_inverse; exits 3
 blender --background --python socket_attach_points.py -- --output drone.png
 blender --background --python socket_attach_points.py -- --falsify adrift.png
@@ -126,7 +126,7 @@ 

Prop Origin Transform - +

@@ -278,10 +278,18 @@

Prop Origin Transform scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/solidify-even-thickness/index.html b/docs/gallery/solidify-even-thickness/index.html index 9138b095..b804d4df 100644 --- a/docs/gallery/solidify-even-thickness/index.html +++ b/docs/gallery/solidify-even-thickness/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Solidify Even Thickness

Solidify's use_even_offset on a folded strip: off, each fold vertex moves t along the bisector of its face normals and the shell thins to t*cos(phi/2); on, it moves t/cos(phi/2) and stays exactly t thick at 60, 90 and 120 degree folds.

- + @@ -56,7 +56,7 @@

Solidify Even Thickness

witnesses Simple-mode Solidify (offset -1, t = 0.16) on a strip folded at 60, 90 and 120 degrees: the even shell is exactly t thick at every fold, its copies t/cos(phi/2) along the bisector; the plain shell is t*cos(phi/2) thick: 0.1386, 0.1131, 0.0800 (max error 3.7e-8). --no-even exits 4 at 0.0800 vs 0.1600.

tags mesh modifiers depsgraph

-
blender --background --python examples/solidify-even-thickness/solidify_even_thickness.py --
+
blender --background --python examples/solidify-even-thickness/solidify_even_thickness.py --
@@ -68,7 +68,7 @@

Solidify Even Thickness

The trap it exposes: use_even_offset is off by default. A script that solidifies a bent panel, a folded bracket or a box with thickness = t gets a wall that is noticeably thinner at every corner, down to half at a 120° bend, and nothing errors. --no-even leaves the flag off on the shell that the check expects to be even, and check 4 fails with the measured thickness.

The still shows both shells in teal glaze side by side on a walnut plinth, viewed nearly end-on. The cut section, Solidify's rim, is drawn in selection-orange enamel through material_offset_rim, so the band's width is the shell's thickness. On the left, with use_even_offset = False, the band pinches at every fold, and to half at the sharp V. A red line on that cut face traces where a t-thick shell's inner face would run: it is the even shell's own evaluated copy positions, placed through the plain shell's transform, so the gap between the orange band and the red line is the thickness lost at each fold, drawn at true size. On the right, with use_even_offset = True, the band stays one width all the way round. Engraved brass plaques name each setting, and brass dowels hold the raised folds off the plinth.

Run#

-
# Cheap correctness check (no render) — the CI check:
+
# Cheap correctness check (no render) — the CI check:
 blender --background --python solidify_even_thickness.py --
 
 # Falsifier: leave use_even_offset off on the shell checked as even. Must exit 4.
@@ -121,7 +121,7 @@ 

LOD Decimate Chain

- +
@@ -273,10 +273,18 @@

LOD Decimate Chain

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/stand-mixer/index.html b/docs/gallery/stand-mixer/index.html index d1d0e188..95d850a7 100644 --- a/docs/gallery/stand-mixer/index.html +++ b/docs/gallery/stand-mixer/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Stand Mixer

A procedural retro tilt-head kitchen stand mixer on a honed limestone countertop section — a cast enamel body lofted from superellipse sections (a pebble-shaped base foot on four rubber feet, a neck leaning back as it rises, a streamlined motor head) hinged at the back on a pin through two neck knuckles; a chrome trim band wrapped on the head's own sections, a blank badge, a speed lever and a tilt-lock lever; an attachment hub with its cap and thumb screw on the nose; a planetary socket carrying a flat beater whose frame follows the bowl's wall into a 4.5 L brushed stainless bowl with a rolled rim and strap handle, clamped under three bayonet lugs; a cord to a plug; and a balloon whisk, a measuring cup and two brown eggs in a glazed dish — through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

- + @@ -57,7 +57,7 @@

Stand Mixer

category Household

tags mesh export showcase

-
blender --background --python showcase/stand-mixer/stand_mixer.py --
+
blender --background --python showcase/stand-mixer/stand_mixer.py --
@@ -88,7 +88,7 @@

Falsifiers
FlagBudget violatedExit
--skip-decimateLOD1 ratio band (measured 1.0000)9
--stray-vertloose vertex count is 0 (measured 1, placed inside the envelope)15
--lift-zbounding box zmin is 0 (measured 0.05000)16
--float-footfeet on the counter (one foot 3 mm up: seat −0.00230 m)16
--offset-hingehinge pin coaxial through its knuckles (pin 2 mm aft: 0.002000 m)17
--offset-bowlbowl concentric on its plate (bowl slid 1 mm: 0.001000 m)18
--shallow-bowlbowl capacity (rim 30 mm lower: 3.4959 L)19
--offset-beaterbeater coaxial with the socket (0.000800 m)20
--long-beaterbeater clearance band (frame 2 mm lower: wall 0.00050, floor 0.00050 m)21
--bunch-feetstance (feet under the neck: margin −0.0785 m)22
--loose-capone connected assembly (hub cap 5 mm off the hub: 2 components)23

--offset-bowl still leaves the beater 1.72 mm clear of the wall and all three lugs engaged, and --offset-beater leaves it 1.87 mm clear, so only the concentric and coaxial budgets see them. --float-foot leaves three feet and the counter grounding the piece. --bunch-feet keeps every foot under the base and on the counter, so only the mass centre moves out of the support polygon.

Run#

-
blender --background --python stand_mixer.py --
+
blender --background --python stand_mixer.py --
 blender --background --python stand_mixer.py -- --skip-decimate
 blender --background --python stand_mixer.py -- --stray-vert
 blender --background --python stand_mixer.py -- --lift-z
@@ -146,7 +146,7 @@ 

Wooden Barrel

- +

@@ -298,10 +298,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/stone-archway/index.html b/docs/gallery/stone-archway/index.html index c4c19742..5a016db0 100644 --- a/docs/gallery/stone-archway/index.html +++ b/docs/gallery/stone-archway/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -57,7 +57,7 @@

Stone Archway

category Village

tags mesh export showcase

-
blender --background --python showcase/stone-archway/stone_archway.py --
+
blender --background --python showcase/stone-archway/stone_archway.py --
@@ -93,7 +93,7 @@

Exit codes
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)
20Gallery asset-quality violation (render path; the floors' 11 is remapped here)

Run it#

-
# Budget check, no render. ~1.1 s on 4.5, ~1.2 s on 5.1, ~1.4 s on 5.2.
+
# 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 --
 
 # Falsifier: the intrados stops being a circle. Must exit 19.
@@ -147,7 +147,7 @@ 

Wooden Barrel

- +

@@ -299,10 +299,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/stone-well/index.html b/docs/gallery/stone-well/index.html index 946afddb..b3f12f49 100644 --- a/docs/gallery/stone-well/index.html +++ b/docs/gallery/stone-well/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Stone Well

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

- + @@ -57,7 +57,7 @@

Stone Well

category Village

tags mesh export showcase

-
blender --background --python showcase/stone-well/stone_well.py --
+
blender --background --python showcase/stone-well/stone_well.py --
@@ -75,7 +75,7 @@

FalsifiersRun#

-
blender --background --python stone_well.py --
+
blender --background --python stone_well.py --
 blender --background --python stone_well.py -- --skip-decimate
 blender --background --python stone_well.py -- --lift-z
 blender --background --python stone_well.py -- --stand-posts
@@ -128,7 +128,7 @@ 

Campfire

- +
@@ -280,10 +280,18 @@

Campfire

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/street-lantern/index.html b/docs/gallery/street-lantern/index.html index 68a63c4e..35829b34 100644 --- a/docs/gallery/street-lantern/index.html +++ b/docs/gallery/street-lantern/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Street Lantern

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

- + @@ -57,7 +57,7 @@

Street Lantern

category Village

tags mesh export showcase

-
blender --background --python showcase/street-lantern/street_lantern.py --
+
blender --background --python showcase/street-lantern/street_lantern.py --
@@ -86,7 +86,7 @@

FalsifiersRun#

-
blender --background --python street_lantern.py --
+
blender --background --python street_lantern.py --
 blender --background --python street_lantern.py -- --skip-decimate
 blender --background --python street_lantern.py -- --stray-vert
 blender --background --python street_lantern.py -- --lift-z
@@ -140,7 +140,7 @@ 

Wooden Barrel

- +
@@ -292,10 +292,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/sundial/index.html b/docs/gallery/sundial/index.html index b5cecfe7..a784ed37 100644 --- a/docs/gallery/sundial/index.html +++ b/docs/gallery/sundial/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Sundial

A procedural garden sundial — a stepped stone plinth under a tapered octagonal column and cap, a bronze dial plate with inked hour lines and a curved-backed gnomon — carried through UVs, bake, LOD, compound collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

- + @@ -57,7 +57,7 @@

Sundial

category Village

tags mesh export showcase

-
blender --background --python showcase/sundial/sundial.py --
+
blender --background --python showcase/sundial/sundial.py --
@@ -89,7 +89,7 @@

Exit codes
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build, has no UV layer, or a part count is wrong
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 (--lift-z)
17Style angle or hour-line bearings (--wrong-latitude, --linear-hours)
18Gnomon bite, style foot or ink seat (--float-gnomon, --float-lines)
19Noon on the meridian, plumb column or real-world size (--shift-noon, --lean-pedestal)
20Gallery asset-quality violation (render path; the floors' 11 is remapped here)

Run it#

-
# Budget check, no render. A few seconds warm.
+
# Budget check, no render. A few seconds warm.
 blender --background --python sundial.py --
 
 # Falsifier: the lines are spaced at 15 degrees an hour. Must exit 17.
@@ -144,7 +144,7 @@ 

Wooden Barrel

- +

@@ -296,10 +296,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/swatch-grid/index.html b/docs/gallery/swatch-grid/index.html index e4606fbd..ba696025 100644 --- a/docs/gallery/swatch-grid/index.html +++ b/docs/gallery/swatch-grid/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Swatch Grid

Procedural Principled materials — metal and dielectric, the emission pattern, and the cross-version set_specular shim.

- + @@ -56,7 +56,7 @@

Swatch Grid

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 --
+
blender --background --python examples/swatch-grid/swatch_grid.py --
@@ -65,7 +65,7 @@

Swatch Grid

  • 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#

-
# Cheap correctness check (materials + engine-id witness, no render):
+
# Cheap correctness check (materials + engine-id witness, no render):
 blender --background --python swatch_grid.py --
 
 # Falsifier: same RGB on every swatch. Must exit non-zero.
@@ -129,7 +129,7 @@ 

Compositor Glare

- +
@@ -281,10 +281,18 @@

Compositor Glare

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/tavern-stool/index.html b/docs/gallery/tavern-stool/index.html index e9b5ecce..e2d088df 100644 --- a/docs/gallery/tavern-stool/index.html +++ b/docs/gallery/tavern-stool/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -57,7 +57,7 @@

Tavern Stool

category Village

tags mesh export showcase

-
blender --background --python showcase/tavern-stool/tavern_stool.py --
+
blender --background --python showcase/tavern-stool/tavern_stool.py --
@@ -83,7 +83,7 @@

Falsifiers
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#

-
blender --background --python tavern_stool.py --
+
blender --background --python tavern_stool.py --
 blender --background --python tavern_stool.py -- --skip-decimate
 blender --background --python tavern_stool.py -- --stray-vert
 blender --background --python tavern_stool.py -- --lift-z
@@ -137,7 +137,7 @@ 

Wooden Barrel

- +

@@ -289,10 +289,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/temp-override-join/index.html b/docs/gallery/temp-override-join/index.html index 7b53b1b7..c94c0f83 100644 --- a/docs/gallery/temp-override-join/index.html +++ b/docs/gallery/temp-override-join/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -56,7 +56,7 @@

Temp Override Join

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 --
+
blender --background --python examples/temp-override-join/temp_override_join.py --
@@ -66,7 +66,7 @@

Temp Override Join

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#

-
# Cheap correctness check (no render) — the CI check:
+
# Cheap correctness check (no render) — the CI check:
 blender --background --python temp_override_join.py --
 
 # Falsifier: join without temp_override. Must exit non-zero.
@@ -101,7 +101,7 @@ 

Cross Version Pr - +

@@ -253,10 +253,18 @@

Cross Version Pr // Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/terrain-scatter/index.html b/docs/gallery/terrain-scatter/index.html index fa046313..a984c691 100644 --- a/docs/gallery/terrain-scatter/index.html +++ b/docs/gallery/terrain-scatter/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -57,7 +57,7 @@

Terrain Scatter

category Village

tags mesh export showcase

-
blender --background --python showcase/terrain-scatter/terrain_scatter.py --
+
blender --background --python showcase/terrain-scatter/terrain_scatter.py --
@@ -88,7 +88,7 @@

Falsifiers
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#

-
blender --background --python terrain_scatter.py --
+
blender --background --python terrain_scatter.py --
 blender --background --python terrain_scatter.py -- --skip-decimate
 blender --background --python terrain_scatter.py -- --stray-vert
 blender --background --python terrain_scatter.py -- --lift-z
@@ -144,7 +144,7 @@ 

Wooden Barrel

- +

@@ -296,10 +296,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/text-version-stamp/index.html b/docs/gallery/text-version-stamp/index.html index ff121ee1..62b2b3f8 100644 --- a/docs/gallery/text-version-stamp/index.html +++ b/docs/gallery/text-version-stamp/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -56,7 +56,7 @@

Text Version Stamp

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 --
+
blender --background --python examples/text-version-stamp/text_version_stamp.py --
@@ -68,7 +68,7 @@

StagingRun#

-
# Cheap correctness check (no render) — the CI check:
+
# Cheap correctness check (no render) — the CI check:
 blender --background --python text_version_stamp.py --
 
 # Falsifier: body is not version_string. Must exit non-zero.
@@ -122,7 +122,7 @@ 

Curve Bevel Arc

- +
@@ -274,10 +274,18 @@

Curve Bevel Arc

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/toboggan/index.html b/docs/gallery/toboggan/index.html index f870b200..8466573b 100644 --- a/docs/gallery/toboggan/index.html +++ b/docs/gallery/toboggan/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Toboggan

A procedural runner sled — two steel-shod timber runners with high front horns and a near-flat tail, tenoned posts and bearers under a screwed seven-slat deck, one painted steering bar drilled at its centre, and a three-strand hemp rope through the hole to a wooden toggle — carried through UVs, bake, LOD, compound collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

- + @@ -57,7 +57,7 @@

Toboggan

category Vehicles

tags mesh export showcase

-
blender --background --python showcase/toboggan/toboggan.py --
+
blender --background --python showcase/toboggan/toboggan.py --
@@ -86,7 +86,7 @@

Exit codes
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 shoe floating (--lift-z, --float-shoe)
17Post bite or tenon, or bar protrusion (--short-post, --short-bar)
18Slat, screw, shoe or bolt seat, or the rope in its hole (--float-slats, --float-screws, --lift-runners, --float-bolts, --miss-hole)
19Real-world size or mirrored runners (--tall-posts, --skew-runner)
24Asset-quality floors (examples/gallery_asset_quality.py, render path only; remapped from 11)

Run it#

-
# Budget check, no render. About 2 s on 5.2.1.
+
# Budget check, no render. About 2 s on 5.2.1.
 blender --background --python toboggan.py --
 
 # Falsifier: the rope sits in solid wood, not in the hole. Must exit 18.
@@ -141,7 +141,7 @@ 

Wooden Barrel

- +

@@ -293,10 +293,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/traffic-cones/index.html b/docs/gallery/traffic-cones/index.html index 6d1c33d2..8f83ea23 100644 --- a/docs/gallery/traffic-cones/index.html +++ b/docs/gallery/traffic-cones/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Traffic Cones

A procedural roadworks set on a cut-out patch of asphalt — a broken-edged asphalt slab with a worn white road line and two sealed tar cracks; seven 720 mm PVC traffic cones, each a hollow lathed body with a moulded bead and a rolled lip round an open tip, bonded into a square rubber base with chamfered corners, a raked top, a collar, four moulded lugs and a recess underneath, and hooped with 4 in and 6 in retroreflective collars; three standing in a taper line, three nested in a stack whose wall thickness sets the nesting pitch, and one knocked over onto the edge of its base and the lip of its tip, its roll solved so both touch; a folding A-frame barricade of four square steel legs hinged in pairs on pivot bolts, spread by pinned straps on spacers, each foot in a rubber pad, carrying two boards on each face split on 45-degree lines into orange and white retroreflective stripes and bolted to the legs; and a warning lamp with amber fresnel lenses on a bracket bolted to the top board — through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

- + @@ -57,7 +57,7 @@

Traffic Cones

category Vehicles

tags mesh export showcase

-
blender --background --python showcase/traffic-cones/traffic_cones.py --
+
blender --background --python showcase/traffic-cones/traffic_cones.py --
@@ -87,7 +87,7 @@

Falsifiers
FlagBudget violatedExit
--skip-decimateLOD1 ratio band (measured 1.0000)9
--stray-vertloose vertex count is 0 (measured 1, placed inside the envelope)15
--lift-zbounding box zmin is 0 (measured 0.05000)16
--float-coneevery support bedded in the slab (the right-hand standing cone lifted 4 mm: sink −2.5 mm, the rest 1.5 mm)16
--gap-boardboards bite their legs (the lower front board 3 mm off its legs: −1.5 mm at both)17
--float-bandcollar seat (the left cone's 6 in collar 2 mm out: seat −1.4 mm)18
--lean-coneplumb (the middle standing cone's body tipped 1.5° in its base: tilt 1.431°)19
--skew-legleg mirror symmetry (the right front leg 2.5 mm toward the centre: 2.500 mm)19
--loose-stacknesting (the top cone lifted 10 mm: pitch 0.046, bite −0.576 to −0.505 mm)20
--loose-lampone connected assembly (the lamp 3 mm off its board: 2 components, 9 and 116 shells)21

--float-cone lifts a whole cone while the slab still grounds the box. --lean-cone tips the body and its collars about the collar top and leaves the base flat, so the supports hold; its measured tilt is a little under 1.5° because the slabs it compares are cut level through a leaning body. --skew-leg moves the front leg inward, into the 3 mm gap its washer fills, so it stays clear of its strap and its board's bite is unchanged. --loose-stack first lifted the top cone 12 mm, which put its base bottom on the middle cone's collar-top plane and exited 15 on 192 coplanar pairs; 10 mm keeps every horizontal plane of the two cones at least 2 mm apart. --loose-lamp pulls the lamp level off the board rather than along the board's normal, which had lifted the envelope's top 0.8 mm.

Run#

-
blender --background --python traffic_cones.py --
+
blender --background --python traffic_cones.py --
 blender --background --python traffic_cones.py -- --skip-decimate
 blender --background --python traffic_cones.py -- --stray-vert
 blender --background --python traffic_cones.py -- --lift-z
@@ -146,7 +146,7 @@ 

Wooden Barrel

- +

@@ -298,10 +298,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/treasure-chest/index.html b/docs/gallery/treasure-chest/index.html index 70160aca..dc7279d3 100644 --- a/docs/gallery/treasure-chest/index.html +++ b/docs/gallery/treasure-chest/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -57,7 +57,7 @@

Treasure Chest

category Village

tags mesh export showcase

-
blender --background --python showcase/treasure-chest/treasure_chest.py --
+
blender --background --python showcase/treasure-chest/treasure_chest.py --
@@ -83,7 +83,7 @@

Falsifiers
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#

-
blender --background --python treasure_chest.py --
+
blender --background --python treasure_chest.py --
 blender --background --python treasure_chest.py -- --skip-decimate
 blender --background --python treasure_chest.py -- --stray-vert
 blender --background --python treasure_chest.py -- --lift-z
@@ -136,7 +136,7 @@ 

Wooden Barrel

- +

@@ -288,10 +288,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/triangulate-tangents/index.html b/docs/gallery/triangulate-tangents/index.html index 0b82271e..580809f3 100644 --- a/docs/gallery/triangulate-tangents/index.html +++ b/docs/gallery/triangulate-tangents/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -56,7 +56,7 @@

Triangulate Tangents

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 --
+
blender --background --python examples/triangulate-tangents/triangulate_tangents.py --
@@ -69,7 +69,7 @@

Triangulate Tangents

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#

-
# Cheap correctness check (no render) — the CI check:
+
# Cheap correctness check (no render) — the CI check:
 blender --background --python triangulate_tangents.py --
 
 # Falsifier: every UV at (0, 0). Must exit non-zero (authored UV closed form).
@@ -122,7 +122,7 @@ 

BMesh Gear

- +
@@ -274,10 +274,18 @@

BMesh Gear

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/turntable/index.html b/docs/gallery/turntable/index.html index 62bf5d74..c46476f3 100644 --- a/docs/gallery/turntable/index.html +++ b/docs/gallery/turntable/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Turntable

A slotted-actions Z-rotation turntable keyed through the cross-version channelbag path (get_channelbag_for_slot).

- + @@ -56,7 +56,7 @@

Turntable

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 --
+
blender --background --python examples/turntable/turntable.py --
@@ -64,7 +64,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#

-
# Cheap correctness check only (no render) — the CI smoke check:
+
# Cheap correctness check only (no render) — the CI smoke check:
 blender --background --python turntable.py --
 
 # Falsifier: no rotation keys. Must exit non-zero (keys drive playback).
@@ -110,7 +110,7 @@ 

Damped Track Aim

- +
@@ -262,10 +262,18 @@

Damped Track Aim

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/usd-export-evaluation-mode/index.html b/docs/gallery/usd-export-evaluation-mode/index.html index e781d5d4..280341fe 100644 --- a/docs/gallery/usd-export-evaluation-mode/index.html +++ b/docs/gallery/usd-export-evaluation-mode/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -56,7 +56,7 @@

USD Export Evaluation Mode

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 --
+
blender --background --python examples/usd-export-evaluation-mode/usd_export_evaluation_mode.py --
@@ -69,7 +69,7 @@

USD Export Evaluation Mode

Staging: Catmull-Clark pulls the surface inside the cage, and each level shrinks it by a different amount, so each re-imported goblet is seated on its plinth from its own vertices rather than from the cage's z.

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#

-
blender --background --python usd_export_evaluation_mode.py --
+
blender --background --python usd_export_evaluation_mode.py --
 blender --background --python usd_export_evaluation_mode.py -- --output u.png

Exit codes#

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

@@ -115,7 +115,7 @@

Text Version Stamp

- +
@@ -267,10 +267,18 @@

Text Version Stamp

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/uv-layer-grid/index.html b/docs/gallery/uv-layer-grid/index.html index 0e949924..1f58b041 100644 --- a/docs/gallery/uv-layer-grid/index.html +++ b/docs/gallery/uv-layer-grid/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -56,7 +56,7 @@

UV Layer Grid

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 --
+
blender --background --python examples/uv-layer-grid/uv_layer_grid.py --
@@ -70,7 +70,7 @@

UV Layer Grid

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 oak studio easels, each holding a walnut-framed tile panel that shares one neon checker image, with a brass placard on the easel ledge naming the authoring path. Left, NO UV LAYER, is the hazard — calc_uvs=True alone, so the whole panel is the flat teal of texel (0, 0). Right, UV LAYER FIRST, is the repair (pre-create + calc_uvs) — the 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.2.1: hazard spread 0.0078, repair spread 0.7569; identical on 4.5.11). The render path gates framing through examples/gallery_framing.py (exit 10) before writing the still.

Run#

-
# Cheap correctness check (no render) — the CI check:
+
# Cheap correctness check (no render) — the CI check:
 blender --background --python uv_layer_grid.py --
 
 # Falsifier: pre-create a UV layer on the silent-no-op probe. Must exit non-zero.
@@ -123,7 +123,7 @@ 

Triangulate Tangents< - +

@@ -275,10 +275,18 @@

Triangulate Tangents< // Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/vertex-color-ao/index.html b/docs/gallery/vertex-color-ao/index.html index 1cb49e12..90ca24b6 100644 --- a/docs/gallery/vertex-color-ao/index.html +++ b/docs/gallery/vertex-color-ao/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -56,14 +56,14 @@

Vertex Color AO

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 --
+
blender --background --python examples/vertex-color-ao/vertex_color_ao.py --

A runnable example baking ambient occlusion into a colour attribute — the cheap contact shadows an engine gets for free if it reads vertex colour. Unlike most bakes this one can be checked against a formula rather than against a previous run's numbers: the hemisphere visibility integral has closed forms.

For a point on the floor a distance d from an infinitely wide wall of height H, integrating the cosine-weighted hemisphere over the directions the wall blocks gives (the φ integral collapses to π / √(1+k²)):

-
A(k) = ½ (1 − 1/√(1+k²)),   k = H/d        AO = 1 − A(k)
+
A(k) = ½ (1 − 1/√(1+k²)),   k = H/d        AO = 1 − A(k)

AO depends only on the ratio H/d and increases strictly with d — which is exactly "a concave corner darkens monotonically with depth", as an equation. For an unoccluded flat surface the integral is exactly 1.

The asset, for reuse: Well.Stone, a 2.5 m stone village well — three courses of 16-17 chamfered masonry blocks of irregular width on a 16-slab paved apron, a 15-slab coping ring, a lined bore, two braced timber posts with a crossbeam, an iron-banded windlass with a crank, and a hanging bucket. 11 parts, 11 materials, 15678 vertices. Origin at the ground contact centre (z == 0 is where it rests), identity transforms, datablocks under Well.Stone.*. Every part ships with the AO baked into two colour attributes, and default_color_name points at the linear one so an engine reads the right channel.

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.

@@ -77,7 +77,7 @@

Hazards found while authoringRun#

-
blender --background --python vertex_color_ao.py --
+
blender --background --python vertex_color_ao.py --
 blender --background --python vertex_color_ao.py -- --uniform-hemisphere   # falsifier: uniform instead of cosine-weighted hemisphere; calibration exits 3
 blender --background --python vertex_color_ao.py -- --output well.png
 blender --background --python vertex_color_ao.py -- --falsify inverted.png
@@ -126,7 +126,7 @@

Custom Normals Shade<

- +
@@ -278,10 +278,18 @@

Custom Normals Shade< // Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/vertex-weight-limit/index.html b/docs/gallery/vertex-weight-limit/index.html index 97e06578..c1255f50 100644 --- a/docs/gallery/vertex-weight-limit/index.html +++ b/docs/gallery/vertex-weight-limit/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -56,7 +56,7 @@

Vertex Weight Limit

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 --
+
blender --background --python examples/vertex-weight-limit/vertex_weight_limit.py --
@@ -71,7 +71,7 @@

What the render showsRun#

-
# Cheap correctness check (no render) — the CI check:
+
# Cheap correctness check (no render) — the CI check:
 blender --background --python vertex_weight_limit.py --
 
 # Falsifier: skip the 4-influence prune. Must exit non-zero.
@@ -124,7 +124,7 @@ 

Shape Key Blend

- +
@@ -276,10 +276,18 @@

Shape Key Blend

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/vse-cut-list/index.html b/docs/gallery/vse-cut-list/index.html index ab87cad1..d67cfce2 100644 --- a/docs/gallery/vse-cut-list/index.html +++ b/docs/gallery/vse-cut-list/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -56,7 +56,7 @@

VSE Cut List

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 --
+
blender --background --python examples/vse-cut-list/vse_cut_list.py --
@@ -75,7 +75,7 @@

VSE Cut List

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#

-
# Cheap correctness check (no render) — the CI check:
+
# Cheap correctness check (no render) — the CI check:
 blender --background --python vse_cut_list.py --
 
 # Falsifier: GC T2 -> T1. Must exit non-zero (wiring). `--check-pixels` is a
@@ -131,7 +131,7 @@ 

Compositor Glare

- +
@@ -283,10 +283,18 @@

Compositor Glare

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/vse-gamma-cross/index.html b/docs/gallery/vse-gamma-cross/index.html index 82a29306..6120d3f8 100644 --- a/docs/gallery/vse-gamma-cross/index.html +++ b/docs/gallery/vse-gamma-cross/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -56,7 +56,7 @@

VSE Gamma Cross

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 --
+
blender --background --python examples/vse-gamma-cross/vse_gamma_cross.py --
@@ -69,7 +69,7 @@

VSE Gamma Cross

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#

-
# Correctness check (tiny per-frame sample renders) — the CI check:
+
# Correctness check (tiny per-frame sample renders) — the CI check:
 blender --background --python vse_gamma_cross.py --
 
 # Falsifier: GC T2 -> T1. Must exit non-zero (inputs).
@@ -122,7 +122,7 @@ 

Compositor Glare

- +
@@ -274,10 +274,18 @@

Compositor Glare

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/wall-torch/index.html b/docs/gallery/wall-torch/index.html index bea83b4f..f0f558d8 100644 --- a/docs/gallery/wall-torch/index.html +++ b/docs/gallery/wall-torch/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -57,7 +57,7 @@

Wall Torch

category Village

tags mesh export showcase

-
blender --background --python showcase/wall-torch/wall_torch.py --
+
blender --background --python showcase/wall-torch/wall_torch.py --
@@ -83,7 +83,7 @@

Falsifiers
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#

-
blender --background --python wall_torch.py --
+
blender --background --python wall_torch.py --
 blender --background --python wall_torch.py -- --skip-decimate
 blender --background --python wall_torch.py -- --stray-vert
 blender --background --python wall_torch.py -- --lift-z
@@ -138,7 +138,7 @@ 

Wooden Barrel

- +

@@ -290,10 +290,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/watchtower/index.html b/docs/gallery/watchtower/index.html index b917e71d..45157331 100644 --- a/docs/gallery/watchtower/index.html +++ b/docs/gallery/watchtower/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Watchtower

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

- + @@ -57,7 +57,7 @@

Watchtower

category Village

tags mesh export showcase

-
blender --background --python showcase/watchtower/watchtower.py --
+
blender --background --python showcase/watchtower/watchtower.py --
@@ -81,7 +81,7 @@

Falsifiers
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#

-
blender --background --python watchtower.py --
+
blender --background --python watchtower.py --
 blender --background --python watchtower.py -- --skip-decimate
 blender --background --python watchtower.py -- --stray-vert
 blender --background --python watchtower.py -- --lift-z
@@ -138,7 +138,7 @@ 

Wooden Barrel

- +

@@ -290,10 +290,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/water-trough/index.html b/docs/gallery/water-trough/index.html index 302052e8..f80e1e24 100644 --- a/docs/gallery/water-trough/index.html +++ b/docs/gallery/water-trough/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Water Trough

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

- + @@ -57,7 +57,7 @@

Water Trough

category Village

tags mesh export showcase

-
blender --background --python showcase/water-trough/water_trough.py --
+
blender --background --python showcase/water-trough/water_trough.py --
@@ -93,7 +93,7 @@

FalsifiersRun#

-
blender --background --python water_trough.py --
+
blender --background --python water_trough.py --
 blender --background --python water_trough.py -- --skip-decimate
 blender --background --python water_trough.py -- --stray-vert
 blender --background --python water_trough.py -- --lift-z
@@ -149,7 +149,7 @@ 

Wooden Barrel

- +
@@ -301,10 +301,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/wave-displace/index.html b/docs/gallery/wave-displace/index.html index 212d7c58..7c16a4b6 100644 --- a/docs/gallery/wave-displace/index.html +++ b/docs/gallery/wave-displace/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -56,7 +56,7 @@

Wave Displace

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 --
+
blender --background --python examples/wave-displace/wave_displace.py --
@@ -67,7 +67,7 @@

StagingRun#

-
# Cheap correctness check (no render) — the CI check:
+
# Cheap correctness check (no render) — the CI check:
 blender --background --python wave_displace.py --
 
 # Falsifier: skip the foreach_set displacement. Must exit non-zero (z-span).
@@ -120,7 +120,7 @@ 

Color Attribute Wheel - +

@@ -272,10 +272,18 @@

Color Attribute Wheel scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/weight-rack/index.html b/docs/gallery/weight-rack/index.html index 07af5dc9..a84d2138 100644 --- a/docs/gallery/weight-rack/index.html +++ b/docs/gallery/weight-rack/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Weight Rack

A procedural home-gym strength set — a half rack of punched, numbered square uprights on bolted feet with rubber pads, side rails on bolted flanges, a pull-up bar, J-hooks with UHMW saddles and spotter arms on pull pins seated in the holes, plate-storage horns, an Olympic barbell with a knurled shaft, collars and sleeves loaded with bumpers (steel hubs, raised rims and numerals) and cast-iron change plates behind coiled spring clips, and two hex dumbbells — through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

- + @@ -57,7 +57,7 @@

Weight Rack

category Sports

tags mesh export showcase

-
blender --background --python showcase/weight-rack/weight_rack.py --
+
blender --background --python showcase/weight-rack/weight_rack.py --
@@ -86,7 +86,7 @@

Falsifiers
FlagBudget violatedExit
--skip-decimateLOD1 ratio band (measured 1.0000)9
--stray-vertloose vertex count is 0 (measured 1, placed inside the envelope)15
--lift-zbounding box zmin is 0 (measured 0.05000)16
--float-footevery pad and dumbbell head on the floor (left front pad 3 mm up: its zmin 0.00300, the rest 0)16
--offset-pinpins coaxial with their holes (left spotter and its pin 12 mm up: 0.012 m off the hole axis)17
--float-barbar seated in both saddles (bar, plates and clips 5 mm up: shaft-to-liner +0.0048 m on both)18
--hook-highbar level (right J-hook one hole higher, bar resting across both saddles: tilt 2.644°)19
--gap-plateplates seated (right iron plate and clip 8 mm out along the sleeve: gap +0.0075 m)20
--odd-loadload balance (left 5 kg iron plate swapped for 2.5 kg: mass centre 0.0226 m off the midpoint)21
--loose-hornone connected rack (upper-left horn with its weld plate, bolts and plates 10 mm off the upright: 4 components)22

--offset-pin moves the spotter, not a J-hook, so the bar stays seated and level. --float-bar also splits the assembly, but seating (18) is checked first. --hook-high keeps every pin in a hole (the J-hook moves one full pitch) and both saddle gaps inside their band (−0.215 mm); the bar end moves 0.2 mm in x, inside the bounding-box tolerance. --gap-plate moves the load's mass centre 0.4 mm, inside the balance tolerance. --odd-load keeps every plate seated, since the clip moves in against the thinner plate.

Run#

-
blender --background --python weight_rack.py --
+
blender --background --python weight_rack.py --
 blender --background --python weight_rack.py -- --skip-decimate
 blender --background --python weight_rack.py -- --stray-vert
 blender --background --python weight_rack.py -- --lift-z
@@ -143,7 +143,7 @@ 

Wooden Barrel

- +

@@ -295,10 +295,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/wheelbarrow/index.html b/docs/gallery/wheelbarrow/index.html index 48363ad7..8536a3a7 100644 --- a/docs/gallery/wheelbarrow/index.html +++ b/docs/gallery/wheelbarrow/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Wheelbarrow

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

- + @@ -57,7 +57,7 @@

Wheelbarrow

category Village

tags mesh export showcase

-
blender --background --python showcase/wheelbarrow/wheelbarrow.py --
+
blender --background --python showcase/wheelbarrow/wheelbarrow.py --
@@ -89,7 +89,7 @@

Falsifiers
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#

-
blender --background --python wheelbarrow.py --
+
blender --background --python wheelbarrow.py --
 blender --background --python wheelbarrow.py -- --skip-decimate
 blender --background --python wheelbarrow.py -- --stray-vert
 blender --background --python wheelbarrow.py -- --lift-z
@@ -145,7 +145,7 @@ 

Wooden Barrel

- +

@@ -297,10 +297,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/wingback-armchair/index.html b/docs/gallery/wingback-armchair/index.html index 5b0df77f..d94b0fab 100644 --- a/docs/gallery/wingback-armchair/index.html +++ b/docs/gallery/wingback-armchair/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

Wingback Armchair

A procedural reading corner on a fringed wool rug — a Queen Anne / Chesterfield wingback armchair in oxblood leather whose tall reclined back is deep-button tufted, thirteen buttons on a diamond lattice each seated in a funnelled dimple with the leather puffed into pillows between them and folded into sharp pleats along every line that joins two buttons; two wings sweeping forward from the back into rolled arms, each arm one lofted scroll section with an English roll tucked under on the outside, a crowned front, a piped welt round the front seam and a row of brass nailheads following the scroll; a nailed front rail, a loose cushion with a crowned top and piped welts on both seams, cabriole front legs on pad feet and square rear legs splayed into brass ferrules; beside it a walnut tripod side table with a brass reading lamp (knopped stem, harp, finial, linen shade) and two cloth-bound books — through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

- + @@ -57,7 +57,7 @@

Wingback Armchair

category Household

tags mesh export showcase

-
blender --background --python showcase/wingback-armchair/wingback_armchair.py --
+
blender --background --python showcase/wingback-armchair/wingback_armchair.py --
@@ -88,7 +88,7 @@

Falsifiers
FlagBudget violatedExit
--skip-decimateLOD1 ratio band (measured 1.0000)9
--stray-vertloose vertex count is 0 (measured 1, placed inside the envelope)15
--lift-zbounding box zmin is 0 (measured 0.05000)16
--float-footfeet sunk into the rug (one table pad and its leg's end 3 mm up: −0.00100 m)16
--short-legslegs tenoned into the body (the right rear leg's top 40 mm lower: −0.0038 m)17
--sink-buttonsbuttons seated in their dimples (all 6 mm deeper: seat 0.00850, proud 0.00140 m)18
--odd-wingmirror symmetry (the right wing 6 mm outward: 0.006000 m)19
--drift-buttonsdiamond lattice (the row-2 side buttons and their dimples 18 mm outward, a mirrored pair: spread 0.01358 m)20
--narrow-tripodtable mass centre inside its feet (legs' reach cut to 30%: 0.0280 m)21
--lift-cushioncushion resting on the deck (5 mm up: −0.00300 m)22
--bunch-nailsnailheads evenly spaced (nail 6 of each arm slid 45% of a pitch: spread 0.02699 m)23

--drift-buttons moves a mirrored pair, dimples and all, so the back stays symmetric and every button still sits in its own dimple; only the lattice breaks. --short-legs keeps the leg's foot in its ferrule on the rug. --float-foot lifts the pad with its leg's end, so the leg stays in its pad. --bunch-nails re-seats the slid nails on the arm's surface, so only the spacing breaks.

Run#

-
blender --background --python wingback_armchair.py --
+
blender --background --python wingback_armchair.py --
 blender --background --python wingback_armchair.py -- --skip-decimate
 blender --background --python wingback_armchair.py -- --stray-vert
 blender --background --python wingback_armchair.py -- --lift-z
@@ -146,7 +146,7 @@ 

Wooden Barrel

- +

@@ -298,10 +298,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/wooden-barrel/index.html b/docs/gallery/wooden-barrel/index.html index d13e8829..26128b49 100644 --- a/docs/gallery/wooden-barrel/index.html +++ b/docs/gallery/wooden-barrel/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -57,7 +57,7 @@

Wooden Barrel

category Village

tags mesh export showcase

-
blender --background --python showcase/wooden-barrel/wooden_barrel.py --
+
blender --background --python showcase/wooden-barrel/wooden_barrel.py --
@@ -82,7 +82,7 @@

FalsifiersSurface#

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#

-
blender --background --python wooden_barrel.py --
+
blender --background --python wooden_barrel.py --
 blender --background --python wooden_barrel.py -- --skip-decimate
 blender --background --python wooden_barrel.py -- --stray-vert
 blender --background --python wooden_barrel.py -- --lift-z
@@ -135,7 +135,7 @@ 

Campfire

- +
@@ -287,10 +287,18 @@

Campfire

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/wooden-bucket/index.html b/docs/gallery/wooden-bucket/index.html index 0c573a37..09771eaf 100644 --- a/docs/gallery/wooden-bucket/index.html +++ b/docs/gallery/wooden-bucket/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -57,7 +57,7 @@

Wooden Bucket

category Village

tags mesh export showcase

-
blender --background --python showcase/wooden-bucket/wooden_bucket.py --
+
blender --background --python showcase/wooden-bucket/wooden_bucket.py --
@@ -85,7 +85,7 @@

Rope and earsSurface#

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#

-
blender --background --python wooden_bucket.py --
+
blender --background --python wooden_bucket.py --
 blender --background --python wooden_bucket.py -- --skip-decimate
 blender --background --python wooden_bucket.py -- --stray-vert
 blender --background --python wooden_bucket.py -- --lift-z
@@ -140,7 +140,7 @@ 

Wooden Barrel

- +
@@ -292,10 +292,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/wooden-ladder/index.html b/docs/gallery/wooden-ladder/index.html index ee72ec50..6326a911 100644 --- a/docs/gallery/wooden-ladder/index.html +++ b/docs/gallery/wooden-ladder/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -57,7 +57,7 @@

Wooden Ladder

category Village

tags mesh export showcase

-
blender --background --python showcase/wooden-ladder/wooden_ladder.py --
+
blender --background --python showcase/wooden-ladder/wooden_ladder.py --
@@ -82,7 +82,7 @@

Rungs, wood and staging
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
--float-nutsTie-rod seat20seat bite −0.00350, thread 0.00050 proud

Default exit is 0 on both.

Run#

-
blender --background --python wooden_ladder.py --
+
blender --background --python wooden_ladder.py --
 blender --background --python wooden_ladder.py -- --skip-decimate
 blender --background --python wooden_ladder.py -- --stray-vert
 blender --background --python wooden_ladder.py -- --twin-sole
@@ -136,7 +136,7 @@ 

Wooden Barrel

- +

@@ -288,10 +288,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/docs/gallery/wooden-yoke/index.html b/docs/gallery/wooden-yoke/index.html index 4ee8beea..c30bb3c9 100644 --- a/docs/gallery/wooden-yoke/index.html +++ b/docs/gallery/wooden-yoke/index.html @@ -27,7 +27,7 @@ - + @@ -48,7 +48,7 @@

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.

- + @@ -57,7 +57,7 @@

Wooden Yoke

category Village

tags mesh export showcase

-
blender --background --python showcase/wooden-yoke/wooden_yoke.py --
+
blender --background --python showcase/wooden-yoke/wooden_yoke.py --
@@ -91,7 +91,7 @@

Exit codes
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)
20Gallery asset-quality violation (render path; the floors' 11 is remapped here)

Run it#

-
# Budget check, no render. ~2.5 s on 4.5, ~4.6 s on 5.2.
+
# Budget check, no render. ~2.5 s on 4.5, ~4.6 s on 5.2.
 blender --background --python wooden_yoke.py --
 
 # Falsifier: the bows close too tight for a neck. Must exit 19.
@@ -145,7 +145,7 @@ 

Wooden Barrel

- +

@@ -297,10 +297,18 @@

Wooden Barrel

// Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); diff --git a/scripts/build_gallery.py b/scripts/build_gallery.py index fc493790..9c25fa1d 100644 --- a/scripts/build_gallery.py +++ b/scripts/build_gallery.py @@ -301,6 +301,10 @@ def check_categories(entries: list) -> None: .view-mode .density-btn.active { background: var(--border); color: var(--text); } /* Segmented groups clip overflow, so the ring has to sit inside. */ .density-btn:focus-visible, .chip:focus-visible { outline-offset: -2px; } + /* On a selected (orange) button an orange ring vanishes into the fill + (#397): draw a dark inset ring instead, ~7.8:1 against --select. */ + .density-btn.active:focus-visible, .cat-btn.active:focus-visible, + .chip.active:focus-visible { outline: 2px solid var(--on-select); outline-offset: -4px; } .sort select { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); padding: 0.4rem 0.6rem; min-height: 36px; cursor: pointer; font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.04em; text-transform: uppercase; } @@ -498,9 +502,11 @@ def check_categories(entries: list) -> None: 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; } + display: flex; align-items: baseline; gap: 0.35em; min-width: 0; white-space: nowrap; } + /* Only the name truncates; the arrow is its own box so it never clips (#399). */ + .pager-label { overflow: hidden; text-overflow: ellipsis; min-width: 0; } .pager a:hover { color: var(--select); text-decoration: none; } - .pager [rel="next"] { text-align: right; } + .pager [rel="next"] { justify-content: flex-end; } .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); @@ -596,6 +602,10 @@ def check_categories(entries: list) -> None: var chipsEl = document.getElementById('chips'); var chips = chipsEl ? Array.prototype.slice.call(chipsEl.querySelectorAll('.chip')) : []; var q = document.getElementById('q'); + // Touch devices have no "/" key shortcut to advertise (#399). + if (q && window.matchMedia && window.matchMedia('(hover: none)').matches) { + q.placeholder = 'Search the gallery'; + } var qClear = document.getElementById('qClear'); var count = document.getElementById('count'); var noResults = document.getElementById('noResults'); @@ -1085,10 +1095,18 @@ def check_categories(entries: list) -> None: // Left/right arrows page between entries, but only when nothing has // focus and nothing is selected: a focused control, a scrolled code - // block, or a text selection keeps its own arrow keys. + // block, or a text selection keeps its own arrow keys. A click inside a + // code block does not always move focus (or a selection), so also keep + // the arrows when the last click landed in one that scrolls sideways + // (#398). + var lastPointer = null; + document.addEventListener('pointerdown', function (e) { lastPointer = e.target; }, true); document.addEventListener('keydown', function (e) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return; if (box && box.open) return; + var scroller = lastPointer && lastPointer.closest + && lastPointer.closest('pre, .code-lines, .codewrap'); + if (scroller && scroller.scrollWidth > scroller.clientWidth) return; var ae = document.activeElement; if (ae && ae !== document.body && ae !== document.documentElement) return; var sel = window.getSelection && window.getSelection(); @@ -1236,7 +1254,7 @@ def md_to_html(text: str, resolve, skip_first_h1: bool = True) -> str: code.append(lines[i]) i += 1 i += 1 # closing fence - out.append(f"
{html.escape(chr(10).join(code))}
") + out.append(f'
{html.escape(chr(10).join(code))}
') continue m = re.match(r"^(#{1,3})\s+(.*)$", stripped) @@ -1424,8 +1442,9 @@ 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} ') + name = f'{n}' + label = (f'{name}' if rel == "prev" + else f'{name}') return (f'{label}') cls = "pager pager-foot" if foot else "pager" @@ -1484,7 +1503,7 @@ def build_detail(ex: dict, entries: list, *, base: str, repo_root_url: str, site if script is not None: cmd = f"blender --background --python {ex['dir']}/{script.name} --" parts.append('
') - parts.append(f'
{html.escape(cmd)}
') + parts.append(f'
{html.escape(cmd)}
') parts.append(' ') parts.append("
") @@ -1628,7 +1647,7 @@ def build_index(data: dict, *, base: str, repo_root_url: str, site: str, css_v: '
\n' '
\n' '
\n' - ' \n' ' \n' '
\n' @@ -1673,7 +1692,8 @@ def build_index(data: dict, *, base: str, repo_root_url: str, site: str, css_v: CATEGORIES.get(ex.get("category") or "", "")]).lower() cards.append( CARD - .replace("__LOADING__", "eager" if i < EAGER_CARDS else "lazy") + .replace("__LOADING__", ("eager\" fetchpriority=\"high" if i == 0 else "eager") + if i < EAGER_CARDS else "lazy") .replace("__TAGS__", html.escape(" ".join(ex.get("tags", [])), quote=True)) .replace("__SEARCH__", html.escape(search, quote=True)) .replace("__HREF__", html.escape(f'{ex["name"]}/', quote=True)) diff --git a/scripts/site/template.html.j2 b/scripts/site/template.html.j2 index cf8142bf..74d870fe 100644 --- a/scripts/site/template.html.j2 +++ b/scripts/site/template.html.j2 @@ -444,7 +444,7 @@
Skills - {{ skill_count }} loaded
@@ -476,7 +476,7 @@
Rules - {{ rule_count }} active