Skip to content

Commit 7fff925

Browse files
committed
fix: make uv-layer-grid self-witness its render and restage the clipped hero
The still was the visual proof of the hazard but nothing asserted its pixels, and both panels were clipped by the frame. The script now re-reads its own render at each panel's projected center — the hazard panel must be one flat teal (spread <= 0.02, b>g>r) and the repair a high-contrast checker (spread >= 0.25) — falsified twice with measured errors. Restaged per VISUAL-STYLE: both panels fully in frame and grounded, contained warm wedge, mean luminance 0.274 inside the calibration range. Verified live on 5.1.2 and 4.5.11 LTS. Signed-off-by: fOuttaMyPaint <TMhospitalitystrategies@gmail.com>
1 parent efa8516 commit 7fff925

9 files changed

Lines changed: 205 additions & 41 deletions

File tree

‎README.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,8 @@ result is watertight (every edge borders exactly two faces).
213213

214214
`bmesh.ops.create_grid(..., calc_uvs=True)` is a silent no-op unless a UV layer already
215215
exists — without one an Image Texture samples texel (0, 0) everywhere. Asserts the hazard,
216-
the pre-create repair against closed-form grid UVs, and an explicit assignment fallback.
216+
the pre-create repair against closed-form grid UVs, and an explicit assignment fallback —
217+
then re-reads its own render and fails unless the pixels prove the flat-vs-checker split.
217218

218219
</td>
219220
</tr>
1.76 KB
Loading
49.3 KB
Loading

‎docs/gallery/index.html‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -443,7 +443,7 @@ <h2><a href="png-exr-alpha/">png-exr-alpha</a></h2>
443443
<div class="card-body">
444444
<h2><a href="uv-layer-grid/">uv-layer-grid</a></h2>
445445
<p class="teaches">The UV-layer authoring hazard — bmesh.ops.create_grid(..., calc_uvs=True) is a silent no-op unless a UV layer already exists; without one an Image Texture samples texel (0,0) everywhere.</p>
446-
<p class="witnesses"><span class="tag">witnesses</span> calc_uvs=True alone leaves 0 UV layers; pre-create + calc_uvs fills loops to the closed-form grid UVs ((x/size+1)/2, (y/size+1)/2) within 1e-6; explicit loop assignment is the calc_uvs-free fallback.</p>
446+
<p class="witnesses"><span class="tag">witnesses</span> calc_uvs=True alone leaves 0 UV layers; pre-create + calc_uvs fills loops to the closed-form grid UVs ((x/size+1)/2, (y/size+1)/2) within 1e-6; explicit loop assignment is the calc_uvs-free fallback. The still is self-witnessing: the script re-reads its render and exits non-zero unless the hazard panel is one flat teal and the repaired panel a high-contrast checker.</p>
447447
<a class="card-link" href="uv-layer-grid/">View example <span aria-hidden="true">&rarr;</span></a>
448448
</div>
449449
</article>

‎docs/gallery/uv-layer-grid/index.html‎

Lines changed: 97 additions & 20 deletions
Large diffs are not rendered by default.

‎examples/gallery.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@
264264
"name": "uv-layer-grid",
265265
"dir": "examples/uv-layer-grid",
266266
"teaches": "The UV-layer authoring hazard \u2014 bmesh.ops.create_grid(..., calc_uvs=True) is a silent no-op unless a UV layer already exists; without one an Image Texture samples texel (0,0) everywhere.",
267-
"witnessesFix": "calc_uvs=True alone leaves 0 UV layers; pre-create + calc_uvs fills loops to the closed-form grid UVs ((x/size+1)/2, (y/size+1)/2) within 1e-6; explicit loop assignment is the calc_uvs-free fallback.",
267+
"witnessesFix": "calc_uvs=True alone leaves 0 UV layers; pre-create + calc_uvs fills loops to the closed-form grid UVs ((x/size+1)/2, (y/size+1)/2) within 1e-6; explicit loop assignment is the calc_uvs-free fallback. The still is self-witnessing: the script re-reads its render and exits non-zero unless the hazard panel is one flat teal and the repaired panel a high-contrast checker.",
268268
"hero": "docs/gallery/assets/uv-layer-grid-hero.webp",
269269
"preview": "examples/uv-layer-grid/preview.webp",
270270
"tags": [

‎examples/uv-layer-grid/README.md‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ lifted into its own smoke-gated witness so the contract cannot quietly drift.
3535
error `0.1`).
3636
- *Mesh persistence* — UV layer or loop data lost across `to_mesh`.
3737
- *Explicit path* — loop assignment or the closed form itself regressing.
38+
- *Pixel witness* (with `--output`) — the saved PNG is read back and probed
39+
at each panel's projected center: the hazard panel must be one flat teal
40+
(per-channel spread ≤ 0.02, ordering b>g>r), the repaired panel a
41+
high-contrast checker (spread ≥ 0.25). Falsified twice: secretly giving the
42+
hazard panel UVs exited 11 with measured spread `0.7333`; repainting texel
43+
(0,0) magenta exited 12 with measured rgb `(0.927, 0.118, 0.536)`.
3844

3945
**Version divergence:** none probed — the silent no-op and closed-form UV
4046
fill assert identically on Blender 4.5 LTS and 5.1. The only gate in the file
@@ -44,7 +50,10 @@ is the EEVEE engine id for the optional render (`BLENDER_EEVEE_NEXT` on 4.x,
4450
**Render:** two easel panels sharing one neon checker image. Left is the
4551
hazard (no UV layer) — flat teal of texel (0, 0). Right is the repair
4652
(pre-create + `calc_uvs`) — full magenta/cyan checker. If the UV contract
47-
failed, both panels would read the same.
53+
failed, both panels would read the same — and the still is not just an
54+
illustration: the script re-reads its own render and exits non-zero unless
55+
the pixels prove the flat-vs-checker split (measured on 5.1.2: hazard spread
56+
`0.0078`, repair spread `0.7294`).
4857

4958
## Run
5059

-1.81 KB
Loading

‎examples/uv-layer-grid/uv_layer_grid.py‎

Lines changed: 94 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010
repair path against a closed-form UV grid, and an explicit loop-assignment
1111
fallback that does not depend on `calc_uvs` at all. Pass --output to also
1212
render a still that stages the broken (flat) panel beside the repaired
13-
(checker) panel so the failure mode is visible at a glance:
13+
(checker) panel — and then *witnesses the render itself*: the saved PNG is
14+
read back and probed at each panel's projected center, asserting the broken
15+
panel is one flat teal (texel (0,0)) while the repaired panel carries both
16+
checker colors. If the UV contract failed, the pixels would say so:
1417
1518
blender --background --python uv_layer_grid.py --
1619
blender --background --python uv_layer_grid.py -- --output uv.png
@@ -166,7 +169,7 @@ def eevee_engine_id():
166169

167170

168171
def make_uv_testcard(name, w=256, h=256):
169-
"""Neon checker + corner marker. Texel (0,0) is a flat teal so a missing UV
172+
"""Neon checker whose single texel (0,0) is a flat teal, so a missing UV
170173
layer reads as one solid color instead of a bright false-positive checker."""
171174
img = bpy.data.images.new(name, w, h, alpha=False)
172175
px = [0.0] * (w * h * 4)
@@ -243,15 +246,13 @@ def render_still(path, engine):
243246

244247
# Left: the hazard — calc_uvs alone, no UV layer → flat teal of texel (0,0).
245248
broken = textured_plane("Broken", card, with_uvs=False)
246-
broken.location = (-1.22, 0.0, 1.18)
247-
broken.rotation_euler = (math.radians(62), 0.0, math.radians(10))
248-
broken.scale = (1.15, 1.15, 1.15)
249+
broken.location = (-1.18, 0.0, 0.94)
250+
broken.rotation_euler = (math.radians(62), 0.0, math.radians(8))
249251

250252
# Right: the repair — pre-create UV layer, then calc_uvs fills it.
251253
fixed = textured_plane("Fixed", card, with_uvs=True)
252-
fixed.location = (1.22, 0.0, 1.18)
253-
fixed.rotation_euler = (math.radians(62), 0.0, math.radians(-10))
254-
fixed.scale = (1.15, 1.15, 1.15)
254+
fixed.location = (1.18, 0.0, 0.94)
255+
fixed.rotation_euler = (math.radians(62), 0.0, math.radians(-8))
255256

256257
floor_me = bpy.data.meshes.new("Floor")
257258
bm = bmesh.new()
@@ -290,20 +291,21 @@ def light(name, loc, energy, size, col, rot):
290291
ob.rotation_euler = tuple(math.radians(a) for a in rot)
291292
scene.collection.objects.link(ob)
292293

293-
light("Key", (-3.8, -4.8, 5.8), 360.0, 4.5, (1.0, 0.96, 0.9), (52, 0, -32))
294-
light("Fill", (4.8, -2.8, 1.6), 70.0, 9.0, (0.75, 0.85, 1.0), (72, 0, 52))
295-
light("Rim", (0.2, 4.0, 2.8), 220.0, 3.2, (0.6, 0.78, 1.0), (-62, 0, 178))
296-
# Between the panels and the wall so it only rakes the backdrop.
297-
light("Wedge", (0.5, 5.2, 2.8), 520.0, 7.0, (1.0, 0.72, 0.42), (-78, 0, 180))
294+
light("Key", (-3.8, -4.8, 5.8), 300.0, 4.5, (1.0, 0.96, 0.9), (52, 0, -32))
295+
light("Fill", (4.8, -2.8, 1.6), 55.0, 9.0, (0.75, 0.85, 1.0), (72, 0, 52))
296+
light("Rim", (0.2, 4.0, 2.8), 90.0, 3.2, (0.6, 0.78, 1.0), (-62, 0, 178))
297+
# Between the panels and the wall so it only rakes the backdrop: a
298+
# contained warm pool, corners falling off to dark.
299+
light("Wedge", (0.3, 5.4, 1.9), 380.0, 3.6, (1.0, 0.68, 0.38), (-72, 0, 180))
298300

299301
aim = bpy.data.objects.new("Aim", None)
300-
aim.location = (0.0, 0.0, 1.1)
302+
aim.location = (0.0, 0.0, 0.95)
301303
scene.collection.objects.link(aim)
302304

303305
cam_data = bpy.data.cameras.new("Cam")
304306
cam_data.lens = 45.0
305307
cam = bpy.data.objects.new("Cam", cam_data)
306-
cam.location = (0.0, -5.4, 2.2)
308+
cam.location = (0.0, -6.6, 2.1)
307309
con = cam.constraints.new("TRACK_TO")
308310
con.target = aim
309311
con.track_axis = "TRACK_NEGATIVE_Z"
@@ -329,6 +331,77 @@ def light(name, loc, energy, size, col, rot):
329331
return os.path.exists(path) and os.path.getsize(path) > 0
330332

331333

334+
def patch_stats(px, w, h, cx, cy, half):
335+
"""Mean RGB and per-channel min/max spread of a (2*half)^2 pixel patch."""
336+
sums = [0.0, 0.0, 0.0]
337+
lo = [1.0, 1.0, 1.0]
338+
hi = [0.0, 0.0, 0.0]
339+
n = 0
340+
for y in range(max(0, cy - half), min(h, cy + half)):
341+
for x in range(max(0, cx - half), min(w, cx + half)):
342+
i = (y * w + x) * 4
343+
for c in range(3):
344+
v = px[i + c]
345+
sums[c] += v
346+
lo[c] = min(lo[c], v)
347+
hi[c] = max(hi[c], v)
348+
n += 1
349+
mean = [s / n for s in sums]
350+
spread = max(hi[c] - lo[c] for c in range(3))
351+
return mean, spread
352+
353+
354+
def verify_still(path):
355+
"""Witness the render itself: the broken panel must be one flat teal, the
356+
repaired panel a high-contrast checker. Probes the saved PNG at each
357+
panel's projected center, so a failed UV contract cannot ship a still."""
358+
from bpy_extras.object_utils import world_to_camera_view
359+
360+
scene = bpy.context.scene
361+
bpy.context.view_layer.update()
362+
img = bpy.data.images.load(path)
363+
try:
364+
w, h = img.size
365+
px = [0.0] * (w * h * 4)
366+
img.pixels.foreach_get(px)
367+
finally:
368+
bpy.data.images.remove(img)
369+
370+
half = max(8, int(w * 0.055)) # ~70 px at 1280: spans >1 checker cell
371+
stats = {}
372+
for name in ("Broken", "Fixed"):
373+
obj = bpy.data.objects[name]
374+
ndc = world_to_camera_view(scene, scene.camera, obj.matrix_world.translation)
375+
cx, cy = int(ndc.x * w), int(ndc.y * h)
376+
stats[name] = patch_stats(px, w, h, cx, cy, half)
377+
378+
(b_mean, b_spread), (f_mean, f_spread) = stats["Broken"], stats["Fixed"]
379+
print(
380+
f"pixel witness: broken mean rgb=({b_mean[0]:.3f},{b_mean[1]:.3f},"
381+
f"{b_mean[2]:.3f}) spread={b_spread:.4f}; fixed spread={f_spread:.4f}"
382+
)
383+
if b_spread > 0.02:
384+
return fail(
385+
f"broken panel is not flat (spread {b_spread:.4f} > 0.02) — "
386+
f"the hazard did not render as one color",
387+
11,
388+
)
389+
if not (b_mean[2] > b_mean[1] > b_mean[0]):
390+
return fail(
391+
f"broken panel is not the teal of texel (0,0) "
392+
f"(expected b>g>r, got rgb=({b_mean[0]:.3f},{b_mean[1]:.3f},{b_mean[2]:.3f}))",
393+
12,
394+
)
395+
if f_spread < 0.25:
396+
return fail(
397+
f"repaired panel is not a checker (spread {f_spread:.4f} < 0.25)",
398+
13,
399+
)
400+
if abs(f_mean[0] - b_mean[0]) < 0.1 and f_spread - b_spread < 0.2:
401+
return fail("broken and repaired panels render identically", 14)
402+
return 0
403+
404+
332405
def main():
333406
argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
334407
p = argparse.ArgumentParser()
@@ -344,9 +417,13 @@ def main():
344417
return code
345418
if args.output:
346419
# check() already emptied the scene; rebuild for the still.
347-
if not render_still(os.path.abspath(args.output), args.engine):
420+
out = os.path.abspath(args.output)
421+
if not render_still(out, args.engine):
348422
return fail(f"render produced no file at {args.output}", 10)
349-
print(f"wrote {args.output}")
423+
code = verify_still(out)
424+
if code != 0:
425+
return code
426+
print(f"wrote {args.output} (pixel witness passed)")
350427
return 0
351428

352429

0 commit comments

Comments
 (0)