diff --git a/ROADMAP.md b/ROADMAP.md index dfb14fa..45e8ebd 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 defect worklist (from the #107 survey; restage + re-render into the 0.70–0.90 band with all margins ≥ 0.02, then wire `examples/gallery_framing.py`'s `check_framing`): **depsgraph-export** (fill 0.984x over; left cube clipped by left+bottom edges), **driver-wave** (0.969x over; side margins 0.016), **shader-node-group** (0.969x over; side margins 0.016), **swatch-grid** (0.944y over, marginal), **gp-lineart-contour** (0.978y over; glass base cropped at bottom edge), **png-exr-alpha** (display plinth touches bottom edge, minor), **vertex-weight-limit** (base cropped at bottom edge; x-fill 0.247 with dead space), **text-version-stamp** (max fill 0.663 under; dead lower third), **triangulate-tangents** (0.334x/0.661y under; dead space both sides), **curve-bevel-arc** (right cap margin 0.012 < 0.02, marginal). Triaged as documented deviations instead (README `Framing deviation` notes, no restage): damped-track-aim (radiating composition, calibration member) and wave-displace (edge-to-edge field); sky-texture-sun-elevation is a measurement artifact (world-contract subject — report-only if wired); vse-cut-list is in scope via its `Bay` scene/camera pair. +- 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). ## Future (uncommitted) diff --git a/docs/gallery/assets/curve-bevel-arc-hero.webp b/docs/gallery/assets/curve-bevel-arc-hero.webp index c41e2a7..ee2ac31 100644 Binary files a/docs/gallery/assets/curve-bevel-arc-hero.webp and b/docs/gallery/assets/curve-bevel-arc-hero.webp differ diff --git a/docs/gallery/assets/driver-wave-hero.webp b/docs/gallery/assets/driver-wave-hero.webp index 70bf5f6..13f8e77 100644 Binary files a/docs/gallery/assets/driver-wave-hero.webp and b/docs/gallery/assets/driver-wave-hero.webp differ diff --git a/docs/gallery/assets/gp-lineart-contour-hero.webp b/docs/gallery/assets/gp-lineart-contour-hero.webp index 92a46d6..efde3f8 100644 Binary files a/docs/gallery/assets/gp-lineart-contour-hero.webp and b/docs/gallery/assets/gp-lineart-contour-hero.webp differ diff --git a/docs/gallery/assets/png-exr-alpha-hero.webp b/docs/gallery/assets/png-exr-alpha-hero.webp index 349208d..e439f8b 100644 Binary files a/docs/gallery/assets/png-exr-alpha-hero.webp and b/docs/gallery/assets/png-exr-alpha-hero.webp differ diff --git a/docs/gallery/assets/shader-node-group-hero.webp b/docs/gallery/assets/shader-node-group-hero.webp index c6b802c..4fb7dad 100644 Binary files a/docs/gallery/assets/shader-node-group-hero.webp and b/docs/gallery/assets/shader-node-group-hero.webp differ diff --git a/docs/gallery/assets/swatch-grid-hero.webp b/docs/gallery/assets/swatch-grid-hero.webp index 785e11e..7e0f7ad 100644 Binary files a/docs/gallery/assets/swatch-grid-hero.webp and b/docs/gallery/assets/swatch-grid-hero.webp differ diff --git a/docs/gallery/assets/vse-cut-list-hero.webp b/docs/gallery/assets/vse-cut-list-hero.webp index 5577bab..a49bbdc 100644 Binary files a/docs/gallery/assets/vse-cut-list-hero.webp and b/docs/gallery/assets/vse-cut-list-hero.webp differ diff --git a/docs/gallery/contact-sheets/curve-bevel-arc-contact-sheet.webp b/docs/gallery/contact-sheets/curve-bevel-arc-contact-sheet.webp new file mode 100644 index 0000000..1feada6 Binary files /dev/null and b/docs/gallery/contact-sheets/curve-bevel-arc-contact-sheet.webp differ diff --git a/docs/gallery/contact-sheets/driver-wave-contact-sheet.webp b/docs/gallery/contact-sheets/driver-wave-contact-sheet.webp new file mode 100644 index 0000000..f8f3aa1 Binary files /dev/null and b/docs/gallery/contact-sheets/driver-wave-contact-sheet.webp differ diff --git a/docs/gallery/contact-sheets/gp-lineart-contour-contact-sheet.webp b/docs/gallery/contact-sheets/gp-lineart-contour-contact-sheet.webp index 16cf01e..ec50717 100644 Binary files a/docs/gallery/contact-sheets/gp-lineart-contour-contact-sheet.webp and b/docs/gallery/contact-sheets/gp-lineart-contour-contact-sheet.webp differ diff --git a/docs/gallery/contact-sheets/png-exr-alpha-contact-sheet.webp b/docs/gallery/contact-sheets/png-exr-alpha-contact-sheet.webp index 05f2f8d..5621b46 100644 Binary files a/docs/gallery/contact-sheets/png-exr-alpha-contact-sheet.webp and b/docs/gallery/contact-sheets/png-exr-alpha-contact-sheet.webp differ diff --git a/docs/gallery/contact-sheets/shader-node-group-contact-sheet.webp b/docs/gallery/contact-sheets/shader-node-group-contact-sheet.webp new file mode 100644 index 0000000..c23adfa Binary files /dev/null and b/docs/gallery/contact-sheets/shader-node-group-contact-sheet.webp differ diff --git a/docs/gallery/contact-sheets/swatch-grid-contact-sheet.webp b/docs/gallery/contact-sheets/swatch-grid-contact-sheet.webp new file mode 100644 index 0000000..cde317c Binary files /dev/null and b/docs/gallery/contact-sheets/swatch-grid-contact-sheet.webp differ diff --git a/docs/gallery/contact-sheets/vse-cut-list-contact-sheet.webp b/docs/gallery/contact-sheets/vse-cut-list-contact-sheet.webp index fb9a836..55d8d11 100644 Binary files a/docs/gallery/contact-sheets/vse-cut-list-contact-sheet.webp and b/docs/gallery/contact-sheets/vse-cut-list-contact-sheet.webp differ diff --git a/docs/gallery/curve-bevel-arc/index.html b/docs/gallery/curve-bevel-arc/index.html index a43758c..4f9641c 100644 --- a/docs/gallery/curve-bevel-arc/index.html +++ b/docs/gallery/curve-bevel-arc/index.html @@ -193,6 +193,7 @@

Run

blender --background --python curve_bevel_arc.py -- --output arc.png blender --background --python curve_bevel_arc.py -- --output arc.png --engine cycles

It exits non-zero on failure (wrong point count, bevel, caps, topology, or span). 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

@@ -219,6 +220,11 @@

Source

""" import bpy, bmesh, 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 + N_POINTS = 8 RADIUS = 1.5 BEVEL = 0.15 @@ -383,7 +389,7 @@

Source

cam_data = bpy.data.cameras.new("Cam") cam_data.lens = 50.0 cam = bpy.data.objects.new("Cam", cam_data) - cam.location = (2.4, -3.4, 1.7) + cam.location = (2.55, -3.62, 1.78) scene.collection.objects.link(cam) scene.camera = cam track = cam.constraints.new('TRACK_TO') @@ -405,8 +411,21 @@

Source

scene.render.filepath = path # AgX would wash the rose tube toward salmon (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 11 + return 0 def main(): @@ -423,9 +442,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 11 + rcode = render_still(obj, os.path.abspath(args.output), args.engine) + if rcode: + return rcode print(f"rendered still {args.output}") print("curve-bevel-arc OK") diff --git a/docs/gallery/driver-wave/index.html b/docs/gallery/driver-wave/index.html index 5f16cf9..666fd38 100644 --- a/docs/gallery/driver-wave/index.html +++ b/docs/gallery/driver-wave/index.html @@ -194,6 +194,7 @@

Run

blender --background --python driver_wave.py -- --output driver.png blender --background --python driver_wave.py -- --output driver.png --engine cycles

It exits non-zero on failure (driven value wrong, or the flush-back disagreed). 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

@@ -219,6 +220,11 @@

Source

""" import bpy, bmesh, 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 + COUNT = 16 SPACING = 0.72 BASE = 0.28 @@ -347,7 +353,7 @@

Source

# the frame with a small margin -- the skyline must not clip cam_data = bpy.data.cameras.new("Cam"); cam_data.lens = 42.0 cam = bpy.data.objects.new("Cam", cam_data) - cam.location = (0.0, -13.5, 2.0) + cam.location = (0.0, -15.0, 2.0) cam.rotation_euler = (math.radians(86), 0.0, 0.0) scene.collection.objects.link(cam) scene.camera = cam @@ -366,8 +372,21 @@

Source

scene.render.filepath = path # AgX would wash the orange columns toward tan (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=list(objs), + elements=list(objs), + 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(): @@ -384,9 +403,9 @@

Source

return code if args.output: - if not render_still(objs, os.path.abspath(args.output), args.engine): - print("ERROR: render produced no file", file=sys.stderr) - return 6 + rcode = render_still(objs, os.path.abspath(args.output), args.engine) + if rcode: + return rcode print(f"rendered still {args.output}") print("driver-wave OK") diff --git a/docs/gallery/gp-lineart-contour/index.html b/docs/gallery/gp-lineart-contour/index.html index 5588aa5..cb1f03e 100644 --- a/docs/gallery/gp-lineart-contour/index.html +++ b/docs/gallery/gp-lineart-contour/index.html @@ -195,6 +195,7 @@

Run

# Also render the gallery still: blender --background --python gp_lineart_contour.py -- --output lineart.png

It exits non-zero on failure. 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

@@ -227,6 +228,11 @@

Source

blender --background --python gp_lineart_contour.py -- --output l.png """ import bpy, bmesh, 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 from mathutils import Matrix # Faceted crystal (octahedron-ish) — enough silhouette edges to read at thumbnail @@ -353,7 +359,7 @@

Source

cam_data = bpy.data.cameras.new("Cam") cam_data.lens = 50.0 cam = bpy.data.objects.new("Cam", cam_data) - cam.location = (3.4, -4.2, 2.4) + cam.location = (3.95, -4.9, 2.62) sc.collection.objects.link(cam) sc.camera = cam aim = bpy.data.objects.new("Aim", None) @@ -571,8 +577,27 @@

Source

sc.render.image_settings.file_format = "PNG" sc.render.filepath = path sc.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. The two + # renderable subjects are the crystal source mesh and the GP stroke object. + stage = [o for o in sc.objects if o.name in {"Floor", "Wall"}] + hero = [ + o for o in sc.objects + if o.type in {"MESH", "GREASEPENCIL"} and o not in stage + ] + fcode = gallery_framing.check_framing( + sc, sc.camera, + hero=hero, + elements=hero, + stage=stage, + ) + 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 8 + return 0 def build_scene(): @@ -604,9 +629,9 @@

Source

return code if args.output: - if not render_still(sc, os.path.abspath(args.output), args.engine): - print("ERROR: render produced no file", file=sys.stderr) - return 8 + rcode = render_still(sc, os.path.abspath(args.output), args.engine) + if rcode: + return rcode print(f"rendered still {args.output}") print("gp-lineart-contour OK") diff --git a/docs/gallery/png-exr-alpha/index.html b/docs/gallery/png-exr-alpha/index.html index b9fc644..47d5c47 100644 --- a/docs/gallery/png-exr-alpha/index.html +++ b/docs/gallery/png-exr-alpha/index.html @@ -199,6 +199,7 @@

Run

blender --background --python png_exr_alpha.py -- --output alpha.png blender --background --python png_exr_alpha.py -- --output alpha.png --engine cycles

It exits non-zero on failure and prints every measured error and tolerance on success, so CI logs carry the numbers. 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

@@ -231,6 +232,11 @@

Source

import os import math import 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 import tempfile import struct @@ -792,11 +798,13 @@

Source

(0.0, 9.0, 2.6)) aim = bpy.data.objects.new("Aim", None) - aim.location = (-0.10, 0.2, 1.65) + aim.location = (-0.10, 0.2, 1.38) scene.collection.objects.link(aim) # Gentle 3/4 from the right: both faces stay fully readable while the - # bezels, struts, and plinth show their thickness. + # bezels, struts, and plinth show their thickness. Aim dropped 0.23 from + # the original 1.65 so the plinth clears the bottom edge (framing gate) + # without changing the distance or the composition. cam_data = bpy.data.cameras.new("Cam") cam_data.lens = 50.0 cam = bpy.data.objects.new("Cam", cam_data) @@ -822,8 +830,26 @@

Source

scene.render.filepath = path # AgX desaturates the card toward pastel — Standard keeps the clamp visible. 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. + hero = [ + o for o in scene.objects + if o.name.startswith(("PngDisplay", "ExrDisplay")) and o.type in {"MESH", "FONT"} + ] + plinth = scene.objects.get("Plinth") + fcode = gallery_framing.check_framing( + scene, cam, + hero=hero, + elements=hero + ([plinth] if plinth 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 13 + return 0 def main(): @@ -841,8 +867,11 @@

Source

return code if args.output: bpy.ops.wm.read_factory_settings(use_empty=True) - if not render_still(os.path.abspath(args.output), args.engine): + rcode = render_still(os.path.abspath(args.output), args.engine) + if rcode == 13: return fail(f"render produced no file at {args.output}", 13) + if rcode: + return rcode print(f"wrote {args.output}") return 0 diff --git a/docs/gallery/shader-node-group/index.html b/docs/gallery/shader-node-group/index.html index 5bdb17b..b6f8a01 100644 --- a/docs/gallery/shader-node-group/index.html +++ b/docs/gallery/shader-node-group/index.html @@ -193,6 +193,7 @@

Run

blender --background --python shader_node_group.py -- --output spheres.png blender --background --python shader_node_group.py -- --output spheres.png --engine cycles

It exits non-zero on failure (missing interface sockets, unshared group, or identical instance parameters). 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

@@ -218,6 +219,11 @@

Source

""" import bpy, bmesh, 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 + TINTS = { "SphereA": (0.012, 0.32, 0.30, 1.0), # teal "SphereB": (0.42, 0.02, 0.20, 1.0), # magenta @@ -356,7 +362,7 @@

Source

cam_data = bpy.data.cameras.new("Cam") cam_data.lens = 58.0 cam = bpy.data.objects.new("Cam", cam_data) - cam.location = (0.0, -7.8, 2.6) + cam.location = (0.0, -8.7, 2.6) cam.rotation_euler = (math.radians(78), 0.0, 0.0) scene.collection.objects.link(cam) scene.camera = cam @@ -373,8 +379,21 @@

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. + fcode = gallery_framing.check_framing( + scene, cam, + hero=list(objs), + elements=list(objs), + 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 7 + return 0 def main(): @@ -391,9 +410,9 @@

Source

return code if args.output: - if not render_still(objs, os.path.abspath(args.output), args.engine): - print("ERROR: render produced no file", file=sys.stderr) - return 7 + rcode = render_still(objs, os.path.abspath(args.output), args.engine) + if rcode: + return rcode print(f"rendered still {args.output}") print("shader-node-group OK") diff --git a/docs/gallery/swatch-grid/index.html b/docs/gallery/swatch-grid/index.html index 9b3eae9..21ed800 100644 --- a/docs/gallery/swatch-grid/index.html +++ b/docs/gallery/swatch-grid/index.html @@ -198,7 +198,7 @@

Run

blender --background --python swatch_grid.py -- --output swatch.png --engine cycles --samples 16 --width 960

The script is deterministic and dependency-light (fixed camera and layout, no HDRI, no network). It exits non-zero on any failure, including a render that comes out uniformly black or without the expected six distinct swatch regions — the same honest check the CI smoke gate runs on both Blender 4.5 LTS and 5.1.

Verified

-

Runs headless on Blender 4.5.10 LTS and 5.1.1; exercised on both by the blender-smoke workflow on every PR and weekly schedule.

+

Runs headless on Blender 4.5.10 LTS and 5.1.1; exercised on both by the blender-smoke workflow on every PR and weekly schedule. 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

@@ -228,6 +228,12 @@

Source

""" import bpy import bmesh + +# Shared Layer 1 framing measurement (render path only) — see gallery_framing.py +import os as _os, sys as _sys +_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 import sys import os import math @@ -329,11 +335,12 @@

Source

i += 1 # ortho camera framed on the grid cells: verify_png() samples the image at # each cell center, so every sphere must stay inside its third/half of the - # frame. ortho height = ortho_scale * 9/16 = 4.39 units for a grid that - # spans 4.14 -- nothing clips, nothing drifts out of its cell. + # frame. ortho height = ortho_scale * 9/16 = 4.73 units for a grid that + # spans 4.14 -- nothing clips, nothing drifts out of its cell, and the + # grid fills 0.876 of the frame height (inside the Layer 1 band). cam_d = bpy.data.cameras.new("cam") cam_d.type = 'ORTHO' - cam_d.ortho_scale = 7.8 + cam_d.ortho_scale = 8.4 cam = bpy.data.objects.new("cam", cam_d) cam.location = (0.0, -10.0, 0.0) cam.rotation_euler = (math.radians(90), 0, 0) @@ -453,6 +460,18 @@

Source

# AgX would desaturate the swatches toward pastel -- exactly the material # colors this example exists to show (docs/VISUAL-STYLE.md) sc.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. + spheres = [o for o in sc.objects if o.type == "MESH" and o.name.startswith("S")] + stage = [o for o in sc.objects if o.name == "Wall"] + fcode = gallery_framing.check_framing( + sc, sc.camera, + hero=spheres, + elements=spheres, + stage=stage, + ) + if fcode: + return fcode os.makedirs(os.path.dirname(os.path.abspath(args.output)) or ".", exist_ok=True) bpy.ops.render.render(write_still=True) if not (os.path.exists(args.output) and os.path.getsize(args.output) > 0): diff --git a/docs/gallery/vse-cut-list/index.html b/docs/gallery/vse-cut-list/index.html index d657c5a..a093374 100644 --- a/docs/gallery/vse-cut-list/index.html +++ b/docs/gallery/vse-cut-list/index.html @@ -204,6 +204,7 @@

Run

# Also render the still (EEVEE on a GPU host; --engine cycles on GPU-less): blender --background --python vse_cut_list.py -- --output vse.png

It exits non-zero on failure and prints every measured value and tolerance on success, so CI logs carry the numbers. The blender-smoke workflow runs the check and the pixel witness 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

@@ -259,6 +260,11 @@

Source

import bpy, sys, os, math, argparse, tempfile, warnings, shutil 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 + IS_5X = bpy.app.version >= (5, 0, 0) # Closed-form cut list, end-exclusive [start, end) spans on the timeline. @@ -724,10 +730,12 @@

Source

cam_data = bpy.data.cameras.new("BayCam") cam_data.lens = 52.0 cam = bpy.data.objects.new("BayCam", cam_data) - cam.location = (2.0, -5.2, 1.72) + # Pulled back from (2.0, -5.2, 1.72) so the bezel top and desk base + # clear the frame edges (framing gate on the Bay scene). + cam.location = (3.05, -7.9, 2.0) sc.collection.objects.link(cam) target = bpy.data.objects.new("BayAim", None) - target.location = (0.0, 0.0, 1.28) + target.location = (0.0, 0.0, 1.20) sc.collection.objects.link(target) con = cam.constraints.new("TRACK_TO") con.target = target @@ -749,8 +757,27 @@

Source

sc.render.filepath = path # Standard keeps both the stage saturated and the on-screen frame true sc.view_settings.view_transform = "Standard" + # Layer 1 framing gate (silhouette matte) on the BAY scene — exit 10 on + # violation, before the beauty render. Renderables here are the desk, + # stand, and monitor assembly; floor/wall are the stage. + stage = [o for o in sc.objects if any(k in o.name.lower() for k in ("floor", "wall"))] + hero = [ + o for o in sc.objects + if o.type in gallery_framing._RENDER_TYPES and not o.hide_render and o not in stage + ] + fcode = gallery_framing.check_framing( + sc, cam, + hero=hero, + elements=hero, + stage=stage, + ) + if fcode: + return fcode bpy.ops.render.render(write_still=True, scene=sc.name) - 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 11 + return 0 def render_still(path, engine): @@ -763,7 +790,7 @@

Source

try: frame_path = os.path.join(tmp, "program.png") if not render_frame(sc, frame_path, engine, RENDER_W, RENDER_H): - return False + return 11 bay = bpy.data.scenes.new("Bay") build_stage(bay) # the house dark studio, lights included build_bay(bay, frame_path) @@ -860,8 +887,11 @@

Source

if args.output: # The round-trip reloaded a .blend; start clean for the still. bpy.ops.wm.read_factory_settings(use_empty=True) - if not render_still(os.path.abspath(args.output), args.engine): + rcode = render_still(os.path.abspath(args.output), args.engine) + if rcode == 11: return fail(f"render produced no file at {args.output}", 11) + if rcode: + return rcode print(f"wrote {args.output}") return 0 diff --git a/examples/curve-bevel-arc/README.md b/examples/curve-bevel-arc/README.md index 3dd2cb9..dcea5b6 100644 --- a/examples/curve-bevel-arc/README.md +++ b/examples/curve-bevel-arc/README.md @@ -24,3 +24,5 @@ blender --background --python curve_bevel_arc.py -- --output arc.png --engine cy It exits non-zero on failure (wrong point count, bevel, caps, topology, or span). 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/curve-bevel-arc/curve_bevel_arc.py b/examples/curve-bevel-arc/curve_bevel_arc.py index 7ffe57a..2046059 100644 --- a/examples/curve-bevel-arc/curve_bevel_arc.py +++ b/examples/curve-bevel-arc/curve_bevel_arc.py @@ -17,6 +17,11 @@ """ import bpy, bmesh, 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 + N_POINTS = 8 RADIUS = 1.5 BEVEL = 0.15 @@ -181,7 +186,7 @@ def light(name, loc, energy, size, col): cam_data = bpy.data.cameras.new("Cam") cam_data.lens = 50.0 cam = bpy.data.objects.new("Cam", cam_data) - cam.location = (2.4, -3.4, 1.7) + cam.location = (2.55, -3.62, 1.78) scene.collection.objects.link(cam) scene.camera = cam track = cam.constraints.new('TRACK_TO') @@ -203,8 +208,21 @@ def light(name, loc, energy, size, col): scene.render.filepath = path # AgX would wash the rose tube toward salmon (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 11 + return 0 def main(): @@ -221,9 +239,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 11 + rcode = render_still(obj, os.path.abspath(args.output), args.engine) + if rcode: + return rcode print(f"rendered still {args.output}") print("curve-bevel-arc OK") diff --git a/examples/curve-bevel-arc/preview.webp b/examples/curve-bevel-arc/preview.webp index d4c7e37..8e9d03c 100644 Binary files a/examples/curve-bevel-arc/preview.webp and b/examples/curve-bevel-arc/preview.webp differ diff --git a/examples/driver-wave/README.md b/examples/driver-wave/README.md index e8caaee..76444d1 100644 --- a/examples/driver-wave/README.md +++ b/examples/driver-wave/README.md @@ -28,3 +28,5 @@ blender --background --python driver_wave.py -- --output driver.png --engine cyc It exits non-zero on failure (driven value wrong, or the flush-back disagreed). 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/driver-wave/driver_wave.py b/examples/driver-wave/driver_wave.py index f7a4f0f..f1f0b7a 100644 --- a/examples/driver-wave/driver_wave.py +++ b/examples/driver-wave/driver_wave.py @@ -16,6 +16,11 @@ """ import bpy, bmesh, 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 + COUNT = 16 SPACING = 0.72 BASE = 0.28 @@ -144,7 +149,7 @@ def render_still(objs, path, engine): # the frame with a small margin -- the skyline must not clip cam_data = bpy.data.cameras.new("Cam"); cam_data.lens = 42.0 cam = bpy.data.objects.new("Cam", cam_data) - cam.location = (0.0, -13.5, 2.0) + cam.location = (0.0, -15.0, 2.0) cam.rotation_euler = (math.radians(86), 0.0, 0.0) scene.collection.objects.link(cam) scene.camera = cam @@ -163,8 +168,21 @@ def render_still(objs, path, engine): scene.render.filepath = path # AgX would wash the orange columns toward tan (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=list(objs), + elements=list(objs), + 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(): @@ -181,9 +199,9 @@ def main(): return code if args.output: - if not render_still(objs, os.path.abspath(args.output), args.engine): - print("ERROR: render produced no file", file=sys.stderr) - return 6 + rcode = render_still(objs, os.path.abspath(args.output), args.engine) + if rcode: + return rcode print(f"rendered still {args.output}") print("driver-wave OK") diff --git a/examples/driver-wave/preview.webp b/examples/driver-wave/preview.webp index 6922999..81f7a3d 100644 Binary files a/examples/driver-wave/preview.webp and b/examples/driver-wave/preview.webp differ diff --git a/examples/gallery_framing.py b/examples/gallery_framing.py index b39bb47..3049662 100644 --- a/examples/gallery_framing.py +++ b/examples/gallery_framing.py @@ -223,7 +223,9 @@ def _matte_render(scene, camera, path, width, height): scene.camera = camera if saved["samples"] is not None: scene.eevee.taa_render_samples = 8 - bpy.ops.render.render(write_still=True) + # Render the passed scene, not whatever happens to be active — + # examples may still from a non-context scene (vse-cut-list's Bay). + bpy.ops.render.render(write_still=True, scene=scene.name) finally: rd.engine = saved["engine"] rd.resolution_x, rd.resolution_y, rd.resolution_percentage = saved["res"] diff --git a/examples/gp-lineart-contour/README.md b/examples/gp-lineart-contour/README.md index 4728765..b7c2e99 100644 --- a/examples/gp-lineart-contour/README.md +++ b/examples/gp-lineart-contour/README.md @@ -42,3 +42,5 @@ blender --background --python gp_lineart_contour.py -- --output lineart.png It exits non-zero on failure. 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/gp-lineart-contour/gp_lineart_contour.py b/examples/gp-lineart-contour/gp_lineart_contour.py index 9497285..f155cad 100644 --- a/examples/gp-lineart-contour/gp_lineart_contour.py +++ b/examples/gp-lineart-contour/gp_lineart_contour.py @@ -23,6 +23,11 @@ blender --background --python gp_lineart_contour.py -- --output l.png """ import bpy, bmesh, 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 from mathutils import Matrix # Faceted crystal (octahedron-ish) — enough silhouette edges to read at thumbnail @@ -149,7 +154,7 @@ def setup_camera(sc): cam_data = bpy.data.cameras.new("Cam") cam_data.lens = 50.0 cam = bpy.data.objects.new("Cam", cam_data) - cam.location = (3.4, -4.2, 2.4) + cam.location = (3.95, -4.9, 2.62) sc.collection.objects.link(cam) sc.camera = cam aim = bpy.data.objects.new("Aim", None) @@ -367,8 +372,27 @@ def render_still(sc, path, engine): sc.render.image_settings.file_format = "PNG" sc.render.filepath = path sc.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. The two + # renderable subjects are the crystal source mesh and the GP stroke object. + stage = [o for o in sc.objects if o.name in {"Floor", "Wall"}] + hero = [ + o for o in sc.objects + if o.type in {"MESH", "GREASEPENCIL"} and o not in stage + ] + fcode = gallery_framing.check_framing( + sc, sc.camera, + hero=hero, + elements=hero, + stage=stage, + ) + 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 8 + return 0 def build_scene(): @@ -400,9 +424,9 @@ def main(): return code if args.output: - if not render_still(sc, os.path.abspath(args.output), args.engine): - print("ERROR: render produced no file", file=sys.stderr) - return 8 + rcode = render_still(sc, os.path.abspath(args.output), args.engine) + if rcode: + return rcode print(f"rendered still {args.output}") print("gp-lineart-contour OK") diff --git a/examples/gp-lineart-contour/preview.webp b/examples/gp-lineart-contour/preview.webp index e2cea13..192f676 100644 Binary files a/examples/gp-lineart-contour/preview.webp and b/examples/gp-lineart-contour/preview.webp differ diff --git a/examples/png-exr-alpha/README.md b/examples/png-exr-alpha/README.md index 0fcc875..3b5bf28 100644 --- a/examples/png-exr-alpha/README.md +++ b/examples/png-exr-alpha/README.md @@ -71,3 +71,5 @@ blender --background --python png_exr_alpha.py -- --output alpha.png --engine cy It exits non-zero on failure and prints every measured error and tolerance on success, so CI logs carry the numbers. 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/png-exr-alpha/png_exr_alpha.py b/examples/png-exr-alpha/png_exr_alpha.py index baa1a1a..f9b961d 100644 --- a/examples/png-exr-alpha/png_exr_alpha.py +++ b/examples/png-exr-alpha/png_exr_alpha.py @@ -23,6 +23,11 @@ import os import math import 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 import tempfile import struct @@ -584,11 +589,13 @@ def light(name, loc, energy, size, col, target): (0.0, 9.0, 2.6)) aim = bpy.data.objects.new("Aim", None) - aim.location = (-0.10, 0.2, 1.65) + aim.location = (-0.10, 0.2, 1.38) scene.collection.objects.link(aim) # Gentle 3/4 from the right: both faces stay fully readable while the - # bezels, struts, and plinth show their thickness. + # bezels, struts, and plinth show their thickness. Aim dropped 0.23 from + # the original 1.65 so the plinth clears the bottom edge (framing gate) + # without changing the distance or the composition. cam_data = bpy.data.cameras.new("Cam") cam_data.lens = 50.0 cam = bpy.data.objects.new("Cam", cam_data) @@ -614,8 +621,26 @@ def light(name, loc, energy, size, col, target): scene.render.filepath = path # AgX desaturates the card toward pastel — Standard keeps the clamp visible. 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. + hero = [ + o for o in scene.objects + if o.name.startswith(("PngDisplay", "ExrDisplay")) and o.type in {"MESH", "FONT"} + ] + plinth = scene.objects.get("Plinth") + fcode = gallery_framing.check_framing( + scene, cam, + hero=hero, + elements=hero + ([plinth] if plinth 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 13 + return 0 def main(): @@ -633,8 +658,11 @@ def main(): return code if args.output: bpy.ops.wm.read_factory_settings(use_empty=True) - if not render_still(os.path.abspath(args.output), args.engine): + rcode = render_still(os.path.abspath(args.output), args.engine) + if rcode == 13: return fail(f"render produced no file at {args.output}", 13) + if rcode: + return rcode print(f"wrote {args.output}") return 0 diff --git a/examples/png-exr-alpha/preview.webp b/examples/png-exr-alpha/preview.webp index ddfbe10..d93f2b5 100644 Binary files a/examples/png-exr-alpha/preview.webp and b/examples/png-exr-alpha/preview.webp differ diff --git a/examples/shader-node-group/README.md b/examples/shader-node-group/README.md index f4ee7a9..38c604f 100644 --- a/examples/shader-node-group/README.md +++ b/examples/shader-node-group/README.md @@ -25,3 +25,5 @@ blender --background --python shader_node_group.py -- --output spheres.png --eng It exits non-zero on failure (missing interface sockets, unshared group, or identical instance parameters). 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/shader-node-group/preview.webp b/examples/shader-node-group/preview.webp index 4e100ab..93a11ef 100644 Binary files a/examples/shader-node-group/preview.webp and b/examples/shader-node-group/preview.webp differ diff --git a/examples/shader-node-group/shader_node_group.py b/examples/shader-node-group/shader_node_group.py index bf040b6..8374780 100644 --- a/examples/shader-node-group/shader_node_group.py +++ b/examples/shader-node-group/shader_node_group.py @@ -16,6 +16,11 @@ """ import bpy, bmesh, 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 + TINTS = { "SphereA": (0.012, 0.32, 0.30, 1.0), # teal "SphereB": (0.42, 0.02, 0.20, 1.0), # magenta @@ -154,7 +159,7 @@ def light(name, loc, energy, size, col, rot): cam_data = bpy.data.cameras.new("Cam") cam_data.lens = 58.0 cam = bpy.data.objects.new("Cam", cam_data) - cam.location = (0.0, -7.8, 2.6) + cam.location = (0.0, -8.7, 2.6) cam.rotation_euler = (math.radians(78), 0.0, 0.0) scene.collection.objects.link(cam) scene.camera = cam @@ -171,8 +176,21 @@ 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. + fcode = gallery_framing.check_framing( + scene, cam, + hero=list(objs), + elements=list(objs), + 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 7 + return 0 def main(): @@ -189,9 +207,9 @@ def main(): return code if args.output: - if not render_still(objs, os.path.abspath(args.output), args.engine): - print("ERROR: render produced no file", file=sys.stderr) - return 7 + rcode = render_still(objs, os.path.abspath(args.output), args.engine) + if rcode: + return rcode print(f"rendered still {args.output}") print("shader-node-group OK") diff --git a/examples/swatch-grid/README.md b/examples/swatch-grid/README.md index 7d4bbe8..65121bd 100644 --- a/examples/swatch-grid/README.md +++ b/examples/swatch-grid/README.md @@ -40,3 +40,4 @@ smoke gate runs on both Blender 4.5 LTS and 5.1. Runs headless on **Blender 4.5.10 LTS** and **5.1.1**; exercised on both by the `blender-smoke` workflow on every PR and weekly schedule. +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/swatch-grid/preview.webp b/examples/swatch-grid/preview.webp index 45e49d6..f9da58d 100644 Binary files a/examples/swatch-grid/preview.webp and b/examples/swatch-grid/preview.webp differ diff --git a/examples/swatch-grid/swatch_grid.py b/examples/swatch-grid/swatch_grid.py index f9c115b..a1997d5 100644 --- a/examples/swatch-grid/swatch_grid.py +++ b/examples/swatch-grid/swatch_grid.py @@ -20,6 +20,12 @@ """ import bpy import bmesh + +# Shared Layer 1 framing measurement (render path only) — see gallery_framing.py +import os as _os, sys as _sys +_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 import sys import os import math @@ -121,11 +127,12 @@ def build_scene(mats): i += 1 # ortho camera framed on the grid cells: verify_png() samples the image at # each cell center, so every sphere must stay inside its third/half of the - # frame. ortho height = ortho_scale * 9/16 = 4.39 units for a grid that - # spans 4.14 -- nothing clips, nothing drifts out of its cell. + # frame. ortho height = ortho_scale * 9/16 = 4.73 units for a grid that + # spans 4.14 -- nothing clips, nothing drifts out of its cell, and the + # grid fills 0.876 of the frame height (inside the Layer 1 band). cam_d = bpy.data.cameras.new("cam") cam_d.type = 'ORTHO' - cam_d.ortho_scale = 7.8 + cam_d.ortho_scale = 8.4 cam = bpy.data.objects.new("cam", cam_d) cam.location = (0.0, -10.0, 0.0) cam.rotation_euler = (math.radians(90), 0, 0) @@ -245,6 +252,18 @@ def main(): # AgX would desaturate the swatches toward pastel -- exactly the material # colors this example exists to show (docs/VISUAL-STYLE.md) sc.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. + spheres = [o for o in sc.objects if o.type == "MESH" and o.name.startswith("S")] + stage = [o for o in sc.objects if o.name == "Wall"] + fcode = gallery_framing.check_framing( + sc, sc.camera, + hero=spheres, + elements=spheres, + stage=stage, + ) + if fcode: + return fcode os.makedirs(os.path.dirname(os.path.abspath(args.output)) or ".", exist_ok=True) bpy.ops.render.render(write_still=True) if not (os.path.exists(args.output) and os.path.getsize(args.output) > 0): diff --git a/examples/vse-cut-list/README.md b/examples/vse-cut-list/README.md index a54a09d..87076d4 100644 --- a/examples/vse-cut-list/README.md +++ b/examples/vse-cut-list/README.md @@ -116,3 +116,5 @@ blender --background --python vse_cut_list.py -- --output vse.png It exits non-zero on failure and prints every measured value and tolerance on success, so CI logs carry the numbers. The `blender-smoke` workflow runs the check and the pixel witness 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/vse-cut-list/preview.webp b/examples/vse-cut-list/preview.webp index e03381b..3458671 100644 Binary files a/examples/vse-cut-list/preview.webp and b/examples/vse-cut-list/preview.webp differ diff --git a/examples/vse-cut-list/vse_cut_list.py b/examples/vse-cut-list/vse_cut_list.py index 6c72c07..93de80f 100644 --- a/examples/vse-cut-list/vse_cut_list.py +++ b/examples/vse-cut-list/vse_cut_list.py @@ -46,6 +46,11 @@ import bpy, sys, os, math, argparse, tempfile, warnings, shutil 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 + IS_5X = bpy.app.version >= (5, 0, 0) # Closed-form cut list, end-exclusive [start, end) spans on the timeline. @@ -511,10 +516,12 @@ def render_bay(sc, path, engine, w, h): cam_data = bpy.data.cameras.new("BayCam") cam_data.lens = 52.0 cam = bpy.data.objects.new("BayCam", cam_data) - cam.location = (2.0, -5.2, 1.72) + # Pulled back from (2.0, -5.2, 1.72) so the bezel top and desk base + # clear the frame edges (framing gate on the Bay scene). + cam.location = (3.05, -7.9, 2.0) sc.collection.objects.link(cam) target = bpy.data.objects.new("BayAim", None) - target.location = (0.0, 0.0, 1.28) + target.location = (0.0, 0.0, 1.20) sc.collection.objects.link(target) con = cam.constraints.new("TRACK_TO") con.target = target @@ -536,8 +543,27 @@ def render_bay(sc, path, engine, w, h): sc.render.filepath = path # Standard keeps both the stage saturated and the on-screen frame true sc.view_settings.view_transform = "Standard" + # Layer 1 framing gate (silhouette matte) on the BAY scene — exit 10 on + # violation, before the beauty render. Renderables here are the desk, + # stand, and monitor assembly; floor/wall are the stage. + stage = [o for o in sc.objects if any(k in o.name.lower() for k in ("floor", "wall"))] + hero = [ + o for o in sc.objects + if o.type in gallery_framing._RENDER_TYPES and not o.hide_render and o not in stage + ] + fcode = gallery_framing.check_framing( + sc, cam, + hero=hero, + elements=hero, + stage=stage, + ) + if fcode: + return fcode bpy.ops.render.render(write_still=True, scene=sc.name) - 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 11 + return 0 def render_still(path, engine): @@ -550,7 +576,7 @@ def render_still(path, engine): try: frame_path = os.path.join(tmp, "program.png") if not render_frame(sc, frame_path, engine, RENDER_W, RENDER_H): - return False + return 11 bay = bpy.data.scenes.new("Bay") build_stage(bay) # the house dark studio, lights included build_bay(bay, frame_path) @@ -647,8 +673,11 @@ def main(): if args.output: # The round-trip reloaded a .blend; start clean for the still. bpy.ops.wm.read_factory_settings(use_empty=True) - if not render_still(os.path.abspath(args.output), args.engine): + rcode = render_still(os.path.abspath(args.output), args.engine) + if rcode == 11: return fail(f"render produced no file at {args.output}", 11) + if rcode: + return rcode print(f"wrote {args.output}") return 0