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
28 changes: 25 additions & 3 deletions examples/armature-bend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,33 @@ joints are the same weights the LBS check asserts.
# 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
```

It exits non-zero on failure (edit-bone lifetime violation, LBS deviation, moved root
ring, or an undeformed tip). The `blender-smoke` workflow runs the check on Blender
5.2 LTS and 4.5 LTS.
## Exit codes

Per-script sequential checks. `9` is a valid check code; there is no rule
against it.

| Code | Meaning |
| --- | --- |
| 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 |

The `blender-smoke` workflow runs the check on Blender 5.2 LTS and 4.5 LTS
(5.1 on the weekly cron, the `needs-5.1` PR label, or manual dispatch).
Smoke does not pass `--output` or `--zero-curl`.

9 changes: 8 additions & 1 deletion examples/armature-bend/armature_bend.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,14 @@
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
"""
import bpy, bmesh, sys, os, math, argparse
Expand Down Expand Up @@ -294,10 +298,13 @@ def main():
p.add_argument("--output", default=None, help="optional: render a still PNG here")
p.add_argument("--engine", default="eevee", choices=("eevee", "cycles"),
help="render engine for --output (cycles for GPU-less hosts)")
p.add_argument("--zero-curl", action="store_true",
help="leave pose bones at rest (must fail)")
args = p.parse_args(argv)

bpy.ops.wm.read_factory_settings(use_empty=True)
tube, arm = build_rig(CURL_DEG)
curl = 0.0 if args.zero_curl else CURL_DEG
tube, arm = build_rig(curl)
code = check(tube, arm)
if code:
return code
Expand Down
27 changes: 23 additions & 4 deletions examples/attribute-domain-shear/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,30 @@ per `docs/VISUAL-STYLE.md`.

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

Exits non-zero on failure. The `blender-smoke` workflow runs the check on
Blender 5.2 LTS and 4.5 LTS. 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.
## Exit codes

Per-script sequential checks. `9` is a valid check code; there is no rule
against it. `10` is the shared framing helper.

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

The `blender-smoke` workflow runs the check on Blender 5.2 LTS and 4.5 LTS
(5.1 on the weekly cron, the `needs-5.1` PR label, or manual dispatch).
Smoke does not pass `--output` or `--no-overwrite`.

14 changes: 9 additions & 5 deletions examples/attribute-domain-shear/attribute_domain_shear.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
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
"""
import bpy, bmesh, sys, os, math, argparse, colorsys
Expand Down Expand Up @@ -98,13 +99,14 @@ def assign_corner(me, pal):
return attr


def assign_point_naive(me, pal):
def assign_point_naive(me, pal, overwrite=True):
"""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."""
attr = me.color_attributes.new(ATTR_P, type='FLOAT_COLOR', domain='POINT')
hub_index = 0 # build_fan creates the hub first
for i in range(K):
last = K if overwrite else 1
for i in range(last):
# naive per-wedge pass: set the hub and both ring verts to palette[i]
attr.data[hub_index].color = pal[i]
attr.data[1 + i].color = pal[i]
Expand All @@ -113,7 +115,7 @@ def assign_point_naive(me, pal):
return attr


def check():
def check(overwrite=True):
pal = palette()
expect_shear = closed_form_shear(pal)
print(f"palette K={K} closed_form_shear={expect_shear:.6f}")
Expand All @@ -138,7 +140,7 @@ def check():

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

print(f"binary version: {bpy.app.version} ({bpy.app.version_string})")
bpy.ops.wm.read_factory_settings(use_empty=True)
code = check()
code = check(overwrite=not args.no_overwrite)
if code:
return code

Expand Down
28 changes: 25 additions & 3 deletions examples/color-attribute-wheel/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,33 @@ Color, not just present in the node tree.
# Cheap correctness check (no render) — the CI check:
blender --background --python color_attribute_wheel.py --

# Falsifier: POINT-domain attribute. Must exit non-zero.
blender --background --python color_attribute_wheel.py -- --point-domain

# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts):
blender --background --python color_attribute_wheel.py -- --output wheel.png
blender --background --python color_attribute_wheel.py -- --output wheel.png --engine cycles
```

It exits non-zero on failure (missing/mis-sized/mis-domained attribute, wrong
active attribute, a probe color mismatch, or an unlinked Attribute node). The
`blender-smoke` workflow runs the check on Blender 5.2 LTS and 4.5 LTS.
## Exit codes

Per-script sequential checks. `9` is a valid check code; there is no rule
against it.

| Code | Meaning |
| --- | --- |
| 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 |

The `blender-smoke` workflow runs the check on Blender 5.2 LTS and 4.5 LTS
(5.1 on the weekly cron, the `needs-5.1` PR label, or manual dispatch).
Smoke does not pass `--output` or `--point-domain`.

29 changes: 17 additions & 12 deletions examples/color-attribute-wheel/color_attribute_wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
check. Pass --output to also render a still:

