Skip to content

Commit 89ae4eb

Browse files
feat: add CLI falsifiers and exit tables to thirteen rigging examples (#143)
Default smoke stays green. Each flag feeds degenerate input into an existing assertion, including unevaluated OBJ export and join without temp_override. Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent adf9ace commit 89ae4eb

26 files changed

Lines changed: 494 additions & 102 deletions

File tree

‎examples/armature-bend/README.md‎

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,33 @@ joints are the same weights the LBS check asserts.
3535
# Cheap correctness check (no render) — the CI check:
3636
blender --background --python armature_bend.py --
3737

38+
# Falsifier: rest pose. Must exit non-zero (tip deflection).
39+
blender --background --python armature_bend.py -- --zero-curl
40+
3841
# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts):
3942
blender --background --python armature_bend.py -- --output bend.png
4043
blender --background --python armature_bend.py -- --output bend.png --engine cycles
4144
```
4245

43-
It exits non-zero on failure (edit-bone lifetime violation, LBS deviation, moved root
44-
ring, or an undeformed tip). The `blender-smoke` workflow runs the check on Blender
45-
5.2 LTS and 4.5 LTS.
46+
## Exit codes
47+
48+
Per-script sequential checks. `9` is a valid check code; there is no rule
49+
against it.
50+
51+
| Code | Meaning |
52+
| --- | --- |
53+
| 0 | Success |
54+
| 1 | Uncaught exception (FATAL wrapper) |
55+
| 2 | argparse / usage |
56+
| 3 | `edit_bones` populated in object mode |
57+
| 4 | Edit-mode bone chain off closed form |
58+
| 5 | Evaluated vertex count changed |
59+
| 6 | Evaluated mesh off closed-form LBS |
60+
| 7 | Root ring moved |
61+
| 8 | Tip did not deflect (`--zero-curl` lands here) |
62+
| 9 | `--output` produced no file |
63+
64+
The `blender-smoke` workflow runs the check on Blender 5.2 LTS and 4.5 LTS
65+
(5.1 on the weekly cron, the `needs-5.1` PR label, or manual dispatch).
66+
Smoke does not pass `--output` or `--zero-curl`.
67+

‎examples/armature-bend/armature_bend.py‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,14 @@
1919
The same API works unchanged on Blender 4.5 LTS and 5.1 — no version gate is
2020
needed, which this example demonstrates by running identically on both.
2121
22+
``--zero-curl`` leaves every pose bone at rest and still asserts the tip
23+
deflects. That is the falsifier (``--same-axis`` in export-preset-axis).
24+
2225
By default it runs only the correctness check (no render) — the CI smoke
2326
check. Pass --output to also render a still:
2427
2528
blender --background --python armature_bend.py -- # check only
29+
blender --background --python armature_bend.py -- --zero-curl # must fail
2630
blender --background --python armature_bend.py -- --output b.png # + render
2731
"""
2832
import bpy, bmesh, sys, os, math, argparse
@@ -294,10 +298,13 @@ def main():
294298
p.add_argument("--output", default=None, help="optional: render a still PNG here")
295299
p.add_argument("--engine", default="eevee", choices=("eevee", "cycles"),
296300
help="render engine for --output (cycles for GPU-less hosts)")
301+
p.add_argument("--zero-curl", action="store_true",
302+
help="leave pose bones at rest (must fail)")
297303
args = p.parse_args(argv)
298304

299305
bpy.ops.wm.read_factory_settings(use_empty=True)
300-
tube, arm = build_rig(CURL_DEG)
306+
curl = 0.0 if args.zero_curl else CURL_DEG
307+
tube, arm = build_rig(curl)
301308
code = check(tube, arm)
302309
if code:
303310
return code

‎examples/attribute-domain-shear/README.md‎

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,30 @@ per `docs/VISUAL-STYLE.md`.
5050

5151
```bash
5252
blender --background --python attribute_domain_shear.py --
53+
blender --background --python attribute_domain_shear.py -- --no-overwrite
5354
blender --background --python attribute_domain_shear.py -- --output shear.png
5455
blender --background --python attribute_domain_shear.py -- --output shear.png --engine cycles
5556
```
5657

57-
Exits non-zero on failure. The `blender-smoke` workflow runs the check on
58-
Blender 5.2 LTS and 4.5 LTS. The `--output` render path additionally measures
59-
framing against the Layer 1 band via `examples/gallery_framing.py` (exit 10
60-
on violation) before writing the still.
58+
## Exit codes
59+
60+
Per-script sequential checks. `9` is a valid check code; there is no rule
61+
against it. `10` is the shared framing helper.
62+
63+
| Code | Meaning |
64+
| --- | --- |
65+
| 0 | Success |
66+
| 1 | Uncaught exception (FATAL wrapper) |
67+
| 2 | argparse / usage |
68+
| 3 | CORNER or POINT attribute size wrong |
69+
| 4 | CORNER hub corners off wedge color |
70+
| 5 | POINT hub is not last-write (`--no-overwrite` lands here) |
71+
| 6 | Outer ring verts off last-write order |
72+
| 7 | Measured shear off palette closed form, or ~0 |
73+
| 9 | `--output` produced no file |
74+
| 10 | Gallery framing violation |
75+
76+
The `blender-smoke` workflow runs the check on Blender 5.2 LTS and 4.5 LTS
77+
(5.1 on the weekly cron, the `needs-5.1` PR label, or manual dispatch).
78+
Smoke does not pass `--output` or `--no-overwrite`.
79+

‎examples/attribute-domain-shear/attribute_domain_shear.py‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
check. Pass --output to also render a still:
2727
2828
blender --background --python attribute_domain_shear.py -- # check only
29+
blender --background --python attribute_domain_shear.py -- --no-overwrite # must fail
2930
blender --background --python attribute_domain_shear.py -- --output a.png # + render
3031
"""
3132
import bpy, bmesh, sys, os, math, argparse, colorsys
@@ -98,13 +99,14 @@ def assign_corner(me, pal):
9899
return attr
99100

100101

101-
def assign_point_naive(me, pal):
102+
def assign_point_naive(me, pal, overwrite=True):
102103
"""The AI mistake: author per-wedge colors into a POINT-domain attribute.
103104
Every wedge rewrites the shared hub (and its leading ring vert), so the
104105
last wedge wins — colors shear across every shared vertex."""
105106
attr = me.color_attributes.new(ATTR_P, type='FLOAT_COLOR', domain='POINT')
106107
hub_index = 0 # build_fan creates the hub first
107-
for i in range(K):
108+
last = K if overwrite else 1
109+
for i in range(last):
108110
# naive per-wedge pass: set the hub and both ring verts to palette[i]
109111
attr.data[hub_index].color = pal[i]
110112
attr.data[1 + i].color = pal[i]
@@ -113,7 +115,7 @@ def assign_point_naive(me, pal):
113115
return attr
114116

115117

116-
def check():
118+
def check(overwrite=True):
117119
pal = palette()
118120
expect_shear = closed_form_shear(pal)
119121
print(f"palette K={K} closed_form_shear={expect_shear:.6f}")
@@ -138,7 +140,7 @@ def check():
138140

139141
# --- POINT: the shear, measured against the closed form ---
140142
me_p = build_fan()
141-
attr_p = assign_point_naive(me_p, pal)
143+
attr_p = assign_point_naive(me_p, pal, overwrite=overwrite)
142144
if len(attr_p.data) != len(me_p.vertices) or len(me_p.vertices) != K + 1:
143145
print(f"ERROR: POINT attr size {len(attr_p.data)} != verts {len(me_p.vertices)}",
144146
file=sys.stderr)
@@ -383,11 +385,13 @@ def main():
383385
p.add_argument("--output", default=None, help="optional: render a still PNG here")
384386
p.add_argument("--engine", default="eevee", choices=("eevee", "cycles"),
385387
help="render engine for --output (cycles for GPU-less hosts)")
388+
p.add_argument("--no-overwrite", action="store_true",
389+
help="write only the first POINT wedge (must fail)")
386390
args = p.parse_args(argv)
387391

388392
print(f"binary version: {bpy.app.version} ({bpy.app.version_string})")
389393
bpy.ops.wm.read_factory_settings(use_empty=True)
390-
code = check()
394+
code = check(overwrite=not args.no_overwrite)
391395
if code:
392396
return code
393397

‎examples/color-attribute-wheel/README.md‎

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,33 @@ Color, not just present in the node tree.
2525
# Cheap correctness check (no render) — the CI check:
2626
blender --background --python color_attribute_wheel.py --
2727

28+
# Falsifier: POINT-domain attribute. Must exit non-zero.
29+
blender --background --python color_attribute_wheel.py -- --point-domain
30+
2831
# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts):
2932
blender --background --python color_attribute_wheel.py -- --output wheel.png
3033
blender --background --python color_attribute_wheel.py -- --output wheel.png --engine cycles
3134
```
3235

33-
It exits non-zero on failure (missing/mis-sized/mis-domained attribute, wrong
34-
active attribute, a probe color mismatch, or an unlinked Attribute node). The
35-
`blender-smoke` workflow runs the check on Blender 5.2 LTS and 4.5 LTS.
36+
## Exit codes
37+
38+
Per-script sequential checks. `9` is a valid check code; there is no rule
39+
against it.
40+
41+
| Code | Meaning |
42+
| --- | --- |
43+
| 0 | Success |
44+
| 1 | Uncaught exception (FATAL wrapper) |
45+
| 2 | argparse / usage |
46+
| 3 | Topology ≠ closed form |
47+
| 4 | Color attribute missing |
48+
| 5 | Domain/type ≠ CORNER/FLOAT_COLOR (`--point-domain` lands here) |
49+
| 6 | Attribute sized to verts, not loops |
50+
| 7 | `active_color` not set |
51+
| 8 | Probe loop color off HSV closed form |
52+
| 9 | `--output` produced no file |
53+
54+
The `blender-smoke` workflow runs the check on Blender 5.2 LTS and 4.5 LTS
55+
(5.1 on the weekly cron, the `needs-5.1` PR label, or manual dispatch).
56+
Smoke does not pass `--output` or `--point-domain`.
57+

‎examples/color-attribute-wheel/color_attribute_wheel.py‎

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
check. Pass --output to also render a still:
1919
2020
blender --background --python color_attribute_wheel.py -- # check only
21+
blender --background --python color_attribute_wheel.py -- --point-domain # must fail
2122
blender --background --python color_attribute_wheel.py -- --output w.png # + render
2223
"""
2324
import bpy, bmesh, sys, os, math, colorsys, argparse
@@ -56,7 +57,7 @@ def wheel_geometry():
5657
return coords, hsv
5758

5859

59-
def build_wheel():
60+
def build_wheel(point_domain=False):
6061
bpy.ops.wm.read_factory_settings(use_empty=True)
6162
coords, hsv = wheel_geometry()
6263
me = bpy.data.meshes.new("ColorWheel")
@@ -80,16 +81,18 @@ def build_wheel():
8081
# created via color_attributes (not the deprecated vertex_colors alias),
8182
# sized to loops -- then filled by expanding per-vertex HSV across corners
8283
# with bulk foreach_get / foreach_set, never a per-loop Python assignment.
83-
attr = me.color_attributes.new(ATTR_NAME, type='FLOAT_COLOR', domain='CORNER')
84-
n_loops = len(me.loops)
85-
loop_vert = array('i', [0]) * n_loops
86-
me.loops.foreach_get("vertex_index", loop_vert)
87-
flat = array('f', [0.0]) * (n_loops * 4)
88-
for i, vi in enumerate(loop_vert):
89-
h, s, v = hsv[vi]
90-
r, g, b = colorsys.hsv_to_rgb(h, s, v)
91-
flat[i * 4], flat[i * 4 + 1], flat[i * 4 + 2], flat[i * 4 + 3] = r, g, b, 1.0
92-
attr.data.foreach_set("color", flat)
84+
domain = 'POINT' if point_domain else 'CORNER'
85+
attr = me.color_attributes.new(ATTR_NAME, type='FLOAT_COLOR', domain=domain)
86+
if not point_domain:
87+
n_loops = len(me.loops)
88+
loop_vert = array('i', [0]) * n_loops
89+
me.loops.foreach_get("vertex_index", loop_vert)
90+
flat = array('f', [0.0]) * (n_loops * 4)
91+
for i, vi in enumerate(loop_vert):
92+
h, s, v = hsv[vi]
93+
r, g, b = colorsys.hsv_to_rgb(h, s, v)
94+
flat[i * 4], flat[i * 4 + 1], flat[i * 4 + 2], flat[i * 4 + 3] = r, g, b, 1.0
95+
attr.data.foreach_set("color", flat)
9396
me.color_attributes.active_color = attr # the step AI code most often forgets
9497

9598
obj = bpy.data.objects.new("ColorWheel", me)
@@ -283,9 +286,11 @@ def main():
283286
p.add_argument("--output", default=None, help="optional: render a still PNG here")
284287
p.add_argument("--engine", default="eevee", choices=("eevee", "cycles"),
285288
help="render engine for --output (cycles for GPU-less hosts)")
289+
p.add_argument("--point-domain", action="store_true",
290+
help="create a POINT-domain color attribute (must fail)")
286291
args = p.parse_args(argv)
287292

288-
obj, hsv = build_wheel()
293+
obj, hsv = build_wheel(point_domain=args.point_domain)
289294
code = check(obj, hsv)
290295
if code:
291296
return code

‎examples/damped-track-aim/README.md‎

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,34 @@ wired to `examples/gallery_framing.py`, call it with
2626
# Cheap correctness check (no render) — the CI check:
2727
blender --background --python damped_track_aim.py --
2828

29+
# Falsifier: mute every constraint. Must exit non-zero.
30+
blender --background --python damped_track_aim.py -- --mute
31+
2932
# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts):
3033
blender --background --python damped_track_aim.py -- --output aim.png
3134
blender --background --python damped_track_aim.py -- --output aim.png --engine cycles
3235
```
3336

34-
It exits non-zero on failure (wrong constraint type/target/axis, or evaluated
35-
aim outside the angular epsilon). The `blender-smoke` workflow runs the check
36-
on Blender 5.2 LTS and 4.5 LTS.
37+
## Exit codes
38+
39+
Per-script sequential checks. `9` is a valid check code; there is no rule
40+
against it.
41+
42+
| Code | Meaning |
43+
| --- | --- |
44+
| 0 | Success |
45+
| 1 | Uncaught exception (FATAL wrapper) |
46+
| 2 | argparse / usage |
47+
| 3 | Needle count ≠ 12 |
48+
| 4 | Needle does not carry exactly one DAMPED_TRACK |
49+
| 5 | Constraint target is not Core |
50+
| 6 | `track_axis` is not TRACK_Z |
51+
| 7 | Constraint muted or influence < 1 (`--mute` lands here) |
52+
| 8 | TRACK_TO still present |
53+
| 9 | Evaluated aim dot below 0.998 |
54+
| 10 | `--output` produced no file |
55+
56+
The `blender-smoke` workflow runs the check on Blender 5.2 LTS and 4.5 LTS
57+
(5.1 on the weekly cron, the `needs-5.1` PR label, or manual dispatch).
58+
Smoke does not pass `--output` or `--mute`.
59+

‎examples/damped-track-aim/damped_track_aim.py‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,14 @@
1212
constraint is missing, muted, mistyped as TRACK_TO, or the axis is flipped,
1313
the dot product fails.
1414
15+
``--mute`` mutes every DAMPED_TRACK and still asserts unmute. That is the
16+
falsifier (``--same-axis`` in export-preset-axis).
17+
1518
By default it runs only the correctness check (no render) — the CI smoke
1619
check. Pass --output to also render a still:
1720
1821
blender --background --python damped_track_aim.py -- # check only
22+
blender --background --python damped_track_aim.py -- --mute # must fail
1923
blender --background --python damped_track_aim.py -- --output aim.png
2024
"""
2125
import bpy, bmesh, sys, os, math, argparse
@@ -161,7 +165,7 @@ def make_dielectric(name, color, roughness=0.35):
161165
return mat
162166

163167

164-
def build():
168+
def build(mute=False):
165169
bpy.ops.wm.read_factory_settings(use_empty=True)
166170
col = bpy.context.collection
167171

@@ -186,6 +190,8 @@ def build():
186190
con.name = "AimCore"
187191
con.target = core
188192
con.track_axis = "TRACK_Z"
193+
if mute:
194+
con.mute = True
189195
needles.append(ob)
190196

191197
bpy.context.view_layer.update()
@@ -376,9 +382,11 @@ def main():
376382
choices=("eevee", "cycles"),
377383
help="render engine when --output is set",
378384
)
385+
p.add_argument("--mute", action="store_true",
386+
help="mute every DAMPED_TRACK (must fail)")
379387
args = p.parse_args(argv)
380388

