Skip to content

feat: measurable framing gate + restage hygiene/origin heroes into the band#107

Merged
TMHSDigital merged 4 commits into
mainfrom
feat/framing-gate
Jul 22, 2026
Merged

feat: measurable framing gate + restage hygiene/origin heroes into the band#107
TMHSDigital merged 4 commits into
mainfrom
feat/framing-gate

Conversation

@TMHSDigital

Copy link
Copy Markdown
Owner

Measurable framing gate + restage of the two out-of-band heroes

Layer 1 has mandated "subject fills 70–90 % of frame in at least one axis; nothing that matters touches the frame edge" as prose. It was the only Layer 1 rule with no number attached, and it was the one that kept failing: mesh-hygiene-audit had been pulled back to ~39 % fill and prop-origin-transform had been tightened until the shipped asset cropped both pedestals at the side edges and their tops. This PR gives the rule a closed form, applies it to those two examples, and codifies it so future examples inherit it.

Measurement strategy: silhouette matte (default), projection (documented alternative)

New shared helper examples/gallery_framing.py — the repo's first cross-example import, via a __file__-relative sys.path shim so every example stays standalone-runnable as blender --background --python examples/<name>/<script>.py -- from the repository root (the smoke workflow's exact invocation; verified on both versions below).

  • Silhouette matte (default): hides the stage, sets film_transparent, renders a 320×180 low-sample EEVEE alpha matte, thresholds alpha > 0.5 → true occupied-pixel extent. Exact for round/angled subjects. One matte for the margin union, one for hero-only fill (shared when they coincide).
  • Projection (alternative): unions world-space bbox corners through world_to_camera_view. No extra render, but overestimates round/angled subjects — measured here: hygiene hero fill 0.409 (projection) vs 0.391 (silhouette) on the rotated octagonal valves. Documented in the docstring as the choice for boxy subjects when render time matters.

The helper reports the two Layer 1 clauses separately: fill (hero only; 0.70–0.90 in at least one axis, neither axis above 0.90) and margin (union of every element that matters — hero, placards, comparison props, overlay markers; all four edges ≥ 0.02; union clearance ⇔ per-element clearance). Measured values print in the house log style (framing_fill … ok, framing_margins … ok, framing_ok).

Enforcement decision: hard failure (exit 10), render path only

A framing violation exits non-zero (10) before the beauty render, so a defective composition ships no artifact. Rationale: framing is a Layer 1 mandatory identity rule and the render path is the gallery-artifact path — it should gate exactly like every other contract here; making it a hard gate is precisely what breaks the eyeball-and-overshoot loop that produced this PR. The coupling concern (non-zero has meant API-contract drift) is contained: the gate runs only on the opt-in --output path. Check-only semantics are untouched — no matte ever renders there, and smoke runtimes are unchanged (measured below).

Before/after, measured by the helper (silhouette, 5.1.2)

example before after
mesh-hygiene-audit fill 0.391x0.350 → FAIL (under); margins ok fill 0.803x0.733 ok; margins L0.100 R0.097 B0.050 T0.122 → framing_ok
prop-origin-transform fill 0.906x0.756 → FAIL (over); margins L0.037 R0.056 B0.028 T0.139 fill 0.816x0.678 ok; margins L0.084 R0.100 B0.072 T0.183 → framing_ok

Notes: the prop-origin shipped webp was tighter than current code reproduced (visibly cropped left/right/top) — the code on main already failed the band at 0.906. hygiene moved in 38 mm@(0,−7.8,1.9) → 50 mm@(0,−5.0,1.5) with the backlit through-hole, loose-vert bead, and no-visible-light-shape staging preserved; prop-origin pulled back (1.15,−5.35,1.45) → (1.15,−5.9,1.55) only until margins cleared, conduit part and dark mount well preserved. No check code or closed forms changed; check-only output is byte-identical to main on 5.1.2 and 4.5.11 (diffed).

Falsification evidence (gate proven to fail)

  • hygiene under-fill probe: lens 50→30 → framing_fill x=0.481 … FAIL (margins_ok=True), exit 10, no artifact written. Restored.
  • origin margin probe: aim (1.1,0,0.72)→(2.6,0,0.72) → framing_margins left=0.000 … FAIL, framing_edges touch=left, exit 10, no artifact written. Restored. (First attempt, camera-x shift, was a no-op by construction — TRACK_TO re-aims; recorded for honesty.)
  • Existing checks re-proven: check-only runs exit 0 on both versions with values identical to main (hygiene volume=0.652001 min_area=1.146e-02; origin bare_jump=0.428275 mpi_err=2.980e-08).

