diff --git a/CLAUDE.md b/CLAUDE.md index c4dae1f..a8d59c2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -140,7 +140,7 @@ Stage with **explicit paths only** — never `git add -A` or `git add .`. Cursor ## Quality Gates for Example Runs - `docs/VISUAL-STYLE.md` is the **binding** render standard; deviations are defects. -- **Framing gate:** Layer 1 framing is measured, not eyeballed — the example's `--output` render path calls the shared helper `examples/gallery_framing.py` (`check_framing`, exit 10 on violation) before writing the still: hero fill 0.70–0.90 of frame in at least one axis, every element that matters clearing all four edges by ≥ 0.02. The check-only path never invokes it, so smoke runtimes are unaffected. Documented framing deviations (VISUAL-STYLE Layer 1) call `check_framing(..., deviation="reason")` — the helper reports instead of enforcing, and the reason string is required. +- **Framing gate:** Layer 1 framing is measured, not eyeballed — the example's `--output` render path calls the shared helper `examples/gallery_framing.py` (`check_framing`, exit 10 on violation) before writing the still: hero fill 0.70–0.90 of frame in at least one axis, every element that matters clearing all four edges by ≥ 0.02. The check-only path never invokes it, so smoke runtimes are unaffected. The helper measures; call sites enforce. Bleed compositions call `measure_framing_deviation` and assert their own cap at the call site — `check_framing` has no `deviation=` flag. - **Contact-sheet gate:** composite the candidate hero beside the pinned calibration set — currently `armature-bend`, `damped-track-aim`, `bmesh-gear` — commit the composite under `docs/gallery/contact-sheets/`, link it in the PR body, and report per-criterion verdicts (stage darkness, wedge warmth, subject fill, saturation, thumbnail legibility) including mean luminance versus the calibration images. A claim without the committed composite is not acceptable evidence. **This list is the canonical home of the pinned set** — update it here when a new example outclasses a member; `docs/new-example-prompt.md` points here rather than naming members. The longer "calibration references" list in `docs/VISUAL-STYLE.md` is a style reference, not this contact-sheet set. - **Asset-sheet gate (asset-type examples — game props/kits):** composite the hero asset rendered alone (neutral three-quarter view, plain studio lighting, no staging tricks, no labels, no comparison props) beside the pinned asset-quality reference set — currently `collision-hull-proxy`, `custom-normals-shade`, `vertex-weight-limit`, `lod-decimate-chain` — rendered the same way; commit under `docs/gallery/asset-sheets/`, link it in the PR body, and report a verdict. The asset ships only if it is not identifiable as the least-designed object in that lineup — a strong scene can carry a weak model; this gate removes the scene. **This list is the canonical home of the reference set** — update it here when a new asset outclasses a member. The measurable floors behind the gate (naming, material variation, edge treatment) live in `examples/gallery_asset_quality.py` — render path only, same call pattern as `gallery_framing`, exit 11 on violation — with the calibration table and dropped-floor evidence in `docs/VISUAL-STYLE.md` § Asset quality. - **Falsification:** every check must be proven to fail once — break the contract, observe the non-zero exit, restore — with the probe and the measured error reported in the PR body. An assertion that cannot fail witnesses nothing. diff --git a/docs/VISUAL-STYLE.md b/docs/VISUAL-STYLE.md index f83ea95..8bce084 100644 --- a/docs/VISUAL-STYLE.md +++ b/docs/VISUAL-STYLE.md @@ -68,10 +68,11 @@ the default stage. world or the atmosphere rather than an object on a stage. A deviation requires one line in the example README under a `Framing deviation` heading — what the composition requires and why, exactly as stage - deviations do. An undocumented deviation is a defect. Under a documented - deviation the helper reports rather than enforces: call - `check_framing(..., deviation="reason")`, which prints the numbers with - the reason; an empty reason raises, so no deviation is taken silently. + deviations do. An undocumented deviation is a defect. The helper + measures; the call site enforces. `check_framing` always applies the + Layer 1 band (0 / 10). Bleed compositions call + `measure_framing_deviation`, print the numbers, and assert their own + cap with their own exit code. There is no `deviation=` flag. - Camera: a chosen angle, not the default — typically a 45–55 mm lens, slightly above subject height, aimed with a `TRACK_TO` constraint at an empty on the subject. Flat subjects present toward the camera (lean or diff --git a/docs/gallery/modular-kit-snap/index.html b/docs/gallery/modular-kit-snap/index.html index 142766f..d0851e4 100644 --- a/docs/gallery/modular-kit-snap/index.html +++ b/docs/gallery/modular-kit-snap/index.html @@ -267,15 +267,16 @@

modular-kit-snap

What each check catches on failure (probed with an unsnapped variant built by the same code minus the snap pass — 3 mm end-ring skew, 2 mm y-nudge on two verts): boundary verts off the end planes, worst 3.000e-03 m (exit 3); opposing rings displaced 2.000e-03 (exit 4); tiled joint gap/overlap (exit 5); bbox off the declared tile by 3.000e-03 (exit 6); boundary edges torn off the rim (exit 7); non-watertight detail (exit 8); detail escaping the tile (exit 9); unapplied transforms, default names, or wandering origins (exit 11).

Version witness: check output is byte-identical on Blender 4.5.11 LTS and 5.1.2 — same counts, same zero measured deviations.

Render as proof: a four-segment run — the joints vanish. The falsification variant (--falsify) accumulates a 120 mm gap, 50 mm lateral jogs, and 40 mm floor steps at each joint: floor plates split with dark seams and the trim rails visibly break. The check measures the same failure class at 3 mm; the render exaggerates it to read at frame scale. The render path also gates the asset through examples/gallery_asset_quality.py (naming, material variation, edge treatment — exit 11). An earlier revision shipped flat grey wall panels and was remodeled under that gate: each panel is now an assembly (backing plate + inset panel + four bolt heads), the palette shifted teal-slate so the card does not twin with lightmap-uv-channel's warm cart, and mean luminance was brought into the calibration range (82.7 → 66.1, ceiling 77.7) — no luminance deviation needed.

-

Framing deviation: the still is an interior corridor run — the envelope surrounds the camera on five sides and the tiling joints are the proof, so the subject reads as extending past the frame (the radiating-architecture class of VISUAL-STYLE Layer 1). check_framing measures and reports (fill 1.000/1.000, all-edge bleed) without enforcing, with the reason string at the call site.

+

Framing deviation: the still is an interior corridor run — the envelope surrounds the camera on five sides and the tiling joints are the proof, so the subject reads as extending past the frame. The helper measures; this example enforces a projection-space fill-over cap (FRAME_DEV_MAX = 8.0). Gallery camera scores ~5.06 (pass). --close-camera scores ~30.9 and exits 10. Silhouette saturates at fill 1.0, so the cap is measured with strategy="projection".

Run

blender --background --python modular_kit_snap.py --
 blender --background --python modular_kit_snap.py -- --output corridor.png
-blender --background --python modular_kit_snap.py -- --falsify seams.png
+blender --background --python modular_kit_snap.py -- --falsify seams.png +blender --background --python modular_kit_snap.py -- --output close.png --close-camera

Exit codes

-

Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper. 11 is the shared asset-quality helper.

-

| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Boundary vert count or end-plane membership | | 4 | End rings do not partition evenly, or opposing loops differ | | 5 | Tiled instances do not share boundary positions | | 6 | Shell or whole-asset bbox off the declared tile | | 7 | Boundary edge count, non-manifold edges, or rim off the end planes | | 8 | Detail part not watertight | | 9 | Detail part reaches a tile boundary; also --output produced no file | | 11 | Kit part count, unapplied scale, namespace, or origin; also gallery asset-quality violation |

-

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --falsify.

+

Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is this example's framing-deviation cap (call site, not the shared Layer 1 helper). 11 is the shared asset-quality helper.

+

| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Boundary vert count or end-plane membership | | 4 | End rings do not partition evenly, or opposing loops differ | | 5 | Tiled instances do not share boundary positions | | 6 | Shell or whole-asset bbox off the declared tile | | 7 | Boundary edge count, non-manifold edges, or rim off the end planes | | 8 | Detail part not watertight | | 9 | Detail part reaches a tile boundary; also --output produced no file | | 10 | Framing deviation score exceeds cap (--close-camera) | | 11 | Kit part count, unapplied scale, namespace, or origin; also gallery asset-quality violation |

+

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output, --falsify, or --close-camera.

Source

@@ -324,6 +325,7 @@

Source

blender --background --python modular_kit_snap.py -- # check only blender --background --python modular_kit_snap.py -- --output k.png # + render blender --background --python modular_kit_snap.py -- --falsify f.png # seam variant + blender --background --python modular_kit_snap.py -- --output c.png --close-camera # framing 10 """ import bpy, bmesh, sys, os, math, argparse @@ -346,6 +348,12 @@

Source

EXPECT_PARTS = 21 # shell + 20 detail parts; asserted before the # per-part loops below, which assert nothing on # an empty dict +# Projection-space Layer 1 deviation cap. The corridor is supposed to bleed +# (measured fill_over ~5.06 on the gallery camera). --close-camera pushes +# fill_over to ~30. Helper measures; this example enforces. +FRAME_DEV_MAX = 8.0 +CLOSE_CAM_LOC = (-0.15, -0.08, 1.50) +CLOSE_CAM_LENS = 16.0 # Hollow-rectangle profile (y, z): outer shell corners then inner bore corners, # ordered as one continuous ring so the extrusion's side faces come out quads. @@ -832,7 +840,7 @@

Source

return parts -def render_still(path, engine, falsify=False): +def render_still(path, engine, falsify=False, close_camera=False): """A four-segment run. Snapped: joints vanish. Falsified: each segment accumulates a 120 mm x gap, alternating 50 mm y jogs, and 40 mm floor steps, so every joint reads as a seam — the render-scale exaggeration of @@ -857,9 +865,9 @@

Source

bulkhead = build_bulkhead(sc) cam_data = bpy.data.cameras.new("Cam") - cam_data.lens = 25.0 + cam_data.lens = CLOSE_CAM_LENS if close_camera else 25.0 cam = bpy.data.objects.new("Cam", cam_data) - cam.location = (-0.7, -0.42, 1.58) + cam.location = CLOSE_CAM_LOC if close_camera else (-0.7, -0.42, 1.58) sc.collection.objects.link(cam) aim = bpy.data.objects.new("Aim", None) aim.location = (10.0, 0.35, 1.25) @@ -886,19 +894,25 @@

Source

sc.render.filepath = path # Standard, always — AgX would lift the stage toward grey (VISUAL-STYLE) sc.view_settings.view_transform = "Standard" - # Layer 1 framing gate. The corridor surrounds the camera on five sides - # and reads as extending past the frame — a documented deviation class - # (radiating architecture), measured and reported by the helper. - fcode = gallery_framing.check_framing( + # The corridor surrounds the camera — Layer 1 fill/margin will fail, and + # that is the design. The helper only measures; this example owns the + # cap. Projection so the bleed can grow past 1.0 (silhouette saturates). + res, score = gallery_framing.measure_framing_deviation( sc, cam, hero=all_parts, elements=all_parts + bulkhead, stage=[floor, wall], - deviation="interior corridor run: the envelope surrounds the camera " - "and the tiling joints are the proof; edge bleed is the design", + strategy="projection", ) - if fcode: - return fcode + print(res.report()) + print(f"framing_deviation score={score:.4f} cap={FRAME_DEV_MAX:.4f}") + if score > FRAME_DEV_MAX: + print( + f"ERROR: framing deviation {score:.4f} exceeds cap {FRAME_DEV_MAX:.4f} " + "(--close-camera is the designed fail)", + file=sys.stderr, + ) + return 10 aqcode = gallery_asset_quality.check_asset_quality( sc, cam, hero=aq_hero, stage=[floor, wall]) if aqcode: @@ -916,6 +930,9 @@

Source

p.add_argument("--output", default=None, help="optional: render a still PNG here") p.add_argument("--falsify", default=None, help="optional: render the unsnapped-joint seam variant here") + p.add_argument("--close-camera", action="store_true", + help="falsification: push the camera into the corridor so " + "framing deviation exceeds the cap (needs --output)") p.add_argument("--engine", default="eevee", choices=("eevee", "cycles"), help="render engine for --output/--falsify (cycles for GPU-less hosts)") args = p.parse_args(argv) @@ -927,7 +944,10 @@

Source

return code if args.output: - rcode = render_still(os.path.abspath(args.output), args.engine) + rcode = render_still( + os.path.abspath(args.output), args.engine, + close_camera=args.close_camera, + ) if rcode: return rcode print(f"rendered still {args.output}") diff --git a/docs/gallery/shipping-crate/index.html b/docs/gallery/shipping-crate/index.html index 7ec9f12..41ae7b1 100644 --- a/docs/gallery/shipping-crate/index.html +++ b/docs/gallery/shipping-crate/index.html @@ -273,7 +273,7 @@

Run

blender --background --python shipping_crate.py -- --output crate.png

Smoke does not pass --output or --skip-decimate.

Exit codes

-

File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path (no deviation=).

+

File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path.

| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Mesh did not build / no UV layer | | 4 | Base triangle count outside range | | 5 | Material count ≠ 2 distinct slots | | 6 | UVs outside 0..1 | | 7 | UV AABB overlap above tolerance | | 8 | World AABB off declared outer size | | 9 | LOD ratio band (--skip-decimate lands here) | | 10 | Framing gate (render path only) | | 11 | Collider triangle count above ceiling | | 12 | Bake did not finish or image has no data | | 13 | Export file missing or empty | | 14 | --output produced no file |

diff --git a/docs/gallery/socket-attach-points/index.html b/docs/gallery/socket-attach-points/index.html index b7979a8..985902e 100644 --- a/docs/gallery/socket-attach-points/index.html +++ b/docs/gallery/socket-attach-points/index.html @@ -278,7 +278,7 @@

Run

blender --background --python socket_attach_points.py -- --output drone.png blender --background --python socket_attach_points.py -- --falsify adrift.png blender --background --python socket_attach_points.py -- --probe -

The --output render path additionally gates framing via examples/gallery_framing.py (fill 0.881x, margins 0.066/0.053/0.122/0.106, no edge touched) and the asset floors via examples/gallery_asset_quality.py (32 materials, edge90 0.027, no default names). The --falsify render is a diagnostic, not a gallery hero, so it takes a documented framing deviation — its whole point is that the modules leave the frame.

+

The --output render path additionally gates framing via examples/gallery_framing.py (fill 0.881x, margins 0.066/0.053/0.122/0.106, no edge touched) and the asset floors via examples/gallery_asset_quality.py (32 materials, edge90 0.027, no default names). --falsify is a diagnostic, not a gallery hero: it measures and prints framing but does not enforce the Layer 1 band — modules leave the frame on purpose. --falsify and --probe still exit 0.

Exit codes

Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper. 11 is the shared asset-quality helper.

| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Socket world matrix, orthonormal basis, or determinant | | 4 | Socket pad normal, origin, or up-axis rule | | 5 | Module seating / mount axis / leftover local transform | | 6 | Population, namespace, default datablock name, SKT_ prefix, or skid plane | | 7 | Re-pose: socket or module lost the root | | 8 | Parent-inverse clear, child scale pushdown, or post-apply module drift | | 9 | --output produced no file | | 10 | Gallery framing violation | | 11 | Gallery asset-quality violation |

@@ -1359,17 +1359,19 @@

Source

sc.view_settings.view_transform = "Standard" bpy.context.view_layer.update() - # The gallery still is gated; the --falsify diagnostic is not. Its whole - # point is that the modules fly off the airframe, so measuring it against - # the Layer 1 band would only ever report the breakage as a framing - # violation. The numbers are still printed under an explicit reason. - fcode = gallery_framing.check_framing( - sc, cam, hero=hero, elements=hero, stage=[floor, wall], - deviation=("falsification diagnostic: the sockets are deliberately " - "unparented, so modules leave the frame — this render is " - "evidence, not a gallery hero") if falsify else None) - if fcode: - return fcode + # The gallery still is gated by Layer 1. The --falsify diagnostic is not: + # modules leave the frame on purpose, so enforcing the band would report + # the breakage as a framing violation. Measure and print either way; + # enforce only on the gallery path. + if falsify: + res = gallery_framing.measure_framing( + sc, cam, hero=hero, elements=hero, stage=[floor, wall]) + print(res.report()) + else: + fcode = gallery_framing.check_framing( + sc, cam, hero=hero, elements=hero, stage=[floor, wall]) + if fcode: + return fcode aqcode = gallery_asset_quality.check_asset_quality(sc, cam, hero=hero, stage=[floor, wall]) if aqcode: diff --git a/examples/gallery_framing.py b/examples/gallery_framing.py index 30a4180..2aca716 100644 --- a/examples/gallery_framing.py +++ b/examples/gallery_framing.py @@ -54,15 +54,12 @@ runs only on the opt-in render path; check-only semantics (non-zero == API-contract drift) are unchanged. -Deviations: VISUAL-STYLE Layer 1 allows a documented framing deviation -when the bleed or the scale is the design — radiating subjects that read -as extending past the frame, edge-to-edge fields where the fill is the -point, subjects whose contract is the world or the atmosphere. The -deviation must be visible in the example's source and render log: pass -``deviation="reason"`` to `check_framing`, which switches enforcement to -reporting and prints the reason with the numbers. An empty or whitespace -reason raises ValueError — a deviation cannot be taken silently. The -README must carry the same one-line note, exactly as stage deviations do. +The helper measures; call sites enforce. `check_framing` always applies +the Layer 1 band (0/10). Compositions whose contract *is* the bleed +(interior corridor, radiating architecture) do not pass a `deviation=` +flag — that parameter is gone. They call `measure_framing_deviation`, +which returns the FramingResult plus a scalar score, and assert their +own cap at the call site with their own exit code. Scope: the helper measures the (scene, camera) pair it is handed — pass the scene the still actually renders from. Multi-scene examples (e.g. @@ -70,8 +67,7 @@ while ``bpy.context.scene`` is the cut-list scene) must pass that scene and its camera explicitly. Examples whose subject IS the world or the atmosphere (sky-texture-sun-elevation) have no renderable hero for the -matte to isolate — the number mismeasures a reference prop, so they take -a documented deviation and report rather than enforce. +matte to isolate — they do not call this helper. """ import os import sys @@ -386,32 +382,44 @@ def measure_framing(scene, camera, hero, elements, stage=(), strategy=DEFAULT_ST return res -def check_framing(scene, camera, hero, elements, stage=(), strategy=DEFAULT_STRATEGY, - deviation=None): +def deviation_score(res): + """How far *res* sits outside the Layer 1 band. 0.0 iff ``res.ok``. + + The score is the max of: fill overshoot past FILL_MAX, fill undershoot + below FILL_MIN when neither axis is in band, and the worst margin + shortfall below MARGIN_MIN. Call sites that own a bleed composition + compare this to their own cap; they do not ask ``check_framing`` to + skip enforcement. + """ + fill = max(res.fill_x, res.fill_y) + fill_over = max(0.0, fill - FILL_MAX) + in_band = any(FILL_MIN <= f <= FILL_MAX for f in (res.fill_x, res.fill_y)) + fill_under = 0.0 if (in_band or fill > FILL_MAX) else max(0.0, FILL_MIN - fill) + margin_short = max(0.0, MARGIN_MIN - min(res.margins.values())) + return max(fill_over, fill_under, margin_short) + + +def measure_framing_deviation(scene, camera, hero, elements, stage=(), + strategy=DEFAULT_STRATEGY): + """Measure framing and return ``(FramingResult, deviation_score)``. + + Additive: ``check_framing`` still returns 0 or EXIT_FRAMING (10) and + is unchanged for callers that ignore this function. Bleed compositions + call this, print ``result.report()``, and enforce their own cap. + """ + res = measure_framing(scene, camera, hero, elements, stage=stage, strategy=strategy) + return res, deviation_score(res) + + +def check_framing(scene, camera, hero, elements, stage=(), strategy=DEFAULT_STRATEGY): """Measure, print the numbers, and gate: 0 pass, EXIT_FRAMING (10) on violation. Render path only — never call this from an example's check-only path. - - deviation — None (default) enforces the band. A non-empty reason string - documents a legitimate framing deviation (VISUAL-STYLE Layer 1): the - numbers are still measured and printed with the reason, but the gate - reports instead of enforcing and always returns 0. An empty or - whitespace-only reason raises ValueError — no silent opt-outs. + Always enforces the Layer 1 band. Compositions that must bleed call + ``measure_framing_deviation`` and assert at the call site. """ - if deviation is not None and not str(deviation).strip(): - raise ValueError( - "check_framing deviation requires a non-empty reason string; " - "a framing deviation cannot be taken silently" - ) res = measure_framing(scene, camera, hero, elements, stage=stage, strategy=strategy) print(res.report()) - if deviation is not None: - print( - f'framing_deviation reason="{str(deviation).strip()}" ' - f"enforcement=report fill_ok={res.fill_ok} " - f"margins_ok={res.margins_ok} (numbers printed, gate not enforced)" - ) - return 0 if not res.ok: print( "ERROR: framing violation — Layer 1 requires fill " diff --git a/examples/modular-kit-snap/README.md b/examples/modular-kit-snap/README.md index 985a3a5..331e516 100644 --- a/examples/modular-kit-snap/README.md +++ b/examples/modular-kit-snap/README.md @@ -70,10 +70,11 @@ needed. **Framing deviation:** the still is an interior corridor run — the envelope surrounds the camera on five sides and the tiling joints are the proof, so -the subject reads as extending past the frame (the radiating-architecture -class of VISUAL-STYLE Layer 1). `check_framing` measures and reports -(fill 1.000/1.000, all-edge bleed) without enforcing, with the reason -string at the call site. +the subject reads as extending past the frame. The helper measures; +this example enforces a projection-space fill-over cap (`FRAME_DEV_MAX = +8.0`). Gallery camera scores ~5.06 (pass). `--close-camera` scores ~30.9 +and exits 10. Silhouette saturates at fill 1.0, so the cap is measured +with `strategy="projection"`. ## Run @@ -81,13 +82,14 @@ string at the call site. blender --background --python modular_kit_snap.py -- blender --background --python modular_kit_snap.py -- --output corridor.png blender --background --python modular_kit_snap.py -- --falsify seams.png +blender --background --python modular_kit_snap.py -- --output close.png --close-camera ``` ## Exit codes Per-script sequential checks. `9` is a valid check code; there is no rule -against it. `10` is the shared framing helper. `11` is the shared asset-quality -helper. +against it. `10` is this example's framing-deviation cap (call site, not +the shared Layer 1 helper). `11` is the shared asset-quality helper. | Code | Meaning | | --- | --- | @@ -101,8 +103,9 @@ helper. | 7 | Boundary edge count, non-manifold edges, or rim off the end planes | | 8 | Detail part not watertight | | 9 | Detail part reaches a tile boundary; also `--output` produced no file | +| 10 | Framing deviation score exceeds cap (`--close-camera`) | | 11 | Kit part count, unapplied scale, namespace, or origin; also gallery asset-quality violation | The `blender-smoke` workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the `needs-5.1` PR label, or manual dispatch). -Smoke does not pass `--output` or `--falsify`. +Smoke does not pass `--output`, `--falsify`, or `--close-camera`. diff --git a/examples/modular-kit-snap/modular_kit_snap.py b/examples/modular-kit-snap/modular_kit_snap.py index 8924325..54654d5 100644 --- a/examples/modular-kit-snap/modular_kit_snap.py +++ b/examples/modular-kit-snap/modular_kit_snap.py @@ -39,6 +39,7 @@ blender --background --python modular_kit_snap.py -- # check only blender --background --python modular_kit_snap.py -- --output k.png # + render blender --background --python modular_kit_snap.py -- --falsify f.png # seam variant + blender --background --python modular_kit_snap.py -- --output c.png --close-camera # framing 10 """ import bpy, bmesh, sys, os, math, argparse @@ -61,6 +62,12 @@ EXPECT_PARTS = 21 # shell + 20 detail parts; asserted before the # per-part loops below, which assert nothing on # an empty dict +# Projection-space Layer 1 deviation cap. The corridor is supposed to bleed +# (measured fill_over ~5.06 on the gallery camera). --close-camera pushes +# fill_over to ~30. Helper measures; this example enforces. +FRAME_DEV_MAX = 8.0 +CLOSE_CAM_LOC = (-0.15, -0.08, 1.50) +CLOSE_CAM_LENS = 16.0 # Hollow-rectangle profile (y, z): outer shell corners then inner bore corners, # ordered as one continuous ring so the extrusion's side faces come out quads. @@ -547,7 +554,7 @@ def build_bulkhead(sc): return parts -def render_still(path, engine, falsify=False): +def render_still(path, engine, falsify=False, close_camera=False): """A four-segment run. Snapped: joints vanish. Falsified: each segment accumulates a 120 mm x gap, alternating 50 mm y jogs, and 40 mm floor steps, so every joint reads as a seam — the render-scale exaggeration of @@ -572,9 +579,9 @@ def render_still(path, engine, falsify=False): bulkhead = build_bulkhead(sc) cam_data = bpy.data.cameras.new("Cam") - cam_data.lens = 25.0 + cam_data.lens = CLOSE_CAM_LENS if close_camera else 25.0 cam = bpy.data.objects.new("Cam", cam_data) - cam.location = (-0.7, -0.42, 1.58) + cam.location = CLOSE_CAM_LOC if close_camera else (-0.7, -0.42, 1.58) sc.collection.objects.link(cam) aim = bpy.data.objects.new("Aim", None) aim.location = (10.0, 0.35, 1.25) @@ -601,19 +608,25 @@ def render_still(path, engine, falsify=False): sc.render.filepath = path # Standard, always — AgX would lift the stage toward grey (VISUAL-STYLE) sc.view_settings.view_transform = "Standard" - # Layer 1 framing gate. The corridor surrounds the camera on five sides - # and reads as extending past the frame — a documented deviation class - # (radiating architecture), measured and reported by the helper. - fcode = gallery_framing.check_framing( + # The corridor surrounds the camera — Layer 1 fill/margin will fail, and + # that is the design. The helper only measures; this example owns the + # cap. Projection so the bleed can grow past 1.0 (silhouette saturates). + res, score = gallery_framing.measure_framing_deviation( sc, cam, hero=all_parts, elements=all_parts + bulkhead, stage=[floor, wall], - deviation="interior corridor run: the envelope surrounds the camera " - "and the tiling joints are the proof; edge bleed is the design", + strategy="projection", ) - if fcode: - return fcode + print(res.report()) + print(f"framing_deviation score={score:.4f} cap={FRAME_DEV_MAX:.4f}") + if score > FRAME_DEV_MAX: + print( + f"ERROR: framing deviation {score:.4f} exceeds cap {FRAME_DEV_MAX:.4f} " + "(--close-camera is the designed fail)", + file=sys.stderr, + ) + return 10 aqcode = gallery_asset_quality.check_asset_quality( sc, cam, hero=aq_hero, stage=[floor, wall]) if aqcode: @@ -631,6 +644,9 @@ def main(): p.add_argument("--output", default=None, help="optional: render a still PNG here") p.add_argument("--falsify", default=None, help="optional: render the unsnapped-joint seam variant here") + p.add_argument("--close-camera", action="store_true", + help="falsification: push the camera into the corridor so " + "framing deviation exceeds the cap (needs --output)") p.add_argument("--engine", default="eevee", choices=("eevee", "cycles"), help="render engine for --output/--falsify (cycles for GPU-less hosts)") args = p.parse_args(argv) @@ -642,7 +658,10 @@ def main(): return code if args.output: - rcode = render_still(os.path.abspath(args.output), args.engine) + rcode = render_still( + os.path.abspath(args.output), args.engine, + close_camera=args.close_camera, + ) if rcode: return rcode print(f"rendered still {args.output}") diff --git a/examples/socket-attach-points/README.md b/examples/socket-attach-points/README.md index 64b2982..b449ef6 100644 --- a/examples/socket-attach-points/README.md +++ b/examples/socket-attach-points/README.md @@ -127,9 +127,10 @@ The `--output` render path additionally gates framing via `examples/gallery_framing.py` (fill **0.881x**, margins **0.066/0.053/0.122/0.106**, no edge touched) and the asset floors via `examples/gallery_asset_quality.py` (32 materials, `edge90` **0.027**, no -default names). The `--falsify` render is a diagnostic, not a gallery hero, so -it takes a documented framing deviation — its whole point is that the modules -leave the frame. +default names). `--falsify` is a diagnostic, not a gallery hero: it +measures and prints framing but does not enforce the Layer 1 band — +modules leave the frame on purpose. `--falsify` and `--probe` still +exit 0. ## Exit codes diff --git a/examples/socket-attach-points/socket_attach_points.py b/examples/socket-attach-points/socket_attach_points.py index 3c08c80..196d66a 100644 --- a/examples/socket-attach-points/socket_attach_points.py +++ b/examples/socket-attach-points/socket_attach_points.py @@ -1067,17 +1067,19 @@ def render_still(path, engine, falsify=False): sc.view_settings.view_transform = "Standard" bpy.context.view_layer.update() - # The gallery still is gated; the --falsify diagnostic is not. Its whole - # point is that the modules fly off the airframe, so measuring it against - # the Layer 1 band would only ever report the breakage as a framing - # violation. The numbers are still printed under an explicit reason. - fcode = gallery_framing.check_framing( - sc, cam, hero=hero, elements=hero, stage=[floor, wall], - deviation=("falsification diagnostic: the sockets are deliberately " - "unparented, so modules leave the frame — this render is " - "evidence, not a gallery hero") if falsify else None) - if fcode: - return fcode + # The gallery still is gated by Layer 1. The --falsify diagnostic is not: + # modules leave the frame on purpose, so enforcing the band would report + # the breakage as a framing violation. Measure and print either way; + # enforce only on the gallery path. + if falsify: + res = gallery_framing.measure_framing( + sc, cam, hero=hero, elements=hero, stage=[floor, wall]) + print(res.report()) + else: + fcode = gallery_framing.check_framing( + sc, cam, hero=hero, elements=hero, stage=[floor, wall]) + if fcode: + return fcode aqcode = gallery_asset_quality.check_asset_quality(sc, cam, hero=hero, stage=[floor, wall]) if aqcode: diff --git a/showcase/README.md b/showcase/README.md index cf414f6..31c3c96 100644 --- a/showcase/README.md +++ b/showcase/README.md @@ -40,8 +40,10 @@ entry in `showcase/gallery.json`, and a rendered still. - **Rendered still and gallery entry.** Showcase pieces are visual by definition. The pathology / sidecar exemption does not apply. Call `examples/gallery_framing.check_framing` on the `--output` path only. - Do not pass `deviation=`. Do not move or modify `gallery_framing.py` — - import it by resolving the repo root (see the shipping-crate script). + The `deviation=` parameter is gone; bleed compositions call + `measure_framing_deviation` and assert at the call site. Do not move + or modify `gallery_framing.py` — import it by resolving the repo root + (see the shipping-crate script). - **Composition.** The README names which shipped skills and snippets the piece composes. Duplicated helpers stay inlined or copied; showcase scripts do not import snippets as a package. diff --git a/showcase/shipping-crate/README.md b/showcase/shipping-crate/README.md index d3304c5..6d7b715 100644 --- a/showcase/shipping-crate/README.md +++ b/showcase/shipping-crate/README.md @@ -51,7 +51,7 @@ Smoke does not pass `--output` or `--skip-decimate`. ## Exit codes File-local. `9` is a valid check code. `10` is reserved for -`gallery_framing.check_framing` on the `--output` path (no `deviation=`). +`gallery_framing.check_framing` on the `--output` path. | Code | Meaning | | --- | --- |