From 5346fb65dfdf34081a46d8fe1b47269027d82588 Mon Sep 17 00:00:00 2001 From: fOuttaMyPaint Date: Fri, 3 Jul 2026 17:33:15 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20two=20more=20smoke-gated=20examples=20?= =?UTF-8?q?=E2=80=94=20bmesh-gear=20and=20shader-node-group?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bmesh-gear witnesses the bmesh ownership contract as its subject: a 14-tooth parametric gear (profile ring, face, extrude_face_region) with bm.free() in a try/finally. Because construction is parametric the check asserts closed-form topology — verts 2x(4xteeth), faces sides+2, edges 3xprofile — and watertightness (every edge borders exactly two faces). shader-node-group witnesses the node-group contract: a TintedGloss group declared via tree.interface.new_socket (the 4.x/5.x interface API), instanced in two materials. Asserts the interface sockets exist, the group datablock is shared (users == 2), and the per-instance Tint values differ — the render is the proof: two spheres, one group, two colors (teal and magenta, extending the gallery's one-accent-per-render system; the gear is machined steel). Both verified headless on Blender 5.1.1 and 4.4.3 and wired into the smoke workflow for 4.5 LTS + 5.1, with READMEs, hero/preview renders from each example's own --output path, gallery + manifest + README rows. Gallery grows to 8. Co-Authored-By: Claude Fable 5 --- .cursor-plugin/plugin.json | 2 + .github/workflows/blender-smoke.yml | 19 + README.md | 28 ++ docs/gallery/assets/bmesh-gear-hero.webp | Bin 0 -> 12828 bytes .../assets/shader-node-group-hero.webp | Bin 0 -> 17596 bytes docs/gallery/bmesh-gear/index.html | 438 +++++++++++++++++ docs/gallery/index.html | 24 + docs/gallery/shader-node-group/index.html | 447 ++++++++++++++++++ examples/bmesh-gear/README.md | 26 + examples/bmesh-gear/bmesh_gear.py | 196 ++++++++ examples/bmesh-gear/preview.webp | Bin 0 -> 11136 bytes examples/gallery.json | 18 + examples/shader-node-group/README.md | 27 ++ examples/shader-node-group/preview.webp | Bin 0 -> 15414 bytes .../shader-node-group/shader_node_group.py | 205 ++++++++ 15 files changed, 1430 insertions(+) create mode 100644 docs/gallery/assets/bmesh-gear-hero.webp create mode 100644 docs/gallery/assets/shader-node-group-hero.webp create mode 100644 docs/gallery/bmesh-gear/index.html create mode 100644 docs/gallery/shader-node-group/index.html create mode 100644 examples/bmesh-gear/README.md create mode 100644 examples/bmesh-gear/bmesh_gear.py create mode 100644 examples/bmesh-gear/preview.webp create mode 100644 examples/shader-node-group/README.md create mode 100644 examples/shader-node-group/preview.webp create mode 100644 examples/shader-node-group/shader_node_group.py diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index d6240f9..79ed2c0 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -59,9 +59,11 @@ "templates/headless-batch-script-template" ], "examples": [ + "examples/bmesh-gear", "examples/depsgraph-export", "examples/driver-wave", "examples/gn-sdf-remesh", + "examples/shader-node-group", "examples/swatch-grid", "examples/turntable", "examples/wave-displace" diff --git a/.github/workflows/blender-smoke.yml b/.github/workflows/blender-smoke.yml index 7e78cb8..daf04a8 100644 --- a/.github/workflows/blender-smoke.yml +++ b/.github/workflows/blender-smoke.yml @@ -175,3 +175,22 @@ jobs: # on failure. xvfb-run -a "$BLENDER" --background \ --python examples/driver-wave/driver_wave.py -- + + - name: Shipped example - bmesh gear (ownership + watertight topology) + run: | + set -euo pipefail + # Frame-independent check only (no render): parametric gear via bmesh with + # bm.free() in try/finally; asserts closed-form vert/edge/face counts and + # that every edge borders exactly two faces. Exits non-zero on failure. + xvfb-run -a "$BLENDER" --background \ + --python examples/bmesh-gear/bmesh_gear.py -- + + - name: Shipped example - shader node group (interface sockets + sharing) + run: | + set -euo pipefail + # Frame-independent check only (no render): TintedGloss group declared via + # tree.interface.new_socket, instanced by two materials; asserts the sockets + # exist, the group datablock is shared (users == 2), and the per-instance + # Tint values differ. Exits non-zero on failure. + xvfb-run -a "$BLENDER" --background \ + --python examples/shader-node-group/shader_node_group.py -- diff --git a/README.md b/README.md index c3d1ce9..67f1f87 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,34 @@ A `driver_namespace` function driving sixteen column heights through SCRIPTED dr Witnesses the evaluation contract: driven values appear after a view-layer update on the evaluated copy **and** the flushed-back original, and both must match the closed form. + + + + +Bmesh gear: a machined steel 14-tooth gear at a three-quarter angle on a dark studio floor + + + +### [bmesh-gear](examples/bmesh-gear/) + +A 14-tooth gear built entirely with bmesh — with `bm.free()` in a `try`/`finally`, as the +ownership contract demands. Asserts the closed-form vert/edge/face counts and that the +result is watertight (every edge borders exactly two faces). + + + + + +Shader node group: a teal sphere and a magenta sphere sharing one TintedGloss node group with different Tint parameters + + + +### [shader-node-group](examples/shader-node-group/) + +One reusable `TintedGloss` group declared via `tree.interface.new_socket`, instanced in two +materials with different Tint values. Witnesses the grouping contract: shared datablock +(`users == 2`), parameters on the group **node** — two spheres, one group, two colors. + diff --git a/docs/gallery/assets/bmesh-gear-hero.webp b/docs/gallery/assets/bmesh-gear-hero.webp new file mode 100644 index 0000000000000000000000000000000000000000..3550b1c370d72bfa6fc7a2f099ed5323f779deba GIT binary patch literal 12828 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 literal 0 HcmV?d00001 diff --git a/docs/gallery/assets/shader-node-group-hero.webp b/docs/gallery/assets/shader-node-group-hero.webp new file mode 100644 index 0000000000000000000000000000000000000000..c6b802c6324cc5233794fabd78c07396b09b440c GIT binary patch literal 17596 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 literal 0 HcmV?d00001 diff --git a/docs/gallery/bmesh-gear/index.html b/docs/gallery/bmesh-gear/index.html new file mode 100644 index 0000000..028cc4a --- /dev/null +++ b/docs/gallery/bmesh-gear/index.html @@ -0,0 +1,438 @@ + + + + + + bmesh-gear — Examples — Blender Developer Tools + + + + + + + + + + + + + + + + + + +
+

bmesh-gear

+

A 14-tooth gear built entirely with bmesh — profile ring, face, extrude — with bm.free() in a try/finally, exactly as the ownership contract demands.

+
+
+ +

Rendered headless by the example itself — click to zoom.

+
witnesses Parametric bmesh topology is exactly predictable: verts, edges, and faces match their closed forms, and every edge borders exactly two faces (watertight).
+
+
blender --background --python examples/bmesh-gear/bmesh_gear.py --
+ +
+
+

A runnable example that builds a 14-tooth gear entirely with bmesh — profile ring, face, extrude_face_region, translate — following the ownership contract from mesh-editing-and-bmesh and the always-free-bmesh rule: every bmesh.new() is paired with bm.free() in a try/finally.

+

What it witnesses: parametric bmesh construction has exactly predictable topology. The check asserts the closed-form counts — verts = 2 × (4 × teeth), faces = sides + 2 caps, edges = 3 × profile — and that the result is watertight (every edge borders exactly two faces). If an op leaks geometry or a face fails to close, the math catches it.

+

Run

+
# Cheap correctness check (no render) — the CI check:
+blender --background --python bmesh_gear.py --
+
+# 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 4.5 LTS and 5.1.

+
+
+

Source

+
+ examples/bmesh-gear/bmesh_gear.py + View on GitHub → +
+
"""A parametric gear built entirely with bmesh — a runnable example.
+
+Witnesses the bmesh ownership contract from mesh-editing-and-bmesh and the
+always-free-bmesh rule: every `bmesh.new()` is paired with `bm.free()` in a
+`try`/`finally`, and because the construction is parametric the resulting
+topology is exactly predictable. The check asserts the closed-form counts —
+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).
+
+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 -- --output g.png  # + render
+"""
+import bpy, bmesh, sys, os, math, argparse
+
+TEETH = 14
+R_ROOT = 1.0
+R_TIP = 1.25
+DEPTH = 0.6
+# fraction of a tooth period spent at the tip vs the root
+TOOTH_DUTY = 0.45
+
+
+def gear_profile():
+    """Vertex ring for the gear silhouette: 4 verts per tooth (root-root-tip-tip)."""
+    coords = []
+    step = 2 * math.pi / TEETH
+    for i in range(TEETH):
+        a0 = i * step
+        half = step * TOOTH_DUTY / 2
+        flank = step * (0.5 - TOOTH_DUTY / 2) / 2
+        mid = a0 + step / 2
+        coords.append((a0 + flank, R_ROOT))
+        coords.append((mid - half, R_TIP))
+        coords.append((mid + half, R_TIP))
+        coords.append((a0 + step - flank, R_ROOT))
+    return [(r * math.cos(a), r * math.sin(a), 0.0) for a, r in coords]
+
+
+def build_gear():
+    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))
+        bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
+        bm.to_mesh(me)
+    finally:
+        bm.free()  # the contract this example witnesses
+    obj = bpy.data.objects.new("Gear", me)
+    bpy.context.collection.objects.link(obj)
+    return obj
+
+
+def check(obj):
+    me = obj.data
+    profile = 4 * TEETH
+    expect_v = 2 * profile          # bottom ring + extruded top ring
+    expect_f = profile + 2          # side quads + two caps
+    expect_e = 3 * profile          # two rings + verticals
+    got = (len(me.vertices), len(me.edges), len(me.polygons))
+    if got != (expect_v, expect_e, expect_f):
+        print(f"ERROR: topology {got} != expected {(expect_v, expect_e, expect_f)}",
+              file=sys.stderr)
+        return 3
+
+    # watertight: every edge borders exactly two faces
+    bm = bmesh.new()
+    try:
+        bm.from_mesh(me)
+        bad = sum(1 for e in bm.edges if len(e.link_faces) != 2)
+    finally:
+        bm.free()
+    if bad:
+        print(f"ERROR: {bad} non-manifold edge(s) — gear is not watertight", file=sys.stderr)
+        return 4
+
+    print(f"teeth={TEETH} verts={got[0]} edges={got[1]} faces={got[2]} watertight=True")
+    return 0
+
+
+def eevee_engine_id():
+    return 'BLENDER_EEVEE' if bpy.app.version >= (5, 0, 0) else 'BLENDER_EEVEE_NEXT'
+
+
+def render_still(obj, path, engine):
+    scene = bpy.context.scene
+    for poly in obj.data.polygons:
+        poly.use_smooth = False  # crisp machined facets
+    mat = bpy.data.materials.new("Steel")
+    mat.use_nodes = True
+    bsdf = mat.node_tree.nodes["Principled BSDF"]
+    bsdf.inputs["Base Color"].default_value = (0.75, 0.77, 0.8, 1.0)
+    bsdf.inputs["Metallic"].default_value = 1.0
+    bsdf.inputs["Roughness"].default_value = 0.45
+    obj.data.materials.append(mat)
+    obj.location = (0.0, 0.0, 0.85)
+    obj.rotation_euler = (math.radians(38), 0.0, math.radians(22))
+
+    floor_me = bpy.data.meshes.new("Floor")
+    bm = bmesh.new()
+    try:
+        bmesh.ops.create_grid(bm, x_segments=1, y_segments=1, size=30.0)
+        bm.to_mesh(floor_me)
+    finally:
+        bm.free()
+    fmat = bpy.data.materials.new("Studio")
+    fmat.use_nodes = True
+    fb = fmat.node_tree.nodes["Principled BSDF"]
+    fb.inputs["Base Color"].default_value = (0.055, 0.06, 0.07, 1.0)
+    fb.inputs["Roughness"].default_value = 0.5
+    floor_me.materials.append(fmat)
+    floor = bpy.data.objects.new("Floor", floor_me)
+    scene.collection.objects.link(floor)
+    wall = bpy.data.objects.new("Wall", floor_me.copy())
+    wall.location = (0.0, 9.0, 0.0)
+    wall.rotation_euler = (math.radians(90), 0.0, 0.0)
+    scene.collection.objects.link(wall)
+
+    world = bpy.data.worlds.new("World")
+    world.use_nodes = True
+    # metals reflect the environment: keep a faint cool ambient so flanks never go black
+    world.node_tree.nodes["Background"].inputs["Color"].default_value = (0.035, 0.04, 0.05, 1.0)
+    scene.world = world
+
+    def light(name, loc, energy, size, col, rot):
+        ld = bpy.data.lights.new(name, 'AREA')
+        ld.energy = energy; ld.size = size; ld.color = col
+        ob = bpy.data.objects.new(name, ld)
+        ob.location = loc
+        ob.rotation_euler = tuple(math.radians(a) for a in rot)
+        scene.collection.objects.link(ob)
+
+    # metals live on reflections: big soft key, strong cool fill, warm rim kept low
+    light("Key", (-3.5, -4.5, 5.5), 1400.0, 7.0, (1.0, 0.98, 0.94), (48, 0, -35))
+    light("Fill", (5.0, -3.5, 2.5), 600.0, 9.0, (0.8, 0.87, 1.0), (65, 0, 50))
+    light("Rim", (1.5, 4.5, 2.2), 700.0, 4.0, (1.0, 0.7, 0.4), (-82, 0, 165))
+
+    cam_data = bpy.data.cameras.new("Cam")
+    cam_data.lens = 55.0
+    cam = bpy.data.objects.new("Cam", cam_data)
+    cam.location = (0.0, -7.6, 4.2)
+    cam.rotation_euler = (math.radians(66), 0.0, 0.0)
+    scene.collection.objects.link(cam)
+    scene.camera = cam
+
+    scene.render.engine = 'CYCLES' if engine == 'cycles' else eevee_engine_id()
+    if engine == 'cycles':
+        scene.cycles.samples = 32
+    else:
+        try:
+            scene.eevee.taa_render_samples = 64
+        except AttributeError:
+            pass
+    scene.render.resolution_x = 1280
+    scene.render.resolution_y = 720
+    scene.render.image_settings.file_format = 'PNG'
+    scene.render.filepath = path
+    bpy.ops.render.render(write_still=True)
+    return os.path.exists(path) and os.path.getsize(path) > 0
+
+
+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: render a still PNG here")
+    p.add_argument("--engine", default="eevee", choices=("eevee", "cycles"),
+                   help="render engine for --output (cycles for GPU-less hosts)")
+    args = p.parse_args(argv)
+
+    obj = build_gear()
+    code = check(obj)
+    if code:
+        return code
+
+    if args.output:
+        if not render_still(obj, os.path.abspath(args.output), args.engine):
+            print("ERROR: render produced no file", file=sys.stderr)
+            return 6
+        print(f"rendered still {args.output}")
+
+    print("bmesh-gear 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)
+
+
+
+ +
+
+ generated from examples/gallery.json + CC-BY-NC-ND-4.0 + exit 0 +
+
+ + + diff --git a/docs/gallery/index.html b/docs/gallery/index.html index 5d4b39e..f171e6c 100644 --- a/docs/gallery/index.html +++ b/docs/gallery/index.html @@ -175,12 +175,14 @@

Examples Gallery

@@ -252,6 +254,28 @@

driver-wave

View example +
+ + bmesh-gear — A 14-tooth gear built entirely with bmesh — profile ring, face, extrude — with bm + +
+

bmesh-gear

+

A 14-tooth gear built entirely with bmesh — profile ring, face, extrude — with bm.free() in a try/finally, exactly as the ownership contract demands.

+

witnesses Parametric bmesh topology is exactly predictable: verts, edges, and faces match their closed forms, and every edge borders exactly two faces (watertight).

+ View example +
+
+
+ + shader-node-group — One reusable shader group declared via tree + +
+

shader-node-group

+

One reusable shader group declared via tree.interface.new_socket, instanced in two materials with different Tint values — two spheres, one group, two colors.

+

witnesses Grouping contract: interface sockets appear on every instance, both materials share one group datablock (users == 2), and per-material parameters live on the group node, not inside the tree.

+ View example +
+