381-
core, needles = build()
389+
core, needles = build(mute=args.mute)
382390
code = check(core, needles)
383391
if code != 0:
384392
return code

‎examples/depsgraph-export/README.md‎

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ export) rather than the unmodified base mesh.
1717
# Cheap correctness check (writes an OBJ to a temp path, asserts the counts) — the CI check:
1818
blender --background --python depsgraph_export.py --
1919

20+
# Falsifier: apply_modifiers=False. Must exit non-zero (export ≠ evaluated).
21+
blender --background --python depsgraph_export.py -- --unevaluated
22+
2023
# Also render a still of base vs evaluated (EEVEE on a GPU host; cycles on GPU-less hosts):
2124
blender --background --python depsgraph_export.py -- --output depsgraph.png
2225
blender --background --python depsgraph_export.py -- --output depsgraph.png --engine cycles
@@ -25,8 +28,27 @@ blender --background --python depsgraph_export.py -- --output depsgraph.png --en
2528
blender --background --python depsgraph_export.py -- --obj exported.obj
2629
```
2730

28-
It exits non-zero on failure (modifier not applied, or exported count ≠ evaluated count). The
29-
`blender-smoke` workflow runs this check on Blender 5.2 LTS and 4.5 LTS: base 8 → evaluated/exported
30-
98 vertices with a 2-level SUBSURF.
31+
## Exit codes
32+
33+
Per-script sequential checks. `9` is a valid check code; there is no rule
34+
against it. `10` is the shared framing helper.
35+
36+
| Code | Meaning |
37+
| --- | --- |
38+
| 0 | Success |
39+
| 1 | Uncaught exception (FATAL wrapper) |
40+
| 2 | argparse / usage |
41+
| 3 | Evaluated mesh did not apply the modifier |
42+
| 4 | No OBJ written |
43+
| 5 | Export vert count ≠ evaluated (`--unevaluated` lands here) |
44+
| 6 | `--output` produced no file |
45+
| 10 | Gallery framing violation |
46+
47+
`--obj` is a path selector, not a falsifier.
48+
49+
The `blender-smoke` workflow runs the check on Blender 5.2 LTS and 4.5 LTS
50+
(5.1 on the weekly cron, the `needs-5.1` PR label, or manual dispatch).
51+
Smoke does not pass `--output`, `--obj`, or `--unevaluated`.
52+
3153

3254
The `--output` render path additionally measures framing against the Layer 1 band via `examples/gallery_framing.py` (exit 10 on violation) before writing the still.

0 commit comments

Comments
 (0)