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")