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
22 changes: 20 additions & 2 deletions examples/bmesh-gear/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,28 @@ faces). If an op leaks geometry or a face fails to close, the math catches it.
# Cheap correctness check (no render) — the CI check:
blender --background --python bmesh_gear.py --

# Falsifier: skip the extrude. Must exit non-zero (topology).
blender --background --python bmesh_gear.py -- --no-extrude

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

It exits non-zero on failure (topology mismatch or non-manifold edges). 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 (`--no-extrude` lands here) |
| 4 | Non-manifold edges |
| 6 | `--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 `--no-extrude`.
18 changes: 13 additions & 5 deletions examples/bmesh-gear/bmesh_gear.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@
verts = 2 x (4 x teeth), faces = sides + 2 caps, edges = 3 x profile — and
that the mesh is watertight (every edge borders exactly 2 faces).

``--no-extrude`` skips the face-region extrude and still runs the closed-form
count check, so verts stay at one ring. 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 bmesh_gear.py -- # check only
blender --background --python bmesh_gear.py -- --no-extrude # must fail
blender --background --python bmesh_gear.py -- --output g.png # + render
"""
import bpy, bmesh, sys, os, math, argparse
Expand Down Expand Up @@ -39,16 +44,17 @@ def gear_profile():
return [(r * math.cos(a), r * math.sin(a), 0.0) for a, r in coords]


def build_gear():
def build_gear(no_extrude=False):
bpy.ops.wm.read_factory_settings(use_empty=True)
me = bpy.data.meshes.new("Gear")
bm = bmesh.new()
try:
verts = [bm.verts.new(co) for co in gear_profile()]
face = bm.faces.new(verts)
ext = bmesh.ops.extrude_face_region(bm, geom=[face])
top_verts = [e for e in ext["geom"] if isinstance(e, bmesh.types.BMVert)]
bmesh.ops.translate(bm, verts=top_verts, vec=(0.0, 0.0, DEPTH))
if not no_extrude:
ext = bmesh.ops.extrude_face_region(bm, geom=[face])
top_verts = [e for e in ext["geom"] if isinstance(e, bmesh.types.BMVert)]
bmesh.ops.translate(bm, verts=top_verts, vec=(0.0, 0.0, DEPTH))
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
bm.to_mesh(me)
finally:
Expand Down Expand Up @@ -193,9 +199,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("--no-extrude", action="store_true",
help="skip the face-region extrude (must fail)")
args = p.parse_args(argv)

obj = build_gear()
obj = build_gear(no_extrude=args.no_extrude)
code = check(obj)
if code:
return code
Expand Down
28 changes: 24 additions & 4 deletions examples/collision-hull-proxy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,32 @@ through the shell in frame.
# Cheap correctness check (no render) — the CI check:
blender --background --python collision_hull_proxy.py --

# Falsifier: shrink hull vertices. Must exit non-zero (containment).
blender --background --python collision_hull_proxy.py -- --shrink-hull

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

It exits non-zero on failure (render geometry escaping a hull, inverted
winding, a non-watertight or non-convex piece, Euler drift, or a piece over
the 255-face budget). 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 | Render vertex escapes its hull (`--shrink-hull` lands here) |
| 4 | Hull edge does not border exactly two faces |
| 5 | Signed volume ≤ 0 (inverted winding) |
| 6 | Hull vertex off its own face plane |
| 7 | Euler characteristic ≠ 2 |
| 8 | Piece over the 255-face collision budget |
| 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 `--shrink-hull`.
11 changes: 11 additions & 0 deletions examples/collision-hull-proxy/collision_hull_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,15 @@
details (grooves) never touch the hull; proud details cost hull faces. That
trade-off IS the collision-authoring lesson.

``--shrink-hull`` scales each hull's vertices by 0.5 and still runs the
containment plane test, so render verts escape. 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 collision_hull_proxy.py -- # check only
blender --background --python collision_hull_proxy.py -- --shrink-hull # must fail
blender --background --python collision_hull_proxy.py -- --output h.png # + render
"""
import bpy, bmesh, sys, os, math, argparse
Expand Down Expand Up @@ -485,12 +490,18 @@ 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("--shrink-hull", action="store_true",
help="scale hull vertices by 0.5 (must fail)")
args = p.parse_args(argv)

groups = build_hydrant()
pieces = []
for g in groups:
hull = build_hull(f"{g['name']}Hull", collect_points(g["cage"]))
if args.shrink_hull:
for v in hull.data.vertices:
v.co *= 0.5
hull.data.update()
pieces.append((g["name"], hull, collect_points(g["render"])))
code = check(pieces)
if code:
Expand Down
27 changes: 23 additions & 4 deletions examples/degenerate-bevel-weld/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,30 @@ broken state is in-frame by design.

```bash
blender --background --python degenerate_bevel_weld.py --
blender --background --python degenerate_bevel_weld.py -- --both-safe
blender --background --python degenerate_bevel_weld.py -- --output bevel.png
blender --background --python degenerate_bevel_weld.py -- --output bevel.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 | Safe bevel produced zero-area faces |
| 4 | Degenerate bevel zero-area count ≠ closed form (`--both-safe` lands here) |
| 5 | min_area collapse under 1e5× |
| 6 | Coincident-position count off the closed form |
| 7 | Safe GLB carries degenerate triangles |
| 8 | Degenerate GLB triangle or position count drifted |
| 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 `--both-safe`.
8 changes: 5 additions & 3 deletions examples/degenerate-bevel-weld/degenerate_bevel_weld.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,10 @@ def export_glb(ob, path):
return path


def check():
def check(both_safe=False):
tmp = tempfile.mkdtemp(prefix="bevelweld_")
safe = beveled_box(DIMS, SAFE_OFFSET)
degen = beveled_box(DIMS, DEGEN_OFFSET)
degen = beveled_box(DIMS, SAFE_OFFSET if both_safe else DEGEN_OFFSET)

# --- 1. threshold: zero-area faces flip on at offset == half min dim ---
safe_za = zero_area_count(safe)
Expand Down Expand Up @@ -391,11 +391,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("--both-safe", action="store_true",
help="bevel the degenerate box at the safe offset (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(both_safe=args.both_safe)
if code:
return code

Expand Down
26 changes: 23 additions & 3 deletions examples/gn-instance-grid/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,31 @@ counts fail.
# Cheap correctness check (no render) — the CI check:
blender --background --python gn_instance_grid.py --

# Falsifier: 1×1 grid. Must exit non-zero (corner instance missing).
blender --background --python gn_instance_grid.py -- --one-cell

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

It exits non-zero on failure (wrong carrier, topology mismatch, missing material, or
misplaced corner). 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 | Carrier vertex count ≠ 1 |
| 4 | Corner instance vert count ≠ 8 (`--one-cell` lands here) |
| 5 | Evaluated topology ≠ 72 verts / 54 faces |
| 6 | Set Material did not carry Lime |
| 7 | Corner instance center off the closed-form grid point |
| 8 | `--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 `--one-cell`.
20 changes: 14 additions & 6 deletions examples/gn-instance-grid/gn_instance_grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,15 @@
left as empty instance geometry, and that a corner instance sits at its
closed-form grid coordinate.

``--one-cell`` builds a 1x1 grid and still asserts 3x3 realized topology,
so the corner instance is missing. 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 gn_instance_grid.py -- # check only
blender --background --python gn_instance_grid.py -- --one-cell # must fail
blender --background --python gn_instance_grid.py -- --output g.png # + render
"""
import bpy, bmesh, sys, os, math, argparse
Expand All @@ -31,7 +36,7 @@
CORNER_CENTER = (GRID_HALF, GRID_HALF, CUBE_SIZE / 2)


def build_instance_grid_tree(material=None):
def build_instance_grid_tree(material=None, grid_x=GRID_X, grid_y=GRID_Y):
tree = bpy.data.node_groups.new("InstanceGrid", 'GeometryNodeTree')
# generative: no Group Input — the tree owns the geometry
tree.interface.new_socket(
Expand All @@ -42,8 +47,8 @@ def build_instance_grid_tree(material=None):
grid = tree.nodes.new('GeometryNodeMeshGrid')
grid.inputs["Size X"].default_value = GRID_SIZE
grid.inputs["Size Y"].default_value = GRID_SIZE
grid.inputs["Vertices X"].default_value = GRID_X
grid.inputs["Vertices Y"].default_value = GRID_Y
grid.inputs["Vertices X"].default_value = grid_x
grid.inputs["Vertices Y"].default_value = grid_y

cube = tree.nodes.new('GeometryNodeMeshCube')
cube.inputs["Size"].default_value = (CUBE_SIZE, CUBE_SIZE, CUBE_SIZE)
Expand Down Expand Up @@ -75,7 +80,7 @@ def build_instance_grid_tree(material=None):
return tree


def build():
def build(one_cell=False):
bpy.ops.wm.read_factory_settings(use_empty=True)
# carrier mesh is unused by the generative tree; one vertex is enough
me = bpy.data.meshes.new("Carrier")
Expand All @@ -89,7 +94,8 @@ def build():
bsdf.inputs["Base Color"].default_value = (0.22, 0.95, 0.06, 1.0) # lime
bsdf.inputs["Roughness"].default_value = 0.22

tree = build_instance_grid_tree(material=mat)
gx = gy = 1 if one_cell else GRID_X
tree = build_instance_grid_tree(material=mat, grid_x=gx, grid_y=gy)
mod = obj.modifiers.new("instance_grid", 'NODES')
mod.node_group = tree
return obj, mat
Expand Down Expand Up @@ -246,9 +252,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("--one-cell", action="store_true",
help="instance a 1x1 grid (must fail)")
args = p.parse_args(argv)

obj, _mat = build()
obj, _mat = build(one_cell=args.one_cell)
code = check(obj)
if code:
return code
Expand Down
25 changes: 21 additions & 4 deletions examples/gn-sdf-remesh/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,29 @@ output — this example does that and asserts the material survives onto the eva
# Cheap correctness check only (no render) — the CI smoke check:
blender --background --python gn_sdf_remesh.py --

# Falsifier: no SDF modifier. Must exit non-zero (evaluated == base).
blender --background --python gn_sdf_remesh.py -- --no-sdf

# Also render the remeshed result (EEVEE on a GPU host; --engine cycles on GPU-less hosts):
blender --background --python gn_sdf_remesh.py -- --output remesh.png
blender --background --python gn_sdf_remesh.py -- --output remesh.png --engine cycles
```

By default it runs only the **frame-independent correctness check**: the depsgraph-evaluated
vertex count must be > 0 AND differ from the base mesh (the remesh produced geometry). It
exits non-zero on failure — the same check the `blender-smoke` workflow runs 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 | SDF remesh produced no/unchanged geometry (`--no-sdf` lands here) |
| 4 | `--output` produced no file |
| 5 | Wrong-era EEVEE engine id was accepted |
| 6 | Input material dropped by remesh |

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-sdf`.
6 changes: 5 additions & 1 deletion examples/gn-sdf-remesh/gn_sdf_remesh.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
both builds.

blender --background --python gn_sdf_remesh.py -- # correctness check only
blender --background --python gn_sdf_remesh.py -- --no-sdf # must fail
blender --background --python gn_sdf_remesh.py -- --output r.png # also render the result
blender --background --python gn_sdf_remesh.py -- --output r.png --engine cycles # GPU-less
"""
Expand Down Expand Up @@ -108,6 +109,8 @@ def main():
p = argparse.ArgumentParser()
p.add_argument("--output", default=None, help="optional: render the remeshed result to this PNG")
p.add_argument("--engine", choices=["auto", "cycles"], default="auto")
p.add_argument("--no-sdf", action="store_true",
help="skip attaching the SDF remesh modifier (must fail)")
args = p.parse_args(argv)

# EEVEE-id inversion witnessed for real: the OTHER era's id must be
Expand All @@ -125,7 +128,8 @@ def main():
base = len(obj.data.vertices)
src_mat = obj.data.materials[0] if obj.data.materials else None
tree, link_valid = build_remesh_via_sdf(material=src_mat)
obj.modifiers.new("sdf", 'NODES').node_group = tree
if not args.no_sdf:
obj.modifiers.new("sdf", 'NODES').node_group = tree
dg = bpy.context.evaluated_depsgraph_get(); ev = obj.evaluated_get(dg)
m = ev.to_mesh(); evc = len(m.vertices)
mat_names = [mm.name for mm in m.materials if mm is not None]
Expand Down
Loading
Loading