Survey of every shipped hero (read-only worklist — not fixed here)

Ran the helper across all 23 examples (each staged by its own unmodified --output path; union-of-renderables approximates hero fill). 22 of 23 measured; vse-cut-list is not matte-surveyable (its still is a sequencer composite, no scene camera). 13 fall outside the band:

  • Edge-touch/margin: damped-track-aim (touch L/R/B, fill 1.000 — a pinned calibration member), wave-displace (touch L/R/B, 1.000), depsgraph-export (touch L/B, 0.984), gp-lineart-contour (touch B), png-exr-alpha (touch B), vertex-weight-limit (touch B), curve-bevel-arc (right margin 0.012)
  • Over-fill: driver-wave (0.969x), shader-node-group (0.969x), swatch-grid (0.944y)
  • Under-fill: text-version-stamp (0.663 max), triangulate-tangents (0.661 max), sky-texture-sun-elevation (0.489 max — world-subject deviation example; matte measures only its sun disk, so this one is likely a measurement artifact of a legitimate deviation)

Survey caveats: union fill includes placards (slightly inflates); world/VSE/compositor-artifact subjects can't be matte-measured faithfully. The other 9 measured examples are in band, including both restaged heroes.

Contact-sheet gate (Pass 5)

Composites committed and linked below; each pairs the candidate with the pinned set armature-bend, damped-track-aim, bmesh-gear (reading order TL→BR, candidate last):

Per-criterion verdicts — stage darkness ✓ both (near-black floor/wall, horizon in shadow); wedge warmth ✓ both (warm pool consistent with calibration); subject fill ✓ both (comparable to bmesh-gear/armature-bend, no longer odd one out at either extreme); saturation ✓ (brass in family with the gold gear; teal consistent with its prior accepted hero); thumbnail legibility ✓ both (hole+bead / conduit+well read at cell scale). Mean luminance vs calibration 70.8 / 44.5 / 77.7: hygiene 64.4, origin 61.9 — inside the calibration range for both. Neither candidate is sortable as the odd one out.

Visual-pass log

  • Pass 1 (full size + measured numbers): hygiene 1 render iteration (in band on first measured restage, 0.803/0.733); origin 1 render iteration (0.816/0.678). The helper's numbers replaced the judgment call; no visual defects at full size.
  • Pass 2 (thumbnail, 400 px): both read in under a second — dirty/clean valve pair with glowing hole; TRAP conduit vs seated MPI flange.
  • Pass 3 (100 % crops, 5 regions each): no clipping, no blown highlights, no visible helpers/light shapes, placards crisp; origin pedestal tops now fully in frame (previously cropped).
  • Pass 4 (Playwright in-situ): build_gallery.py regenerated (index + 38 detail pages); served docs/, captured index at 1280 and 390 plus both detail pages. Finding: Chromium scroll-anchoring defeats scrollTo(0,0) viewport captures after lazy-load (scrollY stayed ~3.3–3.5k) — full-page captures + local crops used instead. Both cards correct at both widths; both detail pages show hero, teaches, witnesses callout, run command correctly.
  • Pass 5 (contact sheet): verdicts above; composites committed.

Generated HTML read character by character post-regen: both <img alt> texts (index cards + detail heroes) and both witnesses callouts complete — no truncation, no duplicated words, no generator artifacts.

Runtime / binaries

  • E:/Blender-Developer-Tools/.scratch/blender-5.1.2-windows-x64/blender.exe — reports Blender 5.1.2 (build 2026-05-19)
  • E:/Blender-Developer-Tools/.scratch/blender-4.5.11-windows-x64/blender.exe — reports Blender 4.5.11 LTS (build 2026-06-23)

Check-only runtime, old vs new (median-noise sub-second runs): 5.1.2 hygiene 779→753 ms, origin 780→742 ms; 4.5.11 hygiene 596→582 ms, origin 582→587 ms — unchanged; CI smoke jobs invoke exactly these check-only paths, so they do not start rendering. Render path with gate exercised on both versions: exit 0, identical framing numbers (0.803/0.733 and 0.816/0.678 on 4.5.11 too).

Live-run vs inspection-only

  • Live run (5.1.2): both render paths incl. gate, both falsification probes, webp asset conversion, contact-sheet compositing, gallery regen, Playwright captures, full 22-example survey.
  • Live run (4.5.11): both check-only paths, both render paths incl. gate.
  • Inspection only: correctness of the survey's heuristic hero classification per example; the assumption that CI smoke runtimes track local check-only timings.

