Skip to content

Commit 6cc85f8

Browse files
TMHSDigitalclaude
andcommitted
fix(examples): redesign image-pixels-testcard as a hooded reference monitor on a walnut desk
The monitor was a plain slab on a stand. It is now a broadcast reference display on a walnut desk with drawer pedestals: a slim beveled bezel over a stepped rear housing with a vent, a matte sun hood framing the picture, input keys and the teal power LED on the bottom bezel, and the machined stand. The emissive matte screen, Closest interpolation and the card data are unchanged, and so are the checks. The render path now runs the gallery_framing gate (exit 10), which it previously skipped. The desk is passed as stage, like the floor, since its bottom-edge crop is the composition. "--output produced no file" moves from exit 10 to 12, and the README exit table is updated to match. Evidence (live runs, local): - E:/Blender-Developer-Tools/.scratch/blender-5.2.1-windows-x64/blender.exe (Blender 5.2.1 LTS): check-only exit 0 (byte round-trip max err 0.0019608, float 2.97e-08, imposter read 0.0, byte PNG reload 0.0019608). --wrong-origin exits 4 (byte round-trip error 0.9215687). --output exit 0. - E:/Blender-Developer-Tools/.scratch/blender-4.5.11-windows-x64/blender.exe (Blender 4.5.11 LTS): check and --output exit 0, same numbers, framing_ok. - Framing: fill x=0.503 y=0.750; margins l=0.241 r=0.256 b=0.167 t=0.083. - Contact sheet: candidate luma 0.241 / stage 0.154 / warmth -0.043 / sat 0.230 (the cyan/blue bars pull warmth negative), against armature-bend 0.243/0.192/-0.144/0.237, damped-track-aim 0.237/0.218/+0.105/0.178, bmesh-gear 0.237/0.252/+0.092/0.153. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent f5effd7 commit 6cc85f8

7 files changed

Lines changed: 89 additions & 26 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ shader `Attribute` node is actually linked to Base Color.
298298
</tr>
299299
<tr>
300300
<td width="46%" valign="middle">
301-
<a href="examples/image-pixels-testcard/"><img src="examples/image-pixels-testcard/preview.webp" alt="Image pixels testcard: a studio monitor on a machined metal stand showing a procedural broadcast test card — seven neon color bars behind the classic dark circle, a luminance ramp, and a PLUGE row with a white bottom-left origin marker — in a dark studio with a warm pool raking the back wall" /></a>
301+
<a href="examples/image-pixels-testcard/"><img src="examples/image-pixels-testcard/preview.webp" alt="Image pixels testcard: a hooded broadcast reference monitor on a walnut desk showing a procedural broadcast test card — seven neon color bars behind the classic dark circle, a luminance ramp, and a PLUGE row with a white bottom-left origin marker — in a dark studio with a warm pool raking the back wall" /></a>
302302
</td>
303303
<td valign="middle">
304304

17 KB
Loading
21.4 KB
Loading

‎examples/gallery.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@
273273
"name": "image-pixels-testcard",
274274
"dir": "examples/image-pixels-testcard",
275275
"teaches": "The Image pixel-buffer contract — a procedural broadcast test card written into bpy.data.images.new() with one pixels.foreach_set (589,824 floats), byte vs float_buffer storage, scale() reallocation, and the save() vs save_render() lifecycle.",
276-
"alt": "A monitor showing a colour-bar test card with a black circle and a greyscale strip.",
276+
"alt": "A hooded broadcast reference monitor on a walnut desk showing a colour-bar test card with a black circle, a greyscale ramp and a PLUGE row.",
277277
"witnessesFix": "pixels is always flat RGBA (channels == 4 even with alpha=False), byte storage quantizes at exactly ≤ 0.5/255 and strictly > 0, stale-size bulk reads raise after scale() — and save() silently flips source to FILE and drops the buffer, so later pixels reads come from whatever sits on disk (proven with an imposter file).",
278278
"hero": "docs/gallery/assets/image-pixels-testcard-hero.webp",
279279
"preview": "examples/image-pixels-testcard/preview.webp",

‎examples/image-pixels-testcard/README.md‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ against it.
8282
| 7 | `save()` source/buffer-drop contract drifted |
8383
| 8 | `save_render()` flipped source or disturbed the buffer |
8484
| 9 | Byte PNG save/reload error |
85-
| 10 | `--output` produced no file |
85+
| 10 | Framing gate violation on the `--output` path (`gallery_framing`) |
86+
| 12 | `--output` produced no file |
8687

