@weasel-js/text 1.9.4 → 1.10.1

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.
package/dist/index.d.ts CHANGED
@@ -1,8 +1,33 @@
1
- import { S as StyledRun, T as TextStyle, L as LaidOutRuns, R as ResolvedTextStyle, a as ResolvedRun, b as LayoutRunsOpts, c as TextDirection, B as BidiResolver } from './test-seams-Czz9ua7X.js';
2
- export { d as BidiAnalysis, e as BidiReordering, D as DEFAULT_DECORATION_METRICS, f as DEFAULT_TEXT_STYLE, g as DecorationKind, F as FontVariantCaps, h as LAYOUT_CACHE_STRUCTURAL_LIMIT, i as LAYOUT_CACHE_VARIANT_LIMIT, j as LaidOutCell, k as LaidOutDecoration, l as LaidOutGroup, m as LaidOutLineBox, n as LaidOutOutlineGlyph, o as LaidOutQuad, M as MARKDOWN_RUN_GRAMMAR, p as RunFlag, q as RunGrammar, r as RunMarker, s as RunSourceMap, t as SCRIPT_METRICS, u as SMALL_CAPS_SCALE, v as ScriptPreset, w as SmallCapsText, x as TextAlign, y as TextPaint, z as TextTransform, A as TransformedRunText, C as cachedLayoutRuns, E as decorationMetrics, G as fontString, H as isBoldWeight, I as isSmallCapsLetter, J as layoutRuns, K as markdownToRuns, N as numericWeight, O as resolveAlign, P as resolveRunFace, Q as resolveRuns, U as resolveTextStyle, V as runsToMarkdown, W as runsToPlainText, X as scriptMetrics, Y as scriptMetricsFor, Z as smallCapsScale, _ as smallCapsScaleFor, $ as smallCapsText, a0 as toRuns, a1 as transformRunTexts } from './test-seams-Czz9ua7X.js';
3
- import { FillStyle, Stroke } from '@weasel-js/paint';
4
- import { Rect } from '@weasel-js/geom';
5
- import { FaceMetrics } from '@weasel-js/font';
1
+ import { S as StyledRun, T as TextStyle, L as LaidOutRuns, R as ResolvedTextStyle, a as ResolvedRun, b as LayoutRunsOpts, c as TextPaint } from './test-seams-CuLNcDew.js';
2
+ export { B as BidiAnalysis, d as BidiReordering, e as BidiResolver, D as DEFAULT_TEXT_STYLE, F as FontVariantCaps, f as LAYOUT_CACHE_STRUCTURAL_LIMIT, g as LAYOUT_CACHE_VARIANT_LIMIT, h as LaidOutCell, i as LaidOutLineBox, M as MARKDOWN_RUN_GRAMMAR, P as PlacedGlyph, j as RunFlag, k as RunGrammar, l as RunMarker, m as RunSourceMap, n as SCRIPT_METRICS, o as SMALL_CAPS_SCALE, p as ScriptPreset, q as SmallCapsText, r as TextAlign, s as TextDirection, t as TextTransform, u as TransformedRunText, v as cachedLayoutRuns, w as fontString, x as isBoldWeight, y as isSmallCapsLetter, z as layoutRuns, A as markdownToRuns, C as numericWeight, E as paragraphDirection, G as registerBidi, H as resolveAlign, I as resolveRunFace, J as resolveRuns, K as resolveTextStyle, N as runsToMarkdown, O as runsToPlainText, Q as scriptMetrics, U as scriptMetricsFor, V as smallCapsScale, W as smallCapsScaleFor, X as smallCapsText, Y as toRuns, Z as transformRunTexts } from './test-seams-CuLNcDew.js';
3
+ import { FaceRuleMetrics, FaceMetrics, FontStyle } from '@weasel-js/font';
4
+ import { Fill, Stroke } from '@weasel-js/paint';
5
+ import { Rect, PathInWorldPose, PolygonPath } from '@weasel-js/geom';
6
+ import { Mixed } from '@weasel-js/registry';
7
+ export { MIXED, Mixed } from '@weasel-js/registry';
8
+
9
+ /** The three text decoration rules, in the order every tier paints them. */
10
+ type DecorationKind = 'underline' | 'strikethrough' | 'overline';
11
+ /**
12
+ * Decoration placement and weight for a face that states none, as fractions
13
+ * of the run's `fontSize`. Offsets are the *top* edge of the rule, measured
14
+ * down from the baseline — so the two rules that sit above it are negative.
15
+ *
16
+ * Derived, not measured: they apply to an atlas baked before `gen-font`
17
+ * recorded `faceMetrics`, a custom outline parser that reports none, and the
18
+ * canvas tier, which has no font tables to read.
19
+ */
20
+ declare const DEFAULT_DECORATION_METRICS: Readonly<Record<DecorationKind, FaceRuleMetrics>>;
21
+ /**
22
+ * Where each rule sits for a face, in ems: the face's own underline and
23
+ * strikeout where it states them, the defaults where it does not. No font
24
+ * table places an overline, so it keeps the default offset and takes the
25
+ * underline's weight, as browsers draw it.
26
+ *
27
+ * Every text tier places its rules through this, so one face puts a rule in
28
+ * the same place whichever tier paints it.
29
+ */
30
+ declare function decorationMetrics(face?: FaceMetrics): Readonly<Record<DecorationKind, FaceRuleMetrics>>;
6
31
 