blender --background --python color_attribute_wheel.py -- # check only
blender --background --python color_attribute_wheel.py -- --point-domain # must fail
blender --background --python color_attribute_wheel.py -- --output w.png # + render
"""
import bpy, bmesh, sys, os, math, colorsys, argparse
Expand Down Expand Up @@ -56,7 +57,7 @@ def wheel_geometry():
return coords, hsv


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

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

obj, hsv = build_wheel()
obj, hsv = build_wheel(point_domain=args.point_domain)
code = check(obj, hsv)
if code:
return code
Expand Down
29 changes: 26 additions & 3 deletions examples/damped-track-aim/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,34 @@ wired to `examples/gallery_framing.py`, call it with
# Cheap correctness check (no render) — the CI check:
blender --background --python damped_track_aim.py --

# Falsifier: mute every constraint. Must exit non-zero.
blender --background --python damped_track_aim.py -- --mute

# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts):
blender --background --python damped_track_aim.py -- --output aim.png
blender --background --python damped_track_aim.py -- --output aim.png --engine cycles
```

It exits non-zero on failure (wrong constraint type/target/axis, or evaluated
aim outside the angular epsilon). The `blender-smoke` workflow runs the check
on Blender 5.2 LTS and 4.5 LTS.
## Exit codes

Per-script sequential checks. `9` is a valid check code; there is no rule
against it.

| Code | Meaning |
| --- | --- |
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Needle count ≠ 12 |
| 4 | Needle does not carry exactly one DAMPED_TRACK |
| 5 | Constraint target is not Core |
| 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 | `--output` produced no file |

The `blender-smoke` workflow runs the check on Blender 5.2 LTS and 4.5 LTS
(5.1 on the weekly cron, the `needs-5.1` PR label, or manual dispatch).
Smoke does not pass `--output` or `--mute`.

12 changes: 10 additions & 2 deletions examples/damped-track-aim/damped_track_aim.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,14 @@
constraint is missing, muted, mistyped as TRACK_TO, or the axis is flipped,
the dot product fails.

``--mute`` mutes every DAMPED_TRACK and still asserts unmute. 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 damped_track_aim.py -- # check only
blender --background --python damped_track_aim.py -- --mute # must fail
blender --background --python damped_track_aim.py -- --output aim.png
"""
import bpy, bmesh, sys, os, math, argparse
Expand Down Expand Up @@ -161,7 +165,7 @@ def make_dielectric(name, color, roughness=0.35):
return mat


def build():
def build(mute=False):
bpy.ops.wm.read_factory_settings(use_empty=True)
col = bpy.context.collection

Expand All @@ -186,6 +190,8 @@ def build():
con.name = "AimCore"
con.target = core
con.track_axis = "TRACK_Z"
if mute:
con.mute = True
needles.append(ob)

bpy.context.view_layer.update()
Expand Down Expand Up @@ -376,9 +382,11 @@ def main():
choices=("eevee", "cycles"),
help="render engine when --output is set",
)
p.add_argument("--mute", action="store_true",
help="mute every DAMPED_TRACK (must fail)")
args = p.parse_args(argv)

core, needles = build()
core, needles = build(mute=args.mute)
code = check(core, needles)
if code != 0:
return code
Expand Down
28 changes: 25 additions & 3 deletions examples/depsgraph-export/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ export) rather than the unmodified base mesh.
# Cheap correctness check (writes an OBJ to a temp path, asserts the counts) — the CI check:
blender --background --python depsgraph_export.py --

# Falsifier: apply_modifiers=False. Must exit non-zero (export ≠ evaluated).
blender --background --python depsgraph_export.py -- --unevaluated

# Also render a still of base vs evaluated (EEVEE on a GPU host; cycles on GPU-less hosts):
blender --background --python depsgraph_export.py -- --output depsgraph.png
blender --background --python depsgraph_export.py -- --output depsgraph.png --engine cycles
Expand All @@ -25,8 +28,27 @@ blender --background --python depsgraph_export.py -- --output depsgraph.png --en
blender --background --python depsgraph_export.py -- --obj exported.obj
```

It exits non-zero on failure (modifier not applied, or exported count ≠ evaluated count). The
`blender-smoke` workflow runs this check on Blender 5.2 LTS and 4.5 LTS: base 8 → evaluated/exported
98 vertices with a 2-level SUBSURF.
## Exit codes

Per-script sequential checks. `9` is a valid check code; there is no rule
against it. `10` is the shared framing helper.

| Code | Meaning |
| --- | --- |
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Evaluated mesh did not apply the modifier |
| 4 | No OBJ written |
| 5 | Export vert count ≠ evaluated (`--unevaluated` lands here) |
| 6 | `--output` produced no file |
| 10 | Gallery framing violation |

`--obj` is a path selector, not a falsifier.

The `blender-smoke` workflow runs the check on Blender 5.2 LTS and 4.5 LTS
(5.1 on the weekly cron, the `needs-5.1` PR label, or manual dispatch).
Smoke does not pass `--output`, `--obj`, or `--unevaluated`.


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