vgpu-render 0.0.0-stage → 0.2.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.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +605 -2
  3. package/dist/assets/fira-mono-medium-subset.d.ts +4 -0
  4. package/dist/assets/fira-mono-medium-subset.js +889 -0
  5. package/dist/assets/fira-mono-msdf-atlas.d.ts +13 -0
  6. package/dist/assets/fira-mono-msdf-atlas.js +6222 -0
  7. package/dist/camera-2d.d.ts +43 -0
  8. package/dist/camera-2d.js +75 -0
  9. package/dist/color-space.d.ts +32 -0
  10. package/dist/color-space.js +45 -0
  11. package/dist/curve.d.ts +16 -0
  12. package/dist/curve.js +46 -0
  13. package/dist/debug-draw-3d.d.ts +14 -0
  14. package/dist/debug-draw-3d.js +36 -0
  15. package/dist/frame-draw-list-2d.d.ts +177 -0
  16. package/dist/frame-draw-list-2d.js +378 -0
  17. package/dist/frame-draw-list.d.ts +413 -0
  18. package/dist/frame-draw-list.js +556 -0
  19. package/dist/geometry-generators.d.ts +101 -0
  20. package/dist/geometry-generators.js +481 -0
  21. package/dist/gpu-debug-draw-2d.d.ts +48 -0
  22. package/dist/gpu-debug-draw-2d.js +450 -0
  23. package/dist/gpu-debug-draw-3d.d.ts +52 -0
  24. package/dist/gpu-debug-draw-3d.js +892 -0
  25. package/dist/gpu-instancing.d.ts +34 -0
  26. package/dist/gpu-instancing.js +93 -0
  27. package/dist/gpu-text-3d.d.ts +98 -0
  28. package/dist/gpu-text-3d.js +514 -0
  29. package/dist/index.d.ts +21 -0
  30. package/dist/index.js +20 -0
  31. package/dist/infinite-grid-2d.d.ts +73 -0
  32. package/dist/infinite-grid-2d.js +253 -0
  33. package/dist/infinite-grid-3d.d.ts +134 -0
  34. package/dist/infinite-grid-3d.js +407 -0
  35. package/dist/mesh-batch-3d.d.ts +10 -0
  36. package/dist/mesh-batch-3d.js +67 -0
  37. package/dist/shape2d-shader.d.ts +14 -0
  38. package/dist/shape2d-shader.js +288 -0
  39. package/dist/text-atlas.d.ts +176 -0
  40. package/dist/text-atlas.js +498 -0
  41. package/dist/text-font.d.ts +190 -0
  42. package/dist/text-font.js +261 -0
  43. package/dist/text-layout.d.ts +175 -0
  44. package/dist/text-layout.js +390 -0
  45. package/dist/text-msdf-baked.d.ts +41 -0
  46. package/dist/text-msdf-baked.js +117 -0
  47. package/dist/text-msdf-font.d.ts +85 -0
  48. package/dist/text-msdf-font.js +159 -0
  49. package/dist/text-msdf.d.ts +70 -0
  50. package/dist/text-msdf.js +242 -0
  51. package/dist/text-raster.d.ts +40 -0
  52. package/dist/text-raster.js +60 -0
  53. package/dist/text-sdf.d.ts +28 -0
  54. package/dist/text-sdf.js +119 -0
  55. package/dist/vector-math.d.ts +19 -0
  56. package/dist/vector-math.js +37 -0
  57. package/package.json +80 -4
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Browser-side font loading and glyph rasterisation for the text renderer.
3
+ *
4
+ * reference rasterises glyphs with swash into a `FontAtlas`
5
+ * (`the reference implementation`); the equivalent rasteriser in a browser is
6
+ * Canvas2D, so `fillText` plays the role of `swash::scale::Render` and the browser's font
7
+ * stack stands in for font discovery. The bundled font is inlined exactly the way reference
8
+ * inlines `FiraMono-subset.ttf` with `include_bytes!`, so nothing is fetched at runtime.
9
+ *
10
+ * The subset deliberately has no CJK coverage; pass a different family through the text
11
+ * options when those are needed.
12
+ *
13
+ * Everything here is browser-only. Layout depends on the `TextMeasurer` interface alone, so
14
+ * unit tests keep running in plain Node, and the GPU tests inject their own rasteriser.
15
+ */
16
+ import { FIRA_MONO_MEDIUM_SUBSET_BASE64, FIRA_MONO_MEDIUM_SUBSET_TTF_BASE64 } from "./assets/fira-mono-medium-subset.js";
17
+ import { coverageToSdf, DEFAULT_SDF_RANGE } from "./text-sdf.js";
18
+ import { fontFaceKey } from "./text-layout.js";
19
+ /**
20
+ * Family name the bundled font is registered under. It is deliberately not `"Fira Mono"` so
21
+ * a locally installed face can never shadow the inlined one.
22
+ */
23
+ export const DEFAULT_FONT_FAMILY = "vgpu-render-fira-mono";
24
+ /**
25
+ * Fira Mono's hhea metrics are 935 / -265 per 1000 upem, i.e. exactly the 1.2 em line box
26
+ * reference default `LineHeight` assumes. They are the fallback when a browser reports no font
27
+ * bounding box.
28
+ */
29
+ const FALLBACK_ASCENT_RATIO = 0.935;
30
+ const FALLBACK_DESCENT_RATIO = 0.265;
31
+ const INITIAL_SURFACE_SIZE = 256;
32
+ /** Decodes a base64 payload into bytes. Shared by the inlined font and the prebaked atlas. */
33
+ export function decodeBase64Bytes(base64) {
34
+ const binary = atob(base64);
35
+ const bytes = new Uint8Array(binary.length);
36
+ for (let index = 0; index < binary.length; index++)
37
+ bytes[index] = binary.charCodeAt(index);
38
+ return bytes;
39
+ }
40
+ const registeredFamilies = new Map();
41
+ /**
42
+ * Registers a family whose weights are separate faces, each carrying its own `weight` descriptor.
43
+ *
44
+ * This is the only way to get more than one drawn weight out of Canvas2D: the font shorthand carries a
45
+ * single synthesised bold step, and `font-variation-settings` does not exist there, but the browser
46
+ * does select a face by the weight it was registered with. Registering the same family once per
47
+ * weight therefore turns `weight: 700` into a real bold face instead of an emboldened regular one.
48
+ *
49
+ * Idempotent per family, like `loadDefaultFont`, and browser-only.
50
+ */
51
+ export function loadFontFaces(family, faces) {
52
+ const existing = registeredFamilies.get(family);
53
+ if (existing !== undefined)
54
+ return existing;
55
+ const registration = registerFontFaces(family, faces);
56
+ registeredFamilies.set(family, registration);
57
+ return registration;
58
+ }
59
+ async function registerFontFaces(family, faces) {
60
+ const fonts = typeof document === "undefined" ? undefined : document.fonts;
61
+ if (fonts === undefined || typeof FontFace === "undefined") {
62
+ throw new Error("registering a font family requires a browser environment");
63
+ }
64
+ const loaded = faces.map(async (face) => {
65
+ const definition = new FontFace(family, decodeBase64Bytes(face.woff2Base64), { weight: String(face.weight) });
66
+ await definition.load();
67
+ return definition;
68
+ });
69
+ for (const definition of await Promise.all(loaded))
70
+ fonts.add(definition);
71
+ }
72
+ /** Decodes the inlined WOFF2 bytes of the bundled font, which is what `FontFace` takes. */
73
+ export function decodeEmbeddedFont() {
74
+ return decodeBase64Bytes(FIRA_MONO_MEDIUM_SUBSET_BASE64);
75
+ }
76
+ /**
77
+ * Decodes the inlined TrueType bytes of the same subset. The outline-based encoders need this one:
78
+ * `opentype.js` reads glyph outlines, and it cannot decompress the WOFF2 container.
79
+ */
80
+ export function decodeEmbeddedTrueTypeFont() {
81
+ return decodeBase64Bytes(FIRA_MONO_MEDIUM_SUBSET_TTF_BASE64);
82
+ }
83
+ let defaultFont;
84
+ /**
85
+ * Registers the bundled font with the document, once per page. Measuring or drawing with
86
+ * `DEFAULT_FONT_FAMILY` before this resolves falls back to another face, so await it before
87
+ * the first frame that draws text.
88
+ */
89
+ export function loadDefaultFont() {
90
+ defaultFont ??= registerDefaultFont();
91
+ return defaultFont;
92
+ }
93
+ async function registerDefaultFont() {
94
+ const fonts = typeof document === "undefined" ? undefined : document.fonts;
95
+ if (fonts === undefined || typeof FontFace === "undefined") {
96
+ throw new Error("loading the bundled text font requires a browser environment");
97
+ }
98
+ const face = new FontFace(DEFAULT_FONT_FAMILY, decodeEmbeddedFont());
99
+ await face.load();
100
+ fonts.add(face);
101
+ }
102
+ /**
103
+ * The CSS font shorthand that selects `face` at `fontSize`.
104
+ *
105
+ * Order follows the shorthand's grammar — style, weight, stretch, size, family — because Canvas2D
106
+ * parses the value as one. Two of the axes are conditional:
107
+ *
108
+ * - `font-stretch` keywords are only kept by the parser when the face has a `wdth` axis. A static
109
+ * face drops the keyword and draws every stop identically, which is why the bundled subset cannot
110
+ * show a `FontWidth` difference at all.
111
+ * - `italic` is the only slant Canvas2D keeps; `oblique <angle>` reads back as plain `italic`.
112
+ *
113
+ * A weight the loaded face does not carry is synthesised by the browser (one emboldened step, which
114
+ * keeps the advance), so `weight` at least changes what is drawn for the bundled static face.
115
+ */
116
+ export function canvasFontString(face, fontSize) {
117
+ const style = face.style === "italic" ? "italic " : "";
118
+ const stretch = face.width === "normal" ? "" : `${face.width} `;
119
+ return `${style}${face.weight} ${stretch}${fontSize}px "${face.family}"`;
120
+ }
121
+ /**
122
+ * Canvas2D glyph source, doubling as the layout `TextMeasurer` so the canvas is only set up
123
+ * once per canvas-configured face and every `measureText` result is cached.
124
+ *
125
+ * Metrics follow Canvas2D's convention: the pen sits at the origin and the baseline is y = 0
126
+ * with y growing downwards. `actualBoundingBoxLeft` is a *leftward* distance, so a glyph that
127
+ * starts right of the pen reports a negative value — negating it yields the ink's left edge
128
+ * in pen space, which is what the atlas stores.
129
+ */
130
+ export class CanvasGlyphRasterizer {
131
+ encoding;
132
+ distanceRange;
133
+ rasterScale;
134
+ /**
135
+ * Whether the renderer drives this source's raster size from the size a block covers on screen.
136
+ * A Canvas2D face can be asked for any size, so it defaults to `true`; a source that needs a fixed
137
+ * raster — a pixel-accurate comparison against another renderer, say — turns it off.
138
+ */
139
+ dynamicRaster;
140
+ #surface;
141
+ #metrics = new Map();
142
+ #vertical = new Map();
143
+ constructor(options = {}) {
144
+ this.encoding = options.encoding ?? "bitmap";
145
+ this.distanceRange = options.distanceRange ?? DEFAULT_SDF_RANGE;
146
+ this.rasterScale = options.rasterScale ?? 1;
147
+ this.dynamicRaster = options.dynamicRaster ?? true;
148
+ }
149
+ #ensureSurface() {
150
+ if (this.#surface === undefined) {
151
+ if (typeof document === "undefined") {
152
+ throw new Error("glyph rasterisation requires a browser environment");
153
+ }
154
+ const canvas = document.createElement("canvas");
155
+ canvas.width = INITIAL_SURFACE_SIZE;
156
+ canvas.height = INITIAL_SURFACE_SIZE;
157
+ const context = canvas.getContext("2d", { willReadFrequently: true });
158
+ if (context === null)
159
+ throw new Error("a 2D canvas context is required to rasterise glyphs");
160
+ this.#surface = { canvas, context };
161
+ }
162
+ return this.#surface;
163
+ }
164
+ #font(face, fontSize) {
165
+ return canvasFontString(face, fontSize);
166
+ }
167
+ verticalMetrics(face, fontSize) {
168
+ const key = `${fontFaceKey(face)}|${fontSize}`;
169
+ const cached = this.#vertical.get(key);
170
+ if (cached !== undefined)
171
+ return cached;
172
+ const context = this.#ensureSurface().context;
173
+ context.font = this.#font(face, fontSize);
174
+ const metrics = context.measureText("M");
175
+ const ascent = metrics.fontBoundingBoxAscent > 0 ? metrics.fontBoundingBoxAscent : fontSize * FALLBACK_ASCENT_RATIO;
176
+ const descent = metrics.fontBoundingBoxDescent > 0 ? metrics.fontBoundingBoxDescent : fontSize * FALLBACK_DESCENT_RATIO;
177
+ const value = { ascent, descent };
178
+ this.#vertical.set(key, value);
179
+ return value;
180
+ }
181
+ glyphMetrics(character, face, fontSize) {
182
+ const key = `${fontFaceKey(face)}|${fontSize}|${character}`;
183
+ const cached = this.#metrics.get(key);
184
+ if (cached !== undefined)
185
+ return cached;
186
+ const context = this.#ensureSurface().context;
187
+ context.font = this.#font(face, fontSize);
188
+ const metrics = context.measureText(character);
189
+ const bearingLeft = -metrics.actualBoundingBoxLeft;
190
+ const value = {
191
+ advance: metrics.width,
192
+ width: metrics.actualBoundingBoxRight - bearingLeft,
193
+ height: metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent,
194
+ bearingLeft,
195
+ bearingTop: -metrics.actualBoundingBoxAscent,
196
+ };
197
+ this.#metrics.set(key, value);
198
+ return value;
199
+ }
200
+ rasterize(character, face, rasterSize) {
201
+ // The raster size *is* the face size here: `measureText` and the fill both work in the texels the
202
+ // atlas will store, and the layout's own measurements never pass through this path.
203
+ const glyph = this.glyphMetrics(character, face, rasterSize);
204
+ if (!(glyph.width > 0) || !(glyph.height > 0))
205
+ return undefined;
206
+ // The ink box is fractional; the mask rounds up so no edge pixel is lost.
207
+ const width = Math.max(1, Math.ceil(glyph.width));
208
+ const height = Math.max(1, Math.ceil(glyph.height));
209
+ const { canvas, context } = this.#ensureSurface();
210
+ if (canvas.width < width || canvas.height < height) {
211
+ canvas.width = Math.max(canvas.width, width);
212
+ canvas.height = Math.max(canvas.height, height);
213
+ }
214
+ context.clearRect(0, 0, width, height);
215
+ context.font = this.#font(face, rasterSize);
216
+ context.textAlign = "left";
217
+ context.textBaseline = "alphabetic";
218
+ context.fillStyle = "#fff";
219
+ // Place the pen so the ink box starts at (0, 0): the baseline sits `-bearingTop` down.
220
+ context.fillText(character, -glyph.bearingLeft, -glyph.bearingTop);
221
+ const pixels = context.getImageData(0, 0, width, height).data;
222
+ const coverage = new Uint8Array(width * height);
223
+ for (let index = 0; index < coverage.length; index++)
224
+ coverage[index] = pixels[index * 4 + 3];
225
+ return {
226
+ width,
227
+ height,
228
+ texels: encodeCoverage(coverage, width, height, this.encoding, this.distanceRange),
229
+ inkWidth: glyph.width,
230
+ inkHeight: glyph.height,
231
+ bearingLeft: glyph.bearingLeft,
232
+ bearingTop: glyph.bearingTop,
233
+ advance: glyph.advance,
234
+ };
235
+ }
236
+ }
237
+ /** Packs Canvas2D coverage into the RGBA layout the chosen encoding expects. */
238
+ export function encodeCoverage(coverage, width, height, encoding, range) {
239
+ const texels = new Uint8Array(coverage.length * 4);
240
+ if (encoding === "bitmap") {
241
+ // A tintable alpha mask: RGB stays 255 so the shader can colour it.
242
+ for (let index = 0; index < coverage.length; index++) {
243
+ const target = index * 4;
244
+ texels[target] = 255;
245
+ texels[target + 1] = 255;
246
+ texels[target + 2] = 255;
247
+ texels[target + 3] = coverage[index];
248
+ }
249
+ return texels;
250
+ }
251
+ // A signed distance in red, and the same distance in alpha so the shader can outline the glyph.
252
+ // It comes from the mask's coverage rather than from the outline, which makes it a little coarser
253
+ // than the msdf encoder's — fine for a ring, not what the edge itself is rebuilt from.
254
+ const field = coverageToSdf(coverage, width, height, range);
255
+ for (let index = 0; index < field.length; index++) {
256
+ const target = index * 4;
257
+ texels[target] = field[index];
258
+ texels[target + 3] = field[index];
259
+ }
260
+ return texels;
261
+ }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Text layout for the debug text renderer.
3
+ *
4
+ * Ported from reference `TextPipeline::update_text_layout_info`
5
+ * (`the reference implementation`): a text block is wrapped to its `TextBounds`, split
6
+ * into lines, justified, and turned into one positioned glyph per character. Each glyph
7
+ * carries its ink box and resolved colour, which is what the GPU pass later instantiates as
8
+ * a quad.
9
+ *
10
+ * Two deliberate differences from reference:
11
+ *
12
+ * - There is no shaping. Characters advance one at a time and never form ligatures or
13
+ * reorder runs, which suits the monospaced debug font the library bundles.
14
+ * - Every metric comes from an injected `TextMeasurer`. reference reads parley and swash here;
15
+ * staying behind an interface keeps this module free of browser APIs and unit-testable in
16
+ * plain Node, and it is also the seam the GPU tests use to feed synthetic glyphs.
17
+ *
18
+ * Layout happens in one local space: origin at the block's anchor point, x to the right and
19
+ * **y down**, matching Canvas2D. The renderer flips y for spaces where up is +y.
20
+ */
21
+ export type Rgba = readonly [number, number, number, number];
22
+ /** Vertical font metrics, in the same units as the font size the measurer was asked for. */
23
+ export type VerticalMetrics = {
24
+ readonly ascent: number;
25
+ readonly descent: number;
26
+ };
27
+ /**
28
+ * Per-character metrics, mirroring the `Placement` swash reports to reference: an advance along
29
+ * the baseline plus the ink box relative to the pen.
30
+ */
31
+ export type GlyphMetrics = {
32
+ /** Pen advance for this character. */
33
+ readonly advance: number;
34
+ /** Ink box width. Zero for whitespace. */
35
+ readonly width: number;
36
+ /** Ink box height. Zero for whitespace. */
37
+ readonly height: number;
38
+ /** Ink box left edge relative to the pen, y down: positive moves right. */
39
+ readonly bearingLeft: number;
40
+ /** Ink box top edge relative to the baseline, y down: positive is below the baseline. */
41
+ readonly bearingTop: number;
42
+ };
43
+ /**
44
+ * reference `FontStyle`, minus `Oblique(angle)`.
45
+ *
46
+ * Canvas2D accepts the `italic` keyword in its font shorthand and silently drops the slant angle —
47
+ * `oblique 14deg` reads back as `italic` — so an oblique angle could not be honoured and is left out
48
+ * rather than accepted and ignored. A face without an italic face is slanted by the browser.
49
+ */
50
+ export type TextFontStyle = "normal" | "italic";
51
+ /**
52
+ * reference `FontWidth` stops, spelled as the CSS `font-stretch` keywords Canvas2D understands.
53
+ *
54
+ * reference takes a percentage (`FontWidth::SEMI_CONDENSED` is 87.5), but Canvas2D rejects a percentage in
55
+ * the font shorthand and only accepts these nine keywords. The axis is inert unless the face actually
56
+ * carries a `wdth` axis: a static face like the bundled subset keeps the keyword out of its resolved
57
+ * font, so every stop draws the same width there.
58
+ */
59
+ export type TextFontWidth = "ultra-condensed" | "extra-condensed" | "condensed" | "semi-condensed" | "normal" | "semi-expanded" | "expanded" | "extra-expanded" | "ultra-expanded";
60
+ /** Every `FontWidth` stop, in reference order. */
61
+ export declare const TEXT_FONT_WIDTHS: readonly TextFontWidth[];
62
+ /** Every `FontStyle` variant this implementation can honour. */
63
+ export declare const TEXT_FONT_STYLES: readonly TextFontStyle[];
64
+ /** reference `FontWeight::NORMAL`, and the value the CSS font shorthand defaults to. */
65
+ export declare const DEFAULT_FONT_WEIGHT = 400;
66
+ /** reference `FontWeight` range, 1 to 1000. */
67
+ export declare const MIN_FONT_WEIGHT = 1;
68
+ export declare const MAX_FONT_WEIGHT = 1000;
69
+ /**
70
+ * One face request: everything that decides which glyphs a character rasterises to.
71
+ *
72
+ * This is the unit the glyph atlas keys on, so two blocks that differ only in weight cannot share a
73
+ * rasterised glyph. A source with a single fixed face reports the parts it cannot change through
74
+ * `GlyphRasterizer.faceKey`.
75
+ */
76
+ export type FontFace = {
77
+ readonly family: string;
78
+ readonly weight: number;
79
+ readonly style: TextFontStyle;
80
+ readonly width: TextFontWidth;
81
+ };
82
+ /** A face with the axes left optional, which is how callers and spans express one. */
83
+ export type FontFaceOptions = {
84
+ readonly family: string;
85
+ readonly weight?: number | undefined;
86
+ readonly style?: TextFontStyle | undefined;
87
+ readonly width?: TextFontWidth | undefined;
88
+ };
89
+ /** Folds a face's optional axes into the complete request the measurer and the atlas take. */
90
+ export declare function resolveFontFace(options: FontFaceOptions): FontFace;
91
+ /** Every field that can change which glyphs are rasterised, joined into one cache key. */
92
+ export declare function fontFaceKey(face: FontFace): string;
93
+ /** Source of glyph metrics. The browser implementation measures through Canvas2D. */
94
+ export interface TextMeasurer {
95
+ /** Vertical metrics for a `fontSize`-sized instance of `face`. */
96
+ verticalMetrics(face: FontFace, fontSize: number): VerticalMetrics;
97
+ /** Metrics for one character. Whitespace returns a zero-size ink box. */
98
+ glyphMetrics(character: string, face: FontFace, fontSize: number): GlyphMetrics;
99
+ }
100
+ /** Reference `Justify`. `start`/`end` resolve to left/right in this LTR-only layout. */
101
+ export type TextJustify = "left" | "center" | "right" | "justified" | "start" | "end";
102
+ /** Reference `LineBreak`, minus the Unicode-correct word segmentation reference gets from parley. */
103
+ export type TextLineBreak = "word" | "character" | "word-or-character" | "none";
104
+ /** Normalized anchor: x 0 = left, 1 = right; y 0 = top, 1 = bottom (local y grows down). */
105
+ export type TextAnchorPoint = readonly [number, number];
106
+ /** Reference `Anchor` presets, named for the block corner that lands on the position. */
107
+ export type TextAnchor = "center" | "top-left" | "top-center" | "top-right" | "center-left" | "center-right" | "bottom-left" | "bottom-center" | "bottom-right";
108
+ export declare function resolveTextAnchor(anchor: TextAnchor): TextAnchorPoint;
109
+ /** One section of a text block, the equivalent of a reference `TextSpan`. */
110
+ export type TextSpanInput = {
111
+ readonly text: string;
112
+ readonly color?: Rgba;
113
+ readonly fontSize?: number;
114
+ readonly fontFamily?: string;
115
+ readonly weight?: number;
116
+ readonly style?: TextFontStyle;
117
+ readonly width?: TextFontWidth;
118
+ };
119
+ /** reference `LineHeight` default, `RelativeToFont(1.2)`. */
120
+ export declare const DEFAULT_LINE_HEIGHT_SCALE = 1.2;
121
+ export type TextLayoutOptions = {
122
+ readonly color: Rgba;
123
+ readonly fontSize: number;
124
+ readonly fontFamily: string;
125
+ /** Block default for `FontFace.weight`; spans override it. */
126
+ readonly weight?: number | undefined;
127
+ /** Block default for `FontFace.style`; spans override it. */
128
+ readonly style?: TextFontStyle | undefined;
129
+ /** Block default for `FontFace.width`; spans override it. */
130
+ readonly width?: TextFontWidth | undefined;
131
+ /**
132
+ * Absolute line height in layout units, reference `LineHeight::Px`. When omitted the line height
133
+ * is `lineHeightScale` times the line's own largest font size, which is reference
134
+ * `LineHeight::RelativeToFont` — the default, and the reason a line holding a larger span gets
135
+ * a taller box rather than being clipped to the block's own size.
136
+ */
137
+ readonly lineHeight?: number | undefined;
138
+ /** Multiple of the line's largest font size, reference `LineHeight::RelativeToFont`. Default `1.2`. */
139
+ readonly lineHeightScale?: number | undefined;
140
+ /**
141
+ * Added after every character, the last one on a line included: parley adds it to each cluster's
142
+ * advance, and the trailing whitespace a line breaks at carries its share into the block width.
143
+ */
144
+ readonly letterSpacing: number;
145
+ readonly justify: TextJustify;
146
+ readonly lineBreak: TextLineBreak;
147
+ /**
148
+ * `TextBounds`. Only `width` feeds layout: it is the wrap width. `height` is not a layout input,
149
+ * matching reference, whose `Text2d` never truncates lines against it (it only sizes the node).
150
+ */
151
+ readonly bounds?: readonly [number, number] | undefined;
152
+ readonly anchor: TextAnchorPoint;
153
+ };
154
+ /** One laid-out glyph, ready to become an instance once its atlas rectangle is known. */
155
+ export type PositionedGlyph = {
156
+ readonly character: string;
157
+ readonly face: FontFace;
158
+ readonly fontSize: number;
159
+ readonly color: Rgba;
160
+ /** Ink box centre relative to the block's anchor point, y down. */
161
+ readonly center: readonly [number, number];
162
+ readonly width: number;
163
+ readonly height: number;
164
+ };
165
+ export type TextLayoutResult = {
166
+ readonly glyphs: readonly PositionedGlyph[];
167
+ /** Block size before the anchor offset is applied. */
168
+ readonly size: readonly [number, number];
169
+ readonly lineCount: number;
170
+ };
171
+ /**
172
+ * Lays out one text block. `options.anchor` decides which part of the block lands on the
173
+ * position the renderer is given; the returned glyph centres are already offset for it.
174
+ */
175
+ export declare function layoutText(spans: readonly TextSpanInput[], options: TextLayoutOptions, measurer: TextMeasurer): TextLayoutResult;