Skip to content

Commit f5649d9

Browse files
feat: add VSE linear-modifiers and GN socket-rename cross-version examples (#145)
* feat: witness VSE use_linear_modifiers removal across 4.5, 5.1, and 5.2 Naive getattr still works on 4.5.11 and 5.1.2 and raises AttributeError on 5.2.1. Version-guarded hasattr then read exits 0 on all three; --assume-present is red only where the RNA is gone. Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> * feat: witness GN Compare and Random Value socket identifier collapse on 5.2 Enabled-name lookup wires on 4.5.11, 5.1.2, and 5.2.1. Pre-5.2 identifiers Min_001 and A_INT are absent on 5.2; --legacy-ids is red only there. Regenerates gallery pages so the new still is in the index. Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> --------- Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 8e3495c commit f5649d9

65 files changed

Lines changed: 2388 additions & 233 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.cursor-plugin/plugin.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@
101101
"examples/gn-instance-grid",
102102
"examples/gn-modifier-inputs",
103103
"examples/gn-sdf-remesh",
104+
"examples/gn-socket-rename",
104105
"examples/gn-zone-iterate",
105106
"examples/gp-lineart-contour",
106107
"examples/grease-pencil-rosette",
@@ -131,6 +132,7 @@
131132
"examples/vertex-weight-limit",
132133
"examples/vse-cut-list",
133134
"examples/vse-gamma-cross",
135+
"examples/vse-linear-modifiers",
134136
"examples/wave-displace"
135137
]
136138
}

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ a `.cursor-plugin/plugin.json` manifest so the ecosystem drift checker
2020
classifies it as a `cursor-plugin`. This is content the AI loads when the user
2121
asks Blender questions or works on Blender add-ons in Cursor or Claude Code.
2222

23-
The content base is 16 skills, 9 rules, 3 templates, 27 snippets, and 55
23+
The content base is 16 skills, 9 rules, 3 templates, 27 snippets, and 57
2424
examples (counts are CI-enforced against README.md and the manifest). The full
2525
inventory tables and per-item purposes live in `CLAUDE.md`. Example anatomy
2626
and authoring rules: copy `examples/bmesh-gear/`; the render look is specified

CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ skills/<skill-name>/SKILL.md - AI workflow definitions, 16 total
2121
rules/<rule-name>.mdc - Anti-pattern rules, 9 total
2222
templates/<template-name>/ - Starter projects, 3 total
2323
snippets/<snippet-name>.py - Standalone code patterns, 27 total
24-
examples/<name>/ - Runnable smoke-gated examples, 55 total (+ gallery.json)
24+
examples/<name>/ - Runnable smoke-gated examples, 57 total (+ gallery.json)
2525
scripts/build_gallery.py - Regenerates docs/gallery/ from gallery.json (stdlib only)
2626
scripts/site/ - Vendored landing-page build (Jinja2)
2727
docs/gallery/ - Committed generated gallery pages + hero renders
@@ -99,7 +99,7 @@ v0.2.0: Principled BSDF material, driver-with-custom-function via `driver_namesp
9999

100100
AI asset pipeline track: `decimate_to_budget.py`, `convex_hull_collider.py`, `lod_chain.py` (helper duplicated, not imported), `gltf_draco_export.py`, `export_preset_unity.py`, `export_preset_godot.py`, `export_preset_unreal.py`, `setup_bake_target_image.py`, `bake_normal_high_to_low.py`, `save_baked_image.py`.
101101

102-
## Examples (55)
102+
## Examples (57)
103103

104104
Runnable scripts at `examples/<name>/`, each asserting a real API contract with
105105
deterministic checks (exit non-zero on failure) and optionally rendering a still via

README.md

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
</p>
1919

2020
<p align="center">
21-
<strong>16 skills</strong> &nbsp;&bull;&nbsp; <strong>9 rules</strong> &nbsp;&bull;&nbsp; <strong>3 templates</strong> &nbsp;&bull;&nbsp; <strong>27 snippets</strong> &nbsp;&bull;&nbsp; <strong>55 examples</strong>
21+
<strong>16 skills</strong> &nbsp;&bull;&nbsp; <strong>9 rules</strong> &nbsp;&bull;&nbsp; <strong>3 templates</strong> &nbsp;&bull;&nbsp; <strong>27 snippets</strong> &nbsp;&bull;&nbsp; <strong>57 examples</strong>
2222
</p>
2323

2424
<p align="center">
@@ -36,7 +36,7 @@
3636

3737
## Overview
3838

39-
This repository ships **16 skills, 9 rules, 3 templates, 27 snippets, and 55 examples** for Blender Python development targeting Blender 5.2 LTS (current stable) with Blender 4.5 LTS fallback support. Blender 5.1 is prior stable.
39+
This repository ships **16 skills, 9 rules, 3 templates, 27 snippets, and 57 examples** for Blender Python development targeting Blender 5.2 LTS (current stable) with Blender 4.5 LTS fallback support. Blender 5.1 is prior stable.
4040

4141
The content is consumed by AI coding agents (Cursor, Claude Code, any MCP-capable client) when working on Blender add-ons, geometry nodes scripts, batch pipelines, or animation tooling. There is no build step. Edit the markdown and Python files directly.
4242

@@ -203,7 +203,7 @@ Gallery still is a dual-elevation diptych so the contract reads at thumbnail sca
203203
</details>
204204

205205
<details>
206-
<summary><strong>Mesh, curves &amp; text</strong> — 11 examples</summary>
206+
<summary><strong>Mesh, curves &amp; text</strong> — 12 examples</summary>
207207

208208
<table>
209209
<tr>
@@ -370,14 +370,28 @@ inside the effect. Per-frame sample renders assert the closed form (mid dips
370370
0.115 below the sRGB lerp), and the AgX-default sampling trap is documented
371371
(`view_transform = 'Standard'` is mandatory for any pixel witness).
372372

373+
</td>
374+
</tr>
375+
<tr>
376+
<td width="46%" valign="middle">
377+
check-only, no gallery still — no geometry
378+
</td>
379+
<td valign="middle">
380+
381+
### [vse-linear-modifiers](examples/vse-linear-modifiers/)
382+
383+
`ColorStrip.use_linear_modifiers` is a bool on 4.5.11 and 5.1.2; the same
384+
getattr is `AttributeError` on 5.2.1. Version-guarded `hasattr` then read
385+
exits 0 on all three. `--assume-present` is red only on 5.2.
386+
373387
</td>
374388
</tr>
375389
</table>
376390

377391
</details>
378392

379393
<details>
380-
<summary><strong>Geometry Nodes</strong> — 5 examples</summary>
394+
<summary><strong>Geometry Nodes</strong> — 6 examples</summary>
381395

382396
<table>
383397
<tr>
@@ -436,6 +450,21 @@ evaluated cubes against closed forms — Repeat `8×(1+N)` with X-centers at
436450
nodes exist. Unpaired evaluates empty; For Each's main Geometry socket is a
437451
passthrough.
438452

453+
</td>
454+
</tr>
455+
<tr>
456+
<td width="46%" valign="middle">
457+
<a href="examples/gn-socket-rename/"><img src="examples/gn-socket-rename/preview.webp" alt="GN socket rename: a copper jo-block column on a steel plinth in a dark studio, the column switched in by Compare and tagged by Random Value, warm wedge on the back wall" /></a>
458+
</td>
459+
<td valign="middle">
460+
461+
### [gn-socket-rename](examples/gn-socket-rename/)
462+
463+
Compare INT and Random Value FLOAT socket identifiers collapsed in 5.2
464+
(`A_INT` / `Min_001` gone; `A` / `Min` reused). Enabled-name lookup wires
465+
on 4.5, 5.1, and 5.2. Asserts 16 verts and POINT `gauge_h=1.80` on eight
466+
column verts. `--legacy-ids` is red only on 5.2.
467+
439468
</td>
440469
</tr>
441470
<tr>

ROADMAP.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,10 @@ Not committed; target list for the next content version. (v0.3.0 shipped the smo
140140
- ~~Vertex-colour AO witness~~ **SHIPPED** as `examples/vertex-color-ao/` — baked occlusion in a colour attribute on a stone village well, checked against a closed form rather than a captured value: the cosine-weighted hemisphere integral for an infinitely wide wall of height H at distance d is `AO = 1 − ½(1 − 1/√(1+k²))`, `k = H/d`, matched to **6.760e-04** across k = 60.0…0.60 (gate 2.5e-03, QMC noise ~1/√n at 4096 samples); unoccluded plate bakes to **exactly 1.0**; strictly monotone 0.508301 → 0.928467; asset values in [0,1] with spread 1.0; **`BYTE_COLOR` is sRGB-encoded 8-bit, not linear** (0.735 → 0.7379107, peak round-trip error 3.782e-03, matching an independent encode/quantise/decode model to 3.189e-07) while `FLOAT_COLOR` is exact; both survive depsgraph evaluation at deviation 0.0; `bmesh.ops.bevel` offsets along **cached** face normals and flips outward past 90° of staleness (12 mm below ground on a ring of boxes, fixed by `normal_update()`); point-domain AO needs subdivision or the crevice gradient never reaches the attribute; `color_attributes` enumeration order differs between 4.5.11 and 5.1.2 — look up by name; byte-identical on 4.5.11 and 5.1.2 (6966 verts)
141141
- ~~High-to-low tangent normal bake~~ **SHIPPED** as `examples/bake-normal-high-to-low/` — Cycles CPU `type='NORMAL'` cage bake onto a `DECIMATE COLLAPSE` hatch; statistical gates (detail frac 0.7211 / MAD 0.09356 vs flat 0.0000 / 0.00277); `--flat-source` exits 5; RNA identical on 4.5.11, 5.1.2, 5.2.1; byte-identity is not the contract
142142
- UV atlas **utilization** witness — coverage/wasted-texel closed forms for a packed lightmap atlas (the non-overlap, unit-square, margin, and active/active_render contracts shipped in `lightmap-uv-channel`; utilization is the remaining unbuilt slice of the old "UV atlas pack" candidate)
143+
- ~~VSE `use_linear_modifiers` removal~~ **SHIPPED** as `examples/vse-linear-modifiers/` — bool on 4.5.11 and 5.1.2, `AttributeError` on 5.2.1; `--assume-present` red only on 5.2; pathology, no gallery still
144+
- ~~GN Compare / Random Value socket identifier rename~~ **SHIPPED** as `examples/gn-socket-rename/``A_INT` / `Min_001` present on 4.5.11 and 5.1.2, absent on 5.2.1; enabled-name lookup wires all three; `--legacy-ids` red only on 5.2
145+
- GN evaluated `to_mesh().name` no longer equals the original mesh name (5.2 contract change)
146+
- `MeshAutomaskingSettings` move: old `Brush` automasking attributes gone on 5.2
143147
- ~~GAMMA_CROSS blend-curve witness~~ **SHIPPED** as `examples/vse-gamma-cross/` — the cross blends in a gamma-0.5 space: `((1-t)·√A + t·√B)²` with `t = (frame − start)/duration`, never 1 inside the effect; mid-cross dips 0.115 below the sRGB lerp from crimson/teal (closed form (0.341, 0.349, 0.463) confirmed per frame); AgX-default sampling poisons the fit (0.146 red-channel error, `view_transform='Standard'` mandatory); deleting a consumed input orphans-and-deletes the effect — follow-up to `vse-cut-list`
144148
- Falsy `bpy_prop_collection` trap snippet: an empty collection is falsy, so `editor.strips or editor.sequences` silently falls through to the legacy accessor on an empty timeline — always branch on `hasattr`; likely generalizes across the API (found authoring `vse-cut-list`)
145149
- ~~Collision compound witness~~ **SHIPPED** as `examples/collision-hull-proxy/` — game-prop collision as a compound of convex pieces, each a `bmesh.ops.convex_hull` of a coarse `sec(π/n)`-inflated cage (containment 4.4e-08, watertight, positive signed volume, Euler 2, per-piece 255-face budget: body 70, caps 60×3, compound 250); a hull of the dense render mesh measures 380 faces — over budget — which is why pipelines hull cages; proud details cost cage rows, concave grooves are free; byte-identical on 4.5.11 and 5.1.2

docs/gallery/armature-bend/index.html

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -268,10 +268,16 @@ <h2>Run</h2>
268268
<pre><code># Cheap correctness check (no render) — the CI check:
269269
blender --background --python armature_bend.py --
270270

271+
# Falsifier: rest pose. Must exit non-zero (tip deflection).
272+
blender --background --python armature_bend.py -- --zero-curl
273+
271274
# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts):
272275
blender --background --python armature_bend.py -- --output bend.png
273276
blender --background --python armature_bend.py -- --output bend.png --engine cycles</code></pre>
274-
<p>It exits non-zero on failure (edit-bone lifetime violation, LBS deviation, moved root ring, or an undeformed tip). The <code>blender-smoke</code> workflow runs the check on Blender 5.2 LTS and 4.5 LTS.</p>
277+
<h2>Exit codes</h2>
278+
<p>Per-script sequential checks. <code>9</code> is a valid check code; there is no rule against it.</p>
279+
<p>| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | <code>edit_bones</code> 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 (<code>--zero-curl</code> lands here) | | 9 | <code>--output</code> produced no file |</p>
280+
<p>The <code>blender-smoke</code> workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the <code>needs-5.1</code> PR label, or manual dispatch). Smoke does not pass <code>--output</code> or <code>--zero-curl</code>.</p>
275281
</section>
276282
<section class="detail-section src">
277283
<h2>Source</h2>
@@ -300,10 +306,14 @@ <h2>Source</h2>
300306
The same API works unchanged on Blender 4.5 LTS and 5.1 — no version gate is
301307
needed, which this example demonstrates by running identically on both.
302308

309+
``--zero-curl`` leaves every pose bone at rest and still asserts the tip
310+
deflects. That is the falsifier (``--same-axis`` in export-preset-axis).
311+
303312
By default it runs only the correctness check (no render) — the CI smoke
304313
check. Pass --output to also render a still:
305314

306315
blender --background --python armature_bend.py -- # check only
316+
blender --background --python armature_bend.py -- --zero-curl # must fail
307317
blender --background --python armature_bend.py -- --output b.png # + render
308318
&quot;&quot;&quot;</span>
309319
<span class="k">import</span> bpy, bmesh, sys, os, math, argparse
@@ -575,10 +585,13 @@ <h2>Source</h2>
575585
p.add_argument(<span class="s">&quot;--output&quot;</span>, default=<span class="k">None</span>, help=<span class="s">&quot;optional: render a still PNG here&quot;</span>)
576586
p.add_argument(<span class="s">&quot;--engine&quot;</span>, default=<span class="s">&quot;eevee&quot;</span>, choices=(<span class="s">&quot;eevee&quot;</span>, <span class="s">&quot;cycles&quot;</span>),
577587
help=<span class="s">&quot;render engine for --output (cycles for GPU-less hosts)&quot;</span>)
588+
p.add_argument(<span class="s">&quot;--zero-curl&quot;</span>, action=<span class="s">&quot;store_true&quot;</span>,
589+
help=<span class="s">&quot;leave pose bones at rest (must fail)&quot;</span>)
578590
args = p.parse_args(argv)
579591

580592
bpy.ops.wm.read_factory_settings(use_empty=<span class="k">True</span>)
581-
tube, arm = build_rig(CURL_DEG)
593+
curl = <span class="n">0.0</span> <span class="k">if</span> args.zero_curl <span class="k">else</span> CURL_DEG
594+
tube, arm = build_rig(curl)
582595
code = check(tube, arm)
583596
<span class="k">if</span> code:
584597
<span class="k">return</span> code
6.37 KB
Loading

docs/gallery/attribute-domain-shear/index.html

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -268,9 +268,13 @@ <h1>attribute-domain-shear</h1>
268268
<p><strong>Render as proof:</strong> dual pinwheel from the same closed-form palette the check asserts. CORNER (left) holds eight crisp petals to the hub; naive POINT (right) smears — petal colors bleed across the shared hub and ring verts into a swirl. The broken state is in-frame by design: the right fan *is* the falsification variant. Fully matte petal materials (<code>Specular IOR Level = 0</code>) so the flat color data carries no specular line, per <code>docs/VISUAL-STYLE.md</code>.</p>
269269
<h2>Run</h2>
270270
<pre><code>blender --background --python attribute_domain_shear.py --
271+
blender --background --python attribute_domain_shear.py -- --no-overwrite
271272
blender --background --python attribute_domain_shear.py -- --output shear.png
272273
blender --background --python attribute_domain_shear.py -- --output shear.png --engine cycles</code></pre>
273-
<p>Exits non-zero on failure. The <code>blender-smoke</code> workflow runs the check on Blender 5.2 LTS and 4.5 LTS. The <code>--output</code> render path additionally measures framing against the Layer 1 band via <code>examples/gallery_framing.py</code> (exit 10 on violation) before writing the still.</p>
274+
<h2>Exit codes</h2>
275+
<p>Per-script sequential checks. <code>9</code> is a valid check code; there is no rule against it. <code>10</code> is the shared framing helper.</p>
276+
<p>| Code | Meaning | | --- | --- | | 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 (<code>--no-overwrite</code> lands here) | | 6 | Outer ring verts off last-write order | | 7 | Measured shear off palette closed form, or ~0 | | 9 | <code>--output</code> produced no file | | 10 | Gallery framing violation |</p>
277+
<p>The <code>blender-smoke</code> workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the <code>needs-5.1</code> PR label, or manual dispatch). Smoke does not pass <code>--output</code> or <code>--no-overwrite</code>.</p>
274278
</section>
275279
<section class="detail-section src">
276280
<h2>Source</h2>
@@ -306,6 +310,7 @@ <h2>Source</h2>
306310
check. Pass --output to also render a still:
307311

308312
blender --background --python attribute_domain_shear.py -- # check only
313+
blender --background --python attribute_domain_shear.py -- --no-overwrite # must fail
309314
blender --background --python attribute_domain_shear.py -- --output a.png # + render
310315
&quot;&quot;&quot;</span>
311316
<span class="k">import</span> bpy, bmesh, sys, os, math, argparse, colorsys
@@ -378,13 +383,14 @@ <h2>Source</h2>
378383
<span class="k">return</span> attr
379384

380385

381-
<span class="k">def</span> assign_point_naive(me, pal):
386+
<span class="k">def</span> assign_point_naive(me, pal, overwrite=<span class="k">True</span>):
382387
<span class="s">&quot;&quot;&quot;The AI mistake: author per-wedge colors into a POINT-domain attribute.
383388
Every wedge rewrites the shared hub (and its leading ring vert), so the
384389
last wedge wins — colors shear across every shared vertex.&quot;&quot;&quot;</span>
385390
attr = me.color_attributes.new(ATTR_P, type=<span class="s">&#x27;FLOAT_COLOR&#x27;</span>, domain=<span class="s">&#x27;POINT&#x27;</span>)
386391
hub_index = <span class="n">0</span> <span class="c"># build_fan creates the hub first</span>
387-
<span class="k">for</span> i <span class="k">in</span> range(K):
392+
last = K <span class="k">if</span> overwrite <span class="k">else</span> <span class="n">1</span>
393+
<span class="k">for</span> i <span class="k">in</span> range(last):
388394
<span class="c"># naive per-wedge pass: set the hub and both ring verts to palette[i]</span>
389395
attr.data[hub_index].color = pal[i]
390396
attr.data[<span class="n">1</span> + i].color = pal[i]
@@ -393,7 +399,7 @@ <h2>Source</h2>
393399
<span class="k">return</span> attr
394400

395401

396-
<span class="k">def</span> check():
402+
<span class="k">def</span> check(overwrite=<span class="k">True</span>):
397403
pal = palette()
398404
expect_shear = closed_form_shear(pal)
399405
print(<span class="s">f&quot;</span><span class="s">palette K=</span>{K}<span class="s"> closed_form_shear=</span>{expect_shear:<span class="s">.6f</span>}<span class="s">&quot;</span>)
@@ -418,7 +424,7 @@ <h2>Source</h2>
418424

419425
<span class="c"># --- POINT: the shear, measured against the closed form ---</span>
420426
me_p = build_fan()
421-
attr_p = assign_point_naive(me_p, pal)
427+
attr_p = assign_point_naive(me_p, pal, overwrite=overwrite)
422428
<span class="k">if</span> len(attr_p.data) != len(me_p.vertices) <span class="k">or</span> len(me_p.vertices) != K + <span class="n">1</span>:
423429
print(<span class="s">f&quot;</span><span class="s">ERROR: POINT attr size </span>{len(attr_p.data)}<span class="s"> != verts </span>{len(me_p.vertices)}<span class="s">&quot;</span>,
424430
file=sys.stderr)
@@ -663,11 +669,13 @@ <h2>Source</h2>
663669
p.add_argument(<span class="s">&quot;--output&quot;</span>, default=<span class="k">None</span>, help=<span class="s">&quot;optional: render a still PNG here&quot;</span>)
664670
p.add_argument(<span class="s">&quot;--engine&quot;</span>, default=<span class="s">&quot;eevee&quot;</span>, choices=(<span class="s">&quot;eevee&quot;</span>, <span class="s">&quot;cycles&quot;</span>),
665671
help=<span class="s">&quot;render engine for --output (cycles for GPU-less hosts)&quot;</span>)
672+
p.add_argument(<span class="s">&quot;--no-overwrite&quot;</span>, action=<span class="s">&quot;store_true&quot;</span>,
673+
help=<span class="s">&quot;write only the first POINT wedge (must fail)&quot;</span>)
666674
args = p.parse_args(argv)
667675

668676
print(<span class="s">f&quot;</span><span class="s">binary version: </span>{bpy.app.version}<span class="s"> (</span>{bpy.app.version_string}<span class="s">)</span><span class="s">&quot;</span>)
669677
bpy.ops.wm.read_factory_settings(use_empty=<span class="k">True</span>)
670-
code = check()
678+
code = check(overwrite=<span class="k">not</span> args.no_overwrite)
671679
<span class="k">if</span> code:
672680
<span class="k">return</span> code
673681

0 commit comments

Comments
 (0)