8788
The `blender-smoke` workflow runs the check on Blender 5.2 LTS and 4.5 LTS
8889
(5.1 on the weekly cron, the `needs-5.1` PR label, or manual dispatch).
@@ -91,8 +92,12 @@ Smoke does not pass `--output` or `--wrong-origin`.
9192
In the render, `Closest` interpolation keeps the pixel grid honest —
9293
the jagged circle edge is the 512 × 288 buffer itself, and the white marker in the
9394
PLUGE row sits at the bottom-left because that is where pixel (0, 0) lives. The
94-
monitor is staged as a designed object — beveled dark-polymer case, machined metal
95-
stand, teal power LED — on the dark studio stage from `docs/VISUAL-STYLE.md`
95+
monitor is staged as a broadcast reference display — slim beveled bezel over a
96+
stepped rear housing, a matte sun hood framing the picture, input keys and a teal
97+
power LED on the bottom bezel, a machined stand — on a walnut desk with drawer
98+
pedestals, on the dark studio stage from `docs/VISUAL-STYLE.md`
9699
(Standard view transform; warm key, cool fill and rim; a warm pool raking the back
97100
wall). The screen stays emissive and matte — specular off, emission strength 1.0 —
98-
so the card's values read exactly.
101+
so the card's values read exactly. The render path gates framing through
102+
`examples/gallery_framing.py` (exit 10) before writing; the desk is passed as
103+
stage, like the floor, because its crop at the frame bottom is the composition.

‎examples/image-pixels-testcard/image_pixels_testcard.py‎

