Skip to content

Commit 07bef23

Browse files
TMHSDigitalclaude
andcommitted
fix(examples): lean the bmesh-gear on a display wedge
The gear leaned 46 degrees back with nothing behind it, and its lowest tooth was sunk 4.5 cm into the floor. The gear is now seated from its lowest evaluated vertex (3 mm bite) and carried by a matte inclined wedge whose slope lies in the back-cap plane. The camera rides with the seat height. bmesh-gear is a pinned calibration reference: luma 0.305 -> 0.302, stage 0.237 -> 0.236, warmth +0.370 -> +0.374, saturation 0.195 -> 0.195. Render path only; exit codes are unchanged on all three binaries. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com>
1 parent 1812640 commit 07bef23

6 files changed

Lines changed: 121 additions & 9 deletions

File tree

10.3 KB
Loading

‎docs/gallery/bmesh-gear/index.html‎

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@
1616
<meta property="og:image" content="https://tmhsdigital.github.io/Blender-Developer-Tools/gallery/assets/bmesh-gear-hero.webp" />
1717
<meta property="og:image:width" content="1280" />
1818
<meta property="og:image:height" content="720" />
19-
<meta property="og:image:alt" content="A gold fourteen-tooth gear lying at an angle, its face covered in concentric grooves." />
19+
<meta property="og:image:alt" content="A gold fourteen-tooth gear leaning back on a dark display wedge, its face covered in concentric grooves." />
2020
<meta property="og:site_name" content="Blender Developer Tools" />
2121
<meta name="twitter:card" content="summary_large_image" />
2222
<meta name="twitter:title" content="bmesh-gear — Examples — Blender Developer Tools" />
2323
<meta name="twitter:description" content="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." />
2424
<meta name="twitter:image" content="https://tmhsdigital.github.io/Blender-Developer-Tools/gallery/assets/bmesh-gear-hero.webp" />
25-
<meta name="twitter:image:alt" content="A gold fourteen-tooth gear lying at an angle, its face covered in concentric grooves." />
25+
<meta name="twitter:image:alt" content="A gold fourteen-tooth gear leaning back on a dark display wedge, its face covered in concentric grooves." />
2626
<style>
2727
/* fonts are deployed by the landing build (docs/fonts/) */
2828
@font-face { font-family: 'Barlow Condensed'; font-weight: 600; font-display: swap;
@@ -268,8 +268,8 @@ <h1>bmesh-gear</h1>
268268
<p>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.</p>
269269
</header>
270270
<main id="main">
271-
<button class="detail-hero" id="heroZoom" type="button" aria-label="View full size: A gold fourteen-tooth gear lying at an angle, its face covered in concentric grooves.">
272-
<img src="../assets/bmesh-gear-hero.webp" alt="A gold fourteen-tooth gear lying at an angle, its face covered in concentric grooves." width="1280" height="720" />
271+
<button class="detail-hero" id="heroZoom" type="button" aria-label="View full size: A gold fourteen-tooth gear leaning back on a dark display wedge, its face covered in concentric grooves.">
272+
<img src="../assets/bmesh-gear-hero.webp" alt="A gold fourteen-tooth gear leaning back on a dark display wedge, its face covered in concentric grooves." width="1280" height="720" />
273273
</button>
274274
<p class="zoom-hint">Rendered headless by the example itself — click to zoom.</p>
275275
<div class="callout"><span class="tag">witnesses</span> Parametric bmesh topology is exactly predictable: verts, edges, and faces match their closed forms, and every edge borders exactly two faces (watertight).</div>
@@ -280,6 +280,7 @@ <h1>bmesh-gear</h1>
280280
<section class="detail-section md">
281281
<p>A runnable example that builds a 14-tooth gear entirely with bmesh — profile ring, face, <code>extrude_face_region</code>, translate — following the ownership contract from <a href="https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/skills/mesh-editing-and-bmesh/SKILL.md"><code>mesh-editing-and-bmesh</code></a> and the <a href="https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/rules/always-free-bmesh.mdc"><code>always-free-bmesh</code></a> rule: every <code>bmesh.new()</code> is paired with <code>bm.free()</code> in a <code>try</code>/<code>finally</code>.</p>
282282
<p><strong>What it witnesses:</strong> 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.</p>
283+
<p>The still leans the gear back on a matte inclined display wedge. Both contacts are derived from the posed mesh: the lowest back-cap vertex sits 3 mm into the floor, and the wedge&#x27;s slope lies in the back-cap plane, so the lean is carried rather than held in the air.</p>
283284
<h2>Run</h2>
284285
<pre><code># Cheap correctness check (no render) — the CI check:
285286
blender --background --python bmesh_gear.py --
@@ -322,6 +323,7 @@ <h2>Source</h2>
322323
blender --background --python bmesh_gear.py -- --output g.png # + render
323324
&quot;&quot;&quot;</span>
324325
<span class="k">import</span> bpy, bmesh, sys, os, math, argparse
326+
<span class="k">from</span> mathutils <span class="k">import</span> Vector
325327

326328
TEETH = <span class="n">14</span>
327329
R_ROOT = <span class="n">1.0</span>
@@ -330,6 +332,11 @@ <h2>Source</h2>
330332
<span class="c"># fraction of a tooth period spent at the tip vs the root</span>
331333
TOOTH_DUTY = <span class="n">0.45</span>
332334

335+
<span class="c"># render staging only (not part of the check)</span>
336+
STAGE_BITE = <span class="n">0.003</span> <span class="c"># contact depth into the floor and into the gear&#x27;s back</span>
337+
WEDGE_SLOPE = <span class="n">1.55</span> <span class="c"># display wedge slope length, up the gear&#x27;s back face</span>
338+
WEDGE_WIDTH = <span class="n">1.8</span> <span class="c"># display wedge width across the gear</span>
339+
333340

334341
<span class="k">def</span> gear_profile():
335342
<span class="s">&quot;&quot;&quot;Vertex ring for the gear silhouette: 4 verts per tooth (root-root-tip-tip).&quot;&quot;&quot;</span>
@@ -425,8 +432,53 @@ <h2>Source</h2>
425432
nt.links.new(rings.outputs[<span class="s">&quot;Fac&quot;</span>], rough.inputs[<span class="s">&quot;Value&quot;</span>])
426433
nt.links.new(rough.outputs[<span class="s">&quot;Result&quot;</span>], bsdf.inputs[<span class="s">&quot;Roughness&quot;</span>])
427434
obj.data.materials.append(mat)
428-
obj.location = (<span class="n">0.0</span>, <span class="n">0.0</span>, <span class="n">0.85</span>)
435+
<span class="c"># the gear leans back on an inclined display wedge. Both contacts are</span>
436+
<span class="c"># derived from the posed mesh: the lowest back-cap vertex bites into</span>
437+
<span class="c"># the floor, and the wedge&#x27;s slope lies in the back-cap plane, sunk into</span>
438+
<span class="c"># the gear, so the lean is carried rather than hung in the air</span>
429439
obj.rotation_euler = (math.radians(<span class="n">46</span>), <span class="n">0.0</span>, math.radians(<span class="n">22</span>))
440+
obj.location = (<span class="n">0.0</span>, <span class="n">0.0</span>, <span class="n">0.0</span>)
441+
bpy.context.view_layer.update()
442+
obj.location.z = -min((obj.matrix_world @ v.co).z <span class="k">for</span> v <span class="k">in</span> obj.data.vertices) - STAGE_BITE
443+
bpy.context.view_layer.update()
444+
mw = obj.matrix_world
445+
rot = mw.to_3x3()
446+
side = (rot @ Vector((<span class="n">1.0</span>, <span class="n">0.0</span>, <span class="n">0.0</span>))).normalized()
447+
up = (rot @ Vector((<span class="n">0.0</span>, <span class="n">1.0</span>, <span class="n">0.0</span>))).normalized()
448+
into = (rot @ Vector((<span class="n">0.0</span>, <span class="n">0.0</span>, <span class="n">1.0</span>))).normalized()
449+
low = min((mw @ v.co <span class="k">for</span> v <span class="k">in</span> obj.data.vertices), key=<span class="k">lambda</span> p: p.z)
450+
centre = mw @ Vector((<span class="n">0.0</span>, <span class="n">0.0</span>, <span class="n">0.0</span>))
451+
foot = low + side * (centre - low).dot(side) + into * STAGE_BITE
452+
top = foot + up * WEDGE_SLOPE
453+
back = Vector((top.x, top.y, -STAGE_BITE))
454+
front = Vector((foot.x, foot.y, -STAGE_BITE))
455+
wedge_me = bpy.data.meshes.new(<span class="s">&quot;Wedge&quot;</span>)
456+
bm = bmesh.new()
457+
<span class="k">try</span>:
458+
half = side * (WEDGE_WIDTH / <span class="n">2</span>)
459+
ring = [[bm.verts.new(p + sgn * half) <span class="k">for</span> p <span class="k">in</span> (front, foot, top, back)]
460+
<span class="k">for</span> sgn <span class="k">in</span> (-<span class="n">1.0</span>, <span class="n">1.0</span>)]
461+
a, b = ring
462+
bm.faces.new(a)
463+
bm.faces.new(b[::-<span class="n">1</span>])
464+
<span class="k">for</span> i <span class="k">in</span> range(<span class="n">4</span>):
465+
j = (i + <span class="n">1</span>) % <span class="n">4</span>
466+
bm.faces.new((a[i], a[j], b[j], b[i]))
467+
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
468+
bm.to_mesh(wedge_me)
469+
<span class="k">finally</span>:
470+
bm.free()
471+
wmat = bpy.data.materials.new(<span class="s">&quot;DisplayStand&quot;</span>)
472+
wmat.use_nodes = <span class="k">True</span>
473+
wb = wmat.node_tree.nodes[<span class="s">&quot;Principled BSDF&quot;</span>]
474+
<span class="c"># matte and dark: the slope faces the key square-on, and at the floor&#x27;s</span>
475+
<span class="c"># finish its specular sheen alone (~45/255, measured) reads as paper</span>
476+
wb.inputs[<span class="s">&quot;Base Color&quot;</span>].default_value = (<span class="n">0.02</span>, <span class="n">0.021</span>, <span class="n">0.025</span>, <span class="n">1.0</span>)
477+
wb.inputs[<span class="s">&quot;Roughness&quot;</span>].default_value = <span class="n">0.9</span>
478+
spec = wb.inputs.get(<span class="s">&quot;Specular IOR Level&quot;</span>) <span class="k">or</span> wb.inputs.get(<span class="s">&quot;Specular&quot;</span>)
479+
spec.default_value = <span class="n">0.1</span>
480+
wedge_me.materials.append(wmat)
481+
scene.collection.objects.link(bpy.data.objects.new(<span class="s">&quot;Wedge&quot;</span>, wedge_me))
430482

431483
floor_me = bpy.data.meshes.new(<span class="s">&quot;Floor&quot;</span>)
432484
bm = bmesh.new()
@@ -473,7 +525,9 @@ <h2>Source</h2>
473525
cam_data = bpy.data.cameras.new(<span class="s">&quot;Cam&quot;</span>)
474526
cam_data.lens = <span class="n">55.0</span>
475527
cam = bpy.data.objects.new(<span class="s">&quot;Cam&quot;</span>, cam_data)
476-
cam.location = (<span class="n">0.0</span>, -<span class="n">7.6</span>, <span class="n">4.2</span>)
528+
<span class="c"># the camera rides with the gear&#x27;s derived seat height (it was posed for</span>
529+
<span class="c"># a gear hung at z=0.85), so the composition is unchanged</span>
530+
cam.location = (<span class="n">0.0</span>, -<span class="n">7.6</span>, <span class="n">4.2</span> + obj.location.z - <span class="n">0.85</span>)
477531
cam.rotation_euler = (math.radians(<span class="n">66</span>), <span class="n">0.0</span>, <span class="n">0.0</span>)
478532
scene.collection.objects.link(cam)
479533
scene.camera = cam
@@ -531,7 +585,7 @@ <h2>Source</h2>
531585
</main>
532586
<dialog class="lightbox" id="lightbox" aria-label="Full-size render">
533587
<button class="lightbox-close" type="button" autofocus>Close</button>
534-
<img src="../assets/bmesh-gear-hero.webp" alt="A gold fourteen-tooth gear lying at an angle, its face covered in concentric grooves." />
588+
<img src="../assets/bmesh-gear-hero.webp" alt="A gold fourteen-tooth gear leaning back on a dark display wedge, its face covered in concentric grooves." />
535589
</dialog>
536590
<footer>
537591
<div class="statusbar">
-523 KB
Loading

‎examples/bmesh-gear/README.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ check asserts the closed-form counts — verts = 2 × (4 × teeth), faces = side
1111
edges = 3 × profile — and that the result is watertight (every edge borders exactly two
1212
faces). If an op leaks geometry or a face fails to close, the math catches it.
1313

14+
The still leans the gear back on a matte inclined display wedge. Both
15+
contacts are derived from the posed mesh: the lowest back-cap vertex sits 3 mm
16+
into the floor, and the wedge's slope lies in the back-cap plane, so the lean
17+
is carried rather than held in the air.
18+
1419
## Run
1520

1621
```bash

‎examples/bmesh-gear/bmesh_gear.py‎

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
blender --background --python bmesh_gear.py -- --output g.png # + render
2020
"""
2121
import bpy, bmesh, sys, os, math, argparse
22+
from mathutils import Vector
2223

2324
TEETH = 14
2425
R_ROOT = 1.0
@@ -27,6 +28,11 @@
2728
# fraction of a tooth period spent at the tip vs the root
2829
TOOTH_DUTY = 0.45
2930

31+
# render staging only (not part of the check)
32+
STAGE_BITE = 0.003 # contact depth into the floor and into the gear's back
33+
WEDGE_SLOPE = 1.55 # display wedge slope length, up the gear's back face
34+
WEDGE_WIDTH = 1.8 # display wedge width across the gear
35+
3036

3137
def gear_profile():
3238
"""Vertex ring for the gear silhouette: 4 verts per tooth (root-root-tip-tip)."""
@@ -122,8 +128,53 @@ def render_still(obj, path, engine):
122128
nt.links.new(rings.outputs["Fac"], rough.inputs["Value"])
123129
nt.links.new(rough.outputs["Result"], bsdf.inputs["Roughness"])
124130
obj.data.materials.append(mat)
125-
obj.location = (0.0, 0.0, 0.85)
131+
# the gear leans back on an inclined display wedge. Both contacts are
132+
# derived from the posed mesh: the lowest back-cap vertex bites into
133+
# the floor, and the wedge's slope lies in the back-cap plane, sunk into
134+
# the gear, so the lean is carried rather than hung in the air
126135
obj.rotation_euler = (math.radians(46), 0.0, math.radians(22))
136+
obj.location = (0.0, 0.0, 0.0)
137+
bpy.context.view_layer.update()
138+
obj.location.z = -min((obj.matrix_world @ v.co).z for v in obj.data.vertices) - STAGE_BITE
139+
bpy.context.view_layer.update()
140+
mw = obj.matrix_world
141+
rot = mw.to_3x3()
142+
side = (rot @ Vector((1.0, 0.0, 0.0))).normalized()
143+
up = (rot @ Vector((0.0, 1.0, 0.0))).normalized()
144+
into = (rot @ Vector((0.0, 0.0, 1.0))).normalized()
145+
low = min((mw @ v.co for v in obj.data.vertices), key=lambda p: p.z)
146+
centre = mw @ Vector((0.0, 0.0, 0.0))
147+
foot = low + side * (centre - low).dot(side) + into * STAGE_BITE
148+
top = foot + up * WEDGE_SLOPE
149+
back = Vector((top.x, top.y, -STAGE_BITE))
150+
front = Vector((foot.x, foot.y, -STAGE_BITE))
151+
wedge_me = bpy.data.meshes.new("Wedge")
152+
bm = bmesh.new()
153+
try:
154+
half = side * (WEDGE_WIDTH / 2)
155+
ring = [[bm.verts.new(p + sgn * half) for p in (front, foot, top, back)]
156+
for sgn in (-1.0, 1.0)]
157+
a, b = ring
158+
bm.faces.new(a)
159+
bm.faces.new(b[::-1])
160+
for i in range(4):
161+
j = (i + 1) % 4
162+
bm.faces.new((a[i], a[j], b[j], b[i]))
163+
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
164+
bm.to_mesh(wedge_me)
165+
finally:
166+
bm.free()
167+
wmat = bpy.data.materials.new("DisplayStand")
168+
wmat.use_nodes = True
169+
wb = wmat.node_tree.nodes["Principled BSDF"]
170+
# matte and dark: the slope faces the key square-on, and at the floor's
171+
# finish its specular sheen alone (~45/255, measured) reads as paper
172+
wb.inputs["Base Color"].default_value = (0.02, 0.021, 0.025, 1.0)
173+
wb.inputs["Roughness"].default_value = 0.9
174+
spec = wb.inputs.get("Specular IOR Level") or wb.inputs.get("Specular")
175+
spec.default_value = 0.1
176+
wedge_me.materials.append(wmat)
177+
scene.collection.objects.link(bpy.data.objects.new("Wedge", wedge_me))
127178

128179
floor_me = bpy.data.meshes.new("Floor")
129180
bm = bmesh.new()
@@ -170,7 +221,9 @@ def light(name, loc, energy, size, col, rot):
170221
cam_data = bpy.data.cameras.new("Cam")
171222
cam_data.lens = 55.0
172223
cam = bpy.data.objects.new("Cam", cam_data)
173-
cam.location = (0.0, -7.6, 4.2)
224+
# the camera rides with the gear's derived seat height (it was posed for
225+
# a gear hung at z=0.85), so the composition is unchanged
226+
cam.location = (0.0, -7.6, 4.2 + obj.location.z - 0.85)
174227
cam.rotation_euler = (math.radians(66), 0.0, 0.0)
175228
scene.collection.objects.link(cam)
176229
scene.camera = cam

‎examples/bmesh-gear/preview.webp‎

10.6 KB
Loading

0 commit comments

Comments
 (0)