7
32
  /**
8
33
  * Line break opportunities per the Unicode Line Breaking Algorithm
@@ -58,7 +83,7 @@ interface TextPose {
58
83
  style?: TextStyle;
59
84
  /** Glyph paint — the same `data.fill` / `data.stroke` a scene text node
60
85
  * carries. Not part of `style`: paint is not typography. */
61
- fill?: FillStyle | null;
86
+ fill?: Fill | null;
62
87
  stroke?: Stroke | null;
63
88
  /** Box vertical alignment within `[y, y+height]`. Default 'top'. */
64
89
  verticalAlign?: TextVerticalAlign;
@@ -107,37 +132,145 @@ interface TextPoseLayout {
107
132
  declare function layoutTextPose(pose: TextPose, scale?: number): TextPoseLayout;
108
133
 
109
134
  /**
110
- * Where the bidi engine arrives. This package cannot import one (see
111
- * `bidiSeam.ts`), and most callers of `layoutRuns` are kit code a consumer
112
- * never touches, so the consumer registers it once and every layout finds it.
135
+ * What to draw for laid-out text: which tier paints each glyph, which glyphs
136
+ * can be staged together, and the decoration rules. Layout places the glyphs
137
+ * and knows nothing of this; no decision here moves one.
138
+ *
139
+ * Underline, strikethrough and overline come out as solid rectangles built
140
+ * from pen positions, not from quad extents: quads exist only for glyphs with
141
+ * ink, so a decorated stretch's spaces would punch holes in a rule derived
142
+ * from them.
113
143
  */
114
144
 
145
+ /** One textured glyph quad, relative to the text's own top-left. */
146
+ interface LaidOutQuad {
147
+ x0: number;
148
+ y0: number;
149
+ x1: number;
150
+ y1: number;
151
+ u0: number;
152
+ v0: number;
153
+ u1: number;
154
+ v1: number;
155
+ /** The glyph's baseline. The synthetic-italic skew leans vertices about it. */
156
+ baselineY: number;
157
+ }
158
+ /**
159
+ * One glyph the renderer should tessellate rather than sample.
160
+ *
161
+ * `d` is em-space SVG path data — 1 unit is 1 em, y grows down, origin on the
162
+ * baseline at the pen. Placing it is therefore a uniform scale by `scale`
163
+ * followed by a translate to `(x, baselineY)`. `key` identifies the cached
164
+ * tessellation — face identity plus codepoint.
165
+ */
166
+ interface LaidOutOutlineGlyph {
167
+ d: string;
168
+ key: string;
169
+ /** Pen position: where em-space x = 0 lands, in world units. */
170
+ x: number;
171
+ /** The glyph's baseline: where em-space y = 0 lands, in world units. */
172
+ baselineY: number;
173
+ /** World units per em — the glyph's `fontSize`, since em space is unit-scale. */
174
+ scale: number;
175
+ }
115
176
  /**
116
- * Give every text layout a bidi engine: `registerBidi(bidi)`, with `bidi`
117
- * from `@weasel-js/bidi`. `layoutRuns` uses it whenever its options carry
118
- * none. Pass `null`, or call the function returned, to take it out again.
177
+ * Glyphs that share a resolved face, glyph source and paint, so a renderer
178
+ * can bind once for all of them. Groups come in the order their first glyph
179
+ * appears, line by line in visual order, and paint in that order.
119
180
  */
120
- declare function registerBidi(engine: BidiResolver | null): () => void;
181
+ interface LaidOutGroup {
182
+ /** Resolved atlas family — may differ from the requested family when the
183
+ * cross-family fallback policy substituted a default. A renderer looks the
184
+ * atlas up by this, exactly. */
185
+ family: string;
186
+ /** Resolved variant — matches the registered atlas and the texture-cache key. */
187
+ weight: number;
188
+ style: FontStyle;
189
+ /** Gap between the request and the resolved match, for the renderer to fake. */
190
+ synthetic: {
191
+ bold: boolean;
192
+ italic: boolean;
193
+ };
194
+ /** Which glyph source this group binds: a baked MSDF atlas, the runtime
195
+ * canvas-SDF dynamic atlas, tessellated font outlines, or the dynamic
196
+ * atlas's color pages, whose quads are emoji painted in their own colors. */
197
+ source: 'atlas' | 'canvas' | 'outline' | 'color';
198
+ /** Dynamic-atlas page index for 'canvas' and 'color' groups, each in its
199
+ * own set of pages; 0 for the others. */
200
+ page: number;
201
+ /** The run's fill. It holds no paint — `null`, or a fill without one — only
202
+ * on an `'outline'` group carrying a stroke, since nothing else can put ink
203
+ * down without a fill. A `'color'` group takes its opacity and nothing else
204
+ * from it. */
205
+ fill: Fill | null;
206
+ /** Outline painted over the glyphs. Only ever set on an `'outline'` group —
207
+ * the SDF tiers have no geometry to stroke, which is why a stroke pulls its
208
+ * run onto the outline tier at any size. A run the tier cannot serve at all
209
+ * (no registered outlines, synthetic bold) stays on the atlas, unstroked. */
210
+ stroke?: Stroke;
211
+ /** Textured glyph quads. Always empty for an `'outline'` group. */
212
+ quads: LaidOutQuad[];
213
+ /** Outline geometry. Always empty for an `'atlas'` / `'canvas'` group. */
214
+ glyphs: LaidOutOutlineGlyph[];
215
+ }
121
216
  /**
122
- * The direction `text`'s first paragraph reads in: `direction` when it states
123
- * one, otherwise its first strong character's. Finding that takes an engine,
124
- * so with none registered the answer is `'ltr'`.
217
+ * One decoration rule: an axis-aligned solid rectangle in the same space as
218
+ * `LaidOutQuad`. Never spans a line break, and carries the fill of the run(s)
219
+ * it decorates so the rule follows the text color.
125
220
  */
126
- declare function paragraphDirection(text: string, direction?: TextDirection | 'auto', engine?: BidiResolver | null): TextDirection;
221
+ interface LaidOutDecoration {
222
+ kind: DecorationKind;
223
+ x0: number;
224
+ y0: number;
225
+ x1: number;
226
+ y1: number;
227
+ fill: Fill;
228
+ }
229
+ /** Options for {@link paintRuns}. */
230
+ interface PaintRunsOpts {
231
+ /**
232
+ * World-space `fontSize` at or above which glyphs are emitted as outline
233
+ * geometry, when the resolved face has outlines registered. Omit (the
234
+ * default) to keep every glyph on its SDF tier.
235
+ *
236
+ * Ignored for a family that resolved to the outline tier because it has no
237
+ * atlas: there the geometry is the only rendering.
238
+ */
239
+ outlineMinSize?: number;
240
+ }
241
+ /** What {@link paintRuns} returns. Relative to the text's own top-left, as the layout is. */
242
+ interface PaintedRuns {
243
+ groups: LaidOutGroup[];
244
+ /** Decoration rules, in line order; within a stretch, underline then
245
+ * strikethrough then overline. Empty when nothing is decorated. */
246
+ decorations: LaidOutDecoration[];
247
+ }
248
+ /**
249
+ * Derive what to draw for `laid`, the layout of `runs` — or of runs that
250
+ * differ from them only in paint and decoration, which a layout does not read.
251
+ */
252
+ declare function paintRuns(laid: LaidOutRuns, runs: readonly ResolvedRun[], opts?: PaintRunsOpts): PaintedRuns;
253
+
254
+ /**
255
+ * {@link paintRuns}, reusing the previous result while the paints, the outline
256
+ * threshold's effect, and the glyphs available are what they were. `laid` must
257
+ * be the layout of `runs`, as for `paintRuns`.
258
+ *
259
+ * **The result is shared and must be treated as immutable.**
260
+ */
261
+ declare function cachedPaintRuns(laid: LaidOutRuns, runs: readonly ResolvedRun[], opts?: PaintRunsOpts): PaintedRuns;
127
262
 
128
263
  /**
129
264
  * Wrap-aware text measurement for a 2D context. Lines end where `layoutRuns`
130
- * ends them — at UAX #14 break opportunities when a word would overflow
131
- * `maxWidth`, and at every hard break — through the same `wrapLines`, with
132
- * widths from `ctx.measureText` plus tracking. A word wider than `maxWidth`
133
- * is emitted on its own line unbroken (caller can decide to clip).
134
- *
135
- * Returns the laid-out lines, the total block height in world units
136
- * (`lines.length * fontSize * lineHeight`), and per-line `lineStarts` —
137
- * the UTF-16 offset of each line's first character in `text`, which
138
- * `caretIndexAt` uses to map a clicked (x, y) back to a source offset.
139
- * Trailing spaces and tabs hang and are not included in `lines[i]`; neither is the
140
- * hard break that closed it.
265
+ * ends them, with widths from `ctx.measureText` plus tracking. A word wider
266
+ * than `maxWidth` is emitted on its own line unbroken.
267
+ *
268
+ * `lineStarts` is the UTF-16 offset of each line's first character in `text`.
269
+ * Trailing spaces and tabs hang and are not included in `lines[i]`; neither is
270
+ * the hard break that closed it.
271
+ *
272
+ * A tab runs to its next stop and `fillText` draws one as a single space, so
273
+ * paint a line from `spans[i]` and not from `lines[i]`.
141
274
  *
142
275
  * The caller owns the `ctx.font` setup — pass a context whose `font`
143
276
  * already matches `style` (use `fontString(style)`).
@@ -146,24 +279,26 @@ declare function paragraphDirection(text: string, direction?: TextDirection | 'a
146
279
  /**
147
280
  * Advance width of `text` in world units, tracking included.
148
281
  *
149
- * `letter-spacing` is not part of the CSS `font` shorthand, so a context
150
- * whose `font` was set from `fontString(style)` measures glyphs only. The
151
- * GL path (`layoutRuns`) adds `letterSpacing` after every grapheme cluster
152
- * including the last, matching CSS, so this does too — and every 2D-side
153
- * width has to go through here or the two paths disagree about where a line
154
- * breaks.
282
+ * `letter-spacing` is not part of the CSS `font` shorthand, so `ctx` measures
283
+ * glyphs only. `letterSpacing` is added after every grapheme cluster
284
+ * including the last, as `layoutRuns` does — every 2D-side width has to go
285
+ * through here or the two paths disagree about where a line breaks.
155
286
  */
156
287
  declare function measuredWidth(ctx: CanvasRenderingContext2D, text: string, style: ResolvedTextStyle): number;
288
+ /** A stretch of a line between tabs, and how far into the line it starts. */
289
+ interface MeasuredSpan {
290
+ text: string;
291
+ x: number;
292
+ }
157
293
  /** Result of `measureText`: wrapped lines, per-line source offsets, and total block height. */
158
294
  interface MeasuredText {
159
295
  lines: string[];
296
+ /** Per line, what to paint and where: its text split at its tabs. */
297
+ spans: MeasuredSpan[][];
160
298
  lineStarts: number[];
161
299
  height: number;
162
300
  }
163
- /**
164
- * Wrap `text` against `maxWidth` at the break opportunities `layoutRuns`
165
- * wraps at, measuring with `ctx`.
166
- */
301
+ /** Wrap `text` against `maxWidth` at the break opportunities `layoutRuns` wraps at, measuring with `ctx`. */
167
302
  declare function measureText(ctx: CanvasRenderingContext2D, text: string, maxWidth: number, style: ResolvedTextStyle): MeasuredText;
168
303
 
169
304
  /** Options for `measureTextBounds`. */
@@ -178,19 +313,12 @@ interface MeasureTextBoundsOpts {
178
313
  }
179
314
  /**
180
315
  * Measure how the GL/MSDF renderer will lay out a single plain-text string,
181
- * using the registered font atlas metrics. Mirrors `textCommand` exactly
182
- * (`resolveTextStyle` → `resolveRuns` → `cachedLayoutRuns`), so the returned
183
- * bounds match what actually gets drawn — use it to size backgrounds/pills, place
184
- * labels, or test overlap without guessing widths.
185
- *
186
- * Pass `opts.maxWidth` to measure word-wrapped height (e.g. for a fixed-width
187
- * text box); omitting it measures the unwrapped single-line width. `opts.lineHeight`
188
- * overrides the style's multiplier without changing `style` itself.
316
+ * using the registered font atlas metrics. Mirrors `textCommand` exactly, so
317
+ * the returned bounds match what actually gets drawn.
189
318
  *
190
319
  * Returns `{ width, height }` in the same units as `style.fontSize`. The font
191
320
  * must already be registered via `registerFont`; an unregistered family falls
192
- * back to the atlas fallback glyph (and warns), so register fonts at app boot
193
- * before measuring.
321
+ * back to the atlas fallback glyph (and warns).
194
322
  */
195
323
  declare function measureTextBounds(text: string, style?: TextStyle, opts?: MeasureTextBoundsOpts): {
196
324
  width: number;
@@ -200,20 +328,16 @@ declare function measureTextBounds(text: string, style?: TextStyle, opts?: Measu
200
328
  /**
201
329
  * Where a text node's lines actually sit inside its pose box.
202
330
  *
203
- * A text pose is a layout box, not a bounding box: `"Away"` in a 309-unit-wide
204
- * box leaves most of the box empty, and anything that treats the pose as the
205
- * node's extent — picking, lasso, clipping, SVG export — claims that empty
206
- * space. `textLineBoxes` returns the per-line rectangles instead.
207
- *
208
- * The numbers come from `layoutTextPose`, the layout every painter of a text
209
- * pose draws, so the boxes cannot drift from what is painted — they wrap only
210
- * where the style declares `wrap`, and honor `align` and `verticalAlign`.
331
+ * A text pose is a layout box, not a bounding box, and anything that treats
332
+ * the pose as the node's extent — picking, lasso, clipping, SVG export —
333
+ * claims its empty space. The numbers come from `layoutTextPose`, the layout
334
+ * every painter of a text pose draws, so the boxes cannot drift from what is
335
+ * painted.
211
336
  *
212
337
  * These are line boxes, not ink boxes: each is `fontSize * lineHeight` tall
213
338
  * from the pen's line top. Ink can escape vertically at a `lineHeight` low
214
- * enough — see the `bounds` note at the end of `layoutRuns` — which is the
215
- * right trade for hit-testing, where a box that hugged the ink would make
216
- * an `x` harder to click than an `X`.
339
+ * enough, which is the right trade for hit-testing, where a box that hugged
340
+ * the ink would make an `x` harder to click than an `X`.
217
341
  */
218
342
 
219
343
  /** Options for {@link textLineBoxes}. */
@@ -230,6 +354,58 @@ interface TextLineBoxesOpts {
230
354
  /** Per-line rectangles for a text pose, in world units, in layout order. */
231
355
  declare function textLineBoxes(pose: TextPose, opts?: TextLineBoxesOpts): Rect[];
232
356
 
357
+ /**
358
+ * Hit-testing for text nodes. `caretIndexAt` reads the caret stops off
359
+ * `layoutTextPose`, the layout the painters draw and `textLineBoxes` picks
360
+ * against, so the caret cannot land on a different line — or between different
361
+ * glyphs — than the one under the pointer.
362
+ */
363
+
364
+ /** Options for `pointInTextPose`. */
365
+ interface PointInTextPoseOpts {
366
+ /** Extra padding (world units) added to the rect on all sides. Default 0. */
367
+ padding?: number;
368
+ }
369
+ /** Coarse pose-rect hit-test for a text node — suitable for click-to-edit dispatch. */
370
+ declare function pointInTextPose(x: number, y: number, pose: TextPose, opts?: PointInTextPoseOpts): boolean;
371
+ /**
372
+ * Map a world-space point inside `pose` to a character offset into the pose's
373
+ * text (0..length). Clicks above the first line clamp to 0; clicks below the
374
+ * last line clamp to the end. Within a line, the caret lands between two
375
+ * glyphs at whichever side of the advance cell's midpoint `x` falls on.
376
+ *
377
+ * Honors `pose.runs` (a mixed-size line snaps on the cells each run actually
378
+ * produced) and `pose.verticalAlign`. The offset is into the runs'
379
+ * concatenated text, which `TextPose` requires to equal `pose.text`.
380
+ */
381
+ declare function caretIndexAt(x: number, y: number, pose: TextPose): number;
382
+
383
+ /**
384
+ * Resize a text node's pose to fit its content.
385
+ *
386
+ * - `axis: 'height'` (default): keep `pose.width`, recompute `height` as
387
+ * the block height plus optional vertical padding — wrapped at the width
388
+ * inside the horizontal padding when the style declares `wrap`.
389
+ * - `axis: 'both'`: ignore wrapping; recompute both `width` (longest
390
+ * `\n`-split line) and `height` (line count × line height) plus padding.
391
+ *
392
+ * `pose.x` / `pose.y` are preserved; for a different growth direction, adjust
393
+ * `x` / `y` after calling.
394
+ */
395
+
396
+ /** Options for `fitTextPose`. */
397
+ interface FitTextPoseOptions {
398
+ /** Which axis (or axes) to recompute. Default: `'height'`. */
399
+ axis?: 'height' | 'both';
400
+ /** Padding (world units) added to all four sides. Default 0. */
401
+ padding?: number | {
402
+ x?: number;
403
+ y?: number;
404
+ };
405
+ }
406
+ /** Recompute a `TextPose`'s `width`/`height` to fit its content; pure helper, doesn't mutate scene state. */
407
+ declare function fitTextPose(pose: TextPose, opts?: FitTextPoseOptions): TextPose;
408
+
233
409
  /** Width-measurement strategy for `layoutMarkdown`; canvas-backed default supplied by `createMarkdownRenderer`. */
234
410
  type MeasureFn = (text: string, fontSize: number, bold: boolean, italic: boolean) => number;
235
411
  /** The face a run's rules and script presets come from, for `layoutMarkdown`.
@@ -266,9 +442,10 @@ interface LayoutResult {
266
442
  /**
267
443
  * Word-wrap parsed runs into lines bounded by `maxWidth`, breaking at the
268
444
  * UAX #14 opportunities `layoutRuns` breaks at; pass `Infinity` for
269
- * single-line layout.
445
+ * single-line layout. A tab is a run of its own, as wide as it takes to reach
446
+ * its next stop, `tabSize` spaces apart.
270
447
  */
271
- declare function layoutMarkdown(runs: StyledRun[], maxWidth: number, fontSize: number, measure: MeasureFn, lineHeightFactor?: number, faceOf?: FaceMetricsFn): LayoutResult;
448
+ declare function layoutMarkdown(runs: StyledRun[], maxWidth: number, fontSize: number, measure: MeasureFn, lineHeightFactor?: number, faceOf?: FaceMetricsFn, tabSize?: number): LayoutResult;
272
449
  /** Pluggable text-painting strategy. The default fills white at `(x, y)`; markdown renderers replace this. */
273
450
  type TextRenderer = (ctx: CanvasRenderingContext2D, text: string, x: number, y: number) => void;
274
451
  /** Font styling options threaded through `createMarkdownRenderer`. */
@@ -281,6 +458,8 @@ interface MarkdownFontOptions {
281
458
  color?: string;
282
459
  /** Multiplier applied to font size for line height. Default 1.3. */
283
460
  lineHeight?: number;
461
+ /** Distance between tab stops, in spaces, as CSS `tab-size`. Default 8. */
462
+ tabSize?: number;
284
463
  }
285
464
  /**
286
465
  * Build a fill+stroke `TextRenderer` pair, laying the text out once.
@@ -298,4 +477,276 @@ declare function createMarkdownRenderer(ctx: CanvasRenderingContext2D, text: str
298
477
  height: number;
299
478
  };
300
479
 
301
- export { BREAK_ALLOWED, BREAK_MANDATORY, BidiResolver, type FaceMetricsFn, LaidOutRuns, type LayoutLine, type LayoutResult, LayoutRunsOpts, type MarkdownFontOptions, type MeasureFn, type MeasureTextBoundsOpts, type MeasuredText, NO_BREAK, type PositionedRun, ResolvedRun, ResolvedTextStyle, StyledRun, TextDirection, type TextLineBoxesOpts, type TextPose, type TextPoseLayout, type TextPoseLayoutInput, type TextRenderer, TextStyle, type TextVerticalAlign, createMarkdownRenderer, isHardLineBreak, layoutMarkdown, layoutTextPose, lineBreakOpportunities, measureText, measureTextBounds, measuredWidth, paragraphDirection, registerBidi, textLineBoxes, textPoseLayoutInput, verticalAlignOffset };
480
+ /** Visual options for `renderLabel`. */
481
+ interface LabelOptions {
482
+ align?: 'center' | 'left';
483
+ fontSize?: number;
484
+ renderText?: TextRenderer;
485
+ /** Override the pill width (content width, excluding padding). */
486
+ width?: number;
487
+ /** Override the pill height (content height, excluding padding). */
488
+ height?: number;
489
+ /** Horizontal padding inside the pill. Default 4 (screen-pixel default). */
490
+ padX?: number;
491
+ /** Vertical padding inside the pill. Default 1 (screen-pixel default). */
492
+ padY?: number;
493
+ /** Corner radius of the pill background. Default 3 (screen-pixel default). */
494
+ cornerRadius?: number;
495
+ }
496
+ /** Render a text label with a 75%-opaque black pill background and white text. */
497
+ declare function renderLabel(ctx: CanvasRenderingContext2D, text: string, x: number, y: number, options?: LabelOptions): void;
498
+ /** Default `TextRenderer` used by `renderLabel`: white fill, no styling. */
499
+ declare const defaultLabelTextRenderer: TextRenderer;
500
+
501
+ /**
502
+ * The kit's one answer to "is this node text?": its data carries a string
503
+ * `text`. Routing, the `textEdit` dep, and `useSceneTextEdit` all ask it.
504
+ */
505
+ declare function isTextData(data: unknown): data is {
506
+ text: string;
507
+ };
508
+
509
+ /**
510
+ * Range-addressed styling over `StyledRun[]`.
511
+ *
512
+ * Character offsets index the concatenated run text (`runsToPlainText`), and
513
+ * a range is half-open: `[start, end)`. Ranges are clamped, never wrapped,
514
+ * and a collapsed or inverted range (`start >= end`) is empty. Offsets are
515
+ * UTF-16 code units, so a boundary can bisect a surrogate pair.
516
+ *
517
+ * **Run-level flags are additive over the node's `TextStyle`: a run can
518
+ * turn `bold` / `italic` / `underline` / `strikethrough` on, never off.**
519
+ * So a flag is stored only when true, and absent reads as `false`. A run
520
+ * that must be lighter than a bold node says so with its own numeric
521
+ * `fontWeight`, which `bold` is a preset over (see {@link supersededKey}).
522
+ * The decorations have no such primitive, and the tri-state is collapsed by
523
+ * choice, to keep one canonical form per styling. Resolution must therefore
524
+ * read decorations as `run.underline || style.underline`, not `??`.
525
+ *
526
+ * Copies here are shallow: a `fill` object is shared by reference between
527
+ * the input runs, the patch, every run the patch touched, and the value
528
+ * `styleAtRange` reports. Treat `fill` values as immutable.
529
+ */
530
+
531
+ /** Every styleable key of a run, each either a concrete value or MIXED (the
532
+ * runs in the range disagree at that key). */
533
+ type RangeStyle = {
534
+ [K in Exclude<keyof StyledRun, 'text'>]?: StyledRun[K] | Mixed;
535
+ };
536
+ /** What `applyStyleToRange` writes. */
537
+ type RunStylePatch = Partial<Omit<StyledRun, 'text'>>;
538
+ /** Every key of a run that carries styling — i.e. everything but `text`. */
539
+ type StyleKey = Exclude<keyof StyledRun, 'text'>;
540
+ /**
541
+ * The styling shared by every run overlapping `[start, end)`: a concrete
542
+ * value where the range agrees, `MIXED` where it doesn't. Keys no run
543
+ * in range sets are absent (they inherit the node's `TextStyle`) — except
544
+ * the additive flags, which read as `false`. An empty range reads as `{}`.
545
+ * A reported `fill` is the run's own object, not a copy.
546
+ */
547
+ declare function styleAtRange(runs: readonly StyledRun[], start: number, end: number): RangeStyle;
548
+ /**
549
+ * Does any run carry styling of its own? A flag stored as `false` counts as
550
+ * none, so runs whose styling was toggled back off read as `false`.
551
+ */
552
+ declare function runsCarryStyling(runs: readonly StyledRun[]): boolean;
553
+ /**
554
+ * The key a patch clears without naming it. `bold` is a preset over
555
+ * `fontWeight`, and a run keeps at most one of the two: writing either drops
556
+ * the other, so a Bold toggle is not hidden under a weight it cannot beat and
557
+ * a picked weight is not shadowed by a stale flag. Naming both writes both.
558
+ */
559
+ declare function supersededKey(patch: RunStylePatch): 'bold' | 'fontWeight' | undefined;
560
+ /**
561
+ * `range` with `patch` laid over it, keyed as a write of `patch` would leave
562
+ * the runs — for showing styling armed for the next keystroke over the
563
+ * styling at the caret. A key the patch leaves `undefined` keeps the range's
564
+ * value; the key it supersedes is dropped.
565
+ */
566
+ declare function patchRangeStyle(range: RangeStyle, patch: RunStylePatch): RangeStyle;
567
+ /**
568
+ * Write `patch` over `[start, end)`, splitting runs at the boundaries and
569
+ * coalescing neighbors that end up identical. A patch value of `undefined`
570
+ * or `false` deletes the key instead of storing it.
571
+ *
572
+ * Returns a new array of new runs (though a nested `fill` is shared by
573
+ * reference). Normalization — merging identical neighbors, dropping empty
574
+ * runs — applies to the whole array, not just the patched span, and to every
575
+ * call, including one whose range is empty.
576
+ */
577
+ declare function applyStyleToRange(runs: readonly StyledRun[], start: number, end: number, patch: RunStylePatch): StyledRun[];
578
+
579
+ /**
580
+ * The weight a range renders at, and whether that reads as bold, resolved as
581
+ * `resolveRuns` resolves a run: its own `fontWeight`, else 700 for `bold`,
582
+ * else the node's weight.
583
+ */
584
+ declare function rangeWeight(range: RangeStyle | null, style: TextStyle | undefined): {
585
+ fontWeight: number | Mixed;
586
+ bold: boolean | Mixed;
587
+ };
588
+ /**
589
+ * What is actually rendering across a range: `range` (from `styleAtRange`,
590
+ * or `useTextEdit`'s `rangeStyle`) resolved against the node's own `style`
591
+ * and `paint`, as the canvas resolves runs. `null` reads the node alone.
592
+ *
593
+ * Flags are additive — a run can turn one on, never off — so a node flag
594
+ * makes the flag `true` across the range, `MIXED` runs included. Every other
595
+ * key is the run's value where the range sets one, otherwise the node's.
596
+ * The node level is resolved first, so the result has a value for every
597
+ * key but the run-only ones, and for `fill` unless the node is unfilled.
598
+ * Bold is read off the weight that renders (see {@link rangeWeight}), since
599
+ * a run's own `fontWeight` can take a bold node's text back to regular.
600
+ */
601
+ declare function effectiveRangeStyle(range: RangeStyle | null, style: TextStyle | undefined, paint?: TextPaint): RangeStyle;
602
+
603
+ /**
604
+ * Turning a run-level flag **off** inside a node that sets it.
605
+ *
606
+ * Run flags are additive over the node's `TextStyle` — a run turns `bold` /
607
+ * `italic` / `underline` / `strikethrough` on, never off (see the contract at
608
+ * the top of `rangeStyle.ts`). This resolves it by rewriting rather than by
609
+ * widening the model: clear the flag on the node, and set it on every run
610
+ * *outside* the range. The rendered result is identical, with `StyledRun`
611
+ * unchanged.
612
+ *
613
+ * The alternative was a tri-state run flag (`true` / `false` / inherit). It
614
+ * cannot cover `italic`: that is a boolean on a run but `fontStyle` on the
615
+ * node, so a run's `false` has no node-level boolean to override.
616
+ *
617
+ * `bold` needs neither: a run carries a numeric `fontWeight`, which overrides
618
+ * the node's outright, so lowering bold writes a regular weight over the
619
+ * range and leaves the node and the rest of the text alone.
620
+ */
621
+
622
+ /** The additive run flags. */
623
+ type FlagKey = 'bold' | 'italic' | 'underline' | 'strikethrough' | 'overline';
624
+ /** The result of toggling a style flag over a text range: the rewritten runs
625
+ * and node style. */
626
+ interface SetFlagResult {
627
+ runs: StyledRun[];
628
+ style: TextStyle;
629
+ }
630
+ /** Does the node style carry this flag? */
631
+ declare function nodeHasFlag(style: TextStyle, key: FlagKey): boolean;
632
+ /** The patch that takes bold off a range: the flag, and — in a node that is
633
+ * itself bold — a regular weight to override the node's. */
634
+ declare function unboldPatch(style: TextStyle): RunStylePatch;
635
+ /**
636
+ * Set `key` to `value` over `[start, end)`.
637
+ *
638
+ * Turning a flag **on**, or off in a node that doesn't set it, is the ordinary
639
+ * additive write and leaves `style` alone. Turning it off in a node that *does*
640
+ * set it takes the rewrite: the node flag is cleared and the flag is written
641
+ * onto the complement of the range. Bold is the exception, written as a
642
+ * weight over the range alone (see {@link unboldPatch}).
643
+ *
644
+ * `runs` is normalized on every path, so the complement collapses back to one
645
+ * run when the range is empty and the whole array coalesces as usual.
646
+ */
647
+ declare function setFlagOverRange(runs: readonly StyledRun[], style: TextStyle, start: number, end: number, key: FlagKey, value: boolean): SetFlagResult;
648
+
649
+ /**
650
+ * Synthetic-oblique angle, in radians — 12°, the conventional CSS
651
+ * `font-style: oblique`. Shared by everything that fakes an italic: the SDF
652
+ * tier skews quads by it, the outline tier shears glyph geometry by it, and
653
+ * `textToPath` bakes the same shear into extracted outlines. One constant so a
654
+ * face that falls back to the upright atlas leans the same amount however it
655
+ * ends up being drawn.
656
+ */
657
+ declare const SYNTHETIC_ITALIC_RADIANS = 0.2094;
658
+
659
+ /**
660
+ * Text as path geometry: the glyph outlines a text node draws, placed in
661
+ * world space and composed into one `Path`. The glyphs are the ones
662
+ * `layoutRuns` placed, so they land exactly where the renderer's outline tier
663
+ * puts them, with the tier forced on for every glyph whatever its size.
664
+ */
665
+
666
+ /** The fields of a text node's data that decide its glyph geometry, and the
667
+ * paint `textToPathsByPaint` splits it by. Paint never changes the geometry. */
668
+ interface TextOutlineSource {
669
+ text: string;
670
+ runs?: readonly StyledRun[];
671
+ style?: TextStyle;
672
+ verticalAlign?: TextVerticalAlign;
673
+ /** The node's `data.fill`: `null` is no fill, absent the default black. */
674
+ fill?: Fill | null;
675
+ /** The node's `data.stroke`: absent or `null` is no stroke. */
676
+ stroke?: Stroke | null;
677
+ }
678
+ /** One paint's share of a text node's outlines — see {@link textToPathsByPaint}. */
679
+ interface TextPaintPath {
680
+ /** The resolved fill these glyphs wear: the run's own, else the node's.
681
+ * `null` for unfilled text, painted by its stroke alone. */
682
+ fill: Fill | null;
683
+ /** The resolved stroke — the run's own, else the node's — or absent. */
684
+ stroke?: Stroke;
685
+ /** World-space outlines, as {@link textToPath} builds them. */
686
+ path: PolygonPath;
687
+ }
688
+ /** A text node as {@link textToPath} reads it: its data and its pose. */
689
+ interface TextNodeSource {
690
+ data: TextOutlineSource;
691
+ pose: PathInWorldPose;
692
+ }
693
+ /** Options for {@link textToPath}. */
694
+ interface TextToPathOptions {
695
+ /** The view scale a `{ px }` `fontSize` or `letterSpacing` resolves
696
+ * against, as for `textCommandFromPose`. Default 1. */
697
+ scale?: number;
698
+ }
699
+ /**
700
+ * Why a text node has no outline geometry to give:
701
+ *
702
+ * - `'no-outlines'` — the face that serves it has no outlines registered
703
+ * (`registerFontOutlines`), so there is no geometry to read.
704
+ * - `'outlines-loading'` — registered but not parsed yet. `loadTextOutlines`
705
+ * waits for it; the call has already started the load.
706
+ * - `'outlines-failed'` — registered, and the bytes did not load or parse.
707
+ * - `'synthetic-bold'` — the requested weight is faked from a lighter face.
708
+ * The renderer thickens the distance field for that, and a path has no
709
+ * field to thicken. Converting would silently lighten the text.
710
+ * - `'color-glyph'` — the text holds an emoji, which is a picture in its own
711
+ * colors with no outline. Nothing a caller registers changes that.
712
+ */
713
+ type TextOutlinesFailure = 'no-outlines' | 'color-glyph' | 'outlines-loading' | 'outlines-failed' | 'synthetic-bold';
714
+ /** Thrown by {@link textToPath} when some run of the text has no outline
715
+ * geometry. Names the run's requested face. */
716
+ declare class TextOutlinesError extends Error {
717
+ readonly reason: TextOutlinesFailure;
718
+ readonly family: string;
719
+ readonly weight: number;
720
+ readonly style: FontStyle;
721
+ constructor(reason: TextOutlinesFailure, family: string, weight: number, style: FontStyle, detail: string);
722
+ }
723
+ /**
724
+ * The outline geometry of a text node, in world space: every glyph and every
725
+ * underline, strikethrough and overline rule it draws, as one `'nonzero'`
726
+ * compound path whose filled region is their union. Glyphs keep the font's
727
+ * curves; overlapping contours are not merged — run the result through
728
+ * `pathUnion` for that, at the cost of flattening the curves.
729
+ *
730
+ * `pose` is the node's box, rotation, and `transform`. Throws
731
+ * {@link TextOutlinesError} when any run's face cannot supply outlines — never
732
+ * returns a partial or empty path for that. Text with no ink (empty, or only
733
+ * spaces) returns an empty path.
734
+ */
735
+ declare function textToPath(data: TextOutlineSource, pose: PathInWorldPose, opts?: TextToPathOptions): PolygonPath;
736
+ /**
737
+ * {@link textToPath}, split by paint: one path per distinct resolved fill and
738
+ * stroke, each gathering the glyphs and decoration rules that wear it — a
739
+ * rule takes the fill of the run it underlines. Groups come in the order
740
+ * their paint first appears in the text; a paint whose runs put down no ink
741
+ * has none. Paint resolves as the renderer resolves it. Throws as
742
+ * `textToPath` does.
743
+ */
744
+ declare function textToPathsByPaint(data: TextOutlineSource, pose: PathInWorldPose, opts?: TextToPathOptions): TextPaintPath[];
745
+ /**
746
+ * Load the outlines of every face `data` is set in — the requested faces and
747
+ * the ones they resolve to — so a following {@link textToPath} does not fail
748
+ * with `'outlines-loading'`. Resolves once each has loaded or failed.
749
+ */
750
+ declare function loadTextOutlines(data: TextOutlineSource, opts?: TextToPathOptions): Promise<void>;
751
+
752
+ export { BREAK_ALLOWED, BREAK_MANDATORY, DEFAULT_DECORATION_METRICS, type DecorationKind, type FaceMetricsFn, type FitTextPoseOptions, type FlagKey, type LabelOptions, type LaidOutDecoration, type LaidOutGroup, type LaidOutOutlineGlyph, type LaidOutQuad, LaidOutRuns, type LayoutLine, type LayoutResult, LayoutRunsOpts, type MarkdownFontOptions, type MeasureFn, type MeasureTextBoundsOpts, type MeasuredSpan, type MeasuredText, NO_BREAK, type PaintRunsOpts, type PaintedRuns, type PointInTextPoseOpts, type PositionedRun, type RangeStyle, ResolvedRun, ResolvedTextStyle, type RunStylePatch, SYNTHETIC_ITALIC_RADIANS, type SetFlagResult, type StyleKey, StyledRun, type TextLineBoxesOpts, type TextNodeSource, type TextOutlineSource, TextOutlinesError, type TextOutlinesFailure, TextPaint, type TextPaintPath, type TextPose, type TextPoseLayout, type TextPoseLayoutInput, type TextRenderer, TextStyle, type TextToPathOptions, type TextVerticalAlign, applyStyleToRange, cachedPaintRuns, caretIndexAt, createMarkdownRenderer, decorationMetrics, defaultLabelTextRenderer, effectiveRangeStyle, fitTextPose, isHardLineBreak, isTextData, layoutMarkdown, layoutTextPose, lineBreakOpportunities, loadTextOutlines, measureText, measureTextBounds, measuredWidth, nodeHasFlag, paintRuns, patchRangeStyle, pointInTextPose, rangeWeight, renderLabel, runsCarryStyling, setFlagOverRange, styleAtRange, supersededKey, textLineBoxes, textPoseLayoutInput, textToPath, textToPathsByPaint, unboldPatch, verticalAlignOffset };