feat(udg): fit: slice — 9-slice presses, so a frame stops distorting #149

Merged
peterswimm merged 1 commit from feat/udg-press-fit-slice into main 2026-09-16 18:36:21 +00:00
Owner

Stacked on #148 — base is feat/udg-press-tier, so this diff is only the fit: slice work. Retarget to main once #148 merges.

A frame plate's thickness is filled geometry, so stretching it to a non-square panel scales bar weight anisotropically. At 860×190 the verticals come out roughly 4× the weight of the horizontals, with the hand-drawn wobble pulled into coarse teeth. fit: slice holds the corners rigid and repeats the edges at native scale.

fit: stretch vs 9-slice at 3x

Both spelwork frame pulls now use it:

The two presses

Scope

fit: slice joins stretch · contain · cover · tile — and fit is now a checked enum, so a typo fails by name instead of emitting a dead value.

Two knobs on the plate, deliberately separate. Collapsing them is what makes 9-slice look wrong in practice:

slice: 12% which artwork is the corner — matched to the drawn band
slice_width: 20px how thick the band draws

Left coupled (mask-border-width: auto) this plate renders a 59px band that swamps anything but a tall panel. px, not %: percentage border widths resolve against width for the left/right edges and height for top/bottom, so they go visibly asymmetric on any non-square box. Both live on the plate in assets.yml, not per press — corner size belongs to the drawing.

Three things make it work, each found by rendering rather than by reading a support table:

  1. The -webkit-mask-box-image-* longhands. The shorthand silently yields an empty mask, which floods the panel with raw pigment — the same failure mode as the :root handle bug in #148.
  2. A second, intrinsically-sized plate export. Slice percentages resolve against intrinsic size, and the ordinary fit handle deliberately has none (viewBox-only + preserveAspectRatio="none", which is what makes fit mean anything at all). Emitted only for plates that declare an inset.
  3. The plain mask emitted alongside it. Firefox has no mask-border; without this it would lose the mask entirely rather than degrade to the stretch version.

Testing

  • just check — green. Block count still 33.
  • python -m pytest generator/tests/ — 218 pass, 9 new: the enhancement handles, the fallback mask surviving, fit: slice against a plate with no inset, the fit enum, non-slice pulls emitting no slice handles (an undefined var is what makes the longhands fall through), the sliceable registry reading the manifest rather than dist/, and the variant keeping intrinsic size while the ordinary handle keeps PAR=none.
  • The slice and slice_width values were chosen by rendering sweeps (8/12/16/22/30% and auto/28/20/14px/6%), not picked.
  • The docs' §4 CSS recipe was re-rendered verbatim at 3.5:1 to confirm it is still copy-paste.
  • The review artifact's comparison now drives the shipped handles rather than an ad-hoc variant — which let the one-off builder go.

Impact Statement

Additive and opt-in. fit gains a value; every existing pull keeps its behaviour. Only plates declaring slice: get the second export, so plates.css grows for the frame plate alone (277KB → 456KB; the two line plates are untouched). dist/ churn stays confined to spelwork plus the shared plate bundle.

Progressive enhancement, not a dependency. Blink and WebKit get 9-slice; Firefox degrades to the stretch mask it already had. Nothing regresses anywhere.

Reversibility: high. Remove slice: from a plate and it stops being sliceable; set a pull back to fit: stretch and it renders as before.

What this does not settle. Corner + edge plates remain the fully portable alternative and are still the better answer where corner and edge artwork must differ — 9-slice re-uses one drawing's own corners and cannot express that. Which approach the brand wants is still ville's call; this makes one of the two options real and measurable rather than hypothetical.

Unchanged from #148: a pull still covers its whole element — no anchor or extent — so a rule that should sit at one edge cannot yet be expressed.

Stacked on #148 — **base is `feat/udg-press-tier`**, so this diff is only the `fit: slice` work. Retarget to `main` once #148 merges. A frame plate's thickness is *filled geometry*, so stretching it to a non-square panel scales bar weight anisotropically. At 860×190 the verticals come out roughly 4× the weight of the horizontals, with the hand-drawn wobble pulled into coarse teeth. `fit: slice` holds the corners rigid and repeats the edges at native scale. ![fit: stretch vs 9-slice at 3x](https://git.toilville.dev/Toilville/toilville-design/raw/commit/67fc31e/scratchpad/press-review/04-fit.png) Both spelwork frame pulls now use it: ![The two presses](https://git.toilville.dev/Toilville/toilville-design/raw/commit/67fc31e/scratchpad/press-review/01-presses.png) ## Scope **`fit: slice`** joins `stretch` · `contain` · `cover` · `tile` — and `fit` is now a checked enum, so a typo fails by name instead of emitting a dead value. **Two knobs on the plate, deliberately separate.** Collapsing them is what makes 9-slice look wrong in practice: | | | |---|---| | `slice: 12%` | *which* artwork is the corner — matched to the drawn band | | `slice_width: 20px` | how thick the band *draws* | Left coupled (`mask-border-width: auto`) this plate renders a 59px band that swamps anything but a tall panel. **px, not `%`:** percentage border widths resolve against width for the left/right edges and height for top/bottom, so they go visibly asymmetric on any non-square box. Both live on the **plate** in `assets.yml`, not per press — corner size belongs to the drawing. **Three things make it work**, each found by rendering rather than by reading a support table: 1. The `-webkit-mask-box-image-*` **longhands**. The shorthand silently yields an empty mask, which floods the panel with raw pigment — the same failure mode as the `:root` handle bug in #148. 2. A **second, intrinsically-sized plate export**. Slice percentages resolve against intrinsic size, and the ordinary `fit` handle deliberately has none (viewBox-only + `preserveAspectRatio="none"`, which is what makes `fit` mean anything at all). Emitted only for plates that declare an inset. 3. The plain mask emitted **alongside** it. Firefox has no `mask-border`; without this it would lose the mask entirely rather than degrade to the stretch version. ## Testing - `just check` — green. Block count still 33. - `python -m pytest generator/tests/` — **218 pass, 9 new**: the enhancement handles, the fallback mask surviving, `fit: slice` against a plate with no inset, the `fit` enum, non-slice pulls emitting *no* slice handles (an undefined var is what makes the longhands fall through), the sliceable registry reading the manifest rather than `dist/`, and the variant keeping intrinsic size while the ordinary handle keeps `PAR=none`. - The `slice` and `slice_width` values were chosen by rendering sweeps (8/12/16/22/30% and auto/28/20/14px/6%), not picked. - The docs' §4 CSS recipe was re-rendered **verbatim** at 3.5:1 to confirm it is still copy-paste. - The review artifact's comparison now drives the **shipped** handles rather than an ad-hoc variant — which let the one-off builder go. ## Impact Statement **Additive and opt-in.** `fit` gains a value; every existing pull keeps its behaviour. Only plates declaring `slice:` get the second export, so `plates.css` grows for the frame plate alone (277KB → 456KB; the two line plates are untouched). `dist/` churn stays confined to spelwork plus the shared plate bundle. **Progressive enhancement, not a dependency.** Blink and WebKit get 9-slice; Firefox degrades to the stretch mask it already had. Nothing regresses anywhere. **Reversibility:** high. Remove `slice:` from a plate and it stops being sliceable; set a pull back to `fit: stretch` and it renders as before. **What this does not settle.** Corner + edge plates remain the fully portable alternative and are still the better answer where corner and edge artwork must *differ* — 9-slice re-uses one drawing's own corners and cannot express that. Which approach the brand wants is still ville's call; this makes one of the two options real and measurable rather than hypothetical. **Unchanged from #148:** a pull still covers its whole element — no anchor or extent — so a rule that should sit at one edge cannot yet be expressed.
feat(udg): fit: slice — 9-slice presses, so a frame stops distorting
All checks were successful
PR policy check / policy-gate (pull_request) Successful in 0s
Test Generator / lint-workflows (pull_request) Successful in 21s
Test Generator / lint-prose (pull_request) Successful in 22s
Security Scans / Gitleaks secret scan (pull_request) Successful in 27s
Security Scans / Trivy filesystem scan (pull_request) Successful in 33s
UDG dist drift gate / dist/ matches dtcg + udg sources (pull_request) Successful in 14s
Copy coverage / Design copy reaching rendered HTML (pull_request) Successful in 27s
Block Validation / validate (pull_request) Successful in 54s
Test Generator / test (pull_request) Successful in 3m20s
67fc31ef51
A frame plate's thickness is filled geometry, so stretching it to a non-square
panel scales bar weight anisotropically: at 860x190 the verticals come out ~4x
the weight of the horizontals, with the hand-drawn wobble pulled into coarse
teeth. `fit: slice` holds the corners rigid and repeats the edges at native
scale. Measured at 3x magnification it restores equal bar weight and keeps the
wobble on every edge.

