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: 1 addition & 1 deletion claude/blender-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Applies to: `**/__init__.py`, `**/blender_manifest.toml`. Full rule: [`rules/tar

## type-annotate-props-and-defend-context

Flag two related anti-patterns. (1) bpy.props defined as class-level assignments instead of type annotations (deprecated since 2.8). (2) Code that touches bpy.context.active_object without guarding for None.
Flag two related anti-patterns. (1) bpy.props defined as class-level assignments instead of type annotations (deprecated since 2.8). (2) Code that touches bpy.context.active_object without guarding for None, or that assumes the active object is selected.

Applies to: `**/*.py`. Full rule: [`rules/type-annotate-props-and-defend-context.mdc`](https://github.com/TMHSDigital/Blender-Developer-Tools/blob/main/rules/type-annotate-props-and-defend-context.mdc).

Expand Down
2 changes: 1 addition & 1 deletion claude/skills/blender-rules/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Applies to: `**/__init__.py`, `**/blender_manifest.toml`. Full rule: [`rules/tar

## type-annotate-props-and-defend-context

Flag two related anti-patterns. (1) bpy.props defined as class-level assignments instead of type annotations (deprecated since 2.8). (2) Code that touches bpy.context.active_object without guarding for None.
Flag two related anti-patterns. (1) bpy.props defined as class-level assignments instead of type annotations (deprecated since 2.8). (2) Code that touches bpy.context.active_object without guarding for None, or that assumes the active object is selected.

Applies to: `**/*.py`. Full rule: [`rules/type-annotate-props-and-defend-context.mdc`](https://github.com/TMHSDigital/Blender-Developer-Tools/blob/main/rules/type-annotate-props-and-defend-context.mdc).

Expand Down
7 changes: 5 additions & 2 deletions docs/gallery/bake-normal-high-to-low/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ <h1>Bake Normal High To Low</h1>
<p>A runnable example that cage-bakes a ribbed bronze hatch plate onto a <code>DECIMATE COLLAPSE</code> LOD and asserts the tangent-space normal map carries measurable surface detail — following <a href="https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/skills/bake-high-to-low/SKILL.md"><code>bake-high-to-low</code></a>.</p>
<p><strong>What it witnesses:</strong> Cycles selected-to-active normal bake is a statistical process, not a byte-identical one. A high-poly source produces a map that deviates from flat tangent <code>(0.5, 0.5, 1.0)</code>; the same bake from an undisplaced source does not.</p>
<p>Byte-identity across 4.5 / 5.1 / 5.2 is <strong>not</strong> the contract. Tile order and float accumulation differ even at one CPU sample. The gates are fraction of pixels beyond Euclidean <code>0.04</code> from flat, mean absolute deviation, and a monotonic gap versus a flat control. Tolerances sit well inside the measured gap (detail frac 0.7211 vs flat 0.0000) so they are not tuned-until-green.</p>
<ul><li><strong>Detail bake is not flat.</strong> <code>frac &gt;= 0.40</code> and <code>MAD &gt;= 0.05</code> (measured 0.7211 / 0.09356 on 4.5.11, 5.1.2, and 5.2.1). Catches an inactive Image Texture node, reversed selection, or EEVEE/GPU mis-setup that writes a blank map. MAD floor is half the measured hatch value, still ~30× a flat bake.</li><li><strong>Flat control is flat.</strong> <code>frac &lt;= 0.05</code> and <code>MAD &lt;= 0.03</code> (measured 0.0000 / 0.00277). Catches a noisy or wrongly-typed bake that would also satisfy the detail gates.</li><li><strong>Monotonic gap.</strong> <code>detail_frac - flat_frac &gt;= 0.30</code>. The two maps must separate; a tolerance wide enough to pass both would have no discriminating power.</li><li><strong><code>--flat-source</code> is the falsifier.</strong> Skips the ribs and still runs the detail gates. Must exit 5. Analogous to <code>--same-axis</code> in <a href="https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/export-preset-axis"><code>export-preset-axis</code></a>.</li></ul>
<ul><li><strong>Detail bake is not flat.</strong> <code>frac &gt;= 0.40</code> and <code>MAD &gt;= 0.05</code> (measured 0.7211 / 0.09356 on 4.5.11, 5.1.2, and 5.2.1). Catches an inactive Image Texture node, reversed selection, or EEVEE/GPU mis-setup that writes a blank map. MAD floor is half the measured hatch value, still ~30× a flat bake.</li><li><strong>Flat control is flat.</strong> <code>frac &lt;= 0.05</code> and <code>MAD &lt;= 0.03</code> (measured 0.0000 / 0.00277). Catches a noisy or wrongly-typed bake that would also satisfy the detail gates.</li><li><strong>Monotonic gap.</strong> <code>detail_frac - flat_frac &gt;= 0.30</code>. The two maps must separate; a tolerance wide enough to pass both would have no discriminating power.</li><li><strong><code>--flat-source</code> is the falsifier.</strong> Skips the ribs and still runs the detail gates. Must exit 5. Analogous to <code>--same-axis</code> in <a href="https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/export-preset-axis"><code>export-preset-axis</code></a>.</li><li><strong>The target node must be selected (5.0 and later).</strong> The bake writes into the material&#x27;s active Image Texture node, and on 5.x only if that node is also selected. <code>--unselect-target</code> keeps <code>nodes.active = tex</code> but sets <code>tex.select = False</code>. Measured: 5.0.1, 5.1.2 and 5.2.1 return <code>{&#x27;CANCELLED&#x27;}</code> with Info &quot;No active and selected image texture node found&quot; and leave the pixels untouched, so the run exits 4. On 4.5.11 the bake ignores selection, finishes, and the run <strong>exits 0</strong>. The catalog falsifier therefore carries <code>&quot;min_version&quot;: &quot;5.0&quot;</code> and is recorded as SKIP on 4.5.</li></ul>
<p>Neighbor of <a href="https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/lod-decimate-chain"><code>lod-decimate-chain</code></a> (the LOD is the cage target; collapse keeps UVs) and <a href="https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/image-pixels-testcard"><code>image-pixels-testcard</code></a> (<code>save_render</code>, not <code>Image.save()</code>, if you persist the datablock). UV transfer and atlas packing are out of scope.</p>
<p>The still reads left to right as the pipeline: the baked map as an unlit card, the high-poly source it was baked from, and the collapse-decimated LOD wearing it. Source and LOD share one cast-bronze material, so the only difference between them is real geometry versus the map. The floor captions carry live triangle counts from the evaluated meshes (3200 vs 900). A raking key from low on the left makes both the real relief and the baked relief throw light and shade. If the bake were flat, the card would be uniform <code>(128, 128, 255)</code> periwinkle and the right plate would shade like the undisplaced cage while the middle one kept its ribs.</p>
<p>Staging is render-only. Each panel stands in a low display plinth, lifted so its lowest evaluated point sits 35 mm in the slot. Both panels used to balance on an edge, and the leaning plate pierced the floor. The LOD&#x27;s solidify rim wears plain bronze instead of the baked map. The slight waviness along the plate&#x27;s border is the bake itself, normal-map shading inside the UV margin, and is left as the map produces it. Bake statistics are unchanged: frac 0.7211, MAD 0.09356 on all three binaries.</p>
Expand All @@ -76,12 +76,15 @@ <h2 id="run">Run<a class="anchor" href="#run" aria-label="Link to this section">
# Falsifier: undisplaced high. Must exit non-zero (detail frac gate).
blender --background --python bake_normal_high_to_low.py -- --flat-source

# Falsifier: active but deselected target node. Exits 4 on 5.x, 0 on 4.5 LTS.
blender --background --python bake_normal_high_to_low.py -- --unselect-target

# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts):
blender --background --python bake_normal_high_to_low.py -- --output hatch.png
blender --background --python bake_normal_high_to_low.py -- --output hatch.png --engine cycles</code></pre>
<h2 id="exit-codes">Exit codes<a class="anchor" href="#exit-codes" aria-label="Link to this section">#</a></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>
<div class="table-wrap"><table><thead><tr><th scope="col">Code</th><th scope="col">Meaning</th></tr></thead><tbody><tr><td>0</td><td>Success</td></tr><tr><td>1</td><td>Uncaught exception (FATAL wrapper)</td></tr><tr><td>2</td><td>argparse / usage</td></tr><tr><td>3</td><td>Missing UV layer on the target</td></tr><tr><td>4</td><td>Bake did not <code>FINISHED</code> or image <code>has_data</code> is false</td></tr><tr><td>5</td><td>Detail deviant-pixel fraction below 0.40 (<code>--flat-source</code> lands here)</td></tr><tr><td>6</td><td>Detail MAD below 0.05</td></tr><tr><td>7</td><td>Flat control above 0.05 frac / 0.03 MAD</td></tr><tr><td>8</td><td>Monotonic gap below 0.30</td></tr><tr><td>9</td><td><code>--output</code> produced no file</td></tr><tr><td>10</td><td>Gallery framing violation</td></tr></tbody></table></div>
<div class="table-wrap"><table><thead><tr><th scope="col">Code</th><th scope="col">Meaning</th></tr></thead><tbody><tr><td>0</td><td>Success</td></tr><tr><td>1</td><td>Uncaught exception (FATAL wrapper)</td></tr><tr><td>2</td><td>argparse / usage</td></tr><tr><td>3</td><td>Missing UV layer on the target</td></tr><tr><td>4</td><td>Bake did not <code>FINISHED</code> or image <code>has_data</code> is false (<code>--unselect-target</code> lands here on 5.x)</td></tr><tr><td>5</td><td>Detail deviant-pixel fraction below 0.40 (<code>--flat-source</code> lands here)</td></tr><tr><td>6</td><td>Detail MAD below 0.05</td></tr><tr><td>7</td><td>Flat control above 0.05 frac / 0.03 MAD</td></tr><tr><td>8</td><td>Monotonic gap below 0.30</td></tr><tr><td>9</td><td><code>--output</code> produced no file</td></tr><tr><td>10</td><td>Gallery framing violation</td></tr></tbody></table></div>
<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>.</p>
</section>
<section class="detail-section src" id="source">
Expand Down
12 changes: 10 additions & 2 deletions docs/gallery/driver-wave/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ <h1>Driver Wave</h1>
<p>A runnable example that drives sixteen organ-pipe heights from a custom function registered in <code>bpy.app.driver_namespace</code> — the pattern from <a href="https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/skills/drivers-and-app-handlers/SKILL.md"><code>drivers-and-app-handlers</code></a>. Each column gets a SCRIPTED driver on Z scale whose expression calls <code>wave_scale(i)</code>, producing a sine skyline.</p>
<p><strong>What it witnesses:</strong> the driver evaluation contract. Driven values appear only after a view-layer update, and they land in <strong>two</strong> places that must agree: the depsgraph-evaluated copy (<code>evaluated_get(dg).scale</code>) and the original datablock, which the animation system flushes for display. The check asserts both against the closed-form profile.</p>
<p>Note for real add-ons: <code>driver_namespace</code> entries do <strong>not</strong> persist in <code>.blend</code> files — re-register them from a <code>load_post</code> handler, or every driver that calls them fails on file open. Headless, registering before driver creation (as here) is enough.</p>
<p>Two more facts are asserted:</p>
<ul><li><strong>The custom-function driver is not a simple expression.</strong> Every column&#x27;s <code>driver.is_simple_expression</code> must be <code>False</code>. Only the <a href="https://docs.blender.org/manual/en/latest/animation/drivers/troubleshooting.html">simple-expression subset</a> runs with Python auto-execution off, which is the default (<code>preferences.filepaths.use_scripts_auto_execute</code> is <code>False</code> on 4.5.11 and 5.2.1). A shared <code>.blend</code> with these drivers goes dead in a GUI session unless the file is trusted or Auto Run Python Scripts is on. <code>--background</code> runs do not hit that block, so this check reads the flag instead of observing a dead driver. <code>--simple-expr</code> writes the same profile inline as <code>1.4 + sin(i * 0.6)</code>. The heights still match, but the drivers are now simple, so the run exits 5.</li><li><strong>Pre handlers get no depsgraph.</strong> <code>frame_change_pre</code> and <code>depsgraph_update_pre</code> are called with <code>(scene, None)</code>. <code>frame_change_post</code> and <code>depsgraph_update_post</code> get <code>(scene, Depsgraph)</code>. Measured on 4.5.11 and 5.2.1. <code>--swap-handlers</code> hangs each probe on the opposite list, so the run exits 7.</li></ul>
<h2 id="staging">Staging<a class="anchor" href="#staging" aria-label="Link to this section">#</a></h2>
<p>The sixteen driven objects are the speaking pipes of a small organ facade. They share one open-tube body mesh of unit height (<code>z</code> 0..1), so the driven Z scale <strong>is</strong> each pipe&#x27;s speaking length and the pipe tops trace <code>wave_scale</code> directly; the rim annulus is horizontal and stays crisp under any Z scale. Everything else — the walnut windchest and case back, the side towers with brass finials, a brass foot cone and a mouth under each pipe — is render-only staging built around the driven bodies. The case back sits behind the pipes so their tops read as a wave against wood rather than fading into the stage. The render path gates framing through <code>examples/gallery_framing.py</code> (exit 10) before writing the still.</p>
<h2 id="run">Run<a class="anchor" href="#run" aria-label="Link to this section">#</a></h2>
Expand All @@ -73,13 +75,19 @@ <h2 id="run">Run<a class="anchor" href="#run" aria-label="Link to this section">
# Falsifier: constant 1.0 expression. Must exit non-zero.
blender --background --python driver_wave.py -- --flat-expr

# Falsifier: same profile as a simple expression. Must exit 5.
blender --background --python driver_wave.py -- --simple-expr

# Falsifier: pre-handler probes on the post lists. Must exit 7.
blender --background --python driver_wave.py -- --swap-handlers

# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts):
blender --background --python driver_wave.py -- --output driver.png
blender --background --python driver_wave.py -- --output driver.png --engine cycles</code></pre>
<h2 id="exit-codes">Exit codes<a class="anchor" href="#exit-codes" aria-label="Link to this section">#</a></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>
<div class="table-wrap"><table><thead><tr><th scope="col">Code</th><th scope="col">Meaning</th></tr></thead><tbody><tr><td>0</td><td>Success</td></tr><tr><td>1</td><td>Uncaught exception (FATAL wrapper)</td></tr><tr><td>2</td><td>argparse / usage</td></tr><tr><td>3</td><td>Evaluated Z scale ≠ <code>wave_scale</code> (<code>--flat-expr</code> lands here)</td></tr><tr><td>4</td><td>Original datablock was not flushed</td></tr><tr><td>6</td><td><code>--output</code> produced no file</td></tr><tr><td>10</td><td>Gallery framing violation</td></tr></tbody></table></div>
<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>. Its catalog falsifier is <code>--flat-expr</code> (expects exit 3).</p>
<div class="table-wrap"><table><thead><tr><th scope="col">Code</th><th scope="col">Meaning</th></tr></thead><tbody><tr><td>0</td><td>Success</td></tr><tr><td>1</td><td>Uncaught exception (FATAL wrapper)</td></tr><tr><td>2</td><td>argparse / usage</td></tr><tr><td>3</td><td>Evaluated Z scale ≠ <code>wave_scale</code> (<code>--flat-expr</code> lands here)</td></tr><tr><td>4</td><td>Original datablock was not flushed</td></tr><tr><td>5</td><td>A driver reports <code>is_simple_expression=True</code> (<code>--simple-expr</code> lands here)</td></tr><tr><td>7</td><td>Handler argument types wrong: a <code>_pre</code> handler got a depsgraph or a <code>_post</code> one did not (<code>--swap-handlers</code> lands here)</td></tr><tr><td>6</td><td><code>--output</code> produced no file</td></tr><tr><td>10</td><td>Gallery framing violation</td></tr></tbody></table></div>
<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>. Its catalog falsifiers are <code>--flat-expr</code> (expects exit 3), <code>--simple-expr</code> (exit 5) and <code>--swap-handlers</code> (exit 7).</p>
</section>
<section class="detail-section src" id="source">
<h2>Source</h2>
Expand Down
Loading
Loading