Test plan for reviewers

  1. blender --background --python examples/mesh-hygiene-audit/mesh_hygiene_audit.py -- → exit 0, values as above (4.5 LTS and 5.1).
  2. Same for prop_origin_transform.py.
  3. -- --output out.png on either → framing_* lines print, framing_ok, exit 0, artifact written.
  4. Break framing (lens or aim edit) → exit 10, no artifact.

Layer 1 framing was the only mandatory render rule with no number
attached, so agents eyeballed it and overshot in both directions.
examples/gallery_framing.py measures hero fill (0.70-0.90 of frame in
at least one axis) and all-elements edge margins (>= 0.02) on a staged
scene, defaulting to a silhouette alpha matte (exact for round or
angled subjects) with a bbox-projection strategy documented as the
cheap alternative for boxy subjects. check_framing prints the measured
values in the house log style and exits 10 on violation, render path
only, so check-only smoke runtimes are untouched. It also establishes
the repo's cross-example sharing mechanism: a __file__-relative
sys.path shim that keeps every example standalone-runnable from the
repository root.

Signed-off-by: fOuttaMyPaint <TMhospitalitystrategies@gmail.com>
Both renders sat outside the Layer 1 band in opposite directions, the
failure mode the new helper exists to catch. mesh-hygiene-audit was at
0.391 fill (two small valves in a large empty stage); the camera moves
from 38mm at (0,-7.8,1.9) to 50mm at (0,-5.0,1.5), landing fill at
0.803x0.733 with all margins >= 0.05. The backlit through-hole and
loose-vert bead staging is untouched and no light shape re-enters the
frame. prop-origin-transform was at 0.906 fill with the shipped asset
cropped at both side edges and the tops; the camera pulls back from
(1.15,-5.35,1.45) to (1.15,-5.9,1.55) only until margins clear, landing
fill at 0.816x0.678 with margins >= 0.072. Both render paths now call
gallery_framing.check_framing before the beauty render (exit 10, no
artifact on violation); check code and closed forms are unchanged, and
check-only output is byte-identical to main on 5.1.2 and 4.5.11.
Contact sheets recomposited against the pinned calibration set.

Signed-off-by: fOuttaMyPaint <TMhospitalitystrategies@gmail.com>
Layer 1 framing now carries its numbers where every run reads them:
the 0.70-0.90 fill band in at least one axis, the 2 percent minimum
edge margin, the hero-fill versus all-elements-margin distinction, and
the instruction to measure with examples/gallery_framing.py rather
than eyeball. CLAUDE.md's quality gates gain the matching framing-gate
bullet so the standard example prompt inherits the rule by reference,
plus one line naming the cross-example import shim. A read-only survey
of every shipped hero with the helper found 13 examples outside the
band (worklist for a future pass, not fixed here).

Signed-off-by: fOuttaMyPaint <TMhospitalitystrategies@gmail.com>
Signed-off-by: fOuttaMyPaint <TMhospitalitystrategies@gmail.com>
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Jul 22, 2026
@TMHSDigital
TMHSDigital merged commit d6fa235 into main Jul 22, 2026
9 checks passed
@TMHSDigital
TMHSDigital deleted the feat/framing-gate branch July 22, 2026 20:57
TMHSDigital added a commit that referenced this pull request Jul 22, 2026
…108)

* feat: add deviation opt-out to the framing helper

PR #107's survey put 13 of 22 measured examples outside the band,
including a pinned calibration member whose edge bleed is the design, so
the band gains the same structure the stage rules have: presumption by
default, documented deviation when the bleed or the scale is the point.
check_framing takes deviation=<reason>: with a non-empty reason string
it measures and prints the numbers with the reason but reports instead
of enforcing; an empty or whitespace reason raises ValueError so a
deviation cannot be taken silently. The docstring also scopes the
helper: it measures the (scene, camera) pair it is handed, so
multi-scene examples pass the still's scene explicitly, and
world/atmosphere-subject examples report rather than enforce because
the matte has no renderable hero to isolate. Both current callers keep
the enforcing default; verified unchanged on 5.1.2 and 4.5.11
(0.803/0.733 and 0.816/0.678) and falsified both ways (reasoned
deviation reports with exit 0; empty reason ValueErrors with exit 1).

Signed-off-by: fOuttaMyPaint <TMhospitalitystrategies@gmail.com>

* docs: add the framing deviation clause to VISUAL-STYLE and gates

