Skip to content

Commit 2ad60ca

Browse files
authored
feat: add depsgraph-evaluated export example with smoke gate (#21)
examples/depsgraph-export/ proves modifiers actually ship in exports and demonstrates the depsgraph evaluated_get -> to_mesh -> to_mesh_clear lifetime contract: builds a cube + SUBSURF, measures the evaluated mesh, exports via wm.obj_export, and asserts exported vcount == evaluated (modifier-applied) count > base. Cheap CI check (no render): base 8 -> evaluated/exported 98 on both Blender 4.5.10 LTS and 5.1.1. blender-smoke.yml runs the shipped file check-only on both builds. examples/ stays out of validate-counts (12/6/2/17 unchanged); roadmap decoupled in #19 so no renumber. (Scratch candidate headless-export was NOT promoted -- it duplicates the existing tmpl_gltf smoke coverage.) Signed-off-by: fOuttaMyPaint <TMhospitalitystrategies@gmail.com>
1 parent 8ed8c15 commit 2ad60ca

3 files changed

Lines changed: 89 additions & 0 deletions

File tree

‎.github/workflows/blender-smoke.yml‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,3 +146,12 @@ jobs:
146146
# yields geometry (eval vcount > 0 and != base); exits non-zero on failure.
147147
xvfb-run -a "$BLENDER" --background \
148148
--python examples/gn-sdf-remesh/gn_sdf_remesh.py --
149+
150+
- name: Shipped example - depsgraph-evaluated export
151+
run: |
152+
set -euo pipefail
153+
# Frame-independent check only (no render): asserts wm.obj_export ships the
154+
# depsgraph-evaluated (modifier-applied) geometry -- exported vcount == evaluated
155+
# > base. Exits non-zero on failure.
156+
xvfb-run -a "$BLENDER" --background \
157+
--python examples/depsgraph-export/depsgraph_export.py --
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Depsgraph-Evaluated Export
2+
3+
A runnable example that proves **modifiers actually ship in exports** and demonstrates the
4+
[`depsgraph-and-evaluated-data`](../../skills/depsgraph-and-evaluated-data/SKILL.md) lifetime
5+
contract. It builds a cube with a SUBSURF modifier, measures the evaluated mesh via
6+
`evaluated_get().to_mesh()` (paired with `to_mesh_clear()`), exports through `wm.obj_export`,
7+
and asserts the exported vertex count equals the **evaluated** (modifier-applied) count and is
8+
strictly greater than the base mesh.
9+
10+
**What it witnesses:** the `evaluated_get` → `to_mesh` → `to_mesh_clear` contract, and that
11+
`wm.obj_export` writes the depsgraph-evaluated geometry (so modifiers are baked into the
12+
export) rather than the unmodified base mesh.
13+
14+
## Run
15+
16+
```bash
17+
# Cheap correctness check (writes an OBJ to a temp path, asserts the counts) — the CI check:
18+
blender --background --python depsgraph_export.py --
19+
20+
# Write the exported OBJ to a specific path:
21+
blender --background --python depsgraph_export.py -- --output remeshed.obj
22+
```
23+
24+
It exits non-zero on failure (modifier not applied, or exported count ≠ evaluated count). The
25+
`blender-smoke` workflow runs this check on Blender 4.5 LTS and 5.1: base 8 → evaluated/exported
26+
98 vertices with a 2-level SUBSURF.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""Candidate B: depsgraph-evaluated export (SCRATCH).
2+
3+
Witnesses the depsgraph lifetime contract AND that modifiers actually ship in exports. Builds
4+
a cube with a SUBSURF modifier, measures the evaluated mesh via evaluated_get().to_mesh()
5+
(paired with to_mesh_clear()), exports through wm.obj_export, and asserts the exported vertex
6+
count equals the EVALUATED (modifier-applied) count and is strictly greater than the base.
7+
"""
8+
import bpy, bmesh, sys, os, argparse
9+
10+
def main():
11+
argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
12+
p = argparse.ArgumentParser()
13+
p.add_argument("--output", default=None, help="optional: write the exported OBJ here (else a temp path)")
14+
args = p.parse_args(argv)
15+
16+
bpy.ops.wm.read_factory_settings(use_empty=True)
17+
me = bpy.data.meshes.new("Cube"); bm = bmesh.new(); bmesh.ops.create_cube(bm, size=2.0); bm.to_mesh(me); bm.free()
18+
obj = bpy.data.objects.new("Cube", me); bpy.context.collection.objects.link(obj)
19+
obj.modifiers.new("ss", 'SUBSURF').levels = 2
20+
base = len(obj.data.vertices)
21+
22+
# depsgraph lifetime contract: evaluate, read, then release with to_mesh_clear
23+
dg = bpy.context.evaluated_depsgraph_get()
24+
ev = obj.evaluated_get(dg)
25+
em = ev.to_mesh()
26+
eval_vcount = len(em.vertices)
27+
ev.to_mesh_clear() # must be paired; releases the temporary mesh
28+
29+
import tempfile
30+
out = args.output or os.path.join(tempfile.gettempdir(), "depsgraph_export.obj")
31+
os.makedirs(os.path.dirname(os.path.abspath(out)) or ".", exist_ok=True)
32+
# obj_export writes the evaluated (modifier-applied) geometry by default
33+
bpy.ops.wm.obj_export(filepath=out, export_selected_objects=False)
34+
if not (os.path.exists(out) and os.path.getsize(out) > 0):
35+
print("ERROR: no OBJ written", file=sys.stderr); return 4
36+
exported = 0
37+
with open(out) as f:
38+
for line in f:
39+
if line.startswith("v "): exported += 1
40+
41+
print(f"base_vcount={base} eval_vcount={eval_vcount} exported_vcount={exported}")
42+
if not (eval_vcount > base):
43+
print("ERROR: evaluated mesh did not apply the modifier", file=sys.stderr); return 3
44+
if exported != eval_vcount:
45+
print(f"ERROR: export ({exported}) != evaluated ({eval_vcount}); modifier did not ship",
46+
file=sys.stderr); return 5
47+
print("depsgraph-export OK")
48+
return 0
49+
50+
if __name__ == "__main__":
51+
try:
52+
sys.exit(main())
53+
except Exception as e:
54+
import traceback; traceback.print_exc(); print(f"FATAL: {e}", file=sys.stderr); sys.exit(1)

0 commit comments

Comments
 (0)