Fit a cluster that draws wider than its cells #29

Merged
natsukium merged 1 commit from fix/fit-clusters into main 2026-10-05 02:04:49 +09:00
Owner

Closes #17.

A single glyph wider than its cells is already shrunk to fit them. A multi-codepoint cluster was not: its glyphs were drawn at full size and placed by raw pen advance, so it covered the next character whenever the font could not join what the grid joined.

Changes

  • Cluster fit: a cluster whose summed advance overruns its block shrinks so its union ink fits, and is centred. The trigger, slack and ratio are the single-glyph fit's, so a one-glyph cluster fits as its bare char does. All glyphs and the pen shrink by one factor, so marks stay on their base.
    • Covered: 🇦🇦 (no flag glyph), non-RGI ZWJ sequences, 😀🏻, keycaps swash does not ligate, ※́, and a ❤️ left in one cell by a refused VS16 widen.
  • Block width: the grid's span (char_span) or OSC 66 w, not the base char's width. Sized-cluster alignment now uses the same span; it undercounted a widened ❤️ and overcounted a refused one.
  • Caching: fitted glyphs take their own slot key (SlotKey::FittedGlyph(glyph, px)), because U+0301 can be native in one cluster and shrunk in another. The measurement is memoised in the GlyphIndex, so a font reload drops it; the shape frame carries each cell's fit_px to emission.
  • Ligatures: apply_shaped_run now has the overflow check text-shaping.md described. A ligature wider than its source cells (one pixel per cell of slack) is dropped, and the per-character path draws those characters.
  • Unfitted clusters at the default Top/Left alignment render exactly as before.

