@weasel-js/font 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,562 @@
1
+ /**
2
+ * BmFont JSON types and parser for the MSDF atlas format produced by
3
+ * msdf-bmfont-xml. The parser builds accelerator maps (charMap, kerningMap)
4
+ * for O(1) glyph and kerning lookup during layout.
5
+ */
6
+ interface BmFontInfo {
7
+ face: string;
8
+ size: number;
9
+ }
10
+ /** Font-wide vertical metrics and the atlas page dimensions, in pixels. */
11
+ interface BmFontCommon {
12
+ lineHeight: number;
13
+ base: number;
14
+ scaleW: number;
15
+ scaleH: number;
16
+ }
17
+ /** One glyph: where it sits in the atlas page (`x`/`y`/`width`/`height`),
18
+ * where to draw it relative to the pen (`xoffset`/`yoffset`), and how far the
19
+ * pen then advances. */
20
+ interface BmFontChar {
21
+ id: number;
22
+ x: number;
23
+ y: number;
24
+ width: number;
25
+ height: number;
26
+ xoffset: number;
27
+ yoffset: number;
28
+ xadvance: number;
29
+ page: number;
30
+ }
31
+ /** A kerning pair: `amount` px to add to the advance when `second` follows
32
+ * `first`. Codepoints, not glyph indices. */
33
+ interface BmFontKerning {
34
+ first: number;
35
+ second: number;
36
+ amount: number;
37
+ }
38
+ /** A parsed MSDF atlas: the BmFont record plus the `charMap` / `kerningMap`
39
+ * accelerators layout reads per glyph. */
40
+ interface BmFont {
41
+ info: BmFontInfo;
42
+ common: BmFontCommon;
43
+ chars: BmFontChar[];
44
+ kernings: BmFontKerning[];
45
+ charMap: Map<number, BmFontChar>;
46
+ kerningMap: Map<number, Map<number, number>>;
47
+ }
48
+ /** Two-glyph fixture for unit tests. */
49
+ declare const FIXTURE_FONT: {
50
+ info: {
51
+ face: string;
52
+ size: number;
53
+ };
54
+ common: {
55
+ lineHeight: number;
56
+ base: number;
57
+ scaleW: number;
58
+ scaleH: number;
59
+ };
60
+ chars: {
61
+ id: number;
62
+ x: number;
63
+ y: number;
64
+ width: number;
65
+ height: number;
66
+ xoffset: number;
67
+ yoffset: number;
68
+ xadvance: number;
69
+ page: number;
70
+ }[];
71
+ kernings: {
72
+ first: number;
73
+ second: number;
74
+ amount: number;
75
+ }[];
76
+ };
77
+ /** Validate a BmFont JSON document and build its lookup maps. Throws with the
78
+ * missing or malformed field named. */
79
+ declare function parseBmFont(raw: unknown): BmFont;
80
+
81
+ /**
82
+ * The only thing the glyph tier needs from a GL texture cache. Core's
83
+ * `GLTextureCache` satisfies this structurally — there is no adapter and no
84
+ * registration step, because the type *is* the seam.
85
+ *
86
+ * Declared here rather than imported so this package has zero reach-back into
87
+ * core. If core's cache ever drops one of these methods, the failure surfaces
88
+ * as a type error at the call site in `renderer/draw.ts`, which is where it
89
+ * belongs.
90
+ */
91
+ /** Anything WebGL can upload as a texture. */
92
+ type TexSource = HTMLImageElement | ImageBitmap | ImageData | HTMLCanvasElement;
93
+ /** The texture-upload surface the glyph tier needs. Core's `GLTextureCache`
94
+ * satisfies it structurally, so nothing registers or adapts. */
95
+ interface GlyphTextureSink {
96
+ has(id: string): boolean;
97
+ upload(id: string, source: TexSource): string;
98
+ uploadR8(id: string, width: number, height: number, data: Uint8Array): void;
99
+ subImageR8(id: string, x: number, y: number, w: number, h: number, data: Uint8Array): void;
100
+ }
101
+
102
+ interface FaceMetrics {
103
+ ascent: number;
104
+ descent: number;
105
+ }
106
+ interface RasterizedGlyph {
107
+ width: number;
108
+ height: number;
109
+ alpha: Uint8ClampedArray;
110
+ left: number;
111
+ top: number;
112
+ advance: number;
113
+ }
114
+ interface GlyphRasterizer {
115
+ faceMetrics(family: string, weight: number, style: 'normal' | 'italic'): FaceMetrics;
116
+ rasterize(family: string, weight: number, style: 'normal' | 'italic', codepoint: number): RasterizedGlyph;
117
+ }
118
+
119
+ /**
120
+ * DynamicGlyphAtlas — runtime single-channel SDF glyphs for canvas-sourced
121
+ * (installed machine) fonts. TinySDF technique: canvas fillText at 48 px →
122
+ * Euclidean distance transform → shelf-packed R8 pages (1024², max 4).
123
+ *
124
+ * Baked MSDF atlases always win: this tier only serves families registered
125
+ * via `registerCanvasFont` that have no baked entry (see resolveFontVariant).
126
+ *
127
+ * Faces expose a BmFont-shaped `font` whose charMap grows as glyphs are
128
+ * requested, so `layoutRuns` consumes them through the same code path as
129
+ * baked atlases. A char's advance is valid immediately (measureText); its
130
+ * atlas rect fills in when the bake lands (width 0 / page -1 until then, so
131
+ * layout advances the pen but emits no quad).
132
+ */
133
+
134
+ /** How many glyphs the dynamic atlas will rasterize synchronously before
135
+ * deferring the rest to a later flush. Caps the frame cost of text that
136
+ * suddenly needs many new glyphs; the leftovers arrive a tick later and
137
+ * notify through `onGlyphReady`. */
138
+ declare const DEFAULT_BAKE_BUDGET = 16;
139
+ interface DynamicFace {
140
+ family: string;
141
+ weight: number;
142
+ style: 'normal' | 'italic';
143
+ /** BmFont-shaped view consumed by layoutRuns; charMap grows lazily. */
144
+ font: BmFont;
145
+ /** Char record for `cp`, measured on first request (advance always valid
146
+ * immediately; atlas rect fills in when the bake lands). */
147
+ requestGlyph(cp: number): BmFontChar;
148
+ }
149
+ interface DynamicPage {
150
+ data: Uint8Array;
151
+ /** Bumps on every glyph blit; each blit appends a patch with seq = version. */
152
+ version: number;
153
+ patches: {
154
+ seq: number;
155
+ x: number;
156
+ y: number;
157
+ w: number;
158
+ h: number;
159
+ }[];
160
+ }
161
+ /** One family served by the dynamic canvas-SDF tier. See `listCanvasFonts`. */
162
+ interface CanvasFontEntry {
163
+ family: string;
164
+ /** `'explicit'` — a `registerCanvasFont` call; `'auto'` — the `'canvas'`
165
+ * fallback policy enrolled it on a miss, and the enrollment lapses if
166
+ * that policy changes. */
167
+ enrollment: 'explicit' | 'auto';
168
+ }
169
+ /** Mark `family` as canvas-sourced: when no baked atlas covers it,
170
+ * resolveFontVariant serves it from this dynamic atlas. */
171
+ declare function registerCanvasFont(family: string): void;
172
+ /**
173
+ * Will `family` be served by the dynamic canvas-SDF tier *right now*?
174
+ *
175
+ * Service, not membership — the answer depends on the fallback policy in
176
+ * force and can change without any enrollment call:
177
+ * - explicitly enrolled via `registerCanvasFont` → `true` under every
178
+ * policy; a consumer naming a family outranks the policy.
179
+ * - auto-enrolled by the `'canvas'` policy → `true` only while that policy
180
+ * is still in force. The enrollment lapses rather than being discarded,
181
+ * so returning to `'canvas'` makes it `true` again.
182
+ * - never enrolled → `false`, including under `'canvas'` (that policy
183
+ * enrolls lazily, on the first miss).
184
+ *
185
+ * The membership reading would answer `true` for an auto-enrolled family
186
+ * under `'substitute'` / `'none'`, where nothing routes to this tier — a
187
+ * caller predicting what renders would be told the opposite of the truth.
188
+ * Mirrors `listFonts`, which reports the baked registry alone for the same
189
+ * reason: "enrolled" is not "will render".
190
+ */
191
+ declare function isCanvasFont(family: string): boolean;
192
+ /**
193
+ * Every family the dynamic canvas-SDF tier will serve *right now* — the
194
+ * enumeration companion to `isCanvasFont`, which answers one family at a
195
+ * time. Same "service, not membership" rule: an auto-enrolled family appears
196
+ * only while the `'canvas'` policy is in force.
197
+ *
198
+ * Exists so a consumer can offer these families in a font picker.
199
+ * `listFonts` reports the baked registry alone, so without this the only
200
+ * ways to present canvas-served families were to hard-code a list beside the
201
+ * `registerCanvasFont` calls or to omit them — a UI inventing its own answer
202
+ * about what will render.
203
+ *
204
+ * `enrollment` distinguishes a consumer's explicit `registerCanvasFont` from
205
+ * a lazy enrollment the policy made on a miss; both render identically, but
206
+ * only the explicit one survives a policy change.
207
+ */
208
+ declare function listCanvasFonts(): readonly CanvasFontEntry[];
209
+ /** Remove a canvas family. Its faces are dropped; already-baked glyph
210
+ * pixels stay in their pages (no eviction in v1). */
211
+ declare function unregisterCanvasFont(family: string): void;
212
+ /** Reset the per-frame synchronous bake budget. Called by
213
+ * `WeaselRenderer.render()` at frame start; the headless
214
+ * `renderSceneToPixels` path passes Infinity so print never defers. */
215
+ declare function resetBakeBudget(n?: number): void;
216
+ /** Texture-cache key for a dynamic page (parallel to `textureCacheKey`). */
217
+ declare function dynamicPageTextureId(page: number): string;
218
+ /** Bring `cache`'s copy of page `pageIndex` up to date: full R8 upload the
219
+ * first time, `texSubImage2D` patches after. Returns false if the page
220
+ * doesn't exist yet. */
221
+ declare function syncDynamicPageTexture(cache: GlyphTextureSink, pageIndex: number): boolean;
222
+ /** @internal test seam — inject a fake rasterizer (jsdom has no canvas
223
+ * metrics). Pass null to restore the lazy default. */
224
+ declare function __setGlyphRasterizerForTests(r: GlyphRasterizer | null): void;
225
+ /** @internal test seam — inspect CPU-side pages. */
226
+ declare function _getPagesForTests(): readonly DynamicPage[];
227
+ /** @internal test seam — clear all dynamic-font state. */
228
+ declare function _resetDynamicFontsForTests(): void;
229
+
230
+ /**
231
+ * FontRegistry and registerFont() public API.
232
+ *
233
+ * Variants are keyed by (family, weight, style). registerFont() takes a
234
+ * FontVariant alongside the family and the two URLs; the registry stores
235
+ * entries in a two-level Map so resolveFontVariant() can iterate a family's
236
+ * variants for the fallback chain.
237
+ */
238
+
239
+ /** A registered face: its parsed metrics and the atlas image to sample. */
240
+ interface FontEntry {
241
+ font: BmFont;
242
+ bitmap: ImageBitmap;
243
+ }
244
+ /** Which face within a family. Defaults to weight 400, style `'normal'`. */
245
+ interface FontVariant {
246
+ weight?: number;
247
+ style?: 'normal' | 'italic';
248
+ }
249
+ type FontStyle = 'normal' | 'italic';
250
+ /** Test helper. Do not call from product code. */
251
+ declare function _resetFontRegistryForTests(): void;
252
+ /** Exact lookup — does NOT walk the fallback chain. Use `resolveFontVariant` for that. */
253
+ declare function getFont(family: string, weight?: number, style?: FontStyle): FontEntry | null;
254
+ /** One family in the registry and the variants registered for it — what a
255
+ * font picker can honestly offer. */
256
+ interface RegisteredFont {
257
+ family: string;
258
+ variants: readonly {
259
+ weight: number;
260
+ style: FontStyle;
261
+ }[];
262
+ }
263
+ /**
264
+ * Enumerate the registry — what a font picker can honestly offer. Families
265
+ * come back in registration order; variants sorted by weight, then style, so
266
+ * the output is stable enough to assert against.
267
+ */
268
+ declare function listFonts(): readonly RegisteredFont[];
269
+ /**
270
+ * Fetch a baked MSDF atlas and its metrics, and register them as one variant
271
+ * of `family`. Resolves once the face is usable; re-registering a variant that
272
+ * is already present is a no-op.
273
+ */
274
+ declare function registerFont(family: string, variant: FontVariant, metricsUrl: string, atlasUrl: string): Promise<void>;
275
+ /**
276
+ * Ensure the atlas for `(family, weight, style)` is uploaded to
277
+ * `textureCache`. Cache key is `${family}|${weight}|${style}` so each
278
+ * variant occupies its own texture slot.
279
+ */
280
+ declare function ensureFontTexture(family: string, weight: number, style: FontStyle, textureCache: GlyphTextureSink): boolean;
281
+ /** The texture cache key used by `ensureFontTexture` for a given variant. */
282
+ declare function textureCacheKey(family: string, weight: number, style: FontStyle): string;
283
+ /** Kept as a no-op for context-restore call sites; per-cache dedup handles it now. */
284
+ declare function markAllFontsNotUploaded(): void;
285
+ /** What `resolveFontVariant` found: the atlas to draw with, the face it
286
+ * actually landed on after walking the fallback chain, and what has to be
287
+ * synthesized to cover the difference. */
288
+ interface ResolveResult {
289
+ entry: FontEntry | null;
290
+ /**
291
+ * The (family, weight, style) triple that was actually matched. May differ
292
+ * from the requested values when the resolver walked the fallback chain —
293
+ * including `family`, when the cross-family policy substituted a default.
294
+ *
295
+ * This is the atlas identity: pass all three to `getFont` /
296
+ * `textureCacheKey` and the lookup hits. Describing only weight and style
297
+ * here once let a caller key its draw on the *requested* family, which
298
+ * resolves to no atlas at all and paints nothing.
299
+ *
300
+ * The synthetic flags describe the gap between requested and resolved for
301
+ * shader-side compensation. When `entry` is null, these mirror the
302
+ * requested values.
303
+ */
304
+ resolved: {
305
+ family: string;
306
+ weight: number;
307
+ style: FontStyle;
308
+ };
309
+ synthetic: {
310
+ bold: boolean;
311
+ italic: boolean;
312
+ };
313
+ /** Which tier resolved: a baked MSDF atlas, or the runtime canvas-SDF
314
+ * dynamic atlas. Misses report 'atlas' (the default tier). */
315
+ source: 'atlas' | 'canvas';
316
+ /** Set only when source === 'canvas': the dynamic face whose BmFont-shaped
317
+ * `font` layoutRuns consumes in place of `entry.font`. */
318
+ dynamicFace?: DynamicFace;
319
+ /**
320
+ * Set when the requested family was not registered and the fallback policy
321
+ * substituted a different one. Reported structurally so a UI can say
322
+ * "Inter — not loaded, showing Roboto" instead of leaving the user to
323
+ * wonder why the family control did nothing.
324
+ */
325
+ substituted?: {
326
+ requested: string;
327
+ resolved: string;
328
+ };
329
+ }
330
+ /**
331
+ * Resolve a `(family, weight, style)` request to a registered font entry,
332
+ * walking the fallback chain when an exact match isn't available. Returns
333
+ * synthetic flags describing the gap between requested and resolved so the
334
+ * renderer can apply SDF-thicken / vertex-skew fakes.
335
+ */
336
+ declare function resolveFontVariant(family: string, weight: number, style: FontStyle): ResolveResult;
337
+ /**
338
+ * Escalate a *single codepoint* to the dynamic tier when the atlas that
339
+ * resolved for the run has no glyph for it.
340
+ *
341
+ * `resolveFontVariant` answers at family granularity: it picks one tier for a
342
+ * whole run. But a baked MSDF atlas covers a fixed charset, so a run served by
343
+ * a perfectly good atlas can still contain a character that atlas never baked
344
+ * — an em dash, a curly quote, anything outside the subset. The dynamic tier
345
+ * rasterizes on demand from installed fonts and can serve exactly those.
346
+ *
347
+ * Returns a canvas-tier `ResolveResult` whose `dynamicFace` the caller drives
348
+ * with `requestGlyph(cp)`, or `null` when escalation isn't available:
349
+ *
350
+ * - Policy `'none'` documents a miss as a *hard* miss. Quietly reaching for
351
+ * another tier per codepoint would undo that, so it doesn't.
352
+ * - No canvas to rasterize into (SSR, a jsdom test without one) makes the
353
+ * dynamic tier constructible-but-broken; `getDynamicFace` throws and this
354
+ * reports the miss instead of taking the caller down with it.
355
+ *
356
+ * Cheap to call per missing codepoint: faces are cached by variant and glyphs
357
+ * by codepoint, so a repeat is two map lookups.
358
+ */
359
+ declare function resolveGlyphFallback(family: string, weight: number, style: FontStyle): ResolveResult | null;
360
+
361
+ /**
362
+ * Cross-family fallback policy. The per-family chain in `resolveFontVariant`
363
+ * (weight → style → synthetic) has always been rich; what was missing is what
364
+ * happens when the family itself was never registered. That case used to
365
+ * render nothing at all, which is the single most common cause of "my text is
366
+ * invisible".
367
+ */
368
+ /**
369
+ * What happens when a requested family has no baked atlas:
370
+ * - `'substitute'` — render with the default family (see
371
+ * `setDefaultFontFamily`), reporting the swap on `ResolveResult.substituted`.
372
+ * - `'canvas'` — auto-enroll the family with the dynamic canvas-SDF
373
+ * rasterizer. The consumer gets the *real* typeface if the browser has it,
374
+ * at canvas-SDF quality rather than baked-MSDF quality.
375
+ * - `'none'` — hard miss; the run renders nothing.
376
+ */
377
+ type FontFallbackPolicy = 'substitute' | 'canvas' | 'none';
378
+ /** Set what happens when text asks for a family that was never registered.
379
+ * Process-wide; defaults to `'substitute'`. */
380
+ declare function setFontFallbackPolicy(next: FontFallbackPolicy): void;
381
+ /** The cross-family fallback policy currently in force. */
382
+ declare function getFontFallbackPolicy(): FontFallbackPolicy;
383
+ /** Explicit default family for `'substitute'`. When unset, the first
384
+ * registered family wins — the right answer for the common case of an app
385
+ * that registers exactly one. */
386
+ declare function setDefaultFontFamily(family: string): void;
387
+ /** The family `setDefaultFontFamily` named, or `null` if none was set — in
388
+ * which case `'substitute'` falls back to the first registered family. */
389
+ declare function getDefaultFontFamily(): string | null;
390
+ /** Test helper. Do not call from product code. */
391
+ declare function _resetFallbackForTests(): void;
392
+
393
+ /**
394
+ * The parsed-font seam for the outline tier.
395
+ *
396
+ * A face answers exactly one question — "what does codepoint N look like?" —
397
+ * and answers it as **SVG path data in em space**: one unit is one em, y
398
+ * grows downward, and the origin sits on the baseline at the glyph's pen
399
+ * position. That is a complete description of the glyph independent of size,
400
+ * zoom, and position, which is the whole reason the tier exists: tessellate
401
+ * once, transform per instance.
402
+ *
403
+ * ### Why `d` and not a command stream
404
+ *
405
+ * `@weasel-js/font` is a Tier A leaf — `@weasel-js/core` depends on it and
406
+ * never the reverse (see `leaf-purity.test.ts`), so this package cannot name
407
+ * core's `PolygonPath`. The alternative to a string would be re-declaring
408
+ * core's `PATH_M`/`PATH_L`/… opcodes over here and trusting two packages to
409
+ * keep the same numbering forever. SVG `d` is the kit's documented language
410
+ * for geometry crossing a boundary (`docs/conventions.md`), core already
411
+ * ships `pathFromD`, and a glyph outline is exactly the case it describes.
412
+ *
413
+ * The string is also a natural cache key and costs nothing to hold: the
414
+ * parse happens once per glyph, and core caches the *tessellation*, not the
415
+ * text.
416
+ */
417
+ /** Weight/style pair identifying one face within a family. */
418
+ type OutlineFontStyle = 'normal' | 'italic';
419
+ /** A parsed font face, viewed only as a source of glyph outlines. See the
420
+ * module comment for the em-space contract `glyphD` returns. */
421
+ interface OutlineFace {
422
+ /** Font design units per em. Reported for diagnostics; `glyphD` has already
423
+ * divided by it, so callers never need to. */
424
+ unitsPerEm: number;
425
+ /**
426
+ * Em-space SVG path data for `cp`, or `null` when this face has no glyph
427
+ * for the codepoint *or* the glyph has no contours (a space). Both answers
428
+ * mean the same thing to a caller: there is nothing here to tessellate, so
429
+ * fall through to whatever tier would have drawn it.
430
+ */
431
+ glyphD(cp: number): string | null;
432
+ }
433
+ /**
434
+ * Turn font file bytes into a face. Async because the default implementation
435
+ * dynamically imports its parser — see `opentypeParser.ts` for why that
436
+ * matters — and because a caller may want to hand back a face that finishes
437
+ * initializing off-thread.
438
+ */
439
+ type OutlineParser = (bytes: ArrayBuffer) => OutlineFace | Promise<OutlineFace>;
440
+
441
+ /**
442
+ * The outline tier's registry: which faces have real font bytes behind them,
443
+ * and what a given glyph looks like.
444
+ *
445
+ * ### What this tier is for
446
+ *
447
+ * A distance field is a *sampled* representation. The baked MSDF atlas is
448
+ * generated from outlines and holds up well; the dynamic canvas-SDF tier
449
+ * (`../dynamic/`) reconstructs its field from a 48px raster, so magnifying it
450
+ * past a few times the bake size exposes the raster as contour wobble — bumps
451
+ * one bake-texel apart, ±2–3px at 8×. No bake size fixes that, it only moves
452
+ * the size at which it appears (`glyphRasterizer.ts` measures the tradeoff:
453
+ * error is *minimized at* the bake size, so raising it spoils the 12–32px
454
+ * range where most text lives).
455
+ *
456
+ * Above a size threshold the answer is to stop sampling: parse the font, take
457
+ * the glyph's outline, and hand it to the path renderer the kit already has.
458
+ * Exact at every zoom, and — because a glyph becomes an ordinary path — it
459
+ * takes strokes and non-solid fills for free.
460
+ *
461
+ * Below the threshold the atlas still wins, and not narrowly: outlines carry
462
+ * no hinting and no stem darkening, so body text at 12–16px rendered from
463
+ * them looks *worse* than a platform rasterizer that puts stems on the pixel
464
+ * grid. Two tiers with a size threshold is the standard hybrid, not a
465
+ * compromise.
466
+ *
467
+ * ### Metric neutrality (the load-bearing invariant)
468
+ *
469
+ * This tier replaces how a glyph is *painted*, never where it sits. Advances,
470
+ * kerning, line breaking and baselines all keep coming from whichever tier
471
+ * resolved the run — the baked atlas or the canvas face. That is what lets
472
+ * the threshold be a rendering decision: zooming past it swaps glyph geometry
473
+ * with the layout untouched, so text cannot reflow under the user's cursor,
474
+ * and `measureTextBounds` / `textLineBoxes` need to know nothing about
475
+ * outlines at all.
476
+ *
477
+ * The cost is that a face whose real advances disagree with the atlas's
478
+ * inherits the atlas's. For the bundled Inter that is exact (the subset ships
479
+ * from the same source the atlas was baked from), and for a machine font the
480
+ * outline bytes and the canvas metrics come from the same file.
481
+ *
482
+ * ### Availability
483
+ *
484
+ * Everything here degrades rather than blocks. Bytes arrive over `fetch` or
485
+ * `queryLocalFonts` — asynchronous, and in the local case gated behind a
486
+ * permission the user can refuse — so `glyphOutline` answers `null` until a
487
+ * face is loaded and forever if it failed, and the caller falls back down the
488
+ * ladder to SDF. A family must never render *nothing* because the outline
489
+ * tier could not get bytes.
490
+ */
491
+
492
+ /**
493
+ * Where a face's bytes come from. A URL is fetched; a buffer or blob is used
494
+ * as-is; a thunk is called at first use, which is what lets `queryLocalFonts`
495
+ * results be registered eagerly and read lazily — the permission prompt and
496
+ * the (potentially many megabytes of) blob only happen for a family the
497
+ * document actually sets text in.
498
+ */
499
+ type OutlineSource = string | ArrayBuffer | Blob | (() => ArrayBuffer | Blob | Promise<ArrayBuffer | Blob>);
500
+ /** Which face within a family to register outlines for. Defaults to weight
501
+ * 400, style `'normal'`. */
502
+ interface OutlineVariant {
503
+ weight?: number;
504
+ style?: OutlineFontStyle;
505
+ }
506
+ /** Options for registering an outline font. */
507
+ interface OutlineFontOptions {
508
+ /** Override the default opentype.js parser. Mostly a test seam; also the
509
+ * hook for a consumer who already has a font parser in their bundle. */
510
+ parser?: OutlineParser;
511
+ }
512
+ /** Load state of one registered face. */
513
+ type OutlineStatus = 'idle' | 'loading' | 'ready' | 'failed';
514
+ /**
515
+ * Register font file bytes for one face, so text set in it can render from
516
+ * outlines above the size threshold.
517
+ *
518
+ * Registration is cheap and synchronous: nothing is fetched, and the parser
519
+ * is not even imported, until a frame actually asks for a glyph from this
520
+ * face. Registering a face that is never drawn large costs one map entry.
521
+ *
522
+ * The variant must match exactly what the layout tier resolved — this is
523
+ * deliberately *not* a fallback chain. Painting 400-weight outlines at
524
+ * 700-weight advances, or upright outlines where the shader was going to
525
+ * fake an oblique, looks worse than the SDF it replaced; a miss here falls
526
+ * back down the ladder instead, which is always safe.
527
+ */
528
+ declare function registerFontOutlines(family: string, variant: OutlineVariant, source: OutlineSource, opts?: OutlineFontOptions): void;
529
+ /** Drop a registration. Glyphs already handed out stay valid — they are
530
+ * plain path data — but nothing further resolves from this face. */
531
+ declare function unregisterFontOutlines(family: string, variant?: OutlineVariant): void;
532
+ /**
533
+ * Is an outline face registered for this exact variant? Answers `true` while
534
+ * the bytes are still loading — the question is "could this face serve
535
+ * outlines", which is what a caller deciding whether to *ask* wants; use
536
+ * `outlineStatus` for the narrower "can it right now".
537
+ */
538
+ declare function hasFontOutlines(family: string, weight?: number, style?: OutlineFontStyle): boolean;
539
+ /** Load state of a registered face; `null` when nothing is registered. */
540
+ declare function outlineStatus(family: string, weight?: number, style?: OutlineFontStyle): OutlineStatus | null;
541
+ /** Every registered outline face and its load state — the enumeration a
542
+ * debug overlay or font picker needs, mirroring `listFonts`. */
543
+ declare function listFontOutlines(): readonly {
544
+ family: string;
545
+ weight: number;
546
+ style: OutlineFontStyle;
547
+ status: OutlineStatus;
548
+ }[];
549
+ /**
550
+ * Em-space SVG path data for one glyph of a registered outline face, or `null`
551
+ * when there is nothing to draw.
552
+ *
553
+ * Synchronous and non-throwing by design: a face that has not been parsed yet
554
+ * starts loading and answers `null` for now, so a renderer can call this every
555
+ * frame and simply fall through to another text tier until the outline
556
+ * arrives. Results are cached per codepoint.
557
+ */
558
+ declare function glyphOutline(family: string, weight: number, style: OutlineFontStyle, cp: number): string | null;
559
+ /** @internal Test seam — registry and warn-once keys are module state. */
560
+ declare function _resetFontOutlinesForTests(): void;
561
+
562
+ export { _resetFontRegistryForTests as $, outlineStatus as A, type BmFont as B, type CanvasFontEntry as C, DEFAULT_BAKE_BUDGET as D, parseBmFont as E, FIXTURE_FONT as F, type GlyphTextureSink as G, registerCanvasFont as H, registerFont as I, registerFontOutlines as J, resetBakeBudget as K, resolveFontVariant as L, resolveGlyphFallback as M, setDefaultFontFamily as N, type OutlineFontStyle as O, setFontFallbackPolicy as P, syncDynamicPageTexture as Q, type RegisteredFont as R, textureCacheKey as S, type TexSource as T, unregisterCanvasFont as U, unregisterFontOutlines as V, _getPagesForTests as W, _resetDynamicFontsForTests as X, _resetFallbackForTests as Y, _resetFontOutlinesForTests as Z, __setGlyphRasterizerForTests as _, type BmFontChar as a, type BmFontCommon as b, type BmFontInfo as c, type BmFontKerning as d, type FontEntry as e, type FontFallbackPolicy as f, type FontVariant as g, type OutlineFace as h, type OutlineFontOptions as i, type OutlineParser as j, type OutlineSource as k, type OutlineStatus as l, type OutlineVariant as m, type ResolveResult as n, dynamicPageTextureId as o, ensureFontTexture as p, getDefaultFontFamily as q, getFont as r, getFontFallbackPolicy as s, glyphOutline as t, hasFontOutlines as u, isCanvasFont as v, listCanvasFonts as w, listFontOutlines as x, listFonts as y, markAllFontsNotUploaded as z };
@@ -0,0 +1 @@
1
+ export { _ as __setGlyphRasterizerForTests, W as _getPagesForTests, X as _resetDynamicFontsForTests, Y as _resetFallbackForTests, Z as _resetFontOutlinesForTests, $ as _resetFontRegistryForTests } from './test-seams-DLNsXb8P.js';
@@ -0,0 +1,3 @@
1
+ export { __setGlyphRasterizerForTests, _getPagesForTests, _resetDynamicFontsForTests, _resetFallbackForTests, _resetFontOutlinesForTests, _resetFontRegistryForTests } from './chunk-3RSTZDXH.js';
2
+ //# sourceMappingURL=test-seams.js.map
3
+ //# sourceMappingURL=test-seams.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"test-seams.js"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weasel-js/font",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "MSDF font atlases, glyph metrics, and runtime glyph rasterization for @weasel-js/core. Registry, kerning-aware glyph layout, and the SDF text shader source.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -13,6 +13,10 @@
13
13
  "import": "./dist/index.js",
14
14
  "types": "./dist/index.d.ts"
15
15
  },
16
+ "./test-seams": {
17
+ "import": "./dist/test-seams.js",
18
+ "types": "./dist/test-seams.d.ts"
19
+ },
16
20
  "./package.json": "./package.json"
17
21
  },
18
22
  "scripts": {