commit baaebb8639aac1c7468393298a158f75a30efdd5
parent 69043125ad764394a38aabd66ed3e36dbf78b04c
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Mon, 28 Sep 2026 12:33:39 -0400
umtool: svg-faces and the README stop calling the advance sum conservative and 1.3 em a bound (O5 review L1, L2)
Comments and prose only. IBM Plex Sans has positive kern pairs, so the
plain sum of advances is not the conservative side in general; it holds
empirically (real text's worst net kerning +0.50 px). FALLBACK_EM 1.3
covers emoji, flags, CJK and the common scripts, not every fallback glyph
(U+27F9 1.42 em). The file prints identically with comments stripped
(TypeScript printer, removeComments), so an unbranded render is unchanged
without a render. FACTS: the O5 entry's anchors and its "still say it is".
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
3 files changed, 42 insertions(+), 14 deletions(-)
diff --git a/plans/FACTS.md b/plans/FACTS.md
@@ -7121,8 +7121,9 @@ out. O3's facts are the section just above ("O3 — runner lows"). Anchors are a
- **Under `render.brand: "archilyzer-media"` the rail, ledger, scroll and chart SVG text are IBM Plex
Sans, fitted by the face's own advances**, not an average. `svg-faces.mjs`: `FIRA_SANS`
- (`{family, em: 0.5}`, `:44`), `IBM_PLEX_SANS` (a measured face, `:60`), `textWidth(text, size, face,
- {weight, ls})` (`:73`), `FALLBACK_EM = 1.3` for a character Plex does not map (`:41`). **1.3 em
+ (`{family, em: 0.5}`, `:55`), `IBM_PLEX_SANS` (a measured face, `:71`), `textWidth(text, size, face,
+ {weight, ls})` (`:84`), `FALLBACK_EM = 1.3` for a character Plex does not map (`:52`; anchors as
+ of release 13 W2, which lengthened the header comment by eleven lines). **1.3 em
covers emoji, CJK and the common scripts, but not every fallback glyph** (O5 review L2, measured
through the branded conf: `⟹` 1.42 em, `﷽` 1.93, `Ⅷ` 1.31) — a label made mostly of long arrows
would overrun by ~9 %; no real label has one. The table is `face-metrics.mjs`, GENERATED by `fonts/gen-face-metrics.py` (fontTools
@@ -7131,8 +7132,10 @@ out. O3's facts are the section just above ("O3 — runner lows"). Anchors are a
- `render-cards.mjs` `fit(text, size, maxPx, face = FIRA_SANS, run)` (`:428`) and `wrapPx(…, face)`
(`:1010`): an average face (Fira) runs the old code path verbatim; a measured face keeps the
longest prefix whose width — ellipsis, weight and letter-spacing included — fits. **Kerning is
- ignored, and the plain sum is NOT conservative in general** (O5 review L1; `svg-faces.mjs:32-35` and
- the README still say it is): IBM Plex Sans has POSITIVE kerning pairs (412 at wght 400, 348 at 700,
+ ignored, and the plain sum is NOT conservative in general** (O5 review L1; *amended 2026-09-28,
+ release 13 slice W2:* `svg-faces.mjs:32-47`, the `FALLBACK_EM` doc comment and the README's "Why a
+ table, not an average" now say so, and say 1.3 em is not a bound for every glyph — comments and
+ prose only, no code changed, so no render was needed): IBM Plex Sans has POSITIVE kerning pairs (412 at wght 400, 348 at 700,
up to +55 units: `r”`, `y’`, `f”`, `TT`, `AA`; bold `(j` +70), so `T`×60 at 13 px renders 464 px
of ink against a table sum of 446.2. Over the 144 real strings and their capitals the worst net
kerning is +0.50 px, so it is sub-pixel today. The fix, if it ever matters, is positive-only kern
diff --git a/umtool/report-to-video/README.md b/umtool/report-to-video/README.md
@@ -897,10 +897,24 @@ An average cannot promise a column. Measured the same way, Plex Sans averages
and pushing the manifests' longest real text through each call site, even
Fira's 0.50 lets the scroll's bold, letter-spaced column head overrun (152 px
rendered in 140). The table does not: over the same runs, every Plex one
-renders inside its budget (widest 98.9 %). Kerning is ignored: the plain sum of
-advances came out at or above the rendered width of every one of those runs, so
-it is the conservative side. A character the face does not map (emoji, CJK,
-drawn by fontconfig's fallback) counts 1.3 em.
+renders inside its budget (widest 98.9 %).
+
+Two limits, both measured and both sub-visible in the real manifests:
+
+- **Kerning is ignored, and the plain sum is not the conservative side in
+ general.** Plex Sans has positive kern pairs (412 at regular, 348 at bold, up
+ to +55 units: a closing quote after `r`, `y` or `f`, `TT`, `AA`; bold `(j`
+ +70), so text made of them renders wider than the table says — `T` × 60 at
+ 13 px is 464 px of ink against a sum of 446.2. Over the manifests' 144 real
+ strings, and the same in capitals, the worst net kerning is **+0.50 px**, so a
+ fitted run can end half a pixel past its budget at most. If that ever
+ matters, `gen-face-metrics.py` would emit the positive pairs and
+ `textWidth()` add them (the branded path only).
+- **A character the face does not map** (emoji, CJK, drawn by fontconfig's
+ fallback) counts `FALLBACK_EM`, 1.3 em. That covers emoji (~1.23 em), flags,
+ CJK (at most 1 em) and the common scripts, but not every fallback glyph: a few
+ wide symbols exceed it (`⟹` 1.42 em, `﷽` 1.93, `Ⅷ` 1.31), so a label made
+ mostly of them would overrun by about 9 %. No real label has one.
`face-metrics.mjs` is generated: `python3 fonts/gen-face-metrics.py` (fontTools)
rewrites it from the vendored font, and a test fails when it is stale.
diff --git a/umtool/report-to-video/svg-faces.mjs b/umtool/report-to-video/svg-faces.mjs
@@ -29,15 +29,26 @@
// 140). A table does not: fit() keeps the longest prefix whose
// measured width, ellipsis included, is inside the budget.
//
-// The measurement sums advances and ignores kerning: over the manifests'
-// longest real text fitted at every call site, the rendered ink width of every
-// Plex run came out at or under the sum, so the sum is the conservative side.
-// A character the face does not map is drawn by fontconfig's fallback (emoji,
-// CJK) and counted at FALLBACK_EM, wider than either.
+// The measurement sums advances and ignores kerning, and the sum is NOT the
+// conservative side in general: IBM Plex Sans has positive kern pairs (412 at
+// wght 400, 348 at 700, up to +55 units -- a closing quote after r, y or f,
+// TT, AA; bold "(j" +70), so text made of them renders wider than its sum ("T"
+// x60 at 13 px: 464 px of ink against a sum of 446.2). It holds empirically:
+// over the manifests' real text (144 strings, and the same in capitals) the
+// worst net kerning is +0.50 px, so a fitted run can end up to half a pixel
+// past its budget. If that ever matters, gen-face-metrics.py would emit the
+// positive kern pairs and textWidth() add them (the branded path only).
+//
+// A character the face does not map is drawn by fontconfig's fallback and
+// counted at FALLBACK_EM. 1.3 em covers emoji (~1.23 em), flags, CJK (at most
+// 1 em) and the common scripts, but not every fallback glyph: a few wide
+// symbols exceed it (U+27F9 1.42 em, U+FDFD 1.93, U+2167 1.31), so a label made
+// mostly of them would overrun by about 9 %. No real label has one; raising it
+// would only cut text with unmapped characters sooner.
import { IBM_PLEX_SANS_METRICS } from "./face-metrics.mjs";
-/** An unmapped character's advance, in em: wider than a CJK ideograph (1 em) or a colour emoji (~1.25). */
+/** An unmapped character's advance, in em: wider than a CJK ideograph (1 em) or a colour emoji (~1.23), not every fallback glyph (above). */
export const FALLBACK_EM = 1.3;
/** The unbranded face, and its average advance per character. */