Verification

  • just check green.

  • Tests on the pinned font set cover fill and centring, the bare-char equivalence, unfitted clusters, the memo, and the pen under an OSC 66 fractional scale. Mock-atlas tests cover the fitted lookup and the grid-span block.

  • The tests avoid 🇦🇦: swash panics on it in debug builds (#22).

  • Before / after renders (offscreen renderer, Moralerspace Neon HW) of each repro row:

    before / after

Not changed

  • ①́ and 字́ still draw their mark past the cell. Their advance does not overrun, so the fit does not apply; the mark's position comes from the font.
  • Keycaps and ※́ come out small. swash does not ligate them, so the fit shrinks a whole row of side-by-side glyphs into the cell.
Closes #17. A single glyph wider than its cells is already shrunk to fit them. A multi-codepoint cluster was not: its glyphs were drawn at full size and placed by raw pen advance, so it covered the next character whenever the font could not join what the grid joined. ## Changes - **Cluster fit:** a cluster whose summed advance overruns its block shrinks so its union ink fits, and is centred. The trigger, slack and ratio are the single-glyph fit's, so a one-glyph cluster fits as its bare char does. All glyphs and the pen shrink by one factor, so marks stay on their base. - Covered: `🇦🇦` (no flag glyph), non-RGI ZWJ sequences, `😀🏻`, keycaps swash does not ligate, `※́`, and a `❤️` left in one cell by a refused VS16 widen. - **Block width:** the grid's span (`char_span`) or OSC 66 `w`, not the base char's width. Sized-cluster alignment now uses the same span; it undercounted a widened `❤️` and overcounted a refused one. - **Caching:** fitted glyphs take their own slot key (`SlotKey::FittedGlyph(glyph, px)`), because U+0301 can be native in one cluster and shrunk in another. The measurement is memoised in the `GlyphIndex`, so a font reload drops it; the shape frame carries each cell's `fit_px` to emission. - **Ligatures:** `apply_shaped_run` now has the overflow check `text-shaping.md` described. A ligature wider than its source cells (one pixel per cell of slack) is dropped, and the per-character path draws those characters. - Unfitted clusters at the default Top/Left alignment render exactly as before. ## Verification - `just check` green. - Tests on the pinned font set cover fill and centring, the bare-char equivalence, unfitted clusters, the memo, and the pen under an OSC 66 fractional scale. Mock-atlas tests cover the fitted lookup and the grid-span block. - The tests avoid `🇦🇦`: swash panics on it in debug builds (#22). - Before / after renders (offscreen renderer, Moralerspace Neon HW) of each repro row: ![before / after](https://git.natsukium.com/attachments/ebea3559-20c7-4a14-baf3-3873fd14b1c6) ## Not changed - `①́` and `字́` still draw their mark past the cell. Their advance does not overrun, so the fit does not apply; the mark's position comes from the font. - Keycaps and `※́` come out small. swash does not ligate them, so the fit shrinks a whole row of side-by-side glyphs into the cell.
render: fit a cluster that draws wider than its cells
Some checks failed
windows / detect relevant changes (pull_request) Successful in 8s
windows / windows docs-only no-op (pull_request) Has been skipped
darwin / detect relevant changes (pull_request) Successful in 13s
darwin / darwin docs-only no-op (pull_request) Has been skipped
pr / nix flake check (pull_request) Successful in 37s
darwin / build felis (aarch64-darwin) (pull_request) Successful in 1m48s
pr / cargo build / clippy / test / deny (pull_request) Failing after 2m31s
pr / wire schema is compatible with the base (pull_request) Successful in 4s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 54s
windows / cargo nextest (Windows) (pull_request) Successful in 4m19s
windows / cargo clippy (Windows cross) (pull_request) Successful in 32s
windows / frontend smoke (Windows) (pull_request) Successful in 49s
windows / package felis (x86_64-pc-windows-msvc) (pull_request) Has been skipped
1705c03901
A cluster cell rasterized its glyphs at full size and placed them by
raw pen advance, so whenever the face could not join what the grid
joined, the cluster covered its neighbours: a regional-indicator pair
with no flag glyph, a ZWJ sequence the face does not ligate, a skin
tone on a base that takes none, a keycap swash does not ligate, a CJK
`※` with a combining mark. A VS16 widen the grid refused at the last
column left a two-cell emoji in one cell the same way.

Such a cluster now fits by the rule a single overrunning glyph already
follows: its summed advance decides whether it is fitted, the union of
its glyphs' ink decides the size, and it is centred in its block. All
its glyphs and its pen shrink by one factor, so marks stay on their
base. The decision is made where the cluster is shaped, and the frame
carries the fitted size to emission; the fitted glyphs take their own
slot key because a mark such as U+0301 appears native in one cluster
and shrunk in another. Measuring rasterizes every glyph, so the result
is memoised in the glyph index, which a font reload replaces.

The block is the cells the grid gave the cluster, or OSC 66 `w`. The
alignment of sized clusters read the base character's width instead,
which undercounts a widened heart and overcounts a refused one, so it
now uses the same span.

The ligature run gets the overflow check text-shaping.md already
described: a ligature wider than its source cells is dropped and the
per-character path draws them, so a proportional primary face no longer
smears its ligatures over the next cell.
natsukium force-pushed fix/fit-clusters from 1705c03901
Some checks failed
windows / detect relevant changes (pull_request) Successful in 8s
windows / windows docs-only no-op (pull_request) Has been skipped
darwin / detect relevant changes (pull_request) Successful in 13s
darwin / darwin docs-only no-op (pull_request) Has been skipped
pr / nix flake check (pull_request) Successful in 37s
darwin / build felis (aarch64-darwin) (pull_request) Successful in 1m48s
pr / cargo build / clippy / test / deny (pull_request) Failing after 2m31s
pr / wire schema is compatible with the base (pull_request) Successful in 4s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 54s
windows / cargo nextest (Windows) (pull_request) Successful in 4m19s
windows / cargo clippy (Windows cross) (pull_request) Successful in 32s
windows / frontend smoke (Windows) (pull_request) Successful in 49s
windows / package felis (x86_64-pc-windows-msvc) (pull_request) Has been skipped
to cd32546a85
All checks were successful
bench / Criterion full-suite snapshot (pull_request) Has been skipped
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
darwin / detect relevant changes (pull_request) Successful in 8s
darwin / darwin docs-only no-op (pull_request) Has been skipped
windows / detect relevant changes (pull_request) Successful in 14s
windows / windows docs-only no-op (pull_request) Has been skipped
darwin / build felis (aarch64-darwin) (pull_request) Successful in 1m1s
windows / cargo nextest (Windows) (pull_request) Successful in 3m50s
bench / Criterion regression gate (pull_request) Successful in 2m0s
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 1m8s
pr / nix flake check (pull_request) Successful in 36s
pr / cargo build / clippy / test / deny (pull_request) Successful in 2m13s
pr / wire schema is compatible with the base (pull_request) Successful in 2s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 39s
windows / cargo clippy (Windows cross) (pull_request) Successful in 14s
windows / frontend smoke (Windows) (pull_request) Successful in 1m11s
windows / package felis (x86_64-pc-windows-msvc) (pull_request) Has been skipped
2026-10-05 01:47:20 +09:00
Compare
natsukium deleted branch fix/fit-clusters 2026-10-05 02:04:49 +09:00
Sign in to join this conversation.
No reviewers
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
felis-terminal/felis!29
No description provided.