Skip to content

Commit cf9ee1f

Browse files
authored
feat: framing deviation clause + triage of the out-of-band thirteen (#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>
1 parent e777750 commit cf9ee1f

12 files changed

Lines changed: 95 additions & 2 deletions

File tree

CLAUDE.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ cross-example import mechanism (see its docstring).
102102

103103
- Local Blender binaries: check `.scratch/` at the repo root **first** — some machines have no system Blender install, and a prior agent run downloads official releases there (e.g. `.scratch/5.1/blender-5.1.x-.../blender[.exe]`). Then check system installs. Do not probe blindly; locate the binary, run it, and state the **exact binary path and the version the binary itself reports** in every report.
104104
- **5.1 is the local check version. 4.5 LTS is exercised by CI when unavailable locally.** 4.4 is not a substitute for 4.5 and must never be reported as 4.5.
105+
- **CI floats within each series:** `blender-smoke.yml` resolves the highest published point release at run time (`sort -V | tail -1` on the download listing), so a local 4.5.x may lag CI — its 4.5 job ran 4.5.12 LTS on PR #107 while local `.scratch/` held 4.5.11. Either way, state the exact version the binary reports.
105106
- If `.scratch/` lacks a needed version, download an official release from download.blender.org into it. `.scratch` is gitignored.
106107
- In scripts, version-branch on the `bpy.app.version` tuple, never on `bpy.app.version_string` — it reads e.g. `"4.5.11 LTS"`, not bare semver.
107108

@@ -121,10 +122,11 @@ Stage with **explicit paths only** — never `git add -A` or `git add .`. Cursor
121122
## Quality Gates for Example Runs
122123

123124
- `docs/VISUAL-STYLE.md` is the **binding** render standard; deviations are defects.
124-
- **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.
125+
- **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.
125126
- **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.
126127
- **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.
127128
- **After gallery regeneration** (`python scripts/build_gallery.py`), read the **generated HTML** character by character — the `<img alt>` text and witnesses callouts in `docs/gallery/index.html` and `docs/gallery/<name>/index.html` — not just `examples/gallery.json`. Precedent: the `teaches.split(".")[0]` bug truncated 14/21 card alts at dotted API paths like `bmesh.ops` while the source JSON looked fine (fixed in PR #68).
129+
- **Playwright gallery captures:** gallery `<img>` tags lazy-load, and Chromium scroll-anchoring defeats `scrollTo(0, 0)` after images load — take full-page captures and crop locally, not scroll-then-shoot.
128130

129131
## Example-Run Process
130132

ROADMAP.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ Not committed; target list for the next content version. (v0.3.0 shipped the smo
129129
- 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)
130130
- 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`
131131

132+
- 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.
133+
132134
## Future (uncommitted)
133135

134136
- Asset library and asset browser scripting skill

docs/VISUAL-STYLE.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,17 @@ the default stage.
6161
markers — and must clear all four edges by ≥ 2 % of the frame dimension.
6262
Default strategy is the silhouette alpha matte; projection is the cheap
6363
bbox alternative for boxy subjects (tradeoff in the helper's docstring).
64+
- **Framing deviations.** The band and the margin floor are the presumption.
65+
A composition may deviate when the bleed or the scale is the design:
66+
radiating subjects that read as extending past the frame, edge-to-edge
67+
fields where the fill is the point, and subjects whose contract is the
68+
world or the atmosphere rather than an object on a stage. A deviation
69+
requires one line in the example README under a `Framing deviation`
70+
heading — what the composition requires and why, exactly as stage
71+
deviations do. An undocumented deviation is a defect. Under a documented
72+
deviation the helper reports rather than enforces: call
73+
`check_framing(..., deviation="reason")`, which prints the numbers with
74+
the reason; an empty reason raises, so no deviation is taken silently.
6475
- Camera: a chosen angle, not the default — typically a 45–55 mm lens,
6576
slightly above subject height, aimed with a `TRACK_TO` constraint at an
6677
empty on the subject. Flat subjects present toward the camera (lean or

docs/gallery/damped-track-aim/index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,8 @@ <h1>damped-track-aim</h1>
185185
<section class="detail-section md">
186186
<p>A runnable example that aims twelve brass spikes at an ember core with <code>Object.constraints.new(&#x27;DAMPED_TRACK&#x27;)</code> — the data-API path, not <code>bpy.ops.object.constraint_add</code> (which needs an active object and fails in headless loops). Damped Track is the twist-stable aim constraint: it points one local axis at a target without the roll fights Track To is known for.</p>
187187
<p><strong>What it witnesses:</strong> every spike carries exactly one unmuted <code>DAMPED_TRACK</code> bound to the core on <code>TRACK_Z</code>. After a depsgraph update, each evaluated local <code>+Z</code> aligns with the world vector toward the core (dot ≥ 0.998 ≈ 3.6°). A missing constraint, a muted one, a <code>TRACK_TO</code> stand-in, or a flipped axis fails the check.</p>
188+
<h2>Framing deviation</h2>
189+
<p>Radiating composition — the twelve spikes read as converging on the core from beyond the frame, so their tails bleed past the left, right, and bottom edges by design (measured fill 1.000x/0.917y with edge touch on three sides). If wired to <code>examples/gallery_framing.py</code>, call it with <code>deviation=&quot;radiating composition; spikes read as extending past frame&quot;</code>.</p>
188190
<h2>Run</h2>
189191
<pre><code># Cheap correctness check (no render) — the CI check:
190192
blender --background --python damped_track_aim.py --

docs/gallery/sky-texture-sun-elevation/index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,8 @@ <h1>sky-texture-sun-elevation</h1>
190190
<p><strong>Version witness:</strong> <code>sky_type</code> and the dust/aerosol rename are the divergence; <code>sun_elevation</code> itself is stable. Zenith rise differs by model (Nishita vs multiple scattering) but clears the same gate on both binaries.</p>
191191
<h2>Stage deviation</h2>
192192
<p>World carries a Nishita / multiple-scattering Sky Texture instead of the default near-black Background — the contract *is* the sky. Gallery still is a dual-elevation diptych (8° | 55°) so failure (identical panels) is visible at thumbnail scale. Layer 1 still holds: <code>view_transform=&#x27;Standard&#x27;</code>, designed terracotta materials, chosen camera, no helpers in frame.</p>
193+
<h2>Framing deviation</h2>
194+
<p>The hero is the sky itself — a world contract, not an object on the stage. The silhouette matte can only measure the cone reference props (0.369x / 0.489y), so the out-of-band number is a measurement artifact, not the composition; if ever wired to <code>examples/gallery_framing.py</code>, this example reports with <code>deviation=&quot;world-contract subject; matte sees only the cone props&quot;</code> rather than enforcing.</p>
193195
<h2>Run</h2>
194196
<pre><code># Zenith-luminance correctness check (tiny Cycles CPU EXR probes) — the CI check:
195197
blender --background --python sky_texture_sun_elevation.py --

docs/gallery/vse-cut-list/index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,8 @@ <h1>vse-cut-list</h1>
192192
<ul><li>A GAMMA_CROSS asked to outlast its inputs&#x27; overlap is <strong>silently clamped</strong> to the overlap — request length 9 over a (25, 33) overlap, get (25, 33).</li><li>A scene strip pointing at its <strong>own</strong> scene is a feedback loop and renders transparent (alpha 0) — the &quot;stage&quot; is silently absent. Source a separate scene.</li><li>Effect strips <strong>consume</strong> their inputs: input strips never composite on their own channel, and the effect&#x27;s transform applies on top of the inputs&#x27; transforms. Consumption requires the effect on a channel <strong>above</strong> its inputs; below, they keep painting independently.</li><li>An empty <code>bpy_prop_collection</code> is falsy — <code>se.strips or se.sequences</code> silently falls through to the legacy accessor on an empty timeline. Always branch on <code>hasattr</code>.</li></ul>
193193
<p><strong>Version divergence:</strong> the whole example is the divergence — gated on the <code>bpy.app.version</code> tuple (<code>&gt;= (5, 0, 0)</code>), never on <code>version_string</code> (<code>&quot;4.5.11 LTS&quot;</code> is not bare semver). Each side asserts its own canonical contract plus the other side&#x27;s removal/bridge state. Measured values are identical on 4.5.11 and 5.1.1, including the pixel witness.</p>
194194
<p><strong>Render:</strong> the program wall *is* the sequencer output at frame 29 (mid cross) — crimson A, teal B, amber long-runner C, and the 50/50 cross blend, over the Stage scene strip showing the dark studio. An off-by-one in end-exclusive span math drops its cell to the dark stage; the caption strip carries the closed form. The hero presents that authentic frame on a reference monitor in a dark-studio editing bay — the pixels on the screen are the genuine sequencer output (evidence); only the bay around them is staged (presentation). Rendered locally with EEVEE (GPU host); the checks and <code>--check-pixels</code> need no GPU (Cycles CPU).</p>
195+
<h2>Framing measurement</h2>
196+
<p>The gallery still renders from the dedicated <code>Bay</code> scene (camera <code>BayCam</code>), not <code>bpy.context.scene</code> — a framing measurement with <code>examples/gallery_framing.py</code> must be handed that (scene, camera) pair explicitly.</p>
195197
<h2>Run</h2>
196198
<pre><code># Cheap correctness check (no render) — the CI check:
197199
blender --background --python vse_cut_list.py --

docs/gallery/wave-displace/index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,8 @@ <h1>wave-displace</h1>
185185
<section class="detail-section md">
186186
<p>A runnable example that displaces a 96×96 grid (9,409 vertices) into a standing wave using <strong>one <code>foreach_get</code> and one <code>foreach_set</code></strong> — the bulk-IO pattern from <a href="https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/rules/use-foreach-set-for-bulk-data.mdc"><code>use-foreach-set-for-bulk-data</code></a> and the <a href="https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/skills/mesh-editing-and-bmesh/SKILL.md"><code>mesh-editing-and-bmesh</code></a> skill — instead of 9,409 individual <code>mesh.vertices[i].co</code> accesses.</p>
187187
<p><strong>What it witnesses:</strong> the bulk path is not just faster, it is *correct* — the check asserts the flat grid gained the expected Z span (the write actually landed) and that <strong>every</strong> vertex matches the closed-form wave, so a stride or interleave bug in the flat buffer cannot hide behind a lucky probe.</p>
188+
<h2>Framing deviation</h2>
189+
<p>Edge-to-edge field — the displaced sheet fills the frame and crops at the left, right, and bottom edges so the wave reads as a surface, not an object on a stage (measured fill 1.000x/0.817y with edge touch on three sides). If wired to <code>examples/gallery_framing.py</code>, call it with <code>deviation=&quot;edge-to-edge wave field; the fill is the point&quot;</code>.</p>
188190
<h2>Run</h2>
189191
<pre><code># Cheap correctness check (no render) — the CI check:
190192
blender --background --python wave_displace.py --

examples/damped-track-aim/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ bound to the core on `TRACK_Z`. After a depsgraph update, each evaluated local
1212
constraint, a muted one, a `TRACK_TO` stand-in, or a flipped axis fails the
1313
check.
1414

15+
## Framing deviation
16+
17+
Radiating composition — the twelve spikes read as converging on the core from
18+
beyond the frame, so their tails bleed past the left, right, and bottom edges
19+
by design (measured fill 1.000x/0.917y with edge touch on three sides). If
20+
wired to `examples/gallery_framing.py`, call it with
21+
`deviation="radiating composition; spikes read as extending past frame"`.
22+
1523
## Run
1624

1725
```bash

examples/gallery_framing.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,25 @@
5353
render exactly like a failed contract check fails the witness. The gate
5454
runs only on the opt-in render path; check-only semantics (non-zero ==
5555
API-contract drift) are unchanged.
56+
57+
Deviations: VISUAL-STYLE Layer 1 allows a documented framing deviation
58+
when the bleed or the scale is the design — radiating subjects that read
59+
as extending past the frame, edge-to-edge fields where the fill is the
60+
point, subjects whose contract is the world or the atmosphere. The
61+
deviation must be visible in the example's source and render log: pass
62+
``deviation="reason"`` to `check_framing`, which switches enforcement to
63+
reporting and prints the reason with the numbers. An empty or whitespace
64+
reason raises ValueError — a deviation cannot be taken silently. The
65+
README must carry the same one-line note, exactly as stage deviations do.
66+
67+
Scope: the helper measures the (scene, camera) pair it is handed — pass
68+
the scene the still actually renders from. Multi-scene examples (e.g.
69+
vse-cut-list, whose gallery still renders from a dedicated ``Bay`` scene
70+
while ``bpy.context.scene`` is the cut-list scene) must pass that scene
71+
and its camera explicitly. Examples whose subject IS the world or the
72+
atmosphere (sky-texture-sun-elevation) have no renderable hero for the
73+
matte to isolate — the number mismeasures a reference prop, so they take
74+
a documented deviation and report rather than enforce.
5675
"""
5776
import os
5877
import sys
@@ -325,13 +344,32 @@ def measure_framing(scene, camera, hero, elements, stage=(), strategy=DEFAULT_ST
325344
return res
326345

327346

328-
def check_framing(scene, camera, hero, elements, stage=(), strategy=DEFAULT_STRATEGY):
347+
def check_framing(scene, camera, hero, elements, stage=(), strategy=DEFAULT_STRATEGY,
348+
deviation=None):
329349
"""Measure, print the numbers, and gate: 0 pass, EXIT_FRAMING (10) on violation.
330350
331351
Render path only — never call this from an example's check-only path.
352+
353+
deviation — None (default) enforces the band. A non-empty reason string
354+
documents a legitimate framing deviation (VISUAL-STYLE Layer 1): the
355+
numbers are still measured and printed with the reason, but the gate
356+
reports instead of enforcing and always returns 0. An empty or
357+
whitespace-only reason raises ValueError — no silent opt-outs.
332358
"""
359+
if deviation is not None and not str(deviation).strip():
360+
raise ValueError(
361+
"check_framing deviation requires a non-empty reason string; "
362+
"a framing deviation cannot be taken silently"
363+
)
333364
res = measure_framing(scene, camera, hero, elements, stage=stage, strategy=strategy)
334365
print(res.report())
366+
if deviation is not None:
367+
print(
368+
f'framing_deviation reason="{str(deviation).strip()}" '
369+
f"enforcement=report fill_ok={res.fill_ok} "
370+
f"margins_ok={res.margins_ok} (numbers printed, gate not enforced)"
371+
)
372+
return 0
335373
if not res.ok:
336374
print(
337375
"ERROR: framing violation — Layer 1 requires fill "

examples/sky-texture-sun-elevation/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,15 @@ dual-elevation diptych (8° | 55°) so failure (identical panels) is visible at
3939
thumbnail scale. Layer 1 still holds: `view_transform='Standard'`, designed
4040
terracotta materials, chosen camera, no helpers in frame.
4141

42+
## Framing deviation
43+
44+
The hero is the sky itself — a world contract, not an object on the stage.
45+
The silhouette matte can only measure the cone reference props (0.369x /
46+
0.489y), so the out-of-band number is a measurement artifact, not the
47+
composition; if ever wired to `examples/gallery_framing.py`, this example
48+
reports with `deviation="world-contract subject; matte sees only the cone
49+
props"` rather than enforcing.
50+
4251
## Run
4352

4453
```bash

0 commit comments

Comments
 (0)