Three things make it work, each found by rendering rather than by reading a
support table:

  - the -webkit-mask-box-image-* LONGHANDS. The shorthand silently yields an
    empty mask, which floods the panel with raw pigment.
  - a second, intrinsically-sized plate export. Slice percentages resolve
    against intrinsic size, and the ordinary `fit` handle deliberately has none
    (viewBox-only + preserveAspectRatio="none", which is what makes `fit` mean
    anything at all).
  - the plain mask emitted ALONGSIDE it as the fallback. Firefox has no
    mask-border; without this it would lose the mask entirely rather than
    degrade to the stretch version.

Two knobs, deliberately separate — collapsing them is what makes 9-slice look
wrong in practice:

  - `slice` picks WHICH artwork is the corner (12% here, matching the drawn
    band). Too large and the corner art dominates; it also clamps
    unpredictably on small boxes, which is why an exploratory 46% looked fine.
  - `slice_width` picks how thick the band DRAWS (20px). Left at auto it
    inherits the slice size — 59px for this plate, which swamps a short panel.
    px, not %: percentage border widths resolve against width for left/right
    and height for top/bottom, so they go asymmetric on any non-square box.

Both live on the PLATE in assets.yml, not per press: corner size belongs to the
drawing. A press naming `fit: slice` against a plate with no inset is rejected
by name, and `fit` itself is now a checked enum.

The review artifact's comparison now drives the SHIPPED handles rather than an
ad-hoc variant, which let the one-off builder go. Both spelwork frame pulls use
fit: slice, and the docs' §4 recipe was re-rendered verbatim to confirm it is
still copy-paste.

Corner + edge plates remain the portable alternative, and are still the better
answer where corner and edge artwork must DIFFER — 9-slice re-uses one drawing's
own corners and cannot express that.

Verified: just check green, 218 tests pass (9 new).
peterswimm changed target branch from feat/udg-press-tier to main 2026-09-16 18:36:09 +00:00
peterswimm deleted branch feat/udg-press-fit-slice 2026-09-16 18:36:21 +00:00
Member

Decision: 9-slice. One drawing per frame — no separate corner and edge plates, at least to start.

The corner+edge route buys the ability to make a corner look like something other than its edge. Nothing in the system needs that yet: plate.ui.rectangle is the only frame plate, and it is one hand-drawn rectangle whose corners and edges already come from the same hand. Splitting it would produce two drawings that have to be kept in agreement, to express a difference nobody has asked for.

One drawing is also one thing to draw, one thing to keep consistent, and one thing to get wrong.

What would reverse this

A drawing whose corner must differ from its edge — a frame with a mark, a seal or a join at one corner. 9-slice cannot express that, and the moment such a plate is wanted, corner+edge is the right answer for it. That is a per-plate call, not a system-wide one, so this decision does not have to be undone to make it.

Why it costs little to pick

Firefox degrades to the stretch mask it already had, so choosing 9-slice takes nothing away where it isn't supported. And slice/slice_width living on the plate rather than the press is the right split — corner size is a property of the drawing, and putting it at the call site would invite four frames at four corner sizes.

The anisotropy numbers are the argument for the feature itself: verticals at roughly 4× the weight of the horizontals at 860×190 is not a frame, it is two different frames sharing a box.

**Decision: 9-slice. One drawing per frame — no separate corner and edge plates, at least to start.** The corner+edge route buys the ability to make a corner look like something other than its edge. Nothing in the system needs that yet: `plate.ui.rectangle` is the only frame plate, and it is one hand-drawn rectangle whose corners and edges already come from the same hand. Splitting it would produce two drawings that have to be kept in agreement, to express a difference nobody has asked for. One drawing is also one thing to draw, one thing to keep consistent, and one thing to get wrong. ## What would reverse this A drawing whose corner must differ from its edge — a frame with a mark, a seal or a join at one corner. 9-slice cannot express that, and the moment such a plate is wanted, corner+edge is the right answer for it. That is a per-plate call, not a system-wide one, so this decision does not have to be undone to make it. ## Why it costs little to pick Firefox degrades to the stretch mask it already had, so choosing 9-slice takes nothing away where it isn't supported. And `slice`/`slice_width` living on the plate rather than the press is the right split — corner size is a property of the drawing, and putting it at the call site would invite four frames at four corner sizes. The anisotropy numbers are the argument for the feature itself: verticals at roughly 4× the weight of the horizontals at 860×190 is not a frame, it is two different frames sharing a box.
Sign in to join this conversation.
No description provided.