From 36fc3f833ebe172457a6d9cd438a09b7ef4883b2 Mon Sep 17 00:00:00 2001 From: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Date: Sun, 4 Oct 2026 22:18:34 -0400 Subject: [PATCH] feat(skills): link every skill to the examples that prove it 10 of 16 skills linked no example, and an agent had to join three manifests to learn what an example runs, on which Blender, and how to break it. - examples/skills.json (hand-edited) maps each of the 64 examples to the skills it proves; 52 map to at least one. - scripts/build_examples_index.py generates examples/index.json: one record per example with script, skills, render or check-only, min_version, run command, one-line summary, every smoke falsifier with its expected exit, and the README exit-code table. - The same script writes a marked "Runnable examples" section into each SKILL.md, with an absolute link, summary and one falsifier per example. addon-scaffolding, bl-info-migration and ui-panels say no example exists yet and point at #378. - --check runs in Validate and tests/run_all.py. It fails when an example is unmapped, a skill name is unknown, a skill has no example and is not pending, or the generated files are stale. - CLAUDE.md and docs/new-example-prompt.md add the mapping to the wiring list. Gallery version badges and a "falsify it" line on detail pages are left for a follow-up. Refs #377 Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 --- .github/workflows/validate.yml | 3 + CLAUDE.md | 4 +- docs/new-example-prompt.md | 4 +- examples/index.json | 2236 +++++++++++++++++ examples/skills.json | 67 + scripts/build_examples_index.py | 213 ++ skills/addon-scaffolding/SKILL.md | 7 + skills/ai-mesh-cleanup/SKILL.md | 17 + skills/bake-high-to-low/SKILL.md | 9 + skills/bl-info-migration/SKILL.md | 7 + skills/custom-properties/SKILL.md | 9 + skills/depsgraph-and-evaluated-data/SKILL.md | 21 + skills/drivers-and-app-handlers/SKILL.md | 10 + skills/engine-export-presets/SKILL.md | 12 + skills/geometry-nodes-python/SKILL.md | 15 + skills/headless-batch-scripting/SKILL.md | 11 + skills/mesh-editing-and-bmesh/SKILL.md | 30 + skills/operators/SKILL.md | 10 + .../procedural-materials-and-shaders/SKILL.md | 11 + skills/slotted-actions-animation/SKILL.md | 9 + skills/ui-panels/SKILL.md | 7 + skills/vse-python/SKILL.md | 11 + tests/run_all.py | 1 + 23 files changed, 2721 insertions(+), 3 deletions(-) create mode 100644 examples/index.json create mode 100644 examples/skills.json create mode 100644 scripts/build_examples_index.py diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 458fd778..6dac13fa 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -326,6 +326,9 @@ jobs: - name: Check claude/blender-rules.md and the rules skill are regenerated from rules/*.mdc run: python3 scripts/build_claude_rules.py --check + - name: Check examples/index.json and each skill's Runnable examples section are regenerated + run: python3 scripts/build_examples_index.py --check + - name: Check the plugin-dist build assembles, carries every skill, and stays slim run: python3 scripts/build_plugin_dist.py --check diff --git a/CLAUDE.md b/CLAUDE.md index 98dbb604..292551c8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -124,10 +124,10 @@ example still carries a `tests/smoke/catalog.json` row and a falsifier. gallery's source of truth. When authoring a new gallery example, copy the anatomy of `examples/bmesh-gear/` (script structure, README shape, dark-studio render recipe) and wire all of: gallery.json entry (with an `alt` sentence describing the still; the build fails without one), -a `tests/smoke/catalog.json` row, hero webp (1280×720) in +a `tests/smoke/catalog.json` row, an `examples/skills.json` entry naming the skills it proves (`python scripts/build_examples_index.py` then regenerates `examples/index.json` and each skill's Runnable examples section; Validate fails on a missing entry), hero webp (1280×720) in `docs/gallery/assets/` + its 640×360 card variant `-hero-640.webp` (`python scripts/make_thumbs.py`; the gallery build exits 5 without it) + preview webp (1200×675), then run `python scripts/build_gallery.py`. A check-only example wires only the -`tests/smoke/catalog.json` row and its README — it is deliberately absent from +`tests/smoke/catalog.json` row, its `examples/skills.json` entry, and its README — it is deliberately absent from `examples/gallery.json` and `docs/gallery/`. Renders must conform to the gallery look spec at `docs/VISUAL-STYLE.md`. Render paths gate framing through the shared helper `examples/gallery_framing.py` — diff --git a/docs/new-example-prompt.md b/docs/new-example-prompt.md index bf993ce3..e950d083 100644 --- a/docs/new-example-prompt.md +++ b/docs/new-example-prompt.md @@ -97,10 +97,12 @@ shape from neighboring examples and repository configuration, including the exam directory, README, gallery metadata and assets, plugin manifest, smoke catalog (`tests/smoke/catalog.json`, whose row must list the falsifier under `falsifiers` with its exact `expect_exit` so CI re-proves it on every run), +the skills mapping (`examples/skills.json`, then `python scripts/build_examples_index.py` +to regenerate `examples/index.json` and the skills' Runnable examples sections), the top-level README's aggregate counts (it no longer carries a per-example row; the gallery is the catalog), and generated gallery pages. A check-only example skips the gallery metadata, assets, and generated pages — it wires -the plugin manifest, the smoke catalog row, and its README, and nothing else. After regenerating the gallery with +the plugin manifest, the smoke catalog row, the skills mapping, and its README, and nothing else. After regenerating the gallery with `python scripts/build_gallery.py`, read the **generated** output character by character—not only `examples/gallery.json` source fields. Open `docs/gallery/index.html` and `docs/gallery//index.html` and inspect the diff --git a/examples/index.json b/examples/index.json new file mode 100644 index 00000000..bc65cb48 --- /dev/null +++ b/examples/index.json @@ -0,0 +1,2236 @@ +[ + { + "name": "armature-bend", + "path": "examples/armature-bend/", + "script": "examples/armature-bend/armature_bend.py", + "skills": [ + "depsgraph-and-evaluated-data", + "mesh-editing-and-bmesh" + ], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/armature-bend/armature_bend.py", + "summary": "Rigging end to end in the data API — edit_bones chain construction, name-bound vertex groups with smoothstep blend zones, posing, and depsgraph evaluation.", + "falsifiers": [ + { + "args": [ + "--zero-curl" + ], + "expect_exit": 8 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "`edit_bones` populated in object mode", + "4": "Edit-mode bone chain off closed form", + "5": "Evaluated vertex count changed", + "6": "Evaluated mesh off closed-form LBS", + "7": "Root ring moved", + "8": "Tip did not deflect (`--zero-curl` lands here)", + "9": "`--output` produced no file", + "10": "Framing gate violation on the `--output` path (`gallery_framing`)" + } + }, + { + "name": "attribute-domain-shear", + "path": "examples/attribute-domain-shear/", + "script": "examples/attribute-domain-shear/attribute_domain_shear.py", + "skills": [ + "mesh-editing-and-bmesh" + ], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/attribute-domain-shear/attribute_domain_shear.py", + "summary": "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.", + "falsifiers": [ + { + "args": [ + "--no-overwrite" + ], + "expect_exit": 5 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "CORNER or POINT attribute size wrong", + "4": "CORNER hub corners off wedge color", + "5": "POINT hub is not last-write (`--no-overwrite` lands here)", + "6": "Outer ring verts off last-write order", + "7": "Measured shear off palette closed form, or ~0", + "9": "`--output` produced no file", + "10": "Gallery framing violation", + "11": "Asset-quality floor violation (render path only)" + } + }, + { + "name": "bake-normal-high-to-low", + "path": "examples/bake-normal-high-to-low/", + "script": "examples/bake-normal-high-to-low/bake_normal_high_to_low.py", + "skills": [ + "bake-high-to-low" + ], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/bake-normal-high-to-low/bake_normal_high_to_low.py", + "summary": "A collapse-decimated hatch plate receiving a Cycles cage-baked tangent normal map from a ribbed high-poly source", + "falsifiers": [ + { + "args": [ + "--flat-source" + ], + "expect_exit": 5 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "Missing UV layer on the target", + "4": "Bake did not `FINISHED` or image `has_data` is false", + "5": "Detail deviant-pixel fraction below 0.40 (`--flat-source` lands here)", + "6": "Detail MAD below 0.05", + "7": "Flat control above 0.05 frac / 0.03 MAD", + "8": "Monotonic gap below 0.30", + "9": "`--output` produced no file", + "10": "Gallery framing violation" + } + }, + { + "name": "bmesh-gear", + "path": "examples/bmesh-gear/", + "script": "examples/bmesh-gear/bmesh_gear.py", + "skills": [ + "mesh-editing-and-bmesh" + ], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/bmesh-gear/bmesh_gear.py", + "summary": "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.", + "falsifiers": [ + { + "args": [ + "--no-extrude" + ], + "expect_exit": 3 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "Topology ≠ closed form (`--no-extrude` lands here)", + "4": "Non-manifold edges", + "6": "`--output` produced no file", + "10": "`--output` framing violation (Layer 1 fill / margin gate, `gallery_framing`)" + } + }, + { + "name": "boolean-exact-volume", + "path": "examples/boolean-exact-volume/", + "script": "examples/boolean-exact-volume/boolean_exact_volume.py", + "skills": [ + "depsgraph-and-evaluated-data", + "mesh-editing-and-bmesh" + ], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/boolean-exact-volume/boolean_exact_volume.py", + "summary": "The Boolean modifier on the EXACT solver.", + "falsifiers": [ + { + "args": [ + "--float-solver" + ], + "expect_exit": 3 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "A result's volume is off its closed form (`--float-solver` lands here)", + "4": "A result is not a closed 2-manifold (an edge without exactly two faces, or loose geometry)", + "5": "A result's volume is within 0.5 m³ of operand A's (the witness would pass vacuously)", + "6": "`--output` produced no file", + "10": "`--output` framing violation (Layer 1 fill / margin gate, `gallery_framing`)" + } + }, + { + "name": "car-mirror-symmetry", + "path": "examples/car-mirror-symmetry/", + "script": "examples/car-mirror-symmetry/car_mirror_symmetry.py", + "skills": [ + "depsgraph-and-evaluated-data" + ], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/car-mirror-symmetry/car_mirror_symmetry.py", + "summary": "A stylized hatchback lofted as one half (52 stations, 13-point rings) and completed by the Mirror modifier, evaluated through the depsgraph.", + "falsifiers": [ + { + "args": [ + "--no-mirror" + ], + "expect_exit": 5 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "Body datablock is not the authored half", + "4": "Authored centerline vert count ≠ 28", + "5": "Evaluated verts ≠ `2n − c` (`--no-mirror` lands here)", + "6": "Evaluated on-plane verts ≠ centerline; also `--output` produced no file", + "7": "Evaluated Euler characteristic ≠ 2", + "8": "Non-manifold edges in the evaluated shell", + "9": "Evaluated verts lack a mirrored partner", + "10": "Mirror partner deviation above tolerance", + "11": "Evaluated bbox not symmetric about X", + "12": "Mirrored-part origin off the plane", + "13": "Mirrored-part datablock is not the authored half (verts, faces, on-plane verts)", + "14": "Mirrored-part evaluated counts ≠ `2n − weld` / `2f`, or on-plane verts ≠ weld", + "15": "Mirrored-part partner check failed", + "16": "Mirrored-part evaluated mesh stayed on one side", + "17": "Gallery framing violation (`--output` path only; `gallery_framing` returns 10, remapped)", + "18": "Asset-quality floor violation (`--output` path only; `gallery_asset_quality` returns 11, remapped)" + } + }, + { + "name": "coincident-vert-weld", + "path": "examples/coincident-vert-weld/", + "script": "examples/coincident-vert-weld/coincident_vert_weld.py", + "skills": [ + "ai-mesh-cleanup" + ], + "render": false, + "min_version": "4.5", + "run": "blender --background --python examples/coincident-vert-weld/coincident_vert_weld.py", + "summary": "Synthesizes two cubes occupying the same space as one mesh, then asserts the duplicates exist before asserting they cross glTF export.", + "falsifiers": [ + { + "args": [ + "--no-duplicate" + ], + "expect_exit": 3 + }, + { + "args": [ + "--weld" + ], + "expect_exit": 4 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage; also exporter RNA missing expected glTF kwargs", + "3": "Pathology missing: coincident shells (`--no-duplicate` lands here)", + "4": "glTF export handling failed (`--weld` lands here)" + } + }, + { + "name": "collision-hull-proxy", + "path": "examples/collision-hull-proxy/", + "script": "examples/collision-hull-proxy/collision_hull_proxy.py", + "skills": [ + "ai-mesh-cleanup", + "mesh-editing-and-bmesh" + ], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/collision-hull-proxy/collision_hull_proxy.py", + "summary": "A fire hydrant street prop inside its compound collision shell: four convex pieces hulled by bmesh.ops.convex_hull from a coarse inflated cage.", + "falsifiers": [ + { + "args": [ + "--shrink-hull" + ], + "expect_exit": 3 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "Render vertex escapes its hull (`--shrink-hull` lands here)", + "4": "Hull edge does not border exactly two faces", + "5": "Signed volume ≤ 0 (inverted winding)", + "6": "Hull vertex off its own face plane", + "7": "Euler characteristic ≠ 2", + "8": "Piece over the 255-face collision budget", + "9": "`--output` produced no file", + "10": "Gallery framing violation (render path)", + "11": "Gallery asset-quality violation (render path)" + } + }, + { + "name": "color-attribute-wheel", + "path": "examples/color-attribute-wheel/", + "script": "examples/color-attribute-wheel/color_attribute_wheel.py", + "skills": [ + "mesh-editing-and-bmesh" + ], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/color-attribute-wheel/color_attribute_wheel.py", + "summary": "The modern color-attributes API — mesh.color_attributes.new() on the CORNER domain.", + "falsifiers": [ + { + "args": [ + "--point-domain" + ], + "expect_exit": 5 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "Topology ≠ closed form", + "4": "Color attribute missing", + "5": "Domain/type ≠ CORNER/FLOAT_COLOR (`--point-domain` lands here)", + "6": "Attribute sized to verts, not loops", + "7": "`active_color` not set", + "8": "Probe loop color off HSV closed form", + "9": "`--output` produced no file", + "10": "Framing gate violation on the `--output` path (`gallery_framing`)" + } + }, + { + "name": "compositor-glare", + "path": "examples/compositor-glare/", + "script": "examples/compositor-glare/compositor_glare.py", + "skills": [], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/compositor-glare/compositor_glare.py", + "summary": "Bloom where it actually lives — a compositor Glare (Fog Glow) node fed by Render Layers.", + "falsifiers": [ + { + "args": [ + "--threshold-high" + ], + "expect_exit": 3 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "Compositor tree / Glare / Threshold / link-chain contract (`--threshold-high` lands here)", + "4": "Check render missing, tube dark, no halo, or halo does not fall off", + "5": "Halo present with compositing off", + "6": "`--output` produced no file", + "10": "Gallery framing violation (render path; checked before the beauty render)" + } + }, + { + "name": "cross-version-property-delete", + "path": "examples/cross-version-property-delete/", + "script": "examples/cross-version-property-delete/cross_version_property_delete.py", + "skills": [ + "custom-properties" + ], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/cross-version-property-delete/cross_version_property_delete.py", + "summary": "Custom ID properties are removed with del, not property_unset.", + "falsifiers": [ + { + "args": [ + "--skip-delete" + ], + "expect_exit": 7 + }, + { + "args": [ + "--subscript-registered" + ], + "expect_exit": 8, + "min_version": "5.0" + }, + { + "args": [ + "--unset-instead" + ], + "expect_exit": 7 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage; also an active object existed after the data-API build", + "3": "Custom ID property did not land", + "4": "`property_unset` on a custom ID key did not TypeError, or it deleted the key", + "5": "`del` did not report removal", + "6": "Keep lamp lost the ID property", + "7": "Clear lamp still has the ID property (`--skip-delete` / `--unset-instead` land here)", + "8": "Registered-prop contract broke: attribute/`property_unset` failed, the prop sits on the wrong side of the 5.0 ID-property split, or `scene[\"name\"]` raised `KeyError` (`--subscript-registered` on 5.0+; exits 0 on 4.5, where subscript access still works)", + "10": "Gallery framing violation", + "12": "`--output` produced no file" + } + }, + { + "name": "curve-bevel-arc", + "path": "examples/curve-bevel-arc/", + "script": "examples/curve-bevel-arc/curve_bevel_arc.py", + "skills": [], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/curve-bevel-arc/curve_bevel_arc.py", + "summary": "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.", + "falsifiers": [ + { + "args": [ + "--no-caps" + ], + "expect_exit": 6 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "Spline type is not BEZIER", + "4": "Bezier point count ≠ 8", + "5": "`bevel_depth` ≠ 0.15", + "6": "`use_fill_caps` is False (`--no-caps` lands here)", + "7": "Evaluated vert/face count off measured tessellation", + "8": "Tube does not rest on the floor", + "9": "Tube height ≠ 2 × bevel", + "10": "X span off closed form; also gallery framing violation", + "11": "`--output` produced no file; also asset-quality floor violation (`gallery_asset_quality`)" + } + }, + { + "name": "custom-normals-shade", + "path": "examples/custom-normals-shade/", + "script": "examples/custom-normals-shade/custom_normals_shade.py", + "skills": [ + "mesh-editing-and-bmesh" + ], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/custom-normals-shade/custom_normals_shade.py", + "summary": "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.", + "falsifiers": [ + { + "args": [ + "--mismatch-angle" + ], + "expect_exit": 5 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "Legacy shading API present, or modern path missing", + "4": "Non-manifold edges (dihedral test undefined)", + "5": "Sharp set ≠ independent dihedral (`--mismatch-angle` lands here)", + "6": "Evaluated loop normals not welded/split as the sharp set promises", + "7": "Custom split normals lost or dequantized in evaluation", + "8": "`shade_auto_smooth` operator behavior drifted from the version split", + "9": "`--output` produced no file", + "10": "Render path: Layer 1 framing violation (`gallery_framing`)", + "11": "Render path: asset-quality floor violation (`gallery_asset_quality`)" + } + }, + { + "name": "damped-track-aim", + "path": "examples/damped-track-aim/", + "script": "examples/damped-track-aim/damped_track_aim.py", + "skills": [], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/damped-track-aim/damped_track_aim.py", + "summary": "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.", + "falsifiers": [ + { + "args": [ + "--mute" + ], + "expect_exit": 7 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "Lamp count ≠ 12", + "4": "Lamp does not carry exactly one DAMPED_TRACK", + "5": "Constraint target is not Orb", + "6": "`track_axis` is not TRACK_Z", + "7": "Constraint muted or influence < 1 (`--mute` lands here)", + "8": "TRACK_TO still present", + "9": "Evaluated aim dot below 0.998", + "10": "Framing gate violation on the `--output` path (`gallery_framing`)", + "12": "`--output` produced no file" + } + }, + { + "name": "degenerate-bevel-weld", + "path": "examples/degenerate-bevel-weld/", + "script": "examples/degenerate-bevel-weld/degenerate_bevel_weld.py", + "skills": [ + "ai-mesh-cleanup", + "mesh-editing-and-bmesh" + ], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/degenerate-bevel-weld/degenerate_bevel_weld.py", + "summary": "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.", + "falsifiers": [ + { + "args": [ + "--both-safe" + ], + "expect_exit": 4 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "Safe bevel produced zero-area faces", + "4": "Degenerate bevel zero-area count ≠ closed form (`--both-safe` lands here)", + "5": "min_area collapse under 1e5×", + "6": "Coincident-position count off the closed form", + "7": "Safe GLB carries degenerate triangles", + "8": "Degenerate GLB triangle or position count drifted", + "9": "`--output` produced no file", + "10": "Gallery framing violation", + "11": "Asset-quality floor violation (render path only)" + } + }, + { + "name": "depsgraph-export", + "path": "examples/depsgraph-export/", + "script": "examples/depsgraph-export/depsgraph_export.py", + "skills": [ + "depsgraph-and-evaluated-data" + ], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/depsgraph-export/depsgraph_export.py", + "summary": "The depsgraph lifetime contract — evaluated_get().to_mesh() paired with to_mesh_clear() — measured against an OBJ export of the same object.", + "falsifiers": [ + { + "args": [ + "--unevaluated" + ], + "expect_exit": 5 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "Evaluated shell did not apply the modifier (evaluated count not above the cage)", + "4": "No OBJ written", + "5": "Export vert count ≠ evaluated (`--unevaluated` lands here)", + "6": "`--output` produced no file", + "7": "Evaluated shell count ≠ Catmull-Clark closed form", + "10": "Gallery framing violation", + "11": "Asset-quality floor violation (render path only)" + } + }, + { + "name": "driver-wave", + "path": "examples/driver-wave/", + "script": "examples/driver-wave/driver_wave.py", + "skills": [ + "drivers-and-app-handlers" + ], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/driver-wave/driver_wave.py", + "summary": "A driver_namespace function driving sixteen organ-pipe heights through SCRIPTED drivers — the sine skyline of the pipe tops is entirely driver-evaluated.", + "falsifiers": [ + { + "args": [ + "--flat-expr" + ], + "expect_exit": 3 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "Evaluated Z scale ≠ `wave_scale` (`--flat-expr` lands here)", + "4": "Original datablock was not flushed", + "6": "`--output` produced no file", + "10": "Gallery framing violation" + } + }, + { + "name": "eval-mesh-datablock-name", + "path": "examples/eval-mesh-datablock-name/", + "script": "examples/eval-mesh-datablock-name/eval_mesh_datablock_name.py", + "skills": [ + "depsgraph-and-evaluated-data" + ], + "render": false, + "min_version": "4.5", + "run": "blender --background --python examples/eval-mesh-datablock-name/eval_mesh_datablock_name.py", + "summary": "Pathology witness for the 5.2 change in `Object.evaluated_get(depsgraph).data.name`.", + "falsifiers": [ + { + "args": [ + "--assume-distinct-names" + ], + "expect_exit": 6, + "min_version": "5.2" + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "Source cube missing or not 8 verts", + "4": "SUBSURF did not produce a distinct evaluated mesh", + "5": "Source datablock name is not `SourceMesh`", + "6": "Evaluated datablock name wrong for this version (`--assume-distinct-names` on 5.2)", + "7": "`to_mesh().name` is not `SourceMesh`" + } + }, + { + "name": "exit-pre-sidecar", + "path": "examples/exit-pre-sidecar/", + "script": "examples/exit-pre-sidecar/exit_pre_sidecar.py", + "skills": [ + "drivers-and-app-handlers" + ], + "render": false, + "min_version": "5.1", + "run": "blender --background --python examples/exit-pre-sidecar/exit_pre_sidecar.py", + "summary": "`bpy.app.handlers.exit_pre` writes `$BDT_SMOKE_SIDECAR` as Blender dies.", + "falsifiers": [ + { + "args": [ + "--atexit-instead" + ], + "expect_sidecar_fail": true, + "min_version": "5.1" + }, + { + "args": [ + "--no-handler" + ], + "expect_sidecar_fail": true, + "min_version": "5.1" + }, + { + "args": [ + "--silent-handler" + ], + "expect_sidecar_fail": true, + "min_version": "5.1" + }, + { + "args": [ + "--wrong-text" + ], + "expect_sidecar_fail": true, + "min_version": "5.1" + }, + { + "args": [ + "--write-in-main" + ], + "expect_sidecar_fail": true, + "min_version": "5.1" + } + ], + "exit_codes": { + "0": "Success (including `--silent-handler` / `--no-handler` / `--wrong-text` / `--write-in-main` / `--atexit-instead`, which the harness then fails)", + "1": "Uncaught exception (FATAL wrapper); also `$BDT_SMOKE_SIDECAR` unset", + "2": "argparse / usage; also `--force-run` on Blender < 5.1 (`exit_pre` missing or unexpectedly present)", + "77": "`SMOKE_SKIP:` `exit_pre` requires Blender 5.1+" + } + }, + { + "name": "export-preset-axis", + "path": "examples/export-preset-axis/", + "script": "examples/export-preset-axis/export_preset_axis.py", + "skills": [ + "engine-export-presets" + ], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/export-preset-axis/export_preset_axis.py", + "summary": "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", + "falsifiers": [ + { + "args": [ + "--same-axis" + ], + "expect_exit": 9 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage; also exporter RNA missing expected glTF kwargs", + "3": "Source mast is not Z-dominant, or tip drifted", + "4": "glTF reimport produced no mesh", + "5": "Preset disk POSITION is not `(x, z, -y)`, or preset node has rotation", + "6": "Z-up disk POSITION is not raw Z-up; also `--output` produced no file", + "7": "Preset reimport is not standing", + "8": "Z-up reimport is not lying along Y, or reimported tip mismatch", + "9": "Reimported orientations did not differ (`--same-axis` lands here)", + "10": "Gallery framing violation", + "11": "`--same-axis` did not collapse the axis difference; also asset-quality floor violation (render path)", + "12": "Render path: no axis-aligned rotation maps the source onto a re-import, so the gizmo frame could not be measured" + } + }, + { + "name": "gltf-export-roundtrip", + "path": "examples/gltf-export-roundtrip/", + "script": "examples/gltf-export-roundtrip/gltf_export_roundtrip.py", + "skills": [ + "depsgraph-and-evaluated-data", + "engine-export-presets", + "headless-batch-scripting" + ], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/gltf-export-roundtrip/gltf_export_roundtrip.py", + "summary": "A sci-fi supply crate exported to glTF and re-imported, verifying the round-trip against the depsgraph-evaluated mesh within float tolerances.", + "falsifiers": [ + { + "args": [ + "--no-yup" + ], + "expect_exit": 9 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "Exporter/importer RNA kwargs drifted", + "4": "Base cage drifted from its closed form", + "5": "Authored UVs drifted from the box-map closed form", + "6": "Bevel produced no evaluated geometry", + "7": "On-disk node/mesh/generator contract drifted", + "8": "On-disk primitive/material binding count drifted", + "9": "On-disk POSITION bounds ≠ axis-converted bbox (`--no-yup` lands here)", + "10": "On-disk POSITION count ≠ evaluated loop count", + "11": "On-disk UV V-flip failed", + "12": "Re-import did not produce exactly one mesh", + "13": "Re-imported object carries a transform", + "14": "Material names drifted on re-import", + "15": "Re-import vert count ≠ evaluated loop count", + "16": "Round-trip position drift", + "17": "Round-trip normal drift", + "18": "Round-trip UV drift", + "19": "Re-import triangle count drifted", + "20": "Per-triangle material bindings drifted", + "21": "`--output` produced no file", + "22": "Gallery framing violation (render path only; the helper's 10, remapped)", + "23": "Gallery asset-quality violation (render path only; the helper's 11, remapped)" + } + }, + { + "name": "gltf-skin-roundtrip", + "path": "examples/gltf-skin-roundtrip/", + "script": "examples/gltf-skin-roundtrip/gltf_skin_roundtrip.py", + "skills": [ + "depsgraph-and-evaluated-data", + "engine-export-presets" + ], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/gltf-skin-roundtrip/gltf_skin_roundtrip.py", + "summary": "A rigged mech scorpion exported to glTF with skins and re-imported, verifying the skinning contract the geometry round-trip left uncovered.", + "falsifiers": [ + { + "args": [ + "--no-skins" + ], + "expect_exit": 5 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "Exporter RNA missing expected kwargs", + "4": "Vertex group count ≠ bone count", + "5": "Disk skins ≠ 1 (`--no-skins` lands here)", + "6": "Skin joints ≠ bone names", + "7": "Missing JOINTS_0/WEIGHTS_0, or accessor length mismatch", + "8": "Disk weight sums off 1.0", + "9": "Disk verts exceed evaluated loops", + "10": "Armature count after import ≠ 1", + "11": "Bone count drifted", + "12": "Named bone lost", + "13": "Bone parent drifted", + "14": "Rest matrices drifted", + "15": "Skinned mesh count after import ≠ 1", + "16": "Re-import vert count ≠ disk", + "17": "Vertex group names drifted", + "18": "Weight round-trip drifted", + "19": "Deformation round-trip drifted", + "20": "`--output` produced no file" + } + }, + { + "name": "gn-bundle-roundtrip", + "path": "examples/gn-bundle-roundtrip/", + "script": "examples/gn-bundle-roundtrip/gn_bundle_roundtrip.py", + "skills": [ + "geometry-nodes-python" + ], + "render": false, + "min_version": "5.0", + "run": "blender --background --python examples/gn-bundle-roundtrip/gn_bundle_roundtrip.py", + "summary": "Combine Bundle / Separate Bundle packing a cube plus Scale, Offset, and Mark — witnessing `geometry-nodes-python` item-name round-trip, not tree structure.", + "falsifiers": [ + { + "args": [ + "--legacy-rna" + ], + "expect_exit": 7, + "min_version": "5.0" + }, + { + "args": [ + "--mismatch" + ], + "expect_exit": 3, + "min_version": "5.0" + }, + { + "args": [ + "--bypass" + ], + "expect_exit": 4, + "min_version": "5.0" + }, + { + "args": [ + "--mismatch-mark" + ], + "expect_exit": 5, + "min_version": "5.0" + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper); `--force-run` on 4.5 lands here", + "2": "argparse / usage", + "3": "Evaluated vert/face count off closed form (`--mismatch`)", + "4": "Unpacked Scale/Offset x-extent off (`--bypass`, `--pack-scale 1`)", + "5": "`bundle_mark` missing or off closed form (`--mismatch-mark`)", + "6": "Carrier mesh rewritten", + "7": "`--legacy-rna`: 5.x RNA rejects `GeometryNodeCombineBundle` (the catalog falsifier)", + "8": "`--legacy-rna`: `GeometryNodeCombineBundle` was accepted (the rejection contract broke)", + "77": "`SMOKE_SKIP:` Bundles require Blender 5.0+" + } + }, + { + "name": "gn-instance-grid", + "path": "examples/gn-instance-grid/", + "script": "examples/gn-instance-grid/gn_instance_grid.py", + "skills": [ + "geometry-nodes-python" + ], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/gn-instance-grid/gn_instance_grid.py", + "summary": "A generative Geometry Nodes tree — Mesh Grid → Instance on Points (a modeled keycap via Object Info) → Realize Instances → Set Shade Smooth → Set Material.", + "falsifiers": [ + { + "args": [ + "--one-cell" + ], + "expect_exit": 4 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "Carrier vertex count ≠ 1", + "4": "Evaluated topology ≠ 1089 verts / 1089 faces (`--one-cell` lands here)", + "5": "Corner grid cell does not hold exactly one keycap (121 verts)", + "6": "Set Material did not carry `Keycap.PBT` and `Keycap.Accent`", + "7": "Corner keycap off its closed-form grid point or lift", + "8": "`--output` produced no file", + "9": "Accent selection did not cover exactly one keycap at the front-right point", + "10": "Render path: Layer 1 framing violation (`gallery_framing`)", + "11": "Render path: asset-quality floor violation (`gallery_asset_quality`)" + } + }, + { + "name": "gn-modifier-inputs", + "path": "examples/gn-modifier-inputs/", + "script": "examples/gn-modifier-inputs/gn_modifier_inputs.py", + "skills": [ + "geometry-nodes-python" + ], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/gn-modifier-inputs/gn_modifier_inputs.py", + "summary": "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.", + "falsifiers": [ + { + "args": [ + "--same-height" + ], + "expect_exit": 7 + }, + { + "args": [ + "--api", + "dict" + ], + "expect_exit": 5, + "min_version": "5.2" + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "Height input identifier missing on the tree interface", + "4": "Modifiers do not share one node_group", + "5": "Version-path write raised (`--api dict` on 5.2, `--api rna` on 4.5)", + "6": "Version-path read raised", + "7": "Readback ≠ intended height (`--same-height` lands here)", + "8": "Evaluated Z-extent ≠ intended height", + "9": "Evaluated mesh not sitting on z=0", + "10": "Gallery framing violation", + "11": "Evaluated extents not distinct", + "12": "`--output` produced no file" + } + }, + { + "name": "gn-sdf-remesh", + "path": "examples/gn-sdf-remesh/", + "script": "examples/gn-sdf-remesh/gn_sdf_remesh.py", + "skills": [ + "geometry-nodes-python" + ], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/gn-sdf-remesh/gn_sdf_remesh.py", + "summary": "A Geometry Nodes SDF remesh (MeshToSDFGrid → GridToMesh at the SDF zero-level), with a Set Material node carrying the material through the remesh.", + "falsifiers": [ + { + "args": [ + "--no-sdf" + ], + "expect_exit": 3 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "SDF remesh produced no/unchanged geometry (`--no-sdf` lands here)", + "4": "`--output` produced no file", + "5": "Wrong-era EEVEE engine id was accepted", + "6": "Input material dropped by remesh", + "10": "Framing gate violation on the `--output` path (`gallery_framing`)" + } + }, + { + "name": "gn-sim-fountain", + "path": "examples/gn-sim-fountain/", + "script": "examples/gn-sim-fountain/gn_sim_fountain.py", + "skills": [ + "geometry-nodes-python" + ], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/gn-sim-fountain/gn_sim_fountain.py", + "summary": "A Simulation Zone only advances one step per consecutive frame_set.", + "falsifiers": [ + { + "args": [ + "--unpair" + ], + "expect_exit": 3 + }, + { + "args": [ + "--euler" + ], + "expect_exit": 4 + }, + { + "args": [ + "--prebake-trap" + ], + "expect_exit": 5 + }, + { + "args": [ + "--calc-to-frame" + ], + "expect_exit": 6 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "Point count or frame-start state off the launch data", + "4": "Stepped frames off the closed-form arc (position or velocity)", + "5": "Direct jump did not reproduce the one-step trap", + "6": "After the cache fill, random-access frames off the arc (or fill not `FINISHED`)", + "10": "Gallery framing violation", + "12": "`--output` produced no file" + } + }, + { + "name": "gn-socket-rename", + "path": "examples/gn-socket-rename/", + "script": "examples/gn-socket-rename/gn_socket_rename.py", + "skills": [ + "geometry-nodes-python" + ], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/gn-socket-rename/gn_socket_rename.py", + "summary": "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", + "falsifiers": [ + { + "args": [ + "--legacy-ids" + ], + "expect_exit": 5, + "min_version": "5.2" + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "4": "Identifier inventory wrong for this Blender version", + "5": "Pre-5.2 identifier missing (`--legacy-ids` on 5.2)", + "6": "Enabled-name socket lookup failed", + "7": "Evaluated vert count off (Compare did not switch the column in)", + "8": "Evaluated zmax off closed form", + "9": "POINT `gauge_h` not 1.80 on eight column verts", + "10": "Gallery framing violation", + "12": "`--output` produced no file" + } + }, + { + "name": "gn-zone-iterate", + "path": "examples/gn-zone-iterate/", + "script": "examples/gn-zone-iterate/gn_zone_iterate.py", + "skills": [ + "geometry-nodes-python" + ], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/gn-zone-iterate/gn_zone_iterate.py", + "summary": "Repeat Zone and For Each Element only iterate after pair_with_output.", + "falsifiers": [ + { + "args": [ + "--unpair-repeat" + ], + "expect_exit": 3 + }, + { + "args": [ + "--no-offset" + ], + "expect_exit": 4 + }, + { + "args": [ + "--unpair-foreach" + ], + "expect_exit": 5 + }, + { + "args": [ + "--foreach-main" + ], + "expect_exit": 5 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage; also carrier mesh was rewritten", + "3": "Repeat evaluated vert/face count off closed form", + "4": "Repeat X-centers off closed form", + "5": "For Each evaluated vert/face count off closed form", + "6": "For Each Z-centers off closed form", + "10": "Gallery framing violation", + "12": "`--output` produced no file" + } + }, + { + "name": "gp-lineart-contour", + "path": "examples/gp-lineart-contour/", + "script": "examples/gp-lineart-contour/gp_lineart_contour.py", + "skills": [], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/gp-lineart-contour/gp_lineart_contour.py", + "summary": "Grease Pencil LINEART modifier ink via the depsgraph on a cel-shaded lighthouse diorama.", + "falsifiers": [ + { + "args": [ + "--no-contour" + ], + "expect_exit": 4 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage; also GPv3 collection address contract", + "3": "LINEART `thickness` / `radius` trap for this Blender", + "4": "LINEART type, source, or `use_contour` (`--no-contour` lands here)", + "5": "Evaluated drawing below the stroke/point gates", + "6": "Cleared `source_object` still produced strokes", + "7": "Every edge type off still produced strokes, or the rebuild did not recover the same counts", + "8": "`--output` produced no file", + "10": "Gallery framing violation" + } + }, + { + "name": "grease-pencil-rosette", + "path": "examples/grease-pencil-rosette/", + "script": "examples/grease-pencil-rosette/grease_pencil_rosette.py", + "skills": [], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/grease-pencil-rosette/grease_pencil_rosette.py", + "summary": "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.", + "falsifiers": [ + { + "args": [ + "--open-strokes" + ], + "expect_exit": 5 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "GPv3 address / legacy trap for this Blender", + "4": "Object type, layer count, or frame number", + "5": "Stroke topology or cyclic (`--open-strokes` lands here)", + "6": "Lazy attribute layers missing or wrong domain/type", + "7": "Position buffer length or closed-form round-trip", + "8": "Grease Pencil material missing gpencil settings", + "9": "`--output` produced no file", + "10": "Framing gate violation on the `--output` path (`gallery_framing`)" + } + }, + { + "name": "image-pixels-testcard", + "path": "examples/image-pixels-testcard/", + "script": "examples/image-pixels-testcard/image_pixels_testcard.py", + "skills": [], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/image-pixels-testcard/image_pixels_testcard.py", + "summary": "The Image pixel-buffer contract — a procedural broadcast test card written into bpy.data.images.new() with one pixels.foreach_set (589,824 floats).", + "falsifiers": [ + { + "args": [ + "--wrong-origin" + ], + "expect_exit": 4 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "Pixel buffer is not always RGBA", + "4": "Byte round-trip vs closed-form card (`--wrong-origin` lands here)", + "5": "Float-buffer round-trip failed", + "6": "`scale()` did not reallocate, or stale-size read succeeded", + "7": "`save()` source/buffer-drop contract drifted", + "8": "`save_render()` flipped source or disturbed the buffer", + "9": "Byte PNG save/reload error", + "10": "Framing gate violation on the `--output` path (`gallery_framing`)", + "12": "`--output` produced no file" + } + }, + { + "name": "lattice-deform", + "path": "examples/lattice-deform/", + "script": "examples/lattice-deform/lattice_deform.py", + "skills": [ + "depsgraph-and-evaluated-data", + "mesh-editing-and-bmesh" + ], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/lattice-deform/lattice_deform.py", + "summary": "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).", + "falsifiers": [ + { + "args": [ + "--bspline" + ], + "expect_exit": 4 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "Undeformed lattice moved the mesh (identity check)", + "4": "A vertex is off the trilinear closed form, or the vertex count changed (`--bspline` lands here)", + "5": "Max displacement below the floor (the witness would pass vacuously)", + "6": "`--output` produced no file", + "10": "`--output` framing violation (Layer 1 fill / margin gate, `gallery_framing`)" + } + }, + { + "name": "light-link-studio", + "path": "examples/light-link-studio/", + "script": "examples/light-link-studio/light_link_studio.py", + "skills": [], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/light-link-studio/light_link_studio.py", + "summary": "One key, one hero: a light linked to a receiver collection lights only the hero, proven by two pixel renders in one pass.", + "falsifiers": [ + { + "args": [ + "--skip-link" + ], + "expect_exit": 5 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "Light datablock gained `light_linking`", + "4": "Light objects lost `light_linking`", + "5": "`receiver_collection` assignment did not read back (`--skip-link` lands here)", + "6": "Linked state: hero dark or hero/decoy ratio below gate", + "7": "Decoy rise below gate when unlinked", + "8": "Hero drifted across the unlink", + "9": "`--output` produced no file", + "10": "Framing gate violation on the `--output` path (`gallery_framing`)" + } + }, + { + "name": "lightmap-uv-channel", + "path": "examples/lightmap-uv-channel/", + "script": "examples/lightmap-uv-channel/lightmap_uv_channel.py", + "skills": [ + "mesh-editing-and-bmesh" + ], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/lightmap-uv-channel/lightmap_uv_channel.py", + "summary": "A market cart carrying the two-channel UV contract for baked lighting: UV0 untouched, UVLight packed with no overlaps and a respected margin.", + "falsifiers": [ + { + "args": [ + "--overlap-islands" + ], + "expect_exit": 7 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "UV layer names ≠ UV0 / UV1", + "4": "UVMap / UV1 render/clone flags wrong after re-assert", + "5": "Channel 0 (UV0) touched by the UV1 unwrap", + "6": "UV1 loops outside `[0,1]`", + "7": "SAT overlap self-test failed, or UV1 triangle pairs overlap (`--overlap-islands` lands here)", + "8": "Min island distance below margin", + "9": "Part is not watertight", + "10": "Gallery framing violation; also `--output` produced no file", + "11": "Gallery asset-quality violation" + } + }, + { + "name": "lod-decimate-chain", + "path": "examples/lod-decimate-chain/", + "script": "examples/lod-decimate-chain/lod_decimate_chain.py", + "skills": [ + "ai-mesh-cleanup", + "depsgraph-and-evaluated-data", + "mesh-editing-and-bmesh" + ], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/lod-decimate-chain/lod_decimate_chain.py", + "summary": "A retro rocket at LOD0/1/2 via the Decimate modifier evaluated through the depsgraph, with a wireframe of each evaluated mesh.", + "falsifiers": [ + { + "args": [ + "--no-decimate" + ], + "expect_exit": 6 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "Base topology ≠ closed form", + "4": "Base bbox ≠ closed form", + "5": "LOD0 evaluated counts ≠ base", + "6": "Evaluated tris ≥ base (`--no-decimate` lands here)", + "7": "Original datablock mutated after evaluation", + "8": "LOD tris outside ratio bounds", + "9": "LOD bbox lost silhouette-critical dimensions", + "10": "`--output` framing outside the Layer 1 band (`gallery_framing`)", + "11": "`--output` asset below the quality floors (`gallery_asset_quality`)", + "12": "`--output` produced no file" + } + }, + { + "name": "mesh-automasking-settings", + "path": "examples/mesh-automasking-settings/", + "script": "examples/mesh-automasking-settings/mesh_automasking_settings.py", + "skills": [], + "render": false, + "min_version": "4.5", + "run": "blender --background --python examples/mesh-automasking-settings/mesh_automasking_settings.py", + "summary": "Pathology witness for the 5.2 move of sculpt automasking RNA off `Brush` into `MeshAutomaskingSettings`.", + "falsifiers": [ + { + "args": [ + "--assume-brush-attrs" + ], + "expect_exit": 5, + "min_version": "5.2" + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "SCULPT brush was not created", + "4": "`MeshAutomaskingSettings` type presence wrong for this version", + "5": "Old Brush automasking attributes missing when required (`--assume-brush-attrs` on 5.2)", + "6": "Old Brush attributes still present, or mas is None, on 5.2+", + "7": "Current-location read raised or returned the wrong types" + } + }, + { + "name": "mesh-hygiene-audit", + "path": "examples/mesh-hygiene-audit/", + "script": "examples/mesh-hygiene-audit/mesh_hygiene_audit.py", + "skills": [ + "ai-mesh-cleanup", + "mesh-editing-and-bmesh" + ], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/mesh-hygiene-audit/mesh_hygiene_audit.py", + "summary": "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.", + "falsifiers": [ + { + "args": [ + "--inject", + "ngon" + ], + "expect_exit": 3 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "Ngon present (`--inject ngon` lands here)", + "4": "Loose vertices", + "5": "Non-manifold or boundary edges", + "6": "Zero-area faces", + "7": "Winding seam edges, or signed volume ≤ 0", + "8": "Body casting Euler characteristic ≠ 2", + "9": "`--output` produced no file, or dirty staging lost a defect class", + "10": "Gallery framing violation", + "11": "Gallery asset-quality floor violation" + } + }, + { + "name": "modular-kit-snap", + "path": "examples/modular-kit-snap/", + "script": "examples/modular-kit-snap/modular_kit_snap.py", + "skills": [ + "ai-mesh-cleanup", + "mesh-editing-and-bmesh" + ], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/modular-kit-snap/modular_kit_snap.py", + "summary": "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.", + "falsifiers": [ + { + "args": [ + "--seam-gap" + ], + "expect_exit": 5 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "Boundary vert count or end-plane membership", + "4": "End rings do not partition evenly, or opposing loops differ", + "5": "Tiled instances do not share boundary positions (`--seam-gap` lands here)", + "6": "Shell or whole-asset bbox off the declared tile", + "7": "Boundary edge count, non-manifold edges, or rim off the end planes", + "8": "Detail part not watertight", + "9": "Detail part reaches a tile boundary; also `--output` produced no file", + "10": "Framing deviation score exceeds cap (`--close-camera`)", + "11": "Kit part count, unapplied scale, namespace, or origin; also gallery asset-quality violation" + } + }, + { + "name": "ngon-triangulate", + "path": "examples/ngon-triangulate/", + "script": "examples/ngon-triangulate/ngon_triangulate.py", + "skills": [ + "ai-mesh-cleanup", + "mesh-editing-and-bmesh" + ], + "render": false, + "min_version": "4.5", + "run": "blender --background --python examples/ngon-triangulate/ngon_triangulate.py", + "summary": "Synthesizes one hexagon by dissolving a cube edge, then asserts the hygiene / tangent contracts on that defective mesh.", + "falsifiers": [ + { + "args": [ + "--no-dissolve" + ], + "expect_exit": 3 + }, + { + "args": [ + "--skip-triangulate" + ], + "expect_exit": 4 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (the `__main__` wrapper prints `ERROR: : `)", + "2": "argparse / usage", + "3": "Pathology missing: n-gon count, loops, or face count (`--no-dissolve` lands here)", + "4": "`calc_tangents` / triangulate handling (`--skip-triangulate` lands here, via the real handling assertion)" + } + }, + { + "name": "parent-inverse-orrery", + "path": "examples/parent-inverse-orrery/", + "script": "examples/parent-inverse-orrery/parent_inverse_orrery.py", + "skills": [], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/parent-inverse-orrery/parent_inverse_orrery.py", + "summary": "Data-API parenting for a brass orrery — the keep-world idiom (child.parent = pivot; child.matrix_parent_inverse = pivot.matrix_world.inverted()) carrying arms.", + "falsifiers": [ + { + "args": [ + "--skip-mpi" + ], + "expect_exit": 6 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "Bare parenting did not jump", + "4": "Keep-world idiom off", + "5": "Stale `matrix_world` contract broken", + "6": "Planet off closed-form orbit (`--skip-mpi` lands here)", + "7": "Moon off closed-form orbit", + "8": "`--output` produced no file", + "10": "Gallery framing violation (render path only)" + } + }, + { + "name": "png-exr-alpha", + "path": "examples/png-exr-alpha/", + "script": "examples/png-exr-alpha/png_exr_alpha.py", + "skills": [], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/png-exr-alpha/png_exr_alpha.py", + "summary": "Float-image PNG save trap — float_buffer=True Image.save() writes RGBA16 and unpremultiplies as if associated-alpha.", + "falsifiers": [ + { + "args": [ + "--opaque-alpha" + ], + "expect_exit": 5 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage; also closed-form palette no longer stresses unpremul", + "3": "`float_buffer=True` image reports `is_float=False`", + "4": "Float→PNG IHDR is not RGBA16", + "5": "Float→PNG RGB error below floor (`--opaque-alpha` lands here)", + "6": "Float→PNG disagrees with closed-form false-unpremul model", + "7": "Float→EXR round-trip above tolerance", + "8": "`float_buffer=False` image reports `is_float=True`", + "9": "Byte→PNG IHDR is not RGBA8", + "10": "Byte→PNG disagrees with straight-alpha 8-bit model; also gallery framing violation", + "11": "Byte→PNG stress cell looks false-unpremul-mangled", + "12": "EXR `color_mode=RGB` alpha-drop contract", + "13": "`--output` produced no file" + } + }, + { + "name": "prop-origin-transform", + "path": "examples/prop-origin-transform/", + "script": "examples/prop-origin-transform/prop_origin_transform.py", + "skills": [ + "ai-mesh-cleanup", + "operators" + ], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/prop-origin-transform/prop_origin_transform.py", + "summary": "Street pedestal origin-to-base-center + data-API scale apply + matrix_parent_inverse for a flanged conduit elbow.", + "falsifiers": [ + { + "args": [ + "--skip-mpi" + ], + "expect_exit": 8 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "Stale `matrix_world` contract broken", + "4": "World bbox moved across bake", + "5": "Scale after bake is not (1,1,1)", + "6": "Origin not at local base center", + "7": "Bare parenting did not jump", + "8": "MPI did not restore world location (`--skip-mpi` lands here)", + "9": "`--output` produced no file", + "10": "Gallery framing violation", + "11": "Gallery asset-quality violation (render path only)" + } + }, + { + "name": "ray-cast-space", + "path": "examples/ray-cast-space/", + "script": "examples/ray-cast-space/ray_cast_space.py", + "skills": [ + "depsgraph-and-evaluated-data" + ], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/ray-cast-space/ray_cast_space.py", + "summary": "Object.ray_cast is object-local while Scene.ray_cast is world space.", + "falsifiers": [ + { + "args": [ + "--world-to-object" + ], + "expect_exit": 4 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "`Scene.ray_cast` missed, hit another object, or hit off the closed-form world point or normal", + "4": "`Object.ray_cast` missed, or its local hit mapped back by `matrix_world` disagrees with the world point, normal or polygon (`--world-to-object` lands here)", + "5": "The world-coordinate trap did not bite: world coords fed to `Object.ray_cast` landed within `TRAP_MIN` of a true hit", + "6": "`--output` produced no file", + "10": "`--output` framing violation (Layer 1 fill / margin gate, `gallery_framing`)" + } + }, + { + "name": "shader-node-group", + "path": "examples/shader-node-group/", + "script": "examples/shader-node-group/shader_node_group.py", + "skills": [ + "procedural-materials-and-shaders" + ], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/shader-node-group/shader_node_group.py", + "summary": "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.", + "falsifiers": [ + { + "args": [ + "--same-tint" + ], + "expect_exit": 6 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "Interface sockets missing Tint / Roughness / Shader", + "4": "Group datablock `users` ≠ number of mug materials (5)", + "5": "Instance points at a different node tree", + "6": "Instance Tint values not pairwise distinct (`--same-tint` lands here)", + "7": "`--output` produced no file", + "10": "Gallery framing violation" + } + }, + { + "name": "shape-key-blend", + "path": "examples/shape-key-blend/", + "script": "examples/shape-key-blend/shape_key_blend.py", + "skills": [ + "mesh-editing-and-bmesh" + ], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/shape-key-blend/shape_key_blend.py", + "summary": "A relative Tall shape key that turns a squat ceramic jar into a trumpet vase.", + "falsifiers": [ + { + "args": [ + "--zero-blend" + ], + "expect_exit": 5 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "No shape keys on mesh", + "4": "Key names ≠ Basis, Tall", + "5": "Tall.value ≠ 0.5 (`--zero-blend` lands here)", + "6": "Undeformed `mesh.vertices` not at Basis", + "7": "Evaluated vert off closed-form blend", + "8": "Evaluated Z span off closed form", + "9": "Rim radius (flare) off closed form", + "10": "Framing gate (`gallery_framing`, `--output` only)", + "11": "Asset-quality gate (`gallery_asset_quality`, `--output` only)", + "12": "`--output` produced no file" + } + }, + { + "name": "sky-texture-sun-elevation", + "path": "examples/sky-texture-sun-elevation/", + "script": "examples/sky-texture-sun-elevation/sky_texture_sun_elevation.py", + "skills": [ + "procedural-materials-and-shaders" + ], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/sky-texture-sun-elevation/sky_texture_sun_elevation.py", + "summary": "World ShaderNodeTexSky driving Background Color — the sky contract across 4.5 LTS and 5.1.", + "falsifiers": [ + { + "args": [ + "--unlink-sky" + ], + "expect_exit": 6 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "`sky_type` is not the identifier for this Blender", + "4": "`sun_elevation` round-trip failed", + "5": "`dust_density` / `aerosol_density` / `NISHITA` / `MULTIPLE_SCATTERING` trap", + "6": "Sky → Background → World Output links broken (`--unlink-sky` lands here)", + "7": "High-elevation zenith luma below floor", + "8": "Zenith luma did not rise with elevation", + "9": "`--output` produced no file", + "10": "`--output` framing violation (`gallery_framing`, per panel)", + "12": "`--output` still clips to white under Standard (> 0.05 % of pixels)" + } + }, + { + "name": "soccer-ball-goldberg", + "path": "examples/soccer-ball-goldberg/", + "script": "examples/soccer-ball-goldberg/soccer_ball_goldberg.py", + "skills": [ + "depsgraph-and-evaluated-data", + "mesh-editing-and-bmesh" + ], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/soccer-ball-goldberg/soccer_ball_goldberg.py", + "summary": "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.", + "falsifiers": [ + { + "args": [ + "--invert-bind" + ], + "expect_exit": 13 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "Topology ≠ 60/90/32", + "4": "Euler characteristic ≠ 2", + "5": "Face census ≠ 12 pentagons + 20 hexagons", + "6": "Vertex degree not uniform 3; also `--output` produced no file", + "7": "Non-manifold edges", + "8": "Edge lengths not uniform", + "9": "Face planarity off", + "10": "Centroid off origin", + "11": "Circumradius not uniform", + "12": "Panel material count ≠ 2", + "13": "Panel binding not by vertex count (`--invert-bind` lands here)", + "14": "Framing gate (`--output` path only; `gallery_framing` returns 10, remapped)" + } + }, + { + "name": "socket-attach-points", + "path": "examples/socket-attach-points/", + "script": "examples/socket-attach-points/socket_attach_points.py", + "skills": [], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/socket-attach-points/socket_attach_points.py", + "summary": "A survey drone whose named SKT_ empties are the spawn contract: modules parented with an identity local transform seat exactly on their mount pads.", + "falsifiers": [ + { + "args": [ + "--skip-mpi" + ], + "expect_exit": 3 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "Socket world matrix, orthonormal basis, or determinant (`--skip-mpi` lands here)", + "4": "Socket pad normal, origin, or up-axis rule", + "5": "Module seating / mount axis / leftover local transform", + "6": "Population, namespace, default datablock name, SKT_ prefix, or skid plane", + "7": "Re-pose: socket or module lost the root", + "8": "Parent-inverse clear, child scale pushdown, or post-apply module drift", + "9": "`--output` produced no file", + "10": "Gallery framing violation", + "11": "Gallery asset-quality violation" + } + }, + { + "name": "solidify-even-thickness", + "path": "examples/solidify-even-thickness/", + "script": "examples/solidify-even-thickness/solidify_even_thickness.py", + "skills": [ + "depsgraph-and-evaluated-data", + "mesh-editing-and-bmesh" + ], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/solidify-even-thickness/solidify_even_thickness.py", + "summary": "Solidify's use_even_offset on a folded strip.", + "falsifiers": [ + { + "args": [ + "--no-even" + ], + "expect_exit": 4 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "Evaluated topology is not the original surface plus one copy per vertex (2N verts, originals untouched)", + "4": "The even shell is not *t* thick at every fold, or its copies are off t / cos(φ/2) (`--no-even` lands here)", + "5": "The plain shell is off the t · cos(φ/2) closed form at a fold", + "6": "The plain shell thins by less than the floor (the witness would pass vacuously)", + "7": "`--output` produced no file", + "10": "`--output` framing violation (Layer 1 fill / margin gate, `gallery_framing`)" + } + }, + { + "name": "swatch-grid", + "path": "examples/swatch-grid/", + "script": "examples/swatch-grid/swatch_grid.py", + "skills": [ + "procedural-materials-and-shaders" + ], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/swatch-grid/swatch_grid.py", + "summary": "Procedural Principled materials — metal and dielectric, the emission pattern, and the cross-version set_specular shim.", + "falsifiers": [ + { + "args": [ + "--same-base" + ], + "expect_exit": 3 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "Distinct swatch colors ≠ 6 (`--same-base` lands here); also render not six distinct regions", + "4": "`--output` produced no file", + "5": "Wrong-era EEVEE engine id was accepted", + "10": "Gallery framing violation" + } + }, + { + "name": "temp-override-join", + "path": "examples/temp-override-join/", + "script": "examples/temp-override-join/temp_override_join.py", + "skills": [ + "headless-batch-scripting", + "operators" + ], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/temp-override-join/temp_override_join.py", + "summary": "Join seven lantern parts into one object under bpy.context.temp_override — the supported replacement for the removed context.copy() dict-pass form.", + "falsifiers": [ + { + "args": [ + "--no-override" + ], + "expect_exit": 3 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "Mesh object count after join ≠ 1 (`--no-override` lands here)", + "4": "Joined target is not the sole remaining mesh", + "5": "Verts / faces ≠ the sum over the seven parts", + "6": "Source objects still present", + "7": "Local Z span ≠ [0, 2.303]: a part transform was not applied", + "8": "Material slots ≠ the five part materials, once each", + "9": "Faces per material ≠ what the parts brought (indices not remapped)", + "10": "`--output` framing gate (`gallery_framing`)", + "11": "`--output` asset-quality floors (`gallery_asset_quality`)", + "12": "`--output` produced no file" + } + }, + { + "name": "text-version-stamp", + "path": "examples/text-version-stamp/", + "script": "examples/text-version-stamp/text_version_stamp.py", + "skills": [], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/text-version-stamp/text_version_stamp.py", + "summary": "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.", + "falsifiers": [ + { + "args": [ + "--wrong-body" + ], + "expect_exit": 4 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "Not a FONT `TextCurve` with the built-in Bfont", + "4": "`body` is not the live `version_string` (`--wrong-body` lands here)", + "5": "Flat text is not a filled planar mesh", + "6": "Extrude / bevel closed form failed", + "7": "Appending characters did not widen the text", + "8": "Mesh survived `to_mesh_clear()`", + "9": "`--output` produced no file", + "10": "Gallery framing violation", + "13": "`--output`: bundled `datafiles/fonts/Inter.woff2` not found" + } + }, + { + "name": "triangulate-tangents", + "path": "examples/triangulate-tangents/", + "script": "examples/triangulate-tangents/triangulate_tangents.py", + "skills": [ + "mesh-editing-and-bmesh" + ], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/triangulate-tangents/triangulate_tangents.py", + "summary": "A machined buckler verifying the tangent-space contract a game engine's normal mapping depends on.", + "falsifiers": [ + { + "args": [ + "--zero-uv" + ], + "expect_exit": 4 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "Loop-triangle count ≠ closed form", + "4": "Re-fetched UV layer drifted from the polar field (`--zero-uv` lands here)", + "5": "Tangent basis not orthonormal", + "6": "Bitangent ≠ sign × (n × t)", + "7": "Tangents deviate from the edge/UV closed form", + "8": "Flipped tangent inside a clean triangle", + "10": "Gallery framing violation; also `--output` produced no file", + "11": "Asset-quality floor violation (`--output` only; shared helper)" + } + }, + { + "name": "turntable", + "path": "examples/turntable/", + "script": "examples/turntable/turntable.py", + "skills": [ + "slotted-actions-animation" + ], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/turntable/turntable.py", + "summary": "A slotted-actions Z-rotation turntable keyed through the cross-version channelbag path (get_channelbag_for_slot).", + "falsifiers": [ + { + "args": [ + "--no-keys" + ], + "expect_exit": 3 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "Rotation keys do not drive playback (`--no-keys` lands here)", + "4": "`--output` produced no file", + "5": "Wrong-era EEVEE engine id was accepted", + "10": "Framing gate violation on the `--output` path (`gallery_framing`)" + } + }, + { + "name": "unapplied-scale-gltf", + "path": "examples/unapplied-scale-gltf/", + "script": "examples/unapplied-scale-gltf/unapplied_scale_gltf.py", + "skills": [ + "ai-mesh-cleanup", + "engine-export-presets" + ], + "render": false, + "min_version": "4.5", + "run": "blender --background --python examples/unapplied-scale-gltf/unapplied_scale_gltf.py", + "summary": "Synthesizes unapplied non-uniform object scale on a unit cube, then asserts the glTF exporter contract.", + "falsifiers": [ + { + "args": [ + "--identity" + ], + "expect_exit": 3 + }, + { + "args": [ + "--bake" + ], + "expect_exit": 4 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage; also exporter RNA missing expected glTF kwargs", + "3": "Pathology missing: unapplied non-uniform scale (`--identity` lands here)", + "4": "Export handling failed (`--bake` lands here)" + } + }, + { + "name": "usd-export-evaluation-mode", + "path": "examples/usd-export-evaluation-mode/", + "script": "examples/usd-export-evaluation-mode/usd_export_evaluation_mode.py", + "skills": [ + "depsgraph-and-evaluated-data", + "headless-batch-scripting" + ], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/usd-export-evaluation-mode/usd_export_evaluation_mode.py", + "summary": "The USD exporter evaluation_mode chooses viewport versus render modifier quality.", + "falsifiers": [ + { + "args": [ + "--subdivision", + "BEST_MATCH" + ], + "expect_exit": 4 + }, + { + "args": [ + "--evaluation-mode", + "VIEWPORT" + ], + "expect_exit": 4 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage; also base cube verts ≠ 8", + "3": "VIEWPORT TESSELLATE point/face count off closed form", + "4": "RENDER TESSELLATE point/face count or scheme off closed form", + "5": "BEST_MATCH cage point count or scheme off", + "6": "RENDER and VIEWPORT USDA point counts are identical (check), or the re-imported RENDER goblet is not denser than the VIEWPORT one (`--output`)", + "10": "Gallery framing violation", + "12": "`--output` produced no file" + } + }, + { + "name": "uv-layer-grid", + "path": "examples/uv-layer-grid/", + "script": "examples/uv-layer-grid/uv_layer_grid.py", + "skills": [ + "mesh-editing-and-bmesh" + ], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/uv-layer-grid/uv_layer_grid.py", + "summary": "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.", + "falsifiers": [ + { + "args": [ + "--precreate-on-hazard" + ], + "expect_exit": 3 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "Silent-no-op hazard gone (`--precreate-on-hazard` lands here)", + "4": "Grid topology drifted", + "5": "Pre-create + calc_uvs did not persist one UV layer", + "6": "calc_uvs closed-form error", + "7": "Mesh UV round-trip error", + "8": "calc_uvs=False unexpectedly created a UV layer", + "9": "Explicit UV assignment error", + "10": "Framing gate violation on the `--output` path (`gallery_framing`)", + "11": "Broken panel is not flat", + "12": "Broken panel is not texel-(0,0) teal", + "13": "Repaired panel is not a checker", + "14": "Broken and repaired panels render identically", + "15": "`--output` produced no file" + } + }, + { + "name": "vertex-color-ao", + "path": "examples/vertex-color-ao/", + "script": "examples/vertex-color-ao/vertex_color_ao.py", + "skills": [ + "mesh-editing-and-bmesh" + ], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/vertex-color-ao/vertex_color_ao.py", + "summary": "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.", + "falsifiers": [ + { + "args": [ + "--uniform-hemisphere" + ], + "expect_exit": 3 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "Calibration AO off closed form (`--uniform-hemisphere` lands here)", + "4": "Unoccluded plate is not 1.0, or AO is not strictly increasing with distance", + "5": "Bake empty, out of range, or spread too small", + "6": "FLOAT_COLOR / BYTE_COLOR round-trip contract", + "7": "Colour attributes missing, retyped, or drifted under the depsgraph", + "8": "Asset part count, unapplied scale, namespace, render attribute, or ground plane", + "9": "`--output` produced no file", + "10": "Gallery framing violation", + "11": "Gallery asset-quality violation" + } + }, + { + "name": "vertex-weight-limit", + "path": "examples/vertex-weight-limit/", + "script": "examples/vertex-weight-limit/vertex_weight_limit.py", + "skills": [ + "mesh-editing-and-bmesh" + ], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/vertex-weight-limit/vertex_weight_limit.py", + "summary": "A rigged industrial robot arm pruned to the game-engine cap of four bone influences per vertex, through the data API.", + "falsifiers": [ + { + "args": [ + "--skip-limit" + ], + "expect_exit": 4 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "Pre-limit max influences ≠ 5", + "4": "Vertex over the 4-influence cap (`--skip-limit` lands here)", + "5": "Limit changed nothing", + "6": "Weight sums off 1.0 after renormalize", + "7": "Pose damaged by pruning, or evaluated vert count changed", + "8": "Evaluated mesh off LBS over limited weights", + "9": "Root-weighted mount moved", + "10": "Gallery framing violation; also `--output` produced no file", + "11": "Asset-quality floor violation (`--output` only)" + } + }, + { + "name": "vse-cut-list", + "path": "examples/vse-cut-list/", + "script": "examples/vse-cut-list/vse_cut_list.py", + "skills": [ + "vse-python" + ], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/vse-cut-list/vse_cut_list.py", + "summary": "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_*.", + "falsifiers": [ + { + "args": [ + "--swap-inputs" + ], + "expect_exit": 7 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "`.sequences` accessor contract for this Blender", + "4": "Wrong-era `new_effect` end kwarg or TRANSFORM type did not TypeError", + "5": "Strip span or channel off closed form", + "6": "`frame_final_*` deprecation bridge for this Blender", + "7": "GC wiring or strip `.type` enum (`--swap-inputs` lands here)", + "8": "Scene strip span or Stage source", + "9": "Mosaic transform, compositing defaults, text body, or strip color", + "10": "Save/reload round-trip re-assert failed; also gallery framing violation", + "11": "`--output` produced no file", + "12": "`--check-pixels` compositing contract" + } + }, + { + "name": "vse-gamma-cross", + "path": "examples/vse-gamma-cross/", + "script": "examples/vse-gamma-cross/vse_gamma_cross.py", + "skills": [ + "vse-python" + ], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/vse-gamma-cross/vse_gamma_cross.py", + "summary": "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.", + "falsifiers": [ + { + "args": [ + "--swap-inputs" + ], + "expect_exit": 4 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "GC span off closed form", + "4": "GC inputs are not T1 → T2 (`--swap-inputs` lands here)", + "5": "Sample `t` convention drifted", + "6": "Cross sample off the gamma-0.5 closed form", + "7": "Mid-cross lerp deviation missing (naive mix)", + "8": "`--output` produced no file", + "9": "`--output` filmstrip guard: a frame not uniform, or a strip's mid frame off its closed form", + "10": "`--output` framing violation (`gallery_framing`)" + } + }, + { + "name": "vse-linear-modifiers", + "path": "examples/vse-linear-modifiers/", + "script": "examples/vse-linear-modifiers/vse_linear_modifiers.py", + "skills": [ + "vse-python" + ], + "render": false, + "min_version": "4.5", + "run": "blender --background --python examples/vse-linear-modifiers/vse_linear_modifiers.py", + "summary": "Pathology witness for the 5.2 removal of `Strip.use_linear_modifiers`.", + "falsifiers": [ + { + "args": [ + "--assume-present" + ], + "expect_exit": 4, + "min_version": "5.2" + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "COLOR strip was not created", + "4": "`use_linear_modifiers` missing when required (`--assume-present` on 5.2)", + "5": "Attribute still present or getattr silent on 5.2+", + "6": "setattr round-trip failed", + "7": "`hasattr`-guarded read raised" + } + }, + { + "name": "wave-displace", + "path": "examples/wave-displace/", + "script": "examples/wave-displace/wave_displace.py", + "skills": [ + "mesh-editing-and-bmesh" + ], + "render": true, + "min_version": "4.5", + "run": "blender --background --python examples/wave-displace/wave_displace.py", + "summary": "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.", + "falsifiers": [ + { + "args": [ + "--flat" + ], + "expect_exit": 4 + } + ], + "exit_codes": { + "0": "Success", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "4": "Z-span not in the closed-form band (`--flat` lands here)", + "5": "A vertex is off the closed-form wave", + "6": "`--output` produced no file", + "10": "Gallery framing violation (`gallery_framing`)" + } + } +] diff --git a/examples/skills.json b/examples/skills.json new file mode 100644 index 00000000..ea03329f --- /dev/null +++ b/examples/skills.json @@ -0,0 +1,67 @@ +{ + "_comment": "Which skills each example proves. Hand-edited; scripts/build_examples_index.py turns it into examples/index.json and each SKILL.md's Runnable examples section. An empty list means no skill covers the subject yet.", + "armature-bend": ["depsgraph-and-evaluated-data", "mesh-editing-and-bmesh"], + "attribute-domain-shear": ["mesh-editing-and-bmesh"], + "bake-normal-high-to-low": ["bake-high-to-low"], + "bmesh-gear": ["mesh-editing-and-bmesh"], + "boolean-exact-volume": ["depsgraph-and-evaluated-data", "mesh-editing-and-bmesh"], + "car-mirror-symmetry": ["depsgraph-and-evaluated-data"], + "coincident-vert-weld": ["ai-mesh-cleanup"], + "collision-hull-proxy": ["ai-mesh-cleanup", "mesh-editing-and-bmesh"], + "color-attribute-wheel": ["mesh-editing-and-bmesh"], + "compositor-glare": [], + "cross-version-property-delete": ["custom-properties"], + "curve-bevel-arc": [], + "custom-normals-shade": ["mesh-editing-and-bmesh"], + "damped-track-aim": [], + "degenerate-bevel-weld": ["ai-mesh-cleanup", "mesh-editing-and-bmesh"], + "depsgraph-export": ["depsgraph-and-evaluated-data"], + "driver-wave": ["drivers-and-app-handlers"], + "eval-mesh-datablock-name": ["depsgraph-and-evaluated-data"], + "exit-pre-sidecar": ["drivers-and-app-handlers"], + "export-preset-axis": ["engine-export-presets"], + "gltf-export-roundtrip": ["depsgraph-and-evaluated-data", "engine-export-presets", "headless-batch-scripting"], + "gltf-skin-roundtrip": ["depsgraph-and-evaluated-data", "engine-export-presets"], + "gn-bundle-roundtrip": ["geometry-nodes-python"], + "gn-instance-grid": ["geometry-nodes-python"], + "gn-modifier-inputs": ["geometry-nodes-python"], + "gn-sdf-remesh": ["geometry-nodes-python"], + "gn-sim-fountain": ["geometry-nodes-python"], + "gn-socket-rename": ["geometry-nodes-python"], + "gn-zone-iterate": ["geometry-nodes-python"], + "gp-lineart-contour": [], + "grease-pencil-rosette": [], + "image-pixels-testcard": [], + "lattice-deform": ["depsgraph-and-evaluated-data", "mesh-editing-and-bmesh"], + "light-link-studio": [], + "lightmap-uv-channel": ["mesh-editing-and-bmesh"], + "lod-decimate-chain": ["ai-mesh-cleanup", "depsgraph-and-evaluated-data", "mesh-editing-and-bmesh"], + "mesh-automasking-settings": [], + "mesh-hygiene-audit": ["ai-mesh-cleanup", "mesh-editing-and-bmesh"], + "modular-kit-snap": ["ai-mesh-cleanup", "mesh-editing-and-bmesh"], + "ngon-triangulate": ["ai-mesh-cleanup", "mesh-editing-and-bmesh"], + "parent-inverse-orrery": [], + "png-exr-alpha": [], + "prop-origin-transform": ["ai-mesh-cleanup", "operators"], + "ray-cast-space": ["depsgraph-and-evaluated-data"], + "shader-node-group": ["procedural-materials-and-shaders"], + "shape-key-blend": ["mesh-editing-and-bmesh"], + "sky-texture-sun-elevation": ["procedural-materials-and-shaders"], + "soccer-ball-goldberg": ["depsgraph-and-evaluated-data", "mesh-editing-and-bmesh"], + "socket-attach-points": [], + "solidify-even-thickness": ["depsgraph-and-evaluated-data", "mesh-editing-and-bmesh"], + "swatch-grid": ["procedural-materials-and-shaders"], + "temp-override-join": ["headless-batch-scripting", "operators"], + "text-version-stamp": [], + "triangulate-tangents": ["mesh-editing-and-bmesh"], + "turntable": ["slotted-actions-animation"], + "unapplied-scale-gltf": ["ai-mesh-cleanup", "engine-export-presets"], + "usd-export-evaluation-mode": ["depsgraph-and-evaluated-data", "headless-batch-scripting"], + "uv-layer-grid": ["mesh-editing-and-bmesh"], + "vertex-color-ao": ["mesh-editing-and-bmesh"], + "vertex-weight-limit": ["mesh-editing-and-bmesh"], + "vse-cut-list": ["vse-python"], + "vse-gamma-cross": ["vse-python"], + "vse-linear-modifiers": ["vse-python"], + "wave-displace": ["mesh-editing-and-bmesh"] +} diff --git a/scripts/build_examples_index.py b/scripts/build_examples_index.py new file mode 100644 index 00000000..2ef6eacd --- /dev/null +++ b/scripts/build_examples_index.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python3 +"""Generate examples/index.json and each skill's "Runnable examples" section. + +One file describes every example (#377): its script, the skills it proves, +whether it ships a gallery render, the oldest Blender it runs on, the command +to run it, every smoke falsifier with its expected exit, and the README's +exit-code table. Inputs, all hand-edited elsewhere: + + examples/skills.json example -> skills it proves (edit this one) + tests/smoke/catalog.json scripts, min_version, falsifiers + examples/gallery.json which examples render + examples//README.md one-line summary and exit-code table + +The section between the markers in skills/*/SKILL.md is rewritten from the +index; everything outside the markers is left alone. + + python scripts/build_examples_index.py write index.json and SKILL.md sections + python scripts/build_examples_index.py --check exit 1 if either is stale or the mapping is invalid +""" +from __future__ import annotations + +import argparse +import json +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +REPO_URL = "https://github.com/TMHSDigital/Blender-Developer-Tools" +BEGIN = "" +END = "" +# Skills whose examples are still to be written. Listing one that has an +# example is an error, so this shrinks as #378 lands. +PENDING = { + "addon-scaffolding": "#378", + "bl-info-migration": "#378", + "ui-panels": "#378", +} +EXIT_ROW = re.compile(r"^\|\s*(\d+)\s*\|\s*(.+?)\s*\|\s*$", re.M) +SENTENCE = re.compile(r"(.+?[.!?])(?:\s|$)", re.S) +MD_LINK = re.compile(r"\[([^\]]+)\]\([^)]+\)") +MAX_SUMMARY = 200 + + +def _summary(readme: str, teaches: str | None) -> str: + """First sentence of the gallery's `teaches` line, or for a check-only + example, of the README's first paragraph after the title. + + A sentence ends at '.', '!' or '?' followed by whitespace, so a dotted API + path such as `bmesh.ops` never cuts it short (the PR #68 alt-text bug). + """ + if teaches: + body = teaches + else: + paras = [p for p in re.split(r"\n\s*\n", readme) if p.strip()] + body = next((p for p in paras if not p.lstrip().startswith(("#", "<", "!", "|", "```"))), "") + text = MD_LINK.sub(r"\1", " ".join(body.split())) + m = SENTENCE.match(text) + text = (m.group(1) if m else text).strip() + if len(text) <= MAX_SUMMARY: + return text + # Long lead sentences list parts after a dash or colon; keep the claim. + for sep in (" — ", ": ", "; ", ", "): + for m in re.finditer(re.escape(sep), text): + head = text[:m.start()] + if 40 <= len(head) <= MAX_SUMMARY and head.count("(") == head.count(")"): + return _close_code(head) + "." + return _close_code(text[:MAX_SUMMARY].rsplit(" ", 1)[0]) + "…" + + +def _close_code(text: str) -> str: + """Drop a trailing half code span so a cut never leaves a lone backtick.""" + return text.rsplit("`", 1)[0].rstrip(" ,;:(") if text.count("`") % 2 else text.rstrip(" ,;:(") + + +def _exit_codes(readme: str) -> dict[str, str]: + return {code: desc for code, desc in EXIT_ROW.findall(readme)} + + +def build_index() -> tuple[list[dict], list[str]]: + errors: list[str] = [] + mapping = json.loads((ROOT / "examples" / "skills.json").read_text(encoding="utf-8")) + mapping.pop("_comment", None) + catalog = json.loads((ROOT / "tests" / "smoke" / "catalog.json").read_text(encoding="utf-8")) + gallery = {Path(e["dir"]).name: e["teaches"] for e in + json.loads((ROOT / "examples" / "gallery.json").read_text(encoding="utf-8"))["examples"]} + skills = sorted(p.parent.name for p in (ROOT / "skills").glob("*/SKILL.md")) + dirs = sorted(d.name for d in (ROOT / "examples").iterdir() + if d.is_dir() and not d.name.startswith((".", "_"))) + + for name in sorted(set(dirs) - set(mapping)): + errors.append(f"examples/{name} is missing from examples/skills.json") + for name in sorted(set(mapping) - set(dirs)): + errors.append(f"examples/skills.json lists {name}, which is not an example directory") + for name, listed in mapping.items(): + for s in listed: + if s not in skills: + errors.append(f"examples/skills.json: {name} names unknown skill {s!r}") + + index = [] + for name in dirs: + rows = [r for r in catalog if Path(r["script"]).parts[:2] == ("examples", name)] + if not rows: + errors.append(f"examples/{name} has no tests/smoke/catalog.json row") + continue + primary = next((r for r in rows if r["name"] == name), rows[0]) + readme_path = ROOT / "examples" / name / "README.md" + readme = readme_path.read_text(encoding="utf-8") if readme_path.is_file() else "" + falsifiers = [] + for r in rows: + for f in r.get("falsifiers", []): + entry = {"args": f["args"]} + if "expect_exit" in f: + entry["expect_exit"] = f["expect_exit"] + if f.get("expect_sidecar_fail"): + entry["expect_sidecar_fail"] = True + floor = f.get("min_version") or r.get("min_version") + if floor: + entry["min_version"] = floor + if entry not in falsifiers: + falsifiers.append(entry) + index.append({ + "name": name, + "path": f"examples/{name}/", + "script": primary["script"], + "skills": mapping.get(name, []), + "render": name in gallery, + "min_version": primary.get("min_version", "4.5"), + "run": f"blender --background --python {primary['script']}", + "summary": _summary(readme, gallery.get(name)), + "falsifiers": falsifiers, + "exit_codes": _exit_codes(readme), + }) + + proven = {s for e in index for s in e["skills"]} + for s in skills: + if s not in proven and s not in PENDING: + errors.append(f"skills/{s} has no runnable example in examples/skills.json") + if s in proven and s in PENDING: + errors.append(f"skills/{s} now has an example; drop it from PENDING in {Path(__file__).name}") + return index, errors + + +def _flag(f: dict) -> str: + args = " ".join(f["args"]) + if f.get("expect_sidecar_fail"): + return f"`{args}` (harness fails the sidecar)" + return f"`{args}` (exit {f['expect_exit']})" + + +def skill_section(skill: str, index: list[dict]) -> str: + lines = [BEGIN, "## Runnable examples", ""] + mine = [e for e in index if skill in e["skills"]] + if not mine: + lines.append(f"No runnable example proves this skill yet (tracked in " + f"[{PENDING[skill]}]({REPO_URL}/issues/{PENDING[skill].lstrip('#')})).") + else: + lines.append("Each example runs headless, asserts the contract, and exits non-zero when it breaks. " + "Run one with `blender --background --python