Lines changed: 78 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@
3131
"""
3232
import bpy, sys, os, math, argparse, tempfile
3333

34+
# Shared Layer 1 framing measurement (render path only) — see
35+
# gallery_framing.py for the __file__-relative import shim this relies on.
36+
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir))
37+
import gallery_framing # noqa: E402
38+
3439
W, H = 512, 288
3540
BYTE_TOL = 0.5 / 255.0 + 1e-6 # 8-bit quantization: round(v*255)/255, worst case half a step
3641
FLOAT_TOL = 1e-6 # float32 storage of float64 Python values
@@ -250,11 +255,12 @@ def box(name, dims, mat, loc, bevel=0.0):
250255
mod.limit_method = 'ANGLE'
251256
return ob
252257

253-
# -- the monitor: designed object, not a black slab ----------------------
258+
# -- the monitor: a broadcast reference display on a walnut desk ---------
254259
# screen: emissive plane textured with the card (16:9, 3.2 wide), centered
255-
# at standing height on its stand
260+
# at viewing height on its stand; everything stands on the desk top at dz
256261
sw, sh = 3.2, 1.8
257-
scr_z = 1.61
262+
dz = 0.78
263+
scr_z = 1.61 + dz
258264
screen_me = bpy.data.meshes.new("Screen")
259265
bm = bmesh.new()
260266
try:
@@ -289,16 +295,54 @@ def box(name, dims, mat, loc, bevel=0.0):
289295
screen.location = (0.0, 0.0, scr_z)
290296
scene.collection.objects.link(screen)
291297

292-
# case: dark polymer shell with a beveled edge the rim light can draw
298+
# case: a slim beveled bezel frame over a stepped rear electronics housing
293299
case_mat = principled("CasePolymer", (0.030, 0.034, 0.042), 0.38)
294300
metal_mat = principled("StandMetal", (0.055, 0.060, 0.070), 0.36, metallic=0.7)
295-
box("Case", (sw + 0.18, 0.11, sh + 0.18), case_mat, (0.0, 0.057, scr_z), bevel=0.03)
296-
# machined stand: neck + base plate
297-
box("Neck", (0.30, 0.14, 0.64), metal_mat, (0.0, 0.10, 0.37), bevel=0.025)
298-
box("Base", (1.45, 0.85, 0.07), metal_mat, (0.0, 0.12, 0.035), bevel=0.03)
299-
# power LED on the bottom bezel, a small designed detail
301+
hood_mat = principled("HoodMatte", (0.022, 0.023, 0.026), 0.7)
302+
box("Case", (sw + 0.18, 0.07, sh + 0.24), case_mat, (0.0, 0.037, scr_z - 0.03), bevel=0.025)
303+
box("Housing", (sw - 0.55, 0.26, sh - 0.40), case_mat, (0.0, 0.20, scr_z), bevel=0.05)
304+
box("HousingVent", (sw - 1.2, 0.03, 0.30), metal_mat, (0.0, 0.34, scr_z + 0.40), bevel=0.01)
305+
# broadcast sun hood: shallow top and side shades that frame the picture
306+
hood_d = 0.34
307+
top_z = scr_z + sh / 2 + 0.108
308+
box("HoodTop", (sw + 0.25, hood_d, 0.035), hood_mat, (0.0, -hood_d / 2 + 0.01, top_z), bevel=0.01)
309+
for sx in (-1.0, 1.0):
310+
box("HoodSide", (0.035, hood_d, sh + 0.30), hood_mat,
311+
(sx * (sw / 2 + 0.108), -hood_d / 2 + 0.01, scr_z - 0.02), bevel=0.01)
312+
# machined stand: neck + base plate on the desk
313+
box("Neck", (0.30, 0.14, 0.64), metal_mat, (0.0, 0.14, 0.37 + dz), bevel=0.025)
314+
box("Base", (1.45, 0.85, 0.07), metal_mat, (0.0, 0.12, 0.035 + dz), bevel=0.03)
315+
# bottom-bezel controls: a row of input keys and the power LED
316+
key_mat = principled("Keys", (0.10, 0.105, 0.115), 0.45, metallic=0.4)
317+
bez_z = scr_z - sh / 2 - 0.09
318+
for i in range(5):
319+
box("Key", (0.11, 0.02, 0.035), key_mat, (0.55 + 0.15 * i, -0.004, bez_z), bevel=0.006)
300320
glow_mat = principled("Glow", (0.0, 0.0, 0.0), 0.5, emit=(0.10, 0.85, 0.75), estr=6.0)
301-
box("LED", (0.05, 0.02, 0.02), glow_mat, (1.30, -0.006, scr_z - sh / 2 - 0.045))
321+
box("LED", (0.05, 0.02, 0.02), glow_mat, (1.42, -0.006, bez_z))
322+
323+
# the desk: walnut top on two dark drawer pedestals
324+
walnut = principled("Walnut", (0.13, 0.065, 0.03), 0.42)
325+
grain = walnut.node_tree
326+
wave = grain.nodes.new("ShaderNodeTexWave")
327+
wave.bands_direction = 'Y' # grain runs along the desk's length
328+
wave.inputs["Scale"].default_value = 3.5
329+
wave.inputs["Distortion"].default_value = 7.0
330+
wave.inputs["Detail"].default_value = 4.0
331+
ramp = grain.nodes.new("ShaderNodeValToRGB")
332+
ramp.color_ramp.elements[0].color = (0.085, 0.040, 0.018, 1.0)
333+
ramp.color_ramp.elements[1].color = (0.19, 0.10, 0.05, 1.0)
334+
grain.links.new(wave.outputs["Fac"], ramp.inputs["Fac"])
335+
grain.links.new(ramp.outputs["Color"], grain.nodes["Principled BSDF"].inputs["Base Color"])
336+
cab_mat = principled("Cabinet", (0.035, 0.037, 0.042), 0.55)
337+
desk = [box("DeskTop", (5.0, 1.7, 0.08), walnut, (0.0, 0.25, dz - 0.04), bevel=0.02)]
338+
for sx in (-1.0, 1.0):
339+
desk.append(box("DeskPedestal", (1.1, 1.5, dz - 0.08), cab_mat,
340+
(sx * 1.85, 0.30, (dz - 0.08) / 2), bevel=0.015))
341+
for k in range(2):
342+
desk.append(box("DeskDrawer", (0.95, 0.02, 0.26), cab_mat,
343+
(sx * 1.85, -0.455, 0.20 + 0.32 * k), bevel=0.01))
344+
desk.append(box("DeskPull", (0.30, 0.03, 0.025), metal_mat,
345+
(sx * 1.85, -0.475, 0.26 + 0.32 * k), bevel=0.006))
302346

303347
# -- stage: near-black floor + back wall, matte --------------------------
304348
stage_mat = principled("Studio", (0.03, 0.032, 0.037), 0.7)
@@ -335,21 +379,21 @@ def light(name, loc, target, energy, size, col):
335379
ob.rotation_euler = d.to_track_quat('-Z', 'Y').to_euler()
336380
scene.collection.objects.link(ob)
337381

338-
light("Key", (-4.0, -5.0, 6.0), (0.0, 0.0, 1.3), 420.0, 5.0, (1.0, 0.96, 0.9))
339-
light("Fill", (4.2, -1.8, 2.4), (0.0, 0.0, 1.5), 55.0, 9.0, (0.75, 0.85, 1.0))
340-
light("Rim", (3.4, 2.8, 3.4), (0.3, 0.0, 1.6), 320.0, 3.0, (0.6, 0.78, 1.0))
382+
light("Key", (-4.0, -5.0, 6.0 + dz), (0.0, 0.0, 1.3 + dz), 420.0, 5.0, (1.0, 0.96, 0.9))
383+
light("Fill", (4.2, -1.8, 2.4 + dz), (0.0, 0.0, 1.5 + dz), 55.0, 9.0, (0.75, 0.85, 1.0))
384+
light("Rim", (3.4, 2.8, 3.4 + dz), (0.3, 0.0, 1.6 + dz), 320.0, 3.0, (0.6, 0.78, 1.0))
341385
# the signature: a warm pool raking the back wall behind the subject;
342386
# aimed at the wall well above the floor seam so the seam stays in shadow
343-
light("Wedge", (3.2, 5.2, 6.2), (2.4, 8.5, 4.2), 500.0, 5.0, (1.0, 0.76, 0.5))
387+
light("Wedge", (3.2, 5.2, 6.2 + dz), (2.4, 8.5, 4.2 + dz), 500.0, 5.0, (1.0, 0.76, 0.5))
344388

345389
# camera: 50 mm, slightly above the subject, tracking an empty on it
346390
target = bpy.data.objects.new("AimTarget", None)
347-
target.location = (0.0, 0.0, 1.42)
391+
target.location = (0.0, 0.0, 1.12 + dz)
348392
scene.collection.objects.link(target)
349393
cam_data = bpy.data.cameras.new("Cam")
350394
cam_data.lens = 50.0
351395
cam = bpy.data.objects.new("Cam", cam_data)
352-
cam.location = (-4.2, -7.7, 2.85)
396+
cam.location = (-4.3, -8.0, 2.85 + dz)
353397
con = cam.constraints.new('TRACK_TO')
354398
con.target = target
355399
scene.collection.objects.link(cam)
@@ -367,8 +411,20 @@ def light(name, loc, target, energy, size, col):
367411
scene.render.resolution_y = 720
368412
scene.render.image_settings.file_format = 'PNG'
369413
scene.render.filepath = path
414+
bpy.context.view_layer.update()
415+
# Layer 1 framing gate before the beauty render (exit 10 on violation):
416+
# the hero is the monitor; the desk is its support and, like the floor,
417+
# may run out of frame (the bottom crop is the composition)
418+
monitor = [o for o in scene.objects if o.type == 'MESH'
419+
and not o.name.startswith(("Floor", "Wall", "Desk"))]
420+
desk_parts = [o for o in scene.objects if o.name.startswith("Desk")]
421+
fcode = gallery_framing.check_framing(scene, cam, hero=monitor,
422+
elements=monitor,
423+
stage=[floor, wall] + desk_parts)
424+
if fcode:
425+
return fcode
370426
bpy.ops.render.render(write_still=True)
371-
return os.path.exists(path) and os.path.getsize(path) > 0
427+
return 0 if os.path.exists(path) and os.path.getsize(path) > 0 else 12
372428

373429

374430
def main():
@@ -386,9 +442,11 @@ def main():
386442
return code
387443

388444
if args.output:
389-
if not render_still(os.path.abspath(args.output), args.engine):
390-
print("ERROR: render produced no file", file=sys.stderr)
391-
return 10
445+
rcode = render_still(os.path.abspath(args.output), args.engine)
446+
if rcode:
447+
if rcode == 12:
448+
print("ERROR: render produced no file", file=sys.stderr)
449+
return rcode
392450
print(f"rendered still {args.output}")
393451

394452
print("image-pixels-testcard OK")
16.4 KB
Loading

0 commit comments

Comments
 (0)