Layer 1 framing now mirrors the stage rules' structure: the 70-90 fill
band and the 2 percent margin floor remain the presumption, and a
composition may deviate when the bleed or the scale is the design —
radiating subjects, edge-to-edge fields, world or atmosphere contracts.
A deviation requires a one-line README note under a Framing deviation
heading and the helper call switches to check_framing(deviation=reason)
so it reports with the reason printed rather than enforcing; an
undocumented deviation is still a defect, and an empty reason raises.
The CLAUDE.md quality-gates bullet inherits the clause by reference.

Signed-off-by: fOuttaMyPaint <TMhospitalitystrategies@gmail.com>

* docs: reconcile two runtime facts in CLAUDE.md

The smoke workflow floats: it resolves the highest published point
release of each series at run time, so CI ran Blender 4.5.12 LTS on PR
#107 while local .scratch held 4.5.11 — CLAUDE.md now says the series
floats and a local point release may lag, with the exact-reported-version
rule unchanged. Also records the Playwright lesson from #107 next to the
gallery-capture gate: Chromium scroll-anchoring defeats scrollTo(0, 0)
after lazy-load, so captures are full-page, not scroll-then-shoot.

Signed-off-by: fOuttaMyPaint <TMhospitalitystrategies@gmail.com>

* docs: triage the thirteen out-of-band heroes and record the worklist

Full-size and card-scale review of each surveyed hero against its README
and contract. Two are legitimate deviations and now carry the one-line
README Framing deviation note: damped-track-aim (radiating composition —
the spikes read as converging from beyond the frame; it is a pinned
calibration member and the gallery's reference standard stays) and
wave-displace (edge-to-edge field — the fill is the point).
sky-texture-sun-elevation is a measurement artifact: the hero is the sky
itself, so the matte only saw the cone props; its README now documents
report-only. vse-cut-list is in scope — its still renders from the Bay
scene with BayCam, and the README records that the (scene, camera) pair
must be passed explicitly. The remaining ten are genuine defects
(clipped subjects, over-fill, dead space) and land on the ROADMAP
candidate pool as a restage worklist with measured numbers. No
re-renders in this pass.

Signed-off-by: fOuttaMyPaint <TMhospitalitystrategies@gmail.com>

---------

Signed-off-by: fOuttaMyPaint <TMhospitalitystrategies@gmail.com>
TMHSDigital added a commit that referenced this pull request Jul 22, 2026
…es) (#109)

* fix: target the passed scene in gallery_framing matte renders

_matte_render called bpy.ops.render.render(write_still=True) bare, which
renders the ACTIVE context scene — silently wrong for any example that
stills from a non-context scene (vse-cut-list renders its gallery hero
from a dedicated Bay scene via the operator's scene= kwarg). The matte
now renders the scene it was handed, matching that idiom. Both current
callers use the context scene and measure unchanged (0.803/0.733 on
5.1.2 and 4.5.11); the corrected survey that exposed this now captures
each example's rendered scene via a persistent render_pre handler and
re-measured all 38 examples against the scene and camera each actually
renders from.

Signed-off-by: fOuttaMyPaint <TMhospitalitystrategies@gmail.com>

* fix: nudge curve-bevel-arc framing into the band and gate it

Right cap sat 0.012 from the right edge, under the 0.02 floor. Camera
moves from (2.4,-3.4,1.7) to (2.55,-3.62,1.78) — a pure pull-back, no
restage — landing fill 0.738x/0.633y with margins L0.219 R0.044 B0.083
T0.283. The render path now calls gallery_framing.check_framing before
the beauty render (exit 10); check code and closed forms untouched
(eval_verts=1044, eval_faces=1028, x_span=3.292 all print unchanged).
Gate falsified at lens 65 (margins FAIL, exit 10, no artifact) and
restored. Identical framing numbers on 4.5.11.

Signed-off-by: fOuttaMyPaint <TMhospitalitystrategies@gmail.com>

* fix: nudge driver-wave framing into the band and gate it

Sixteen-column skyline over-filled the frame at 0.969x with side margins
0.016. Camera pulls back from (0,-13.5,2.0) to (0,-15.0,2.0) — pure
distance, no restage — landing fill 0.869x/0.333y with margins L0.066
R0.066 B0.367 T0.300. Render path gated with check_framing (exit 10);
check values unchanged (driven_range 0.404..2.399, flushed_to_original).
Gate falsified at lens 55 (fill 0.994 FAIL, exit 10) and restored.
Identical numbers on 4.5.11.

Signed-off-by: fOuttaMyPaint <TMhospitalitystrategies@gmail.com>

* fix: nudge shader-node-group framing into the band and gate it

Sphere pair over-filled the frame at 0.969x with side margins 0.016.
Camera pulls back from (0,-7.8,2.6) to (0,-8.7,2.6) — pure distance, no
restage — landing fill 0.869x/0.650y with margins L0.066 R0.066 B0.256
T0.094. Render path gated with check_framing (exit 10); check values
unchanged (group users=2, distinct instance tints). Gate falsified at
lens 72 (fill 1.000 FAIL, exit 10) and restored. Identical numbers on
4.5.11.

Signed-off-by: fOuttaMyPaint <TMhospitalitystrategies@gmail.com>

* fix: nudge swatch-grid framing into the band and gate it

The ortho grid over-filled vertically at 0.944y. ortho_scale 7.8 -> 8.4
(height 4.39 -> 4.73 for the 4.14-unit grid) lands fill 0.769x/0.878y
with margins L0.116 R0.116 B0.061 T0.061. verify_png's third/half
cell-center sampling tolerates the scale change — distinct_regions=6
and max_pixel=1.000 confirmed at 1280 default and at the smoke
invocation (640, cycles, 8 samples) on 5.1.2 and 4.5.11. Render path
gated with check_framing (exit 10); falsified at ortho_scale 6.5
(fill 1.000 FAIL, exit 10) and restored.

Signed-off-by: fOuttaMyPaint <TMhospitalitystrategies@gmail.com>

* fix: lift png-exr-alpha plinth off the bottom edge and gate it

The shared display plinth touched the bottom frame edge (margin 0.000).
A pull-back first overshot (fill 0.672 under-band), so the fix is an aim
drop instead: aim z 1.65 -> 1.38 at unchanged camera distance pitches
the composition up, landing fill 0.725x/0.628y with margins L0.122
R0.056 B0.028 T0.150 — distance and composition otherwise untouched.
Render path gated with check_framing (exit 10); falsified at lens 64
(fill 0.916 FAIL, exit 10) and restored. Identical numbers on 4.5.11.

Signed-off-by: fOuttaMyPaint <TMhospitalitystrategies@gmail.com>

* fix: nudge gp-lineart-contour framing into the band and gate it

The neon vessel over-filled vertically at 0.978y with its base cropped
by the bottom edge. Camera pulls back from (3.4,-4.2,2.4) to
(3.95,-4.9,2.62) — pure distance, no restage — landing fill
0.463x/0.867y with margins L0.272 R0.266 B0.044 T0.089 (an intermediate
stop landed y at exactly 0.900, on the band ceiling, so the pull-back
goes one step further for cross-version slack). Render path gated with
check_framing (exit 10); check values unchanged (strokes=10, points=34,
restore recovers both). Gate falsified at lens 62 (fill 1.000 FAIL,
exit 10) and restored. 4.5.11 matches (0.469x/0.867y).

Signed-off-by: fOuttaMyPaint <TMhospitalitystrategies@gmail.com>

* fix: reframe vse-cut-list bay into the band and gate the Bay scene

The corrected survey (render_pre capture) measured this hero for the
first time: the monitor top was cropped and the desk bled the bottom
edge (fill 1.000y, touch B+T) — the #107 survey had read bpy.context.scene,
which is the cut-list scene, not the Bay the still renders from. BayCam
pulls back from (2.0,-5.2,1.72) to (3.05,-7.9,2.0) with aim z 1.28 ->
1.20, landing fill 0.506x/0.850y with margins L0.247 R0.247 B0.067
T0.083. This was the most-broken of the seven (frame-filling before),
so the composition change is correspondingly larger; the bezel, stand,
desk, and on-screen cut list are all fully in frame now. The gate runs
on the Bay scene (the helper's scene-targeted matte from the previous
commit), falsified at lens 66 (fill 1.000 FAIL, exit 10) and restored.
check and --check-pixels both exit 0 on 5.1.2 and 4.5.11 with identical
pixel witnesses.

Signed-off-by: fOuttaMyPaint <TMhospitalitystrategies@gmail.com>

* docs: reduce the framing worklist to the recomposition remainder

The corrected survey plus the marginal sweep leaves four examples that
need real reframing rather than a camera nudge: depsgraph-export (cube
amputated), vertex-weight-limit (off-center, base cropped),
text-version-stamp (dead lower third), triangulate-tangents (2x-class
move-in). The seven marginal cases are marked swept and gated;
deviations and the sky-texture artifact stay as documented in #108.

Signed-off-by: fOuttaMyPaint <TMhospitalitystrategies@gmail.com>

---------

Signed-off-by: fOuttaMyPaint <TMhospitalitystrategies@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant