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