Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .cursor-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
"examples/gn-instance-grid",
"examples/gn-modifier-inputs",
"examples/gn-sdf-remesh",
"examples/gn-socket-rename",
"examples/gn-zone-iterate",
"examples/gp-lineart-contour",
"examples/grease-pencil-rosette",
Expand Down Expand Up @@ -131,6 +132,7 @@
"examples/vertex-weight-limit",
"examples/vse-cut-list",
"examples/vse-gamma-cross",
"examples/vse-linear-modifiers",
"examples/wave-displace"
]
}
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ a `.cursor-plugin/plugin.json` manifest so the ecosystem drift checker
classifies it as a `cursor-plugin`. This is content the AI loads when the user
asks Blender questions or works on Blender add-ons in Cursor or Claude Code.

The content base is 16 skills, 9 rules, 3 templates, 27 snippets, and 55
The content base is 16 skills, 9 rules, 3 templates, 27 snippets, and 57
examples (counts are CI-enforced against README.md and the manifest). The full
inventory tables and per-item purposes live in `CLAUDE.md`. Example anatomy
and authoring rules: copy `examples/bmesh-gear/`; the render look is specified
Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ skills/<skill-name>/SKILL.md - AI workflow definitions, 16 total
rules/<rule-name>.mdc - Anti-pattern rules, 9 total
templates/<template-name>/ - Starter projects, 3 total
snippets/<snippet-name>.py - Standalone code patterns, 27 total
examples/<name>/ - Runnable smoke-gated examples, 55 total (+ gallery.json)
examples/<name>/ - Runnable smoke-gated examples, 57 total (+ gallery.json)
scripts/build_gallery.py - Regenerates docs/gallery/ from gallery.json (stdlib only)
scripts/site/ - Vendored landing-page build (Jinja2)
docs/gallery/ - Committed generated gallery pages + hero renders
Expand Down Expand Up @@ -99,7 +99,7 @@ v0.2.0: Principled BSDF material, driver-with-custom-function via `driver_namesp

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

## Examples (55)
## Examples (57)

Runnable scripts at `examples/<name>/`, each asserting a real API contract with
deterministic checks (exit non-zero on failure) and optionally rendering a still via
Expand Down
37 changes: 33 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
</p>

<p align="center">
<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>
<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>
</p>

<p align="center">
Expand All @@ -36,7 +36,7 @@

## Overview

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

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.

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

<details>
<summary><strong>Mesh, curves &amp; text</strong> — 11 examples</summary>
<summary><strong>Mesh, curves &amp; text</strong> — 12 examples</summary>

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

</td>
</tr>
<tr>
<td width="46%" valign="middle">
check-only, no gallery still — no geometry
</td>
<td valign="middle">

### [vse-linear-modifiers](examples/vse-linear-modifiers/)

`ColorStrip.use_linear_modifiers` is a bool on 4.5.11 and 5.1.2; the same
getattr is `AttributeError` on 5.2.1. Version-guarded `hasattr` then read
exits 0 on all three. `--assume-present` is red only on 5.2.

</td>
</tr>
</table>

</details>

<details>
<summary><strong>Geometry Nodes</strong> — 5 examples</summary>
<summary><strong>Geometry Nodes</strong> — 6 examples</summary>

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

</td>
</tr>
<tr>
<td width="46%" valign="middle">
<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>
</td>
<td valign="middle">

### [gn-socket-rename](examples/gn-socket-rename/)

Compare INT and Random Value FLOAT socket identifiers collapsed in 5.2
(`A_INT` / `Min_001` gone; `A` / `Min` reused). Enabled-name lookup wires
on 4.5, 5.1, and 5.2. Asserts 16 verts and POINT `gauge_h=1.80` on eight
column verts. `--legacy-ids` is red only on 5.2.

</td>
</tr>
<tr>
Expand Down
4 changes: 4 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,10 @@ Not committed; target list for the next content version. (v0.3.0 shipped the smo
- ~~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)
- ~~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
- 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)
- ~~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
- ~~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
- GN evaluated `to_mesh().name` no longer equals the original mesh name (5.2 contract change)
- `MeshAutomaskingSettings` move: old `Brush` automasking attributes gone on 5.2
- ~~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`
- 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`)
- ~~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
Expand Down
17 changes: 15 additions & 2 deletions docs/gallery/armature-bend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -268,10 +268,16 @@ <h2>Run</h2>
<pre><code># Cheap correctness check (no render) — the CI check:
blender --background --python armature_bend.py --

# Falsifier: rest pose. Must exit non-zero (tip deflection).
blender --background --python armature_bend.py -- --zero-curl

# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts):
blender --background --python armature_bend.py -- --output bend.png
blender --background --python armature_bend.py -- --output bend.png --engine cycles</code></pre>
<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>
<h2>Exit codes</h2>
<p>Per-script sequential checks. <code>9</code> is a valid check code; there is no rule against it.</p>
<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>
<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>
</section>
<section class="detail-section src">
<h2>Source</h2>
Expand Down Expand Up @@ -300,10 +306,14 @@ <h2>Source</h2>
The same API works unchanged on Blender 4.5 LTS and 5.1 — no version gate is
needed, which this example demonstrates by running identically on both.

``--zero-curl`` leaves every pose bone at rest and still asserts the tip
deflects. That is the falsifier (``--same-axis`` in export-preset-axis).

By default it runs only the correctness check (no render) — the CI smoke
check. Pass --output to also render a still:

blender --background --python armature_bend.py -- # check only
blender --background --python armature_bend.py -- --zero-curl # must fail
blender --background --python armature_bend.py -- --output b.png # + render
&quot;&quot;&quot;</span>
<span class="k">import</span> bpy, bmesh, sys, os, math, argparse
Expand Down Expand Up @@ -575,10 +585,13 @@ <h2>Source</h2>
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>)
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>),
help=<span class="s">&quot;render engine for --output (cycles for GPU-less hosts)&quot;</span>)
p.add_argument(<span class="s">&quot;--zero-curl&quot;</span>, action=<span class="s">&quot;store_true&quot;</span>,
help=<span class="s">&quot;leave pose bones at rest (must fail)&quot;</span>)
args = p.parse_args(argv)

bpy.ops.wm.read_factory_settings(use_empty=<span class="k">True</span>)
tube, arm = build_rig(CURL_DEG)
curl = <span class="n">0.0</span> <span class="k">if</span> args.zero_curl <span class="k">else</span> CURL_DEG
tube, arm = build_rig(curl)
code = check(tube, arm)
<span class="k">if</span> code:
<span class="k">return</span> code
Expand Down
Binary file added docs/gallery/assets/gn-socket-rename-hero.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 14 additions & 6 deletions docs/gallery/attribute-domain-shear/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -268,9 +268,13 @@ <h1>attribute-domain-shear</h1>
<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>
<h2>Run</h2>
<pre><code>blender --background --python attribute_domain_shear.py --
blender --background --python attribute_domain_shear.py -- --no-overwrite
blender --background --python attribute_domain_shear.py -- --output shear.png
blender --background --python attribute_domain_shear.py -- --output shear.png --engine cycles</code></pre>
<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>
<h2>Exit codes</h2>
<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>
<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>
<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>
</section>
<section class="detail-section src">
<h2>Source</h2>
Expand Down Expand Up @@ -306,6 +310,7 @@ <h2>Source</h2>
check. Pass --output to also render a still:

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


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


<span class="k">def</span> check():
<span class="k">def</span> check(overwrite=<span class="k">True</span>):
pal = palette()
expect_shear = closed_form_shear(pal)
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>)
Expand All @@ -418,7 +424,7 @@ <h2>Source</h2>

<span class="c"># --- POINT: the shear, measured against the closed form ---</span>
me_p = build_fan()
attr_p = assign_point_naive(me_p, pal)
attr_p = assign_point_naive(me_p, pal, overwrite=overwrite)
<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>:
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>,
file=sys.stderr)
Expand Down Expand Up @@ -663,11 +669,13 @@ <h2>Source</h2>
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>)
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>),
help=<span class="s">&quot;render engine for --output (cycles for GPU-less hosts)&quot;</span>)
p.add_argument(<span class="s">&quot;--no-overwrite&quot;</span>, action=<span class="s">&quot;store_true&quot;</span>,
help=<span class="s">&quot;write only the first POINT wedge (must fail)&quot;</span>)
args = p.parse_args(argv)

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>)
bpy.ops.wm.read_factory_settings(use_empty=<span class="k">True</span>)
code = check()
code = check(overwrite=<span class="k">not</span> args.no_overwrite)
<span class="k">if</span> code:
<span class="k">return</span> code

Expand Down
Loading
Loading