diff --git a/.github/workflows/blender-smoke.yml b/.github/workflows/blender-smoke.yml index 1d74e60..532d03c 100644 --- a/.github/workflows/blender-smoke.yml +++ b/.github/workflows/blender-smoke.yml @@ -146,3 +146,12 @@ jobs: # yields geometry (eval vcount > 0 and != base); exits non-zero on failure. xvfb-run -a "$BLENDER" --background \ --python examples/gn-sdf-remesh/gn_sdf_remesh.py -- + + - name: Shipped example - depsgraph-evaluated export + run: | + set -euo pipefail + # Frame-independent check only (no render): asserts wm.obj_export ships the + # depsgraph-evaluated (modifier-applied) geometry -- exported vcount == evaluated + # > base. Exits non-zero on failure. + xvfb-run -a "$BLENDER" --background \ + --python examples/depsgraph-export/depsgraph_export.py -- diff --git a/examples/depsgraph-export/README.md b/examples/depsgraph-export/README.md new file mode 100644 index 0000000..3565a7d --- /dev/null +++ b/examples/depsgraph-export/README.md @@ -0,0 +1,26 @@ +# Depsgraph-Evaluated Export + +A runnable example that proves **modifiers actually ship in exports** and demonstrates the +[`depsgraph-and-evaluated-data`](../../skills/depsgraph-and-evaluated-data/SKILL.md) lifetime +contract. It builds a cube with a SUBSURF modifier, measures the evaluated mesh via +`evaluated_get().to_mesh()` (paired with `to_mesh_clear()`), exports through `wm.obj_export`, +and asserts the exported vertex count equals the **evaluated** (modifier-applied) count and is +strictly greater than the base mesh. + +**What it witnesses:** the `evaluated_get` → `to_mesh` → `to_mesh_clear` contract, and that +`wm.obj_export` writes the depsgraph-evaluated geometry (so modifiers are baked into the +export) rather than the unmodified base mesh. + +## Run + +```bash +# Cheap correctness check (writes an OBJ to a temp path, asserts the counts) — the CI check: +blender --background --python depsgraph_export.py -- + +# Write the exported OBJ to a specific path: +blender --background --python depsgraph_export.py -- --output remeshed.obj +``` + +It exits non-zero on failure (modifier not applied, or exported count ≠ evaluated count). The +`blender-smoke` workflow runs this check on Blender 4.5 LTS and 5.1: base 8 → evaluated/exported +98 vertices with a 2-level SUBSURF. diff --git a/examples/depsgraph-export/depsgraph_export.py b/examples/depsgraph-export/depsgraph_export.py new file mode 100644 index 0000000..8e007fd --- /dev/null +++ b/examples/depsgraph-export/depsgraph_export.py @@ -0,0 +1,54 @@ +"""Candidate B: depsgraph-evaluated export (SCRATCH). + +Witnesses the depsgraph lifetime contract AND that modifiers actually ship in exports. Builds +a cube with a SUBSURF modifier, measures the evaluated mesh via evaluated_get().to_mesh() +(paired with to_mesh_clear()), exports through wm.obj_export, and asserts the exported vertex +count equals the EVALUATED (modifier-applied) count and is strictly greater than the base. +""" +import bpy, bmesh, sys, os, argparse + +def main(): + argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] + p = argparse.ArgumentParser() + p.add_argument("--output", default=None, help="optional: write the exported OBJ here (else a temp path)") + args = p.parse_args(argv) + + bpy.ops.wm.read_factory_settings(use_empty=True) + me = bpy.data.meshes.new("Cube"); bm = bmesh.new(); bmesh.ops.create_cube(bm, size=2.0); bm.to_mesh(me); bm.free() + obj = bpy.data.objects.new("Cube", me); bpy.context.collection.objects.link(obj) + obj.modifiers.new("ss", 'SUBSURF').levels = 2 + base = len(obj.data.vertices) + + # depsgraph lifetime contract: evaluate, read, then release with to_mesh_clear + dg = bpy.context.evaluated_depsgraph_get() + ev = obj.evaluated_get(dg) + em = ev.to_mesh() + eval_vcount = len(em.vertices) + ev.to_mesh_clear() # must be paired; releases the temporary mesh + + import tempfile + out = args.output or os.path.join(tempfile.gettempdir(), "depsgraph_export.obj") + os.makedirs(os.path.dirname(os.path.abspath(out)) or ".", exist_ok=True) + # obj_export writes the evaluated (modifier-applied) geometry by default + bpy.ops.wm.obj_export(filepath=out, export_selected_objects=False) + if not (os.path.exists(out) and os.path.getsize(out) > 0): + print("ERROR: no OBJ written", file=sys.stderr); return 4 + exported = 0 + with open(out) as f: + for line in f: + if line.startswith("v "): exported += 1 + + print(f"base_vcount={base} eval_vcount={eval_vcount} exported_vcount={exported}") + if not (eval_vcount > base): + print("ERROR: evaluated mesh did not apply the modifier", file=sys.stderr); return 3 + if exported != eval_vcount: + print(f"ERROR: export ({exported}) != evaluated ({eval_vcount}); modifier did not ship", + file=sys.stderr); return 5 + print("depsgraph-export OK") + return 0 + +if __name__ == "__main__": + try: + sys.exit(main()) + except Exception as e: + import traceback; traceback.print_exc(); print(f"FATAL: {e}", file=sys.stderr); sys.exit(1)