@weasel-js/text 1.9.4 → 1.10.0

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.
@@ -1,5 +1,5 @@
1
- import { ScreenLength, FillStyle, Stroke } from '@weasel-js/paint';
2
- import { FaceMetrics, FontStyle, FaceRuleMetrics } from '@weasel-js/font';
1
+ import { ScreenLength, Fill, Stroke } from '@weasel-js/paint';
2
+ import { FaceMetrics, FontStyle, ResolveResult, GlyphMetrics, BmFont, BmFontChar } from '@weasel-js/font';
3
3
 
4
4
  /**
5
5
  * CSS `text-transform` over styled runs: `uppercase`, `lowercase` and
@@ -115,7 +115,7 @@ interface StyledRun {
115
115
  fontSize?: ScreenLength;
116
116
  /** This run's ink. `null` is no fill at all, leaving the glyphs to `stroke`
117
117
  * alone; absent inherits the node's. */
118
- fill?: FillStyle | null;
118
+ fill?: Fill | null;
119
119
  /** Outline over this run's glyphs. Overrides the node's own `data.stroke`;
120
120
  * absent inherits it. Only painted on the outline tier. */
121
121
  stroke?: Stroke;
@@ -216,15 +216,12 @@ declare function runsToMarkdown(runs: readonly StyledRun[], grammar?: RunGrammar
216
216
  declare function markdownToRuns(input: string, grammar?: RunGrammar): StyledRun[];
217
217
 
218
218
  /**
219
- * Typography for `TextPose` and friends. Every field is optional; consumers
220
- * pass `{}` or override the few they care about. Defaults live in
219
+ * Typography for `TextPose` and friends. Defaults live in
221
220
  * `DEFAULT_TEXT_STYLE` and are applied at render/measure time, never written
222
221
  * back to the pose.
223
222
  *
224
- * Paint is not typography and does not live here. A text node carries its
225
- * fill and stroke in `data.fill` / `data.stroke`, the slots every other node
226
- * kind uses; `resolveTextStyle` takes them as its second argument and
227
- * `StyledRun.fill` / `.stroke` override them per range.
223
+ * Paint does not live here. A text node carries its fill and stroke in
224
+ * `data.fill` / `data.stroke`, the slots every other node kind uses.
228
225
  */
229
226
 
230
227
  /**
@@ -238,12 +235,9 @@ type TextAlign = 'left' | 'center' | 'right' | 'start' | 'end' | 'justify';
238
235
  type TextDirection = 'ltr' | 'rtl';
239
236
  /**
240
237
  * Collapse a possibly reading-order-relative alignment to an absolute edge.
241
- *
242
- * Layout works in absolute edges, so it runs this once per line, against the
243
- * direction of the paragraph the line is in, and `start` / `end` never reach
244
- * the geometry. `justify` collapses to the start edge, which
245
- * is where it sets every line it does not spread; whether to spread is carried
246
- * separately, as `LayoutRunsOpts.justify`.
238
+ * `justify` collapses to the start edge, which is where it sets every line it
239
+ * does not spread; whether to spread is carried separately, as
240
+ * `LayoutRunsOpts.justify`.
247
241
  */
248
242
  declare function resolveAlign(align: TextAlign, direction: TextDirection): 'left' | 'center' | 'right';
249
243
  /** User-facing text style. All fields optional; defaults applied at render time via `resolveTextStyle`. */
@@ -310,9 +304,8 @@ interface TextStyle {
310
304
  fontVariantCaps?: FontVariantCaps;
311
305
  /**
312
306
  * Set the node's text as a superscript or subscript — the default every run
313
- * inherits, exactly as a run's own `StyledRun.script` is for that run. The
314
- * lines keep the height and baseline the unscripted text would have had.
315
- * Absent is ordinary text.
307
+ * inherits. The lines keep the height and baseline the unscripted text would
308
+ * have had. Absent is ordinary text.
316
309
  */
317
310
  script?: 'super' | 'sub';
318
311
  }
@@ -330,7 +323,7 @@ interface ResolvedTextStyle {
330
323
  wrap: boolean;
331
324
  /** `null` is an explicit no-fill, the way it is on every other node kind:
332
325
  * outline-only text, painted by its stroke alone. */
333
- fill: FillStyle | null;
326
+ fill: Fill | null;
334
327
  caretColor: string;
335
328
  selectionBackground: string | null;
336
329
  selectionColor: string | null;
@@ -348,27 +341,19 @@ interface ResolvedTextStyle {
348
341
  }
349
342
  /** Default resolved style used when a `TextPose` omits `style`. */
350
343
  declare const DEFAULT_TEXT_STYLE: ResolvedTextStyle;
351
- /**
352
- * The paint a text node hands its glyphs — `data.fill` and `data.stroke`,
353
- * read straight off the node. Runs inherit these when they name none of
354
- * their own.
355
- */
344
+ /** The paint a text node hands its glyphs — `data.fill` and `data.stroke`. Runs inherit these when they name none of their own. */
356
345
  interface TextPaint {
357
346
  /** `null` is an explicit no-fill and absent takes the default black — the
358
347
  * reading every other node kind gives `data.fill`. Unfilled text paints
359
348
  * through its stroke and nothing else, so it needs one to be visible. */
360
- fill?: FillStyle | null;
349
+ fill?: Fill | null;
361
350
  /**
362
- * Outline painted over the glyph fill. Absent means no outline — there is
363
- * no such thing as a default text stroke.
351
+ * Outline painted over the glyph fill. Absent means no outline.
364
352
  *
365
- * Only glyphs on the outline tier are stroked: above
366
- * `textOutlineMinScreenSize` a glyph is a real `PolygonPath`, so it gets
367
- * the ordinary tessellated ribbon with real joins, caps and miters, in any
368
- * paint. Below it a glyph is a sampled distance field with no geometry to
369
- * stroke, and it renders unstroked rather than approximated. `width` is in
370
- * world units, like every other stroke in the kit — it does not scale with
371
- * `fontSize`.
353
+ * Only glyphs on the outline tier are stroked: below
354
+ * `textOutlineMinScreenSize` a glyph is a sampled distance field with no
355
+ * geometry to stroke, and it renders unstroked. `width` is in world units —
356
+ * it does not scale with `fontSize`.
372
357
  */
373
358
  stroke?: Stroke | null;
374
359
  }
@@ -377,10 +362,7 @@ interface TextPaint {
377
362
  * taking the glyph paint from the node rather than from the style.
378
363
  *
379
364
  * `scale` is the view scale any `{ px }` size resolves against — the same
380
- * mean-of-the-axes factor `resolveStrokeWidth` takes. It has to be applied
381
- * here, at the entry to layout, rather than at draw time: a screen-pixel size
382
- * changes the glyph advances, the wrap points and the measured bounds, and
383
- * everything downstream of `ResolvedTextStyle` reads a plain world number.
365
+ * mean-of-the-axes factor `resolveStrokeWidth` takes.
384
366
  */
385
367
  declare function resolveTextStyle(style?: TextStyle, paint?: TextPaint, scale?: number): ResolvedTextStyle;
386
368
  /** Build a CSS `font` shorthand suitable for `ctx.font`. */
@@ -388,35 +370,17 @@ declare function fontString(s: ResolvedTextStyle): string;
388
370
 
389
371
  /**
390
372
  * Apply node-level `ResolvedTextStyle` defaults to each `StyledRun`,
391
- * producing a fully-resolved run with every styling field set. Downstream
392
- * layout and draw never re-resolve defaults — `ResolvedRun` is the
393
- * canonical shape the renderer consumes.
394
- *
395
- * `bold`/`italic` toggles on a run are folded into `fontWeight`/`fontStyle`:
396
- * `bold: true` → fontWeight 700, `italic: true` → fontStyle 'italic'. A run's
397
- * own numeric `fontWeight` is the primitive `bold` is a preset over, so it
398
- * wins over both the flag and the node weight.
399
- * Explicit `fontFamily` / `fontSize` / `fill` / `letterSpacing` on the run
400
- * override the node-level value (`letterSpacing: 0` on a run is an override,
401
- * not an absence — it zeroes inherited tracking).
373
+ * producing a `ResolvedRun` with every styling field set.
402
374
  *
403
- * `underline` / `strikethrough` / `overline` are *additive*, like
404
- * `bold`/`italic`: a run can turn a decoration on but never off, so they
405
- * resolve as `run.x || style.x` and not `run.x ?? style.x`. See the header of
406
- * `runs/rangeStyle.ts` for why the model collapses the tri-state.
375
+ * `bold`/`italic` on a run fold into `fontWeight`/`fontStyle`; a run's own
376
+ * numeric `fontWeight` wins over both the flag and the node weight.
377
+ * `letterSpacing: 0` on a run is an override, not an absence — it zeroes
378
+ * inherited tracking. The decorations are *additive*: a run can turn one on
379
+ * but never off, so they resolve as `run.x || style.x`.
407
380
  *
408
- * `script` is folded the same way the toggles are, into the two primitives it
409
- * is a preset over: `baselineShift` and `fontScale`. Both come out as one
410
- * world-unit `baselineShift` and a final `fontSize`, so layout never learns
411
- * that superscripts exist — it places a run against a baseline and an offset.
412
- *
413
- * `textTransform` is applied here too, so `text` is what gets drawn. When a
414
- * transform changes a length, `srcMap` says which source characters each
415
- * drawn one came from; see `textTransform.ts`.
416
- *
417
- * `fontVariantCaps: 'small-caps'` is applied after it, over the transformed
418
- * text: lowercase becomes capitals, and `sizeMap` gives those their smaller
419
- * size. See `smallCaps.ts`.
381
+ * `script` folds into the two primitives it is a preset over, and comes out
382
+ * as one world-unit `baselineShift` and a final `fontSize`. `textTransform`
383
+ * is applied here, then small caps over the transformed text.
420
384
  */
421
385
 
422
386
  /** A run with every style resolved against the node's text style — no
@@ -441,7 +405,7 @@ interface ResolvedRun {
441
405
  /** `null` is an explicit no-fill: the glyphs are painted by `stroke` alone,
442
406
  * which only the outline tier can do. Nothing is emitted for a run with
443
407
  * neither. */
444
- fill: FillStyle | null;
408
+ fill: Fill | null;
445
409
  /** Outline over this run's glyphs, or absent for none. Painted only on the
446
410
  * outline tier — a distance field has no geometry to stroke. */
447
411
  stroke?: Stroke;
@@ -455,11 +419,8 @@ interface ResolvedRun {
455
419
  overline: boolean;
456
420
  /**
457
421
  * How far this run sits off the line's shared baseline, in world units;
458
- * positive raises. 0 for ordinary text.
459
- *
460
- * Already multiplied out against the inherited font size, and already
461
- * carrying whatever `script` asked for — layout adds it to a baseline and
462
- * asks nothing about where it came from.
422
+ * positive raises. 0 for ordinary text. Already carries whatever `script`
423
+ * asked for.
463
424
  */
464
425
  baselineShift: number;
465
426
  /**
@@ -479,11 +440,8 @@ type ScriptPreset = Readonly<{
479
440
  * What `script: 'super'` and `script: 'sub'` expand to for a face that states
480
441
  * no script metrics of its own, as fractions of the inherited font size:
481
442
  * `size` scales it, `shift` raises (positive) or lowers (negative) the
482
- * baseline.
483
- *
484
- * These are Adobe's defaults — InDesign and Illustrator ship 58.3% size and
485
- * 33.3% position for both. A face that carries `OS/2` script metrics uses
486
- * those instead; {@link scriptMetricsFor} is what a run actually gets.
443
+ * baseline. These are Adobe's defaults. A face that carries `OS/2` script
444
+ * metrics uses those instead; {@link scriptMetricsFor} is what a run gets.
487
445
  */
488
446
  declare const SCRIPT_METRICS: Readonly<Record<'super' | 'sub', ScriptPreset>>;
489
447
  /** The script presets for a face: its own `OS/2` values where it states
@@ -491,10 +449,8 @@ declare const SCRIPT_METRICS: Readonly<Record<'super' | 'sub', ScriptPreset>>;
491
449
  declare function scriptMetrics(face?: FaceMetrics): Readonly<Record<'super' | 'sub', ScriptPreset>>;
492
450
  /**
493
451
  * The script presets a run set in `(family, weight, style)` gets — resolved
494
- * through the font registry exactly as layout resolves the run, so a
495
- * character panel, an SVG export and the DOM edit overlay read the numbers
496
- * the canvas draws with. Override either half per run with `baselineShift` /
497
- * `fontScale`.
452
+ * through the font registry exactly as layout resolves the run. Override
453
+ * either half per run with `baselineShift` / `fontScale`.
498
454
  */
499
455
  declare function scriptMetricsFor(family: string, weight: number, style: FontStyle): Readonly<Record<'super' | 'sub', ScriptPreset>>;
500
456
  /** A `TextStyle.fontWeight` as a number: the CSS keywords `bold` and
@@ -516,29 +472,6 @@ declare function resolveRunFace(run: StyledRun, style: Pick<ResolvedTextStyle, '
516
472
  * `style`. */
517
473
  declare function resolveRuns(runs: readonly StyledRun[], style: ResolvedTextStyle, viewScale?: number): ResolvedRun[];
518
474
 
519
- /** The three text decoration rules, in the order every tier paints them. */
520
- type DecorationKind = 'underline' | 'strikethrough' | 'overline';
521
- /**
522
- * Decoration placement and weight for a face that states none, as fractions
523
- * of the run's `fontSize`. Offsets are the *top* edge of the rule, measured
524
- * down from the baseline — so the two rules that sit above it are negative.
525
- *
526
- * Derived, not measured: they apply to an atlas baked before `gen-font`
527
- * recorded `faceMetrics`, a custom outline parser that reports none, and the
528
- * canvas tier, which has no font tables to read.
529
- */
530
- declare const DEFAULT_DECORATION_METRICS: Readonly<Record<DecorationKind, FaceRuleMetrics>>;
531
- /**
532
- * Where each rule sits for a face, in ems: the face's own underline and
533
- * strikeout where it states them, the defaults where it does not. No font
534
- * table places an overline, so it keeps the default offset and takes the
535
- * underline's weight, as browsers draw it.
536
- *
537
- * Every text tier places its rules through this, so one face puts a rule in
538
- * the same place whichever tier paints it.
539
- */
540
- declare function decorationMetrics(face?: FaceMetrics): Readonly<Record<DecorationKind, FaceRuleMetrics>>;
541
-
542
475
  /**
543
476
  * The bidi contract, declared here and implemented elsewhere.
544
477
  *
@@ -578,152 +511,85 @@ interface BidiResolver {
578
511
  baseLevel(analysis: BidiAnalysis): number;
579
512
  }
580
513
 
514
+ /** Options for `layoutRuns`. */
515
+ interface LayoutRunsOpts {
516
+ /** Wrap width. `Infinity` never wraps. */
517
+ maxWidth: number;
518
+ /**
519
+ * Width of the box `center` / `right` resolve within, with `x = 0` its left
520
+ * edge. Default `maxWidth`. Separate from it so a box can align text it does
521
+ * not wrap. When both are infinite a line anchors on its own width instead:
522
+ * `x = 0` is its midpoint (`center`) or right edge (`right`).
523
+ */
524
+ alignWidth?: number;
525
+ lineHeight: number;
526
+ /**
527
+ * `start` / `end` resolve against `direction`; `left` / `right` are absolute
528
+ * and ignore it, the way CSS `text-align` treats the same values. `justify`
529
+ * is `justify: true` with the start edge.
530
+ */
531
+ align: TextAlign;
532
+ /**
533
+ * Spread each line that wrapped across `alignWidth`, widening its word gaps
534
+ * (spaces between words; not leading or hanging trailing ones) by equal
535
+ * shares of the slack. A line that closes its paragraph, or has no gap, or
536
+ * no slack, keeps `align` — so `align` is CSS `text-align-last` here.
537
+ * Implied by `align: 'justify'`. Default `false`.
538
+ */
539
+ justify?: boolean;
540
+ /**
541
+ * Bidi engine. Default: the one given to `registerBidi`. With neither, the
542
+ * text lays out in logical order, which is correct for left-to-right text
543
+ * and wrong for right-to-left text — see the warning this emits when it
544
+ * detects the latter without one.
545
+ */
546
+ bidi?: BidiResolver;
547
+ /**
548
+ * Reading direction: the base direction of the bidi analysis, and what
549
+ * `start` / `end` resolve against. Default `'auto'`, each paragraph's own
550
+ * first strong character, which only an engine can find: with none, `'auto'`
551
+ * reads left to right.
552
+ */
553
+ direction?: TextDirection | 'auto';
554
+ /**
555
+ * Distance between tab stops, in spaces of the tab's own run (its advance
556
+ * plus the run's tracking), as CSS `tab-size` counts it. A tab advances to
557
+ * the next stop from the start of its line. Default 8.
558
+ */
559
+ tabSize?: number;
560
+ }
561
+
581
562
  /**
582
- * Runs-aware MSDF layout. Walks `ResolvedRun[]` codepoint-by-codepoint,
583
- * switching atlas per run via `resolveFontVariant`, applying kerning
584
- * (using the left glyph's atlas table and size scaling, including across
585
- * run boundaries), and bucketing emitted quads by
586
- * `(family, resolvedWeight, resolvedStyle, syntheticBold, syntheticItalic, fillKey)`
587
- * so the renderer issues one draw call per atlas+color group.
563
+ * Runs-aware text layout: wraps `ResolvedRun[]` into lines and places every
564
+ * glyph on them. What to draw for the result — which glyphs share a draw,
565
+ * which tier paints each, where the decoration rules go — is `paintRuns`'.
566
+ * Nothing here reads a run's paint, so recoloring text never lays it out again.
588
567
  *
589
568
  * A run's `letterSpacing` (world units, so it does not scale with `fontSize`)
590
569
  * is added to the advance *after* every grapheme cluster of that run,
591
- * including the last one on a line — the CSS `letter-spacing` rule, chosen so
592
- * a DOM overlay rendering the same text can be made to agree. The last code
593
- * point of a cluster carries it, so a combining mark hangs off its base
594
- * before the gap opens. Clusters are segmented per run. Trailing tracking
595
- * therefore widens the measured line width and counts toward wrapping. Spaces
596
- * are tracked like any other character; a newline is not (it consumes no
597
- * advance).
570
+ * including the last one on a line — the CSS `letter-spacing` rule. Trailing
571
+ * tracking therefore widens the measured line width and counts toward
572
+ * wrapping. Spaces are tracked like any other character; a newline is not.
598
573
  *
599
- * Word wrap is applied when `maxWidth` is finite: words — the text between
600
- * UAX #14 break opportunities — are committed to a new line when they would
601
- * exceed the current line width. Forced line breaks are emitted at UAX #14's
602
- * hard breaks — `\n`, CR, CRLF as one, VT, FF, NEL, U+2028 and U+2029 — none
574
+ * Word wrap is applied when `maxWidth` is finite, at UAX #14 break
575
+ * opportunities. Forced line breaks are emitted at UAX #14's hard breaks, none
603
576
  * of which lays out a cell. Every run on a line shares one baseline, sunk to
604
- * clear whichever run reaches highest above it, so mixing sizes or faces aligns
605
- * them the way inline text aligns everywhere else; line height is
577
+ * clear whichever run reaches highest above it; line height is
606
578
  * `max(fontSize * lineHeight)` across the line. A face that states its ascent
607
579
  * and descent sits in its line the way CSS sets it — half the leading above the
608
580
  * ascent, negative when the face is taller than the line, so its glyphs overflow
609
581
  * the box rather than growing it. A run a relative size shrank
610
- * measures at its `strutSize` for both, the way a CSS line keeps its parent's
611
- * strut under a `<sup>`.
612
- *
613
- * A run may also sit off that shared baseline: `ResolvedRun.baselineShift`
614
- * displaces it, which is what `script: 'super' | 'sub'` resolves to. The shift
615
- * moves the run's glyphs, its outline geometry and its own decoration rules
616
- * together, and deliberately does not feed back into the line's baseline or
617
- * its height — a superscript rides on the line rather than reflowing it.
618
- *
619
- * Underline, strikethrough and overline come out on a second channel,
620
- * `decorations` —
621
- * solid rectangles, not textured glyphs, so they cannot ride in a group's
622
- * `quads` (which upload UVs into an MSDF program). They are accumulated
623
- * during the same per-line pen walk that emits quads, *not* reconstructed
624
- * from quad extents afterwards: quads exist only for glyphs with ink, so a
625
- * decorated span's spaces would punch holes in a rule derived from them.
582
+ * measures at its `strutSize` for both.
626
583
  *
627
- * ### Coordinates are origin-relative
584
+ * `ResolvedRun.baselineShift` displaces a run off that shared baseline. The
585
+ * shift moves the run's glyphs and its own decoration rules together, and does
586
+ * not feed back into the line's baseline or its height.
628
587
  *
629
- * Every coordinate out of here is measured from the text's own top-left, not
630
- * from anywhere on the page: the caller translates. That is what lets a text
631
- * node be dragged without re-laying out — `layoutCache` would otherwise need
632
- * the position in its key and miss on every frame of the drag — and it costs
633
- * the caller one addition per vertex, inside loops already walking every one.
634
- * Alignment, wrapping, tracking and decoration placement all read widths and
635
- * pen deltas, never an absolute coordinate, so the translation is exact.
588
+ * Every coordinate out of here is measured from the text's own top-left: the
589
+ * caller translates. That is what lets a text node be dragged without
590
+ * re-laying out.
636
591
  */
637
592
 
638
- /** One textured glyph quad, origin-relative — see the header. */
639
- interface LaidOutQuad {
640
- x0: number;
641
- y0: number;
642
- x1: number;
643
- y1: number;
644
- u0: number;
645
- v0: number;
646
- u1: number;
647
- v1: number;
648
- /** Y coordinate of the line's baseline (penY at quad emission). Used by the
649
- * synthetic-italic vertex skew so above-baseline vertices lean right. */
650
- baselineY: number;
651
- }
652
- /**
653
- * One glyph the renderer should tessellate rather than sample: real font
654
- * outline geometry, for text large enough that a distance field shows the
655
- * raster it was reconstructed from.
656
- *
657
- * `d` is em-space SVG path data straight from `@weasel-js/font`'s outline
658
- * registry — 1 unit is 1 em, y grows down, origin on the baseline at the pen.
659
- * Placing it is therefore a uniform scale by `scale` (world units per em)
660
- * followed by a translate to `(x, baselineY)`, and nothing here depends on
661
- * zoom: the same glyph at the same size in two places is the same geometry
662
- * twice, which is what lets the renderer tessellate it once and cache it.
663
- *
664
- * `key` identifies that cached tessellation — face identity plus codepoint,
665
- * assembled here because layout is where both are in hand.
666
- */
667
- interface LaidOutOutlineGlyph {
668
- d: string;
669
- key: string;
670
- /** Pen position: where em-space x = 0 lands, in world units. */
671
- x: number;
672
- /** The line's baseline: where em-space y = 0 lands, in world units. */
673
- baselineY: number;
674
- /** World units per em — the run's `fontSize`, since em space is unit-scale. */
675
- scale: number;
676
- }
677
- /** Glyphs that share a resolved face, glyph source and paint — one draw call's worth. */
678
- interface LaidOutGroup {
679
- /** Resolved atlas family — may differ from the requested family when the
680
- * cross-family fallback policy substituted a default. This is what the
681
- * renderer looks the atlas up by, so it must be the one that resolves. */
682
- family: string;
683
- /** Resolved variant — matches the registered atlas and the texture-cache key. */
684
- weight: number;
685
- style: FontStyle;
686
- /** Gap between the request and the resolved match. Drives shader uniforms. */
687
- synthetic: {
688
- bold: boolean;
689
- italic: boolean;
690
- };
691
- /** Which glyph source (and therefore shader/texture) this group binds:
692
- * baked MSDF atlas, the runtime canvas-SDF dynamic atlas, or tessellated
693
- * font outlines. */
694
- source: 'atlas' | 'canvas' | 'outline';
695
- /** Dynamic-atlas page index for 'canvas' groups; 0 for the others. */
696
- page: number;
697
- /** `null` for an unfilled run: the group's glyphs are painted by `stroke`
698
- * alone. Only ever null on an `'outline'` group carrying a stroke, since
699
- * nothing else can put ink down without a fill. */
700
- fill: FillStyle | null;
701
- /** Outline painted over the glyphs, or absent for none. Only ever set on
702
- * an `'outline'` group — the SDF tiers have no geometry to stroke, which
703
- * is why a stroke pulls its run onto the outline tier at any size. A run
704
- * the tier cannot serve at all (no registered outlines, synthetic bold)
705
- * stays on the atlas and carries the request no further. */
706
- stroke?: Stroke;
707
- /** Textured glyph quads. Always empty for an `'outline'` group. */
708
- quads: LaidOutQuad[];
709
- /** Outline geometry. Always empty for an `'atlas'` / `'canvas'` group —
710
- * the two channels are exclusive, since the group is also the draw call
711
- * and one draw call binds one program. */
712
- glyphs: LaidOutOutlineGlyph[];
713
- }
714
- /**
715
- * One decoration rule: an axis-aligned solid rectangle in the same world
716
- * space as `LaidOutQuad`. Never spans a line break, and carries the fill of
717
- * the run(s) it decorates so the rule follows the text colour.
718
- */
719
- interface LaidOutDecoration {
720
- kind: DecorationKind;
721
- x0: number;
722
- y0: number;
723
- x1: number;
724
- y1: number;
725
- fill: FillStyle;
726
- }
727
593
  /**
728
594
  * One advance cell: a single code point's slot on a line.
729
595
  *
@@ -740,7 +606,7 @@ interface LaidOutCell {
740
606
  /** One past that source character — `srcIndex + 2` for an astral one. */
741
607
  srcEnd: number;
742
608
  cp: number;
743
- /** Left edge of the cell, post-alignment, origin-relative like `x0`/`x1`.
609
+ /** Left edge of the cell, post-alignment, origin-relative.
744
610
  *
745
611
  * **Not monotonic across `cells`** once a bidi engine reorders a line: cells
746
612
  * stay in logical order and their x values do not. Sort on `x` for visual
@@ -756,6 +622,8 @@ interface LaidOutCell {
756
622
  * Whether this code point can put ink on the page. False for a space and
757
623
  * for a code point no tier could serve — each of which still holds its
758
624
  * slot. A zero-advance combining mark is `true`: it inks without advancing.
625
+ * An emoji drawn as one color glyph inks from its cluster's closing code
626
+ * point, which holds the whole advance; the ones before it are `false`.
759
627
  *
760
628
  * A property of the character and the face, not of this call: it does not
761
629
  * change when a dynamic bake lands or the outline threshold is crossed. A
@@ -771,15 +639,43 @@ interface LaidOutCell {
771
639
  clusterEnd: boolean;
772
640
  }
773
641
  /**
774
- * One laid-out line's box, in the same world space as `LaidOutQuad`.
642
+ * One code point where the pen put it, with what a painter needs to draw it:
643
+ * the tier that serves it and its record there. Carries no paint.
644
+ */
645
+ interface PlacedGlyph {
646
+ /** Index of its run in the runs laid out. */
647
+ run: number;
648
+ cp: number;
649
+ /** The glyph's own size, which small caps sets below its run's. */
650
+ fontSize: number;
651
+ /** The tier serving the glyph, which is not its run's own when the code
652
+ * point escalated to the dynamic tier, and that tier's metrics. */
653
+ resolved: ResolveResult;
654
+ metrics: GlyphMetrics;
655
+ /** The atlas to sample and the glyph's record in it. Both null on the
656
+ * outline tier and for a slot with nothing to draw. A dynamic glyph's
657
+ * record gains its atlas rect in place when its bake lands. */
658
+ font: BmFont | null;
659
+ glyph: BmFontChar | null;
660
+ /** Pen position: the left edge of the glyph's slot. */
661
+ x: number;
662
+ /** Its line's baseline, raised by its run's `baselineShift`. */
663
+ baselineY: number;
664
+ /** The glyph's own advance. */
665
+ advance: number;
666
+ /** How far the pen moved past it: `advance`, its run's tracking, and a
667
+ * justified gap's share of the slack. */
668
+ step: number;
669
+ }
670
+ /**
671
+ * One laid-out line's box.
775
672
  *
776
673
  * `[x0, x1]` is the line's own advance width *after* alignment — not the
777
674
  * wrap box — so a short centered line reports the span it actually occupies.
778
- * `[y0, y1]` is the full line box: `y0` is the pen's line top, `y1` is
779
- * `y0 + max(fontSize * lineHeight)` over the line's runs. Ink can escape it
780
- * vertically (a tall accent, a deep descender at a lineHeight below ~1.06 —
781
- * see the `bounds` note at the end of `layoutRuns`); this is the typographic
782
- * box, not an ink bounding box.
675
+ * `[y0, y1]` is the full line box: `y0` is the pen's line top, and the box is
676
+ * as tall as its tallest run's `lineHeight` times that run's size, or the
677
+ * larger size a shrunken run inherited. Ink can escape it vertically; this is
678
+ * the typographic box, not an ink bounding box.
783
679
  *
784
680
  * Emitted for every line including empty ones, so indices line up with the
785
681
  * wrap. An empty line has `x0 === x1`.
@@ -792,16 +688,18 @@ interface LaidOutLineBox {
792
688
  /** The line's baseline, for callers placing carets or rules against it. */
793
689
  baselineY: number;
794
690
  /**
795
- * One cell per code point on this line, left to right. Every code point the
796
- * line covers has exactly one, drawn or not; only a newline has none, since
797
- * it separates cells rather than being one. The code points are the drawn
798
- * ones, after `textTransform`, so an uppercased `ß` has two cells.
691
+ * One cell per code point on this line, in logical order. Every code point
692
+ * the line covers has exactly one, drawn or not; only a newline has none,
693
+ * since it separates cells rather than being one. The code points are the
694
+ * drawn ones, after `textTransform`, so an uppercased `ß` has two cells.
799
695
  *
800
696
  * A caret stop is a cell's `x`, and the stop closing the line is `x1` /
801
697
  * `srcEnd` — there is one more stop than cell, except that cells sharing a
802
698
  * source span have no stop between them.
803
699
  */
804
700
  cells: LaidOutCell[];
701
+ /** The same code points as placed glyphs, in visual order, left to right. */
702
+ glyphs: PlacedGlyph[];
805
703
  /**
806
704
  * Source offset just past this line's last cell — the closing caret stop's
807
705
  * index, and the only source offset a blank line carries.
@@ -810,93 +708,22 @@ interface LaidOutLineBox {
810
708
  }
811
709
  /** What {@link layoutRuns} returns. Every coordinate is relative to the text's own top-left. */
812
710
  interface LaidOutRuns {
813
- groups: LaidOutGroup[];
814
- /** Decoration rules, in line order; within a span, underline then
815
- * strikethrough then overline. Empty when nothing is decorated. */
816
- decorations: LaidOutDecoration[];
817
- /** Per-line boxes in layout order. Lets a caller reason about where the
818
- * text actually sits inside its wrap box without re-running the wrap —
819
- * `textLineBoxes` builds the text silhouette from these, so picking and
820
- * painting cannot drift apart. */
711
+ /** Per-line boxes in layout order: where the text actually sits inside its
712
+ * wrap box. */
821
713
  lines: LaidOutLineBox[];
822
- /** The text's ink box. `width` is the widest line measured to its last
714
+ /** The text's box. `width` is the widest line measured to its last
823
715
  * non-space cell — a trailing space hangs past the aligned edge, so a
824
- * block wrapped at `maxWidth` never reports wider than it. */
716
+ * block wrapped at `maxWidth` never reports wider than it. `height` is the
717
+ * line boxes stacked. Not an ink box: a decoration rule, or a run shifted
718
+ * off the baseline, can fall outside it. */
825
719
  bounds: {
826
720
  width: number;
827
721
  height: number;
828
722
  };
829
723
  }
830
- /** Options for {@link layoutRuns}. */
831
- interface LayoutRunsOpts {
832
- /** Wrap width. `Infinity` never wraps. */
833
- maxWidth: number;
834
- /**
835
- * Width of the box `center` / `right` resolve within, with `x = 0` its left
836
- * edge. Default `maxWidth`. Separate from it so a box can align text it does
837
- * not wrap. When both are infinite a line anchors on its own width instead:
838
- * `x = 0` is its midpoint (`center`) or right edge (`right`).
839
- */
840
- alignWidth?: number;
841
- lineHeight: number;
842
- /**
843
- * `start` / `end` resolve against `direction`; `left` / `right` are absolute
844
- * and ignore it, the way CSS `text-align` treats the same values. `justify`
845
- * is `justify: true` with the start edge.
846
- */
847
- align: TextAlign;
848
- /**
849
- * Spread each line that wrapped across `alignWidth`, widening its word gaps
850
- * (spaces between words; not leading or hanging trailing ones) by equal
851
- * shares of the slack. A line that closes its paragraph, or has no gap, or
852
- * no slack, keeps `align` — so `align` is CSS `text-align-last` here.
853
- * Implied by `align: 'justify'`. Default `false`.
854
- */
855
- justify?: boolean;
856
- /**
857
- * Bidi engine. Default: the one given to `registerBidi`. With neither, the
858
- * text lays out in logical order, which is correct for left-to-right text
859
- * and wrong for right-to-left text — see the warning this emits when it
860
- * detects the latter without one.
861
- */
862
- bidi?: BidiResolver;
863
- /**
864
- * Reading direction: the base direction of the bidi analysis, and what
865
- * `start` / `end` resolve against. Default `'auto'`, each paragraph's own
866
- * first strong character, which only an engine can find: with none, `'auto'`
867
- * reads left to right.
868
- */
869
- direction?: TextDirection | 'auto';
870
- /**
871
- * Distance between tab stops, in spaces of the tab's own run (its advance
872
- * plus the run's tracking), as CSS `tab-size` counts it. A tab advances to
873
- * the next stop from the start of its line. Default 8.
874
- */
875
- tabSize?: number;
876
- /**
877
- * World-space `fontSize` at or above which glyphs are emitted as outline
878
- * geometry, when the resolved face has outlines registered. Omit (the
879
- * default) to keep every glyph on its SDF tier.
880
- *
881
- * A *world* size rather than a screen size because layout knows nothing
882
- * about the view; `drawText` divides its screen-pixel threshold by the view
883
- * scale before calling, so zooming in lowers the world size that qualifies.
884
- *
885
- * This is a rendering switch only. Advances, kerning, wrapping and
886
- * baselines are identical either way — the outline tier changes what a
887
- * glyph looks like, never where it sits — so measurement callers
888
- * (`measureTextBounds`, `textLineBoxes`) leave it unset and still agree
889
- * with the paint, and crossing the threshold cannot reflow text.
890
- *
891
- * Ignored for a family that resolved to the outline tier because it has no
892
- * atlas: there the geometry is not an upgrade over another rendering, it is
893
- * the only one, so the threshold has nothing to choose between.
894
- */
895
- outlineMinSize?: number;
896
- }
897
724
  /**
898
- * Lay out `runs` into draw groups, decoration rules and line boxes. Wrapping,
899
- * baselines, tracking and decorations follow the rules in this file's module doc.
725
+ * Lay out `runs` into lines of placed glyphs. Wrapping, baselines and tracking
726
+ * follow the rules in this file's module doc.
900
727
  */
901
728
  declare function layoutRuns(runs: readonly ResolvedRun[], opts: LayoutRunsOpts): LaidOutRuns;
902
729
 
@@ -906,11 +733,10 @@ declare function layoutRuns(runs: readonly ResolvedRun[], opts: LayoutRunsOpts):
906
733
  * behind picking, and caret hit-testing all answer from this.
907
734
  *
908
735
  * `layoutRuns` is the most expensive derivation in the kit — it walks every
909
- * codepoint, resolves a face per run, measures, wraps, aligns, and places a
910
- * quad or an outline glyph for each — and `drawText` ran it per text command
911
- * per frame. Measured: 2.0 ms/frame for 200 short labels, 9.3 for 1000, and
912
- * 25.9 for 200 wrapped paragraphs. That last figure is the entire frame
913
- * budget spent before a single triangle is drawn.
736
+ * codepoint, resolves a face per run, measures, wraps, aligns, and places
737
+ * every glyph — and `drawText` ran it per text command per frame. Measured,
738
+ * when it also built the draw groups: 2.0 ms/frame for 200 short labels, 9.3
739
+ * for 1000, and 25.9 for 200 wrapped paragraphs.
914
740
  *
915
741
  * ### Two keys, tried in order
916
742
  *
@@ -928,30 +754,23 @@ declare function layoutRuns(runs: readonly ResolvedRun[], opts: LayoutRunsOpts):
928
754
  * second: an identity hit never pays for it.
929
755
  *
930
756
  * Under either, one entry per distinct `(maxWidth, alignWidth, lineHeight,
931
- * align, outline threshold, direction, bidi engine, tab size)`. Position is deliberately absent: `layoutRuns` emits
932
- * origin-relative geometry and `drawText` translates at upload, so a text node
933
- * dragged across the page keeps hitting the same entry.
934
- *
935
- * ### The outline threshold
936
- *
937
- * `outlineMinSize` is derived from the view scale, so recording it literally
938
- * would miss on every frame of a zoom. It reaches `layoutRuns` through exactly
939
- * one comparison (`run.fontSize < min`, in `outlineFor`), so what the layout
940
- * actually depends on is *which runs clear the bar* — and since the test is a
941
- * plain `>=` on size, that set is pinned by how many of the distinct run sizes
942
- * clear it. Recording that count is exact, not a quantization: the entry stays
943
- * valid across every zoom that doesn't cross a glyph size, and is dropped the
944
- * moment one does.
757
+ * align, direction, bidi engine, tab size)`. Position is deliberately absent:
758
+ * `layoutRuns` emits origin-relative geometry and `drawText` translates at
759
+ * upload, so a text node dragged across the page keeps hitting the same entry.
760
+ * So is everything that only changes what is drawn — a run's fill, stroke and
761
+ * decorations, the outline threshold — which `cachedPaintRuns` keys on
762
+ * instead. Two runs arrays that differ only there share one layout.
945
763
  *
946
764
  * ### What the keys cannot see
947
765
  *
948
766
  * The font set. A face landing changes metrics with no change to the runs, so
949
- * every lookup compares `glyphGeneration()` — advanced by the dynamic SDF
950
- * atlas, by the outline registry, and by `registerFont` — and drops
951
- * everything when it moves. That is the same escape hatch `nodeMemo`'s
952
- * generation counter provides, for the same class of failure. Polled rather
953
- * than subscribed: a module-load subscription is a cross-package side effect
954
- * that makes importing this package require the font package's whole surface.
767
+ * every lookup compares `faceGeneration()` — advanced by a registration, a
768
+ * load, a fallback policy, and the dynamic atlas measuring a face again or
769
+ * emptying its pages — and drops everything when it moves. A deferred bake
770
+ * landing does not advance it: that fills in a glyph record the layout
771
+ * already holds, and moves no pen. Polled rather than subscribed: a
772
+ * module-load subscription is a cross-package side effect that makes importing
773
+ * this package require the font package's whole surface.
955
774
  *
956
775
  * ### Bounds
957
776
  *
@@ -981,4 +800,25 @@ declare function cachedLayoutRuns(runs: readonly ResolvedRun[], opts: LayoutRuns
981
800
  /** Test helper. Do not call from product code. */
982
801
  declare function _resetLayoutCacheForTests(): void;
983
802
 
984
- export { smallCapsText as $, type TransformedRunText as A, type BidiResolver as B, cachedLayoutRuns as C, DEFAULT_DECORATION_METRICS as D, decorationMetrics as E, type FontVariantCaps as F, fontString as G, isBoldWeight as H, isSmallCapsLetter as I, layoutRuns as J, markdownToRuns as K, type LaidOutRuns as L, MARKDOWN_RUN_GRAMMAR as M, numericWeight as N, resolveAlign as O, resolveRunFace as P, resolveRuns as Q, type ResolvedTextStyle as R, type StyledRun as S, type TextStyle as T, resolveTextStyle as U, runsToMarkdown as V, runsToPlainText as W, scriptMetrics as X, scriptMetricsFor as Y, smallCapsScale as Z, smallCapsScaleFor as _, type ResolvedRun as a, toRuns as a0, transformRunTexts as a1, _resetLayoutCacheForTests as a2, type LayoutRunsOpts as b, type TextDirection as c, type BidiAnalysis as d, type BidiReordering as e, DEFAULT_TEXT_STYLE as f, type DecorationKind as g, LAYOUT_CACHE_STRUCTURAL_LIMIT as h, LAYOUT_CACHE_VARIANT_LIMIT as i, type LaidOutCell as j, type LaidOutDecoration as k, type LaidOutGroup as l, type LaidOutLineBox as m, type LaidOutOutlineGlyph as n, type LaidOutQuad as o, type RunFlag as p, type RunGrammar as q, type RunMarker as r, type RunSourceMap as s, SCRIPT_METRICS as t, SMALL_CAPS_SCALE as u, type ScriptPreset as v, type SmallCapsText as w, type TextAlign as x, type TextPaint as y, type TextTransform as z };
803
+ /**
804
+ * Where the bidi engine arrives. This package cannot import one (see
805
+ * `bidiSeam.ts`), and most callers of `layoutRuns` are kit code a consumer
806
+ * never touches, so the consumer registers it once and every layout finds it.
807
+ */
808
+
809
+ /**
810
+ * Give every text layout a bidi engine: `registerBidi(bidi)`, with `bidi`
811
+ * from `@weasel-js/bidi`. `layoutRuns` uses it whenever its options carry
812
+ * none. Pass `null`, or call the function returned, to take it out again.
813
+ */
814
+ declare function registerBidi(engine: BidiResolver | null): () => void;
815
+ /**
816
+ * The direction `text`'s first paragraph reads in: `direction` when it states
817
+ * one, otherwise its first strong character's. Finding that takes an engine,
818
+ * so with none registered the answer is `'ltr'`.
819
+ */
820
+ declare function paragraphDirection(text: string, direction?: TextDirection | 'auto', engine?: BidiResolver | null): TextDirection;
821
+ /** Test helper. Do not call from product code. */
822
+ declare function _resetBidiWarningForTests(): void;
823
+
824
+ export { _resetLayoutCacheForTests as $, markdownToRuns as A, type BidiAnalysis as B, numericWeight as C, DEFAULT_TEXT_STYLE as D, paragraphDirection as E, type FontVariantCaps as F, registerBidi as G, resolveAlign as H, resolveRunFace as I, resolveRuns as J, resolveTextStyle as K, type LaidOutRuns as L, MARKDOWN_RUN_GRAMMAR as M, runsToMarkdown as N, runsToPlainText as O, type PlacedGlyph as P, scriptMetrics as Q, type ResolvedTextStyle as R, type StyledRun as S, type TextStyle as T, scriptMetricsFor as U, smallCapsScale as V, smallCapsScaleFor as W, smallCapsText as X, toRuns as Y, transformRunTexts as Z, _resetBidiWarningForTests as _, type ResolvedRun as a, type LayoutRunsOpts as b, type TextPaint as c, type BidiReordering as d, type BidiResolver as e, LAYOUT_CACHE_STRUCTURAL_LIMIT as f, LAYOUT_CACHE_VARIANT_LIMIT as g, type LaidOutCell as h, type LaidOutLineBox as i, type RunFlag as j, type RunGrammar as k, type RunMarker as l, type RunSourceMap as m, SCRIPT_METRICS as n, SMALL_CAPS_SCALE as o, type ScriptPreset as p, type SmallCapsText as q, type TextAlign as r, type TextDirection as s, type TextTransform as t, type TransformedRunText as u, cachedLayoutRuns as v, fontString as w, isBoldWeight as x, isSmallCapsLetter as y, layoutRuns as z };