Skip to content

Commit 5ef7e2e

Browse files
TMHSDigitalclaude
andcommitted
fix(examples): solidify-even-thickness quality pass: measured full-thickness line, glazed shells, legible plaques
- A red line on the plain shell's cut face marks where a full-thickness inner face would run. It is placed from the even shell's evaluated copy positions, so the visible gap is the real thinning: 0.08 of 0.16 at the sharp V. - Teal-glazed shells, orange enamel cut faces and polished brass. - A walnut plinth with real grain and a chamfer, larger crisp engraving on the plaques, and a camera that looks almost straight at the cut ends. - Checks are unchanged: the default passes on 5.2.1, 4.5.11 and 5.1.2, and --no-even exits 4 on 5.2.1 and 4.5.11. - Contact sheet: luma 0.271, every verdict passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 1987df5 commit 5ef7e2e

7 files changed

Lines changed: 381 additions & 50 deletions

File tree

786 Bytes
Loading
3.78 KB
Loading
1.45 KB
Loading

‎docs/gallery/solidify-even-thickness/index.html‎

Lines changed: 245 additions & 27 deletions
Large diffs are not rendered by default.

‎examples/solidify-even-thickness/README.md‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,16 @@ thinner at every corner, down to half at a 120° bend, and nothing errors. `--no
3434
leaves the flag off on the shell that the check expects to be even, and check 4 fails
3535
with the measured thickness.
3636

37-
The still shows both shells side by side on a walnut plinth, viewed end-on. The cut
38-
section, Solidify's rim, is drawn in selection orange through `material_offset_rim`, so
39-
the band's width is the shell's thickness. On the left, with `use_even_offset = False`,
40-
the band pinches at every fold, and to half at the sharp V. On the right, with
41-
`use_even_offset = True`, it stays one width all the way round. Brass plaques name each
42-
setting, and brass dowels hold the raised folds off the plinth.
37+
The still shows both shells in teal glaze side by side on a walnut plinth, viewed nearly
38+
end-on. The cut section, Solidify's rim, is drawn in selection-orange enamel through
39+
`material_offset_rim`, so the band's width is the shell's thickness. On the left, with
40+
`use_even_offset = False`, the band pinches at every fold, and to half at the sharp V. A
41+
red line on that cut face traces where a *t*-thick shell's inner face would run: it is
42+
the even shell's own evaluated copy positions, placed through the plain shell's
43+
transform, so the gap between the orange band and the red line is the thickness lost at
44+
each fold, drawn at true size. On the right, with `use_even_offset = True`, the band stays
45+
one width all the way round. Engraved brass plaques name each setting, and brass dowels
46+
hold the raised folds off the plinth.
4347

4448
## Run
4549

3.43 KB
Loading

‎examples/solidify-even-thickness/solidify_even_thickness.py‎

Lines changed: 126 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,87 @@ def principled(name, base, rough, metal=0.0, noise=None, coat=0.0, emit=None):
217217
return mat
218218

219219

220+
def glaze(name, dark, light):
221+
"""Glazed ceramic: colour pooling between two tones on low-frequency noise,
222+
a clear coat, and a faint roughness ripple so highlights break up."""
223+
mat = bpy.data.materials.new(name)
224+
mat.use_nodes = True
225+
nt = mat.node_tree
226+
b = nt.nodes["Principled BSDF"]
227+
b.inputs["Roughness"].default_value = 0.22
228+
b.inputs["Coat Weight"].default_value = 0.8
229+
b.inputs["Coat Roughness"].default_value = 0.06
230+
tex = nt.nodes.new("ShaderNodeTexNoise")
231+
tex.inputs["Scale"].default_value = 3.5
232+
tex.inputs["Detail"].default_value = 4.0
233+
ramp = nt.nodes.new("ShaderNodeValToRGB")
234+
ramp.color_ramp.elements[0].position = 0.35
235+
ramp.color_ramp.elements[0].color = (*dark, 1.0)
236+
ramp.color_ramp.elements[1].position = 0.7
237+
ramp.color_ramp.elements[1].color = (*light, 1.0)
238+
nt.links.new(tex.outputs["Fac"], ramp.inputs["Fac"])
239+
nt.links.new(ramp.outputs["Color"], b.inputs["Base Color"])
240+
return mat
241+
242+
243+
def walnut_mat(name):
244+
"""Oiled walnut: banded grain from a distorted wave texture along X,
245+
two-tone ramp, satin roughness (not a mirror)."""
246+
mat = bpy.data.materials.new(name)
247+
mat.use_nodes = True
248+
nt = mat.node_tree
249+
b = nt.nodes["Principled BSDF"]
250+
b.inputs["Coat Weight"].default_value = 0.15
251+
b.inputs["Coat Roughness"].default_value = 0.3
252+
# Object-space coordinates turned 45 degrees about X, so the bands vary
253+
# across both the top and the front face while running along X (the
254+
# plinth's length), the way long-grain stock looks.
255+
coord = nt.nodes.new("ShaderNodeTexCoord")
256+
mapping = nt.nodes.new("ShaderNodeMapping")
257+
mapping.inputs["Rotation"].default_value = (math.radians(45.0), 0.0, 0.0)
258+
mapping.inputs["Scale"].default_value = (0.35, 1.0, 1.0)
259+
nt.links.new(coord.outputs["Object"], mapping.inputs["Vector"])
260+
wave = nt.nodes.new("ShaderNodeTexWave")
261+
wave.wave_type = 'BANDS'
262+
wave.bands_direction = 'Y'
263+
wave.inputs["Scale"].default_value = 7.0
264+
wave.inputs["Distortion"].default_value = 3.5
265+
wave.inputs["Detail"].default_value = 4.0
266+
wave.inputs["Detail Scale"].default_value = 2.0
267+
nt.links.new(mapping.outputs["Vector"], wave.inputs["Vector"])
268+
ramp = nt.nodes.new("ShaderNodeValToRGB")
269+
ramp.color_ramp.elements[0].color = (0.045, 0.019, 0.009, 1.0)
270+
ramp.color_ramp.elements[1].color = (0.15, 0.072, 0.034, 1.0)
271+
nt.links.new(wave.outputs["Fac"], ramp.inputs["Fac"])
272+
nt.links.new(ramp.outputs["Color"], b.inputs["Base Color"])
273+
mr = nt.nodes.new("ShaderNodeMapRange")
274+
mr.inputs["To Min"].default_value = 0.42
275+
mr.inputs["To Max"].default_value = 0.58
276+
nt.links.new(wave.outputs["Fac"], mr.inputs["Value"])
277+
nt.links.new(mr.outputs["Result"], b.inputs["Roughness"])
278+
return mat
279+
280+
281+
def bar_mesh(name, a, b, half_w, half_d, normal):
282+
"""A slim box from point a to point b, half_w wide across the bar in the
283+
plane, half_d thick along *normal*."""
284+
axis = (b - a)
285+
side = axis.cross(normal).normalized() * half_w
286+
nrm = normal.normalized() * half_d
287+
me = bpy.data.meshes.new(name)
288+
bm = bmesh.new()
289+
try:
290+
vs = [bm.verts.new(p + s * side + d * nrm)
291+
for p in (a, b) for s in (-1, 1) for d in (-1, 1)]
292+
for f in ((0, 1, 3, 2), (4, 6, 7, 5), (0, 4, 5, 1), (2, 3, 7, 6), (0, 2, 6, 4), (1, 5, 7, 3)):
293+
bm.faces.new([vs[i] for i in f])
294+
bmesh.ops.recalc_face_normals(bm, faces=bm.faces[:])
295+
bm.to_mesh(me)
296+
finally:
297+
bm.free()
298+
return me
299+
300+
220301
def box_mesh(name, lo, hi):
221302
me = bpy.data.meshes.new(name)
222303
bm = bmesh.new()
@@ -233,12 +314,12 @@ def box_mesh(name, lo, hi):
233314
def render_still(even_obj, plain_obj, path, engine):
234315
scene = bpy.context.scene
235316

236-
# Sheet faces in a pale enamel, the cut section (Solidify's rim) in
237-
# selection orange via material_offset_rim, so the end of each shell
238-
# reads as a band whose width is the thickness.
239-
sheet = principled("SheetEnamel", (0.50, 0.56, 0.62), 0.38, metal=0.15, coat=0.3)
240-
section = principled("CutSection", (1.0, 0.42, 0.04), 0.35,
241-
emit=((1.0, 0.45, 0.06), 0.6))
317+
# Sheet faces in a deep teal glaze, the cut section (Solidify's rim) in
318+
# selection-orange enamel via material_offset_rim, so the end of each
319+
# shell reads as a bright band whose width is the thickness.
320+
sheet = glaze("TealGlaze", (0.025, 0.16, 0.19), (0.05, 0.25, 0.28))
321+
section = principled("CutSection", (1.0, 0.40, 0.03), 0.3, coat=0.5,
322+
emit=((1.0, 0.42, 0.05), 0.35))
242323
for obj in (even_obj, plain_obj):
243324
obj.data.materials.append(sheet)
244325
obj.data.materials.append(section)
@@ -247,17 +328,24 @@ def render_still(even_obj, plain_obj, path, engine):
247328
mod.material_offset_rim = 1
248329

249330
# Stand each shell on the plinth: lowest evaluated point at the plinth top
250-
plinth_top = 0.24
331+
plinth_top = 0.30
251332
plain_obj.location = (-SHELL_GAP, 0.0, 0.0)
252333
bpy.context.view_layer.update()
334+
even_local = evaluated_local_coords(even_obj)
253335
for obj in (even_obj, plain_obj):
254336
lo = min((obj.matrix_world @ c).z for c in evaluated_local_coords(obj))
337+
if obj is plain_obj:
338+
# the red nominal-thickness line drawn on this shell reaches the
339+
# even shell's deeper copies; keep it above the plinth too
340+
lo = min(lo, min((obj.matrix_world @ c).z for c in even_local))
255341
obj.location.z += plinth_top - lo
256342
bpy.context.view_layer.update()
257343

258-
walnut = principled("Walnut", (0.13, 0.055, 0.025), 0.45, noise=40.0, coat=0.4)
259-
brass = principled("Brass", (0.80, 0.58, 0.26), 0.3, metal=1.0)
344+
walnut = walnut_mat("Walnut")
345+
brass = principled("Brass", (0.86, 0.62, 0.27), 0.2, metal=1.0, noise=60.0)
260346
ink = principled("Engraving", (0.012, 0.010, 0.008), 0.7)
347+
deficit = principled("Deficit", (0.85, 0.04, 0.05), 0.3, coat=0.5,
348+
emit=((0.95, 0.05, 0.05), 0.5))
261349
parts = []
262350
pts = profile_points()
263351

@@ -283,7 +371,7 @@ def render_still(even_obj, plain_obj, path, engine):
283371
try:
284372
h = under.z - plinth_top
285373
res = bmesh.ops.create_cone(bm, cap_ends=True, segments=16,
286-
radius1=0.022, radius2=0.022, depth=h)
374+
radius1=0.03, radius2=0.03, depth=h)
287375
for vert in res["verts"]:
288376
vert.co += Vector((under.x, y, plinth_top + h / 2))
289377
bm.to_mesh(rod)
@@ -301,28 +389,46 @@ def render_still(even_obj, plain_obj, path, engine):
301389
plinth.data.materials.append(walnut)
302390
scene.collection.objects.link(plinth)
303391
bev = plinth.modifiers.new("Chamfer", 'BEVEL')
304-
bev.width = 0.03
305-
bev.segments = 2
392+
bev.width = 0.035
393+
bev.segments = 3
306394
parts.append(plinth)
307395

396+
# On the plain shell's cut face, a red line traces where a t-thick shell's
397+
# inner face would run: the even shell's own evaluated front-row copies,
398+
# placed through the plain shell's transform (both shells share one local
399+
# profile). Where the orange band stops short of the red line, that gap
400+
# is the thickness use_even_offset = False loses at each fold.
401+
n = len(even_obj.data.vertices)
402+
target = evaluated_local_coords(even_obj)[n:n + len(pts)]
403+
lift = Vector((0.0, -0.02, 0.0))
404+
line = [plain_obj.matrix_world @ c + lift for c in target]
405+
for a, b in zip(line, line[1:]):
406+
ob = bpy.data.objects.new("NominalLine", bar_mesh("NominalLine", a, b, 0.012, 0.008, Y_AXIS))
407+
ob.data.materials.append(deficit)
408+
scene.collection.objects.link(ob)
409+
parts.append(ob)
410+
308411
# Brass plaques on the plinth's front edge naming each shell's setting
309412
front = -DEPTH / 2 - 0.35
310413
for obj, label in ((plain_obj, "use_even_offset = False"), (even_obj, "use_even_offset = True")):
311414
cx = obj.location.x + span / 2
312415
plate = bpy.data.objects.new("Plaque", box_mesh(
313-
"Plaque", (cx - 0.95, front - 0.012, 0.03), (cx + 0.95, front + 0.01, plinth_top - 0.03)))
416+
"Plaque", (cx - 1.05, front - 0.016, 0.035), (cx + 1.05, front + 0.01, plinth_top - 0.035)))
314417
plate.data.materials.append(brass)
315418
scene.collection.objects.link(plate)
419+
pbev = plate.modifiers.new("Chamfer", 'BEVEL')
420+
pbev.width = 0.008
421+
pbev.segments = 2
316422
parts.append(plate)
317423
cu = bpy.data.curves.new("PlaqueText", 'FONT')
318424
cu.body = label
319-
cu.size = 0.13
425+
cu.size = 0.15
320426
cu.extrude = 0.004
321427
cu.align_x = 'CENTER'
322428
cu.align_y = 'CENTER'
323429
cu.materials.append(ink)
324430
txt = bpy.data.objects.new("PlaqueText", cu)
325-
txt.location = (cx, front - 0.014, plinth_top / 2)
431+
txt.location = (cx, front - 0.018, plinth_top / 2)
326432
txt.rotation_euler = (math.radians(90), 0.0, 0.0)
327433
scene.collection.objects.link(txt)
328434

@@ -369,10 +475,13 @@ def light(name, loc, energy, size, col, aim):
369475
cam_data = bpy.data.cameras.new("Cam")
370476
cam_data.lens = 50.0
371477
cam = bpy.data.objects.new("Cam", cam_data)
372-
cam.location = (centre.x - 1.3, -8.5, 1.75)
478+
# Low and nearly square-on to the cut ends, so the section bands and the
479+
# gauges across them are the subject; a little height keeps the glazed
480+
# sheet surfaces readable behind them.
481+
cam.location = (centre.x - 0.9, -8.6, 1.35)
373482
scene.collection.objects.link(cam)
374483
aim = bpy.data.objects.new("Aim", None)
375-
aim.location = centre + Vector((0.0, 0.0, -0.2))
484+
aim.location = centre + Vector((0.0, 0.0, -0.08))
376485
scene.collection.objects.link(aim)
377486
tr = cam.constraints.new('TRACK_TO')
378487
tr.target = aim

0 commit comments

Comments
 (0)