diff --git a/ROADMAP.md b/ROADMAP.md
index 9df724c..d4fa394 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -129,7 +129,7 @@ Not committed; target list for the next content version. (v0.3.0 shipped the smo
- UV atlas pack witness — lightmap/unique UV island packing with non-overlap + utilization closed forms (deferred behind hygiene/origin; overlaps `uv-layer-grid` / `triangulate-tangents` / `gltf-export-roundtrip` UV arc)
- Attribute domain witness: writing a POINT-domain color attribute and reading it as if it were CORNER (or vice versa) silently shears colors across shared verts — companion to `color-attribute-wheel`
-- Gallery framing recomposition worklist (corrected survey after the survey scene-capture fix; these need real reframing, not a camera nudge — the seven marginal cases were swept mechanically and wired to `check_framing`): **depsgraph-export** (fill 0.984x over; left cube amputated by left+bottom edges — unclipping it changes the composition substantially), **vertex-weight-limit** (fill 0.247x/0.861y; base cropped at bottom edge, off-center with dead space left), **text-version-stamp** (max fill 0.663 under; dead lower third — needs vertical rebalance), **triangulate-tangents** (fill 0.334x/0.661y under; buckler small in a large stage, needs a 2x-class move-in). Already handled: the seven marginal cases (curve-bevel-arc, png-exr-alpha, swatch-grid, driver-wave, shader-node-group, gp-lineart-contour, vse-cut-list) were camera-nudged into the band and gated; deviations documented in #108 (damped-track-aim, wave-displace); sky-texture-sun-elevation is a measurement artifact (world subject — report-only if wired).
+- ~~Gallery framing recomposition worklist~~ **DRAINED**: all four recomposition cases restaged and gated with `check_framing` — depsgraph-export (0.984x/touch→0.812x; cube+dome recentered, aim-based camera), vertex-weight-limit (touch B→0.867y; more frontal camera, lower aim, base in frame), triangulate-tangents (0.334x→0.833y; 2x-class move-in plus Key/Glint trim that also cleared a pre-existing dome blowout), text-version-stamp (0.663x→0.719x; moved in with aim on the stamp center, dead third gone). The seven marginal cases were camera-nudged in the preceding sweep; deviations documented in #108 (damped-track-aim, wave-displace); sky-texture-sun-elevation is a measurement artifact (world subject — report-only if wired). Remaining: nothing on the framing worklist — future examples inherit the gate at authoring time.
## Future (uncommitted)
diff --git a/docs/gallery/assets/depsgraph-export-hero.webp b/docs/gallery/assets/depsgraph-export-hero.webp
index 5c61029..8fc128d 100644
Binary files a/docs/gallery/assets/depsgraph-export-hero.webp and b/docs/gallery/assets/depsgraph-export-hero.webp differ
diff --git a/docs/gallery/assets/text-version-stamp-hero.webp b/docs/gallery/assets/text-version-stamp-hero.webp
index c9787ca..654dff5 100644
Binary files a/docs/gallery/assets/text-version-stamp-hero.webp and b/docs/gallery/assets/text-version-stamp-hero.webp differ
diff --git a/docs/gallery/assets/triangulate-tangents-hero.webp b/docs/gallery/assets/triangulate-tangents-hero.webp
index 58d434e..a22f094 100644
Binary files a/docs/gallery/assets/triangulate-tangents-hero.webp and b/docs/gallery/assets/triangulate-tangents-hero.webp differ
diff --git a/docs/gallery/assets/vertex-weight-limit-hero.webp b/docs/gallery/assets/vertex-weight-limit-hero.webp
index fa32749..d5e6bd5 100644
Binary files a/docs/gallery/assets/vertex-weight-limit-hero.webp and b/docs/gallery/assets/vertex-weight-limit-hero.webp differ
diff --git a/docs/gallery/contact-sheets/depsgraph-export-contact-sheet.webp b/docs/gallery/contact-sheets/depsgraph-export-contact-sheet.webp
index cb7cbf0..7fe083e 100644
Binary files a/docs/gallery/contact-sheets/depsgraph-export-contact-sheet.webp and b/docs/gallery/contact-sheets/depsgraph-export-contact-sheet.webp differ
diff --git a/docs/gallery/contact-sheets/text-version-stamp-contact-sheet.webp b/docs/gallery/contact-sheets/text-version-stamp-contact-sheet.webp
new file mode 100644
index 0000000..a84de1c
Binary files /dev/null and b/docs/gallery/contact-sheets/text-version-stamp-contact-sheet.webp differ
diff --git a/docs/gallery/contact-sheets/triangulate-tangents-contact-sheet.webp b/docs/gallery/contact-sheets/triangulate-tangents-contact-sheet.webp
index d1eb493..fee6430 100644
Binary files a/docs/gallery/contact-sheets/triangulate-tangents-contact-sheet.webp and b/docs/gallery/contact-sheets/triangulate-tangents-contact-sheet.webp differ
diff --git a/docs/gallery/contact-sheets/vertex-weight-limit-contact-sheet.webp b/docs/gallery/contact-sheets/vertex-weight-limit-contact-sheet.webp
index 4d6852d..7754f75 100644
Binary files a/docs/gallery/contact-sheets/vertex-weight-limit-contact-sheet.webp and b/docs/gallery/contact-sheets/vertex-weight-limit-contact-sheet.webp differ
diff --git a/docs/gallery/depsgraph-export/index.html b/docs/gallery/depsgraph-export/index.html
index 2f89930..58b1bf8 100644
--- a/docs/gallery/depsgraph-export/index.html
+++ b/docs/gallery/depsgraph-export/index.html
@@ -196,6 +196,7 @@
Run
# Write the exported OBJ to a specific path:
blender --background --python depsgraph_export.py -- --obj exported.obj
It exits non-zero on failure (modifier not applied, or exported count ≠ evaluated count). The blender-smoke workflow runs this check on Blender 4.5 LTS and 5.1: base 8 → evaluated/exported 98 vertices with a 2-level SUBSURF.
+The --output render path additionally measures framing against the Layer 1 band via examples/gallery_framing.py (exit 10 on violation) before writing the still.
Source
@@ -219,6 +220,11 @@ Source
"""
import bpy, bmesh, sys, os, math, argparse, tempfile
+# Shared Layer 1 framing measurement (render path only) — see gallery_framing.py
+sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir))
+sys.dont_write_bytecode = True # keep examples/__pycache__ out of the repo tree
+import gallery_framing
+
def build():
bpy.ops.wm.read_factory_settings(use_empty=True)
@@ -353,9 +359,18 @@ Source
cam_data = bpy.data.cameras.new("Cam")
cam_data.lens = 50.0
cam = bpy.data.objects.new("Cam", cam_data)
- cam.location = (0.0, -7.4, 3.0)
- cam.rotation_euler = (math.radians(76), 0.0, 0.0)
+ # Reframed: the rotated base cube spans ~5.9 units with the evaluated
+ # sphere, wider than the old (0,-7.4,3.0) frame — the cube was amputated
+ # at the left edge. Aim-based framing on the pair's center.
+ cam.location = (0.0, -9.6, 2.6)
scene.collection.objects.link(cam)
+ aim = bpy.data.objects.new("Aim", None)
+ aim.location = (0.0, 0.0, 0.9)
+ scene.collection.objects.link(aim)
+ con = cam.constraints.new('TRACK_TO')
+ con.target = aim
+ con.track_axis = 'TRACK_NEGATIVE_Z'
+ con.up_axis = 'UP_Y'
scene.camera = cam
scene.render.engine = 'CYCLES' if engine == 'cycles' else eevee_engine_id()
@@ -373,8 +388,21 @@ Source
# AgX would flatten the graphite-vs-green contrast this still hinges on
# (docs/VISUAL-STYLE.md)
scene.view_settings.view_transform = 'Standard'
+ # Layer 1 framing gate (silhouette matte) — exit 10 on violation, before
+ # the beauty render so a defective composition ships no artifact.
+ fcode = gallery_framing.check_framing(
+ scene, cam,
+ hero=[base_obj, obj],
+ elements=[base_obj, obj],
+ stage=[floor, wall],
+ )
+ if fcode:
+ return fcode
bpy.ops.render.render(write_still=True)
- return os.path.exists(path) and os.path.getsize(path) > 0
+ if not (os.path.exists(path) and os.path.getsize(path) > 0):
+ print("ERROR: render produced no file", file=sys.stderr)
+ return 6
+ return 0
def main():
@@ -393,9 +421,9 @@ Source
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
+ rcode = render_still(obj, os.path.abspath(args.output), args.engine)
+ if rcode:
+ return rcode
print(f"rendered still {args.output}")
print("depsgraph-export OK")
diff --git a/docs/gallery/text-version-stamp/index.html b/docs/gallery/text-version-stamp/index.html
index d3b8a18..70185f5 100644
--- a/docs/gallery/text-version-stamp/index.html
+++ b/docs/gallery/text-version-stamp/index.html
@@ -194,6 +194,7 @@ Run
blender --background --python text_version_stamp.py -- --output stamp.png
blender --background --python text_version_stamp.py -- --output stamp.png --engine cycles
It exits non-zero on failure (wrong subclass, missing font, body/version mismatch, non-planar flat text, extrude/bevel closed form off, geometry not regenerating, or a to_mesh_clear() reference surviving). The blender-smoke workflow runs the check on Blender 4.5 LTS and 5.1. The render scales the stamp to a constant width, so the frame holds for any version-string length.
+The --output render path additionally measures framing against the Layer 1 band via examples/gallery_framing.py (exit 10 on violation) before writing the still.
Source
@@ -226,6 +227,11 @@ Source
"""
import bpy, sys, os, math, argparse
+# Shared Layer 1 framing measurement (render path only) — see gallery_framing.py
+sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir))
+sys.dont_write_bytecode = True # keep examples/__pycache__ out of the repo tree
+import gallery_framing
+
EXTRUDE = 0.06
BEVEL = 0.02
TOL = 1e-4
@@ -433,9 +439,18 @@ Source
cam_data = bpy.data.cameras.new("Cam")
cam_data.lens = 50.0
cam = bpy.data.objects.new("Cam", cam_data)
- cam.location = (0.0, -8.5, 1.15)
- cam.rotation_euler = (math.radians(86.5), 0.0, 0.0)
+ # Reframed: the old (0,-8.5,1.15) fixed 86.5° pitch left the stamp at
+ # 0.663 fill with a dead lower third; moved in with an aim on the stamp's
+ # vertical center so text, caption, and underline bar balance the frame.
+ cam.location = (0.0, -6.0, 1.5)
scene.collection.objects.link(cam)
+ aim = bpy.data.objects.new("Aim", None)
+ aim.location = (0.0, 0.0, 1.05)
+ scene.collection.objects.link(aim)
+ con = cam.constraints.new('TRACK_TO')
+ con.target = aim
+ con.track_axis = 'TRACK_NEGATIVE_Z'
+ con.up_axis = 'UP_Y'
scene.camera = cam
scene.render.engine = 'CYCLES' if engine == 'cycles' else eevee_engine_id()
@@ -450,8 +465,22 @@ Source
scene.render.resolution_y = 720
scene.render.image_settings.file_format = 'PNG'
scene.render.filepath = path
+ # Layer 1 framing gate (silhouette matte) — exit 10 on violation, before
+ # the beauty render so a defective composition ships no artifact. The hero
+ # is the version stamp; caption and underline bar count for margins.
+ fcode = gallery_framing.check_framing(
+ scene, cam,
+ hero=[obj],
+ elements=[obj, cap_obj, bar],
+ stage=[floor, wall],
+ )
+ if fcode:
+ return fcode
bpy.ops.render.render(write_still=True)
- return os.path.exists(path) and os.path.getsize(path) > 0
+ if not (os.path.exists(path) and os.path.getsize(path) > 0):
+ print("ERROR: render produced no file", file=sys.stderr)
+ return 9
+ return 0
def main():
@@ -468,9 +497,9 @@ Source
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 9
+ rcode = render_still(obj, os.path.abspath(args.output), args.engine)
+ if rcode:
+ return rcode
print(f"rendered still {args.output}")
print("text-version-stamp OK")
diff --git a/docs/gallery/triangulate-tangents/index.html b/docs/gallery/triangulate-tangents/index.html
index 84f34ab..13d0e81 100644
--- a/docs/gallery/triangulate-tangents/index.html
+++ b/docs/gallery/triangulate-tangents/index.html
@@ -198,6 +198,7 @@ Run
blender --background --python triangulate_tangents.py -- --output buckler.png
blender --background --python triangulate_tangents.py -- --output buckler.png --engine cycles
It exits non-zero on failure (topology drift, reallocated UV layer, non-orthonormal basis, bitangent-convention drift, formula excursion, or a flip inside a smooth field). The blender-smoke workflow runs the check on Blender 4.5 LTS and 5.1.
+The --output render path additionally measures framing against the Layer 1 band via examples/gallery_framing.py (exit 10 on violation) before writing the still.
Source
@@ -243,6 +244,11 @@ Source
import bpy, bmesh, sys, os, math, argparse
import mathutils
+# Shared Layer 1 framing measurement (render path only) — see gallery_framing.py
+sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir))
+sys.dont_write_bytecode = True # keep examples/__pycache__ out of the repo tree
+import gallery_framing
+
SIDES = 48 # buckler lathe resolution
RINGS = 6 # dome profile rings (closed form below)
BOSS_R = 1.0
@@ -571,19 +577,22 @@ Source
# shaped warm key, faint cool fill, cool rim, warm wedge on the back wall
# (docs/VISUAL-STYLE.md); the glint is the anisotropic streak's driver
- light("Key", (-4.0, -5.0, 6.0), 500.0, 4.5, (1.0, 0.96, 0.9), (48, 0, -38))
+ light("Key", (-4.0, -5.0, 6.0), 320.0, 4.5, (1.0, 0.96, 0.9), (48, 0, -38))
light("Fill", (5.0, -4.0, 3.0), 100.0, 9.0, (0.75, 0.85, 1.0), (62, 0, 50))
light("Rim", (0.5, 4.5, 5.0), 340.0, 4.0, (0.6, 0.78, 1.0), (-55, 0, 175))
light("Wedge", (2.5, 3.5, 4.2), 480.0, 6.0, (1.0, 0.76, 0.5), (-72, 0, 195))
- light("Glint", (1.5, -5.5, 5.5), 400.0, 1.2, (1.0, 0.9, 0.75), (42, 0, 15))
+ light("Glint", (1.5, -5.5, 5.5), 150.0, 1.2, (1.0, 0.9, 0.75), (42, 0, 15))
cam_data = bpy.data.cameras.new("Cam")
cam_data.lens = 55.0
cam = bpy.data.objects.new("Cam", cam_data)
- cam.location = (0.6, -8.8, 2.8)
+ # Reframed: the old (0.6,-8.8,2.8) left the buckler at 0.334 fill adrift
+ # in the stage; moved in ~2.2x so the brushed-steel tangent sweep fills
+ # the frame — the grooves are the tangent-field evidence.
+ cam.location = (0.6, -6.35, 2.62)
scene.collection.objects.link(cam)
target = bpy.data.objects.new("Aim", None)
- target.location = (0.0, 0.0, 1.35)
+ target.location = (0.0, 0.0, 1.42)
scene.collection.objects.link(target)
con = cam.constraints.new('TRACK_TO')
con.target = target
@@ -603,8 +612,22 @@ Source
scene.render.filepath = path
# AgX would wash the steel toward chalk (docs/VISUAL-STYLE.md)
scene.view_settings.view_transform = 'Standard'
+ # Layer 1 framing gate (silhouette matte) — exit 10 on violation, before
+ # the beauty render so a defective composition ships no artifact.
+ stand_ob = scene.objects.get("Stand")
+ fcode = gallery_framing.check_framing(
+ scene, cam,
+ hero=[obj],
+ elements=[obj] + ([stand_ob] if stand_ob else []),
+ stage=[floor, wall],
+ )
+ if fcode:
+ return fcode
bpy.ops.render.render(write_still=True)
- return os.path.exists(path) and os.path.getsize(path) > 0
+ if not (os.path.exists(path) and os.path.getsize(path) > 0):
+ print("ERROR: render produced no file", file=sys.stderr)
+ return 10
+ return 0
def main():
@@ -622,9 +645,9 @@ Source
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 10
+ rcode = render_still(obj, os.path.abspath(args.output), args.engine)
+ if rcode:
+ return rcode
print(f"rendered still {args.output}")
print("triangulate-tangents OK")
diff --git a/docs/gallery/vertex-weight-limit/index.html b/docs/gallery/vertex-weight-limit/index.html
index c19bc08..61e22f0 100644
--- a/docs/gallery/vertex-weight-limit/index.html
+++ b/docs/gallery/vertex-weight-limit/index.html
@@ -197,6 +197,7 @@ Run
blender --background --python vertex_weight_limit.py -- --output arm.png
blender --background --python vertex_weight_limit.py -- --output arm.png --engine cycles
It exits non-zero on failure (vacuous authoring, a vertex over the cap, broken weight sums, pose damaged by pruning, LBS drift, or a moved Root mount). The blender-smoke workflow runs the check on Blender 4.5 LTS and 5.1.
+The --output render path additionally measures framing against the Layer 1 band via examples/gallery_framing.py (exit 10 on violation) before writing the still.
Source
@@ -238,6 +239,11 @@ Source
import bpy, bmesh, sys, os, math, argparse
import mathutils
+# Shared Layer 1 framing measurement (render path only) — see gallery_framing.py
+sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir))
+sys.dont_write_bytecode = True # keep examples/__pycache__ out of the repo tree
+import gallery_framing
+
SIDES = 32
MAX_INFLUENCES = 4 # the engine constraint being witnessed
BUMP_R = 1.8 # flex-cuff blend support; guarantees 5 pre-limit
@@ -625,10 +631,13 @@ Source
cam_data = bpy.data.cameras.new("Cam")
cam_data.lens = 52.0
cam = bpy.data.objects.new("Cam", cam_data)
- cam.location = (5.8, -6.9, 2.2)
+ # Reframed: the old (5.8,-6.9,2.2) aim 1.5 cropped the base at the bottom
+ # edge and left the subject adrift right-of-center; slightly more frontal
+ # and lower-aimed so the full base and the lean both sit in frame.
+ cam.location = (4.6, -8.2, 2.3)
scene.collection.objects.link(cam)
target = bpy.data.objects.new("Aim", None)
- target.location = (0.45, 0.0, 1.5)
+ target.location = (0.35, 0.0, 1.25)
scene.collection.objects.link(target)
con = cam.constraints.new('TRACK_TO')
con.target = target
@@ -649,8 +658,21 @@ Source
# AgX would wash the hazard orange and teal accent toward pastel
# (docs/VISUAL-STYLE.md)
scene.view_settings.view_transform = 'Standard'
+ # Layer 1 framing gate (silhouette matte) — exit 10 on violation, before
+ # the beauty render so a defective composition ships no artifact.
+ fcode = gallery_framing.check_framing(
+ scene, cam,
+ hero=[obj],
+ elements=[obj],
+ stage=[floor, wall],
+ )
+ if fcode:
+ return fcode
bpy.ops.render.render(write_still=True)
- return os.path.exists(path) and os.path.getsize(path) > 0
+ if not (os.path.exists(path) and os.path.getsize(path) > 0):
+ print("ERROR: render produced no file", file=sys.stderr)
+ return 10
+ return 0
def main():
@@ -674,9 +696,9 @@ Source
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 10
+ rcode = render_still(obj, os.path.abspath(args.output), args.engine)
+ if rcode:
+ return rcode
print(f"rendered still {args.output}")
print("vertex-weight-limit OK")
diff --git a/examples/depsgraph-export/README.md b/examples/depsgraph-export/README.md
index 593c14c..bfbf8dd 100644
--- a/examples/depsgraph-export/README.md
+++ b/examples/depsgraph-export/README.md
@@ -28,3 +28,5 @@ blender --background --python depsgraph_export.py -- --obj exported.obj
It exits non-zero on failure (modifier not applied, or exported count ≠ evaluated count). The
`blender-smoke` workflow runs this check on Blender 4.5 LTS and 5.1: base 8 → evaluated/exported
98 vertices with a 2-level SUBSURF.
+
+The `--output` render path additionally measures framing against the Layer 1 band via `examples/gallery_framing.py` (exit 10 on violation) before writing the still.
diff --git a/examples/depsgraph-export/depsgraph_export.py b/examples/depsgraph-export/depsgraph_export.py
index 5eaa836..14d7062 100644
--- a/examples/depsgraph-export/depsgraph_export.py
+++ b/examples/depsgraph-export/depsgraph_export.py
@@ -14,6 +14,11 @@
"""
import bpy, bmesh, sys, os, math, argparse, tempfile
+# Shared Layer 1 framing measurement (render path only) — see gallery_framing.py
+sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir))
+sys.dont_write_bytecode = True # keep examples/__pycache__ out of the repo tree
+import gallery_framing
+
def build():
bpy.ops.wm.read_factory_settings(use_empty=True)
@@ -148,9 +153,18 @@ def light(name, loc, energy, size, col, rot):
cam_data = bpy.data.cameras.new("Cam")
cam_data.lens = 50.0
cam = bpy.data.objects.new("Cam", cam_data)
- cam.location = (0.0, -7.4, 3.0)
- cam.rotation_euler = (math.radians(76), 0.0, 0.0)
+ # Reframed: the rotated base cube spans ~5.9 units with the evaluated
+ # sphere, wider than the old (0,-7.4,3.0) frame — the cube was amputated
+ # at the left edge. Aim-based framing on the pair's center.
+ cam.location = (0.0, -9.6, 2.6)
scene.collection.objects.link(cam)
+ aim = bpy.data.objects.new("Aim", None)
+ aim.location = (0.0, 0.0, 0.9)
+ scene.collection.objects.link(aim)
+ con = cam.constraints.new('TRACK_TO')
+ con.target = aim
+ con.track_axis = 'TRACK_NEGATIVE_Z'
+ con.up_axis = 'UP_Y'
scene.camera = cam
scene.render.engine = 'CYCLES' if engine == 'cycles' else eevee_engine_id()
@@ -168,8 +182,21 @@ def light(name, loc, energy, size, col, rot):
# AgX would flatten the graphite-vs-green contrast this still hinges on
# (docs/VISUAL-STYLE.md)
scene.view_settings.view_transform = 'Standard'
+ # Layer 1 framing gate (silhouette matte) — exit 10 on violation, before
+ # the beauty render so a defective composition ships no artifact.
+ fcode = gallery_framing.check_framing(
+ scene, cam,
+ hero=[base_obj, obj],
+ elements=[base_obj, obj],
+ stage=[floor, wall],
+ )
+ if fcode:
+ return fcode
bpy.ops.render.render(write_still=True)
- return os.path.exists(path) and os.path.getsize(path) > 0
+ if not (os.path.exists(path) and os.path.getsize(path) > 0):
+ print("ERROR: render produced no file", file=sys.stderr)
+ return 6
+ return 0
def main():
@@ -188,9 +215,9 @@ def main():
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
+ rcode = render_still(obj, os.path.abspath(args.output), args.engine)
+ if rcode:
+ return rcode
print(f"rendered still {args.output}")
print("depsgraph-export OK")
diff --git a/examples/depsgraph-export/preview.webp b/examples/depsgraph-export/preview.webp
index 7f62723..a36165c 100644
Binary files a/examples/depsgraph-export/preview.webp and b/examples/depsgraph-export/preview.webp differ
diff --git a/examples/text-version-stamp/README.md b/examples/text-version-stamp/README.md
index 3d7e684..73f97c8 100644
--- a/examples/text-version-stamp/README.md
+++ b/examples/text-version-stamp/README.md
@@ -38,3 +38,5 @@ non-planar flat text, extrude/bevel closed form off, geometry not regenerating,
`to_mesh_clear()` reference surviving). The `blender-smoke` workflow runs the check on
Blender 4.5 LTS and 5.1. The render scales the stamp to a constant width, so the frame
holds for any version-string length.
+
+The `--output` render path additionally measures framing against the Layer 1 band via `examples/gallery_framing.py` (exit 10 on violation) before writing the still.
diff --git a/examples/text-version-stamp/preview.webp b/examples/text-version-stamp/preview.webp
index b3ceba7..315e360 100644
Binary files a/examples/text-version-stamp/preview.webp and b/examples/text-version-stamp/preview.webp differ
diff --git a/examples/text-version-stamp/text_version_stamp.py b/examples/text-version-stamp/text_version_stamp.py
index 730685f..28bf92c 100644
--- a/examples/text-version-stamp/text_version_stamp.py
+++ b/examples/text-version-stamp/text_version_stamp.py
@@ -23,6 +23,11 @@
"""
import bpy, sys, os, math, argparse
+# Shared Layer 1 framing measurement (render path only) — see gallery_framing.py
+sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir))
+sys.dont_write_bytecode = True # keep examples/__pycache__ out of the repo tree
+import gallery_framing
+
EXTRUDE = 0.06
BEVEL = 0.02
TOL = 1e-4
@@ -230,9 +235,18 @@ def light(name, loc, energy, size, col, rot):
cam_data = bpy.data.cameras.new("Cam")
cam_data.lens = 50.0
cam = bpy.data.objects.new("Cam", cam_data)
- cam.location = (0.0, -8.5, 1.15)
- cam.rotation_euler = (math.radians(86.5), 0.0, 0.0)
+ # Reframed: the old (0,-8.5,1.15) fixed 86.5° pitch left the stamp at
+ # 0.663 fill with a dead lower third; moved in with an aim on the stamp's
+ # vertical center so text, caption, and underline bar balance the frame.
+ cam.location = (0.0, -6.0, 1.5)
scene.collection.objects.link(cam)
+ aim = bpy.data.objects.new("Aim", None)
+ aim.location = (0.0, 0.0, 1.05)
+ scene.collection.objects.link(aim)
+ con = cam.constraints.new('TRACK_TO')
+ con.target = aim
+ con.track_axis = 'TRACK_NEGATIVE_Z'
+ con.up_axis = 'UP_Y'
scene.camera = cam
scene.render.engine = 'CYCLES' if engine == 'cycles' else eevee_engine_id()
@@ -247,8 +261,22 @@ def light(name, loc, energy, size, col, rot):
scene.render.resolution_y = 720
scene.render.image_settings.file_format = 'PNG'
scene.render.filepath = path
+ # Layer 1 framing gate (silhouette matte) — exit 10 on violation, before
+ # the beauty render so a defective composition ships no artifact. The hero
+ # is the version stamp; caption and underline bar count for margins.
+ fcode = gallery_framing.check_framing(
+ scene, cam,
+ hero=[obj],
+ elements=[obj, cap_obj, bar],
+ stage=[floor, wall],
+ )
+ if fcode:
+ return fcode
bpy.ops.render.render(write_still=True)
- return os.path.exists(path) and os.path.getsize(path) > 0
+ if not (os.path.exists(path) and os.path.getsize(path) > 0):
+ print("ERROR: render produced no file", file=sys.stderr)
+ return 9
+ return 0
def main():
@@ -265,9 +293,9 @@ def main():
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 9
+ rcode = render_still(obj, os.path.abspath(args.output), args.engine)
+ if rcode:
+ return rcode
print(f"rendered still {args.output}")
print("text-version-stamp OK")
diff --git a/examples/triangulate-tangents/README.md b/examples/triangulate-tangents/README.md
index 09bce5f..5ea6729 100644
--- a/examples/triangulate-tangents/README.md
+++ b/examples/triangulate-tangents/README.md
@@ -68,3 +68,5 @@ It exits non-zero on failure (topology drift, reallocated UV layer,
non-orthonormal basis, bitangent-convention drift, formula excursion, or a
flip inside a smooth field). The `blender-smoke` workflow runs the check on
Blender 4.5 LTS and 5.1.
+
+The `--output` render path additionally measures framing against the Layer 1 band via `examples/gallery_framing.py` (exit 10 on violation) before writing the still.
diff --git a/examples/triangulate-tangents/preview.webp b/examples/triangulate-tangents/preview.webp
index 6d449a0..d59fef1 100644
Binary files a/examples/triangulate-tangents/preview.webp and b/examples/triangulate-tangents/preview.webp differ
diff --git a/examples/triangulate-tangents/triangulate_tangents.py b/examples/triangulate-tangents/triangulate_tangents.py
index 907c4e2..c91af01 100644
--- a/examples/triangulate-tangents/triangulate_tangents.py
+++ b/examples/triangulate-tangents/triangulate_tangents.py
@@ -36,6 +36,11 @@
import bpy, bmesh, sys, os, math, argparse
import mathutils
+# Shared Layer 1 framing measurement (render path only) — see gallery_framing.py
+sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir))
+sys.dont_write_bytecode = True # keep examples/__pycache__ out of the repo tree
+import gallery_framing
+
SIDES = 48 # buckler lathe resolution
RINGS = 6 # dome profile rings (closed form below)
BOSS_R = 1.0
@@ -364,19 +369,22 @@ def light(name, loc, energy, size, col, rot):
# shaped warm key, faint cool fill, cool rim, warm wedge on the back wall
# (docs/VISUAL-STYLE.md); the glint is the anisotropic streak's driver
- light("Key", (-4.0, -5.0, 6.0), 500.0, 4.5, (1.0, 0.96, 0.9), (48, 0, -38))
+ light("Key", (-4.0, -5.0, 6.0), 320.0, 4.5, (1.0, 0.96, 0.9), (48, 0, -38))
light("Fill", (5.0, -4.0, 3.0), 100.0, 9.0, (0.75, 0.85, 1.0), (62, 0, 50))
light("Rim", (0.5, 4.5, 5.0), 340.0, 4.0, (0.6, 0.78, 1.0), (-55, 0, 175))
light("Wedge", (2.5, 3.5, 4.2), 480.0, 6.0, (1.0, 0.76, 0.5), (-72, 0, 195))
- light("Glint", (1.5, -5.5, 5.5), 400.0, 1.2, (1.0, 0.9, 0.75), (42, 0, 15))
+ light("Glint", (1.5, -5.5, 5.5), 150.0, 1.2, (1.0, 0.9, 0.75), (42, 0, 15))
cam_data = bpy.data.cameras.new("Cam")
cam_data.lens = 55.0
cam = bpy.data.objects.new("Cam", cam_data)
- cam.location = (0.6, -8.8, 2.8)
+ # Reframed: the old (0.6,-8.8,2.8) left the buckler at 0.334 fill adrift
+ # in the stage; moved in ~2.2x so the brushed-steel tangent sweep fills
+ # the frame — the grooves are the tangent-field evidence.
+ cam.location = (0.6, -6.35, 2.62)
scene.collection.objects.link(cam)
target = bpy.data.objects.new("Aim", None)
- target.location = (0.0, 0.0, 1.35)
+ target.location = (0.0, 0.0, 1.42)
scene.collection.objects.link(target)
con = cam.constraints.new('TRACK_TO')
con.target = target
@@ -396,8 +404,22 @@ def light(name, loc, energy, size, col, rot):
scene.render.filepath = path
# AgX would wash the steel toward chalk (docs/VISUAL-STYLE.md)
scene.view_settings.view_transform = 'Standard'
+ # Layer 1 framing gate (silhouette matte) — exit 10 on violation, before
+ # the beauty render so a defective composition ships no artifact.
+ stand_ob = scene.objects.get("Stand")
+ fcode = gallery_framing.check_framing(
+ scene, cam,
+ hero=[obj],
+ elements=[obj] + ([stand_ob] if stand_ob else []),
+ stage=[floor, wall],
+ )
+ if fcode:
+ return fcode
bpy.ops.render.render(write_still=True)
- return os.path.exists(path) and os.path.getsize(path) > 0
+ if not (os.path.exists(path) and os.path.getsize(path) > 0):
+ print("ERROR: render produced no file", file=sys.stderr)
+ return 10
+ return 0
def main():
@@ -415,9 +437,9 @@ def main():
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 10
+ rcode = render_still(obj, os.path.abspath(args.output), args.engine)
+ if rcode:
+ return rcode
print(f"rendered still {args.output}")
print("triangulate-tangents OK")
diff --git a/examples/vertex-weight-limit/README.md b/examples/vertex-weight-limit/README.md
index 7fb7ea1..6af677c 100644
--- a/examples/vertex-weight-limit/README.md
+++ b/examples/vertex-weight-limit/README.md
@@ -57,3 +57,5 @@ blender --background --python vertex_weight_limit.py -- --output arm.png --engin
It exits non-zero on failure (vacuous authoring, a vertex over the cap, broken
weight sums, pose damaged by pruning, LBS drift, or a moved Root mount). The
`blender-smoke` workflow runs the check on Blender 4.5 LTS and 5.1.
+
+The `--output` render path additionally measures framing against the Layer 1 band via `examples/gallery_framing.py` (exit 10 on violation) before writing the still.
diff --git a/examples/vertex-weight-limit/preview.webp b/examples/vertex-weight-limit/preview.webp
index bbe4261..b9e4434 100644
Binary files a/examples/vertex-weight-limit/preview.webp and b/examples/vertex-weight-limit/preview.webp differ
diff --git a/examples/vertex-weight-limit/vertex_weight_limit.py b/examples/vertex-weight-limit/vertex_weight_limit.py
index a1ad562..3348a33 100644
--- a/examples/vertex-weight-limit/vertex_weight_limit.py
+++ b/examples/vertex-weight-limit/vertex_weight_limit.py
@@ -32,6 +32,11 @@
import bpy, bmesh, sys, os, math, argparse
import mathutils
+# Shared Layer 1 framing measurement (render path only) — see gallery_framing.py
+sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir))
+sys.dont_write_bytecode = True # keep examples/__pycache__ out of the repo tree
+import gallery_framing
+
SIDES = 32
MAX_INFLUENCES = 4 # the engine constraint being witnessed
BUMP_R = 1.8 # flex-cuff blend support; guarantees 5 pre-limit
@@ -419,10 +424,13 @@ def light(name, loc, energy, size, col, rot):
cam_data = bpy.data.cameras.new("Cam")
cam_data.lens = 52.0
cam = bpy.data.objects.new("Cam", cam_data)
- cam.location = (5.8, -6.9, 2.2)
+ # Reframed: the old (5.8,-6.9,2.2) aim 1.5 cropped the base at the bottom
+ # edge and left the subject adrift right-of-center; slightly more frontal
+ # and lower-aimed so the full base and the lean both sit in frame.
+ cam.location = (4.6, -8.2, 2.3)
scene.collection.objects.link(cam)
target = bpy.data.objects.new("Aim", None)
- target.location = (0.45, 0.0, 1.5)
+ target.location = (0.35, 0.0, 1.25)
scene.collection.objects.link(target)
con = cam.constraints.new('TRACK_TO')
con.target = target
@@ -443,8 +451,21 @@ def light(name, loc, energy, size, col, rot):
# AgX would wash the hazard orange and teal accent toward pastel
# (docs/VISUAL-STYLE.md)
scene.view_settings.view_transform = 'Standard'
+ # Layer 1 framing gate (silhouette matte) — exit 10 on violation, before
+ # the beauty render so a defective composition ships no artifact.
+ fcode = gallery_framing.check_framing(
+ scene, cam,
+ hero=[obj],
+ elements=[obj],
+ stage=[floor, wall],
+ )
+ if fcode:
+ return fcode
bpy.ops.render.render(write_still=True)
- return os.path.exists(path) and os.path.getsize(path) > 0
+ if not (os.path.exists(path) and os.path.getsize(path) > 0):
+ print("ERROR: render produced no file", file=sys.stderr)
+ return 10
+ return 0
def main():
@@ -468,9 +489,9 @@ def main():
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 10
+ rcode = render_still(obj, os.path.abspath(args.output), args.engine)
+ if rcode:
+ return rcode
print(f"rendered still {args.output}")
print("vertex-weight-limit OK")