@weasel-js/text 1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 orochi235
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # @weasel-js/text
2
+
3
+ Typography for weasel: styled runs, style resolution, kerned glyph layout,
4
+ wrap and measurement. Glyphs come from
5
+ [`@weasel-js/font`](https://www.npmjs.com/package/@weasel-js/font); nothing
6
+ here knows about a scene graph, a renderer or React.
7
+
8
+ Part of [weasel](https://github.com/orochi235/weasel), a domain-agnostic 2D
9
+ scene-graph canvas kit for React. See the
10
+ [API reference](https://orochi235.github.io/weasel/api/).
11
+
12
+ ## Install
13
+
14
+ ```sh
15
+ npm install @weasel-js/text
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ `layoutRuns` is the glyph walk: it takes fully-resolved runs and returns
21
+ positioned geometry, grouped so that one group is one draw call.
22
+
23
+ ```ts
24
+ import { toRuns, resolveTextStyle, resolveRuns, layoutRuns } from '@weasel-js/text';
25
+
26
+ const style = resolveTextStyle({ fontFamily: 'inter', fontSize: 64 });
27
+ const { groups, lines, bounds } = layoutRuns(resolveRuns(toRuns('Hello'), style), {
28
+ maxWidth: 900,
29
+ lineHeight: 1.2,
30
+ align: 'center',
31
+ });
32
+ ```
33
+
34
+ A group carries either textured quads (from a baked MSDF atlas) or outline
35
+ glyphs — em-space SVG path data plus the pen position, baseline and scale to
36
+ place it — for a consumer that tessellates its own geometry.
37
+
38
+ Metrics come from whichever tier resolved the family: a baked MSDF atlas, or —
39
+ for a family registered with `registerFontOutlines` and no atlas — the parsed
40
+ face itself, so font bytes alone are enough to lay text out.
41
+
42
+ The inline grammar `runsToMarkdown` / `markdownToRuns` speak is a parameter,
43
+ defaulting to `MARKDOWN_RUN_GRAMMAR` (`**bold**`, `*italic*`, `***both***`).
44
+ Pass a `RunGrammar` with different markers to read or write another spelling.
@@ -0,0 +1,531 @@
1
+ import { resolveFontVariant, glyphGeneration, glyphOutline, resolveGlyphFallback } from '@weasel-js/font';
2
+
3
+ // src/layout/layoutRuns.ts
4
+ var UNDERLINE_OFFSET = 0.1;
5
+ var STRIKETHROUGH_OFFSET = -0.3;
6
+ var OVERLINE_OFFSET = -0.9;
7
+ var DECORATION_THICKNESS = 0.05;
8
+ function atlasMetrics(font) {
9
+ return {
10
+ size: font.info.size,
11
+ base: font.common.base,
12
+ advanceOf: (cp) => font.charMap.get(cp)?.xadvance ?? null,
13
+ kernOf: (l, r) => font.kerningMap.get(l)?.get(r) ?? 0
14
+ };
15
+ }
16
+ function fillKey(p) {
17
+ if ("color" in p) return `s:${p.color}:${p.opacity ?? 1}`;
18
+ return `nx:${Math.random()}`;
19
+ }
20
+ function sameFill(a, b) {
21
+ if (a === b) return true;
22
+ if ("color" in a && "color" in b) {
23
+ return a.color === b.color && (a.opacity ?? 1) === (b.opacity ?? 1);
24
+ }
25
+ return false;
26
+ }
27
+ function strokePaints(s) {
28
+ if (s === void 0) return false;
29
+ const w = s.width ?? 1;
30
+ return (typeof w === "number" ? w : w.px) > 0;
31
+ }
32
+ function strokeKey(s) {
33
+ if (!s) return "-";
34
+ const width = s.width ?? 1;
35
+ return [
36
+ // A screen-pixel width keys apart from the equal world-unit number: the two
37
+ // resolve to different ribbons, so they must not share a draw call.
38
+ fillKey(s.paint),
39
+ typeof width === "number" ? width : `px${width.px}`,
40
+ s.join ?? "miter",
41
+ s.cap ?? "butt",
42
+ s.miterLimit ?? "",
43
+ s.align ?? "center",
44
+ (s.dash ?? []).join(",")
45
+ ].join(":");
46
+ }
47
+ function groupKey(family, weight, style, synthetic, fill, stroke, source, page) {
48
+ return `${family}|${weight}|${style}|${synthetic.bold ? 1 : 0}${synthetic.italic ? 1 : 0}|${fillKey(fill)}|${strokeKey(stroke)}|${source}|${page}`;
49
+ }
50
+ function getOrCreateGroup(ctx, run, resolved, page, sourceOverride) {
51
+ const atlasFamily = resolved.resolved.family;
52
+ const resolvedWeight = resolved.resolved.weight;
53
+ const resolvedStyle = resolved.resolved.style;
54
+ const source = sourceOverride ?? resolved.source;
55
+ const key = groupKey(
56
+ atlasFamily,
57
+ resolvedWeight,
58
+ resolvedStyle,
59
+ resolved.synthetic,
60
+ run.fill,
61
+ source === "outline" ? run.stroke : void 0,
62
+ source,
63
+ page
64
+ );
65
+ let g = ctx.groups.get(key);
66
+ if (!g) {
67
+ g = {
68
+ family: atlasFamily,
69
+ weight: resolvedWeight,
70
+ style: resolvedStyle,
71
+ synthetic: { ...resolved.synthetic },
72
+ source,
73
+ page,
74
+ fill: run.fill,
75
+ // Only the outline tier can paint it, and carrying it on an SDF group
76
+ // would be a promise the renderer cannot keep.
77
+ ...source === "outline" && run.stroke !== void 0 ? { stroke: run.stroke } : {},
78
+ quads: [],
79
+ glyphs: []
80
+ };
81
+ ctx.groups.set(key, g);
82
+ }
83
+ return g;
84
+ }
85
+ function resolveGlyph(run, font, resolved, cp) {
86
+ const direct = font.charMap.get(cp);
87
+ if (direct) return { glyph: direct, font, resolved };
88
+ const fallback = resolveGlyphFallback(run.fontFamily, run.fontWeight, run.fontStyle);
89
+ const face = fallback?.dynamicFace;
90
+ if (fallback && face) {
91
+ const glyph = face.requestGlyph(cp);
92
+ if (glyph.xadvance > 0 || glyph.width > 0) {
93
+ return { glyph, font: face.font, resolved: fallback };
94
+ }
95
+ }
96
+ warnMissingGlyphOnce(resolved.resolved.family, cp);
97
+ return null;
98
+ }
99
+ var warnedMissingGlyphs = /* @__PURE__ */ new Set();
100
+ function warnMissingGlyphOnce(family, cp) {
101
+ const key = `${family}|${cp}`;
102
+ if (warnedMissingGlyphs.has(key)) return;
103
+ warnedMissingGlyphs.add(key);
104
+ const ch = String.fromCodePoint(cp);
105
+ console.warn(
106
+ `weasel layoutRuns: no glyph for U+${cp.toString(16).toUpperCase().padStart(4, "0")} (${JSON.stringify(ch)}) in "${family}", and the dynamic tier could not rasterize it \u2014 skipping the character. Bake it into the atlas, or call registerCanvasFont("${family}") to serve missing codepoints from installed fonts.`
107
+ );
108
+ }
109
+ function layoutRuns(runs, opts) {
110
+ const ctx = { groups: /* @__PURE__ */ new Map() };
111
+ const entries = [];
112
+ let prevCp;
113
+ let prevMetrics;
114
+ let prevFontSize;
115
+ let srcIndex = 0;
116
+ for (const run of runs) {
117
+ const resolved = resolveFontVariant(run.fontFamily, run.fontWeight, run.fontStyle);
118
+ const outlineFace = resolved.outlineFace;
119
+ const font = resolved.entry?.font ?? resolved.dynamicFace?.font ?? null;
120
+ const metrics = outlineFace ? { size: 1, base: outlineFace.ascender, advanceOf: (cp) => outlineFace.advanceOf(cp), kernOf: (l, r) => outlineFace.kernOf(l, r) } : font ? atlasMetrics(font) : void 0;
121
+ if (!metrics) {
122
+ srcIndex += run.text.length;
123
+ prevCp = void 0;
124
+ prevMetrics = void 0;
125
+ prevFontSize = void 0;
126
+ continue;
127
+ }
128
+ const scale = run.fontSize / metrics.size;
129
+ const tracking = run.letterSpacing;
130
+ for (const ch of [...run.text]) {
131
+ const cp = ch.codePointAt(0);
132
+ const isNewline = cp === 10;
133
+ const isSpace = cp === 32;
134
+ const srcStart = srcIndex;
135
+ const srcEnd = srcIndex + ch.length;
136
+ srcIndex = srcEnd;
137
+ if (isNewline) {
138
+ entries.push({
139
+ run,
140
+ font,
141
+ metrics,
142
+ glyph: { id: cp, x: 0, y: 0, width: 0, height: 0, xoffset: 0, yoffset: 0, xadvance: 0, page: 0 },
143
+ // A newline consumes no advance, so it takes no tracking either.
144
+ cp,
145
+ advance: 0,
146
+ tracking: 0,
147
+ kerningBefore: 0,
148
+ isSpace: false,
149
+ isNewline: true,
150
+ resolved,
151
+ fontSize: run.fontSize,
152
+ srcIndex: srcStart,
153
+ srcEnd
154
+ });
155
+ prevCp = void 0;
156
+ prevMetrics = void 0;
157
+ prevFontSize = void 0;
158
+ continue;
159
+ }
160
+ if (isSpace) {
161
+ const spaceGlyph = resolved.dynamicFace ? resolved.dynamicFace.requestGlyph(32) : font?.charMap.get(32) ?? null;
162
+ const spaceAdvance = spaceGlyph ? spaceGlyph.xadvance : metrics.advanceOf(32);
163
+ const advance = spaceAdvance !== null ? spaceAdvance * scale : run.fontSize * 0.25;
164
+ let kerningBefore2 = 0;
165
+ if (prevCp !== void 0 && prevMetrics !== void 0 && prevFontSize !== void 0) {
166
+ kerningBefore2 = prevMetrics.kernOf(prevCp, cp) * (prevFontSize / prevMetrics.size);
167
+ }
168
+ entries.push({
169
+ run,
170
+ font,
171
+ metrics,
172
+ glyph: spaceGlyph ?? { id: 32, x: 0, y: 0, width: 0, height: 0, xoffset: 0, yoffset: 0, xadvance: 0, page: 0 },
173
+ cp,
174
+ advance,
175
+ tracking,
176
+ kerningBefore: kerningBefore2,
177
+ isSpace: true,
178
+ isNewline: false,
179
+ resolved,
180
+ fontSize: run.fontSize,
181
+ srcIndex: srcStart,
182
+ srcEnd
183
+ });
184
+ prevCp = cp;
185
+ prevMetrics = metrics;
186
+ prevFontSize = run.fontSize;
187
+ continue;
188
+ }
189
+ if (outlineFace) {
190
+ const adv = metrics.advanceOf(cp);
191
+ if (adv === null) {
192
+ prevCp = cp;
193
+ prevMetrics = metrics;
194
+ prevFontSize = run.fontSize;
195
+ continue;
196
+ }
197
+ let kerningBefore2 = 0;
198
+ if (prevCp !== void 0 && prevMetrics !== void 0 && prevFontSize !== void 0) {
199
+ kerningBefore2 = prevMetrics.kernOf(prevCp, cp) * (prevFontSize / prevMetrics.size);
200
+ }
201
+ entries.push({
202
+ run,
203
+ font: null,
204
+ metrics,
205
+ glyph: null,
206
+ cp,
207
+ advance: adv * scale,
208
+ tracking,
209
+ kerningBefore: kerningBefore2,
210
+ isSpace,
211
+ isNewline: false,
212
+ resolved,
213
+ fontSize: run.fontSize,
214
+ srcIndex: srcStart,
215
+ srcEnd
216
+ });
217
+ prevCp = cp;
218
+ prevMetrics = metrics;
219
+ prevFontSize = run.fontSize;
220
+ continue;
221
+ }
222
+ const hit = resolved.dynamicFace ? { glyph: resolved.dynamicFace.requestGlyph(cp), font, resolved } : resolveGlyph(run, font, resolved, cp);
223
+ if (!hit) {
224
+ prevCp = cp;
225
+ prevMetrics = metrics;
226
+ prevFontSize = run.fontSize;
227
+ continue;
228
+ }
229
+ const glyphFont = hit.font;
230
+ const glyphMetrics = glyphFont === font ? metrics : atlasMetrics(glyphFont);
231
+ const glyphScale = run.fontSize / glyphFont.info.size;
232
+ let kerningBefore = 0;
233
+ if (prevCp !== void 0 && prevMetrics !== void 0 && prevFontSize !== void 0) {
234
+ kerningBefore = prevMetrics.kernOf(prevCp, cp) * (prevFontSize / prevMetrics.size);
235
+ }
236
+ entries.push({
237
+ run,
238
+ font: glyphFont,
239
+ metrics: glyphMetrics,
240
+ glyph: hit.glyph,
241
+ cp,
242
+ advance: hit.glyph.xadvance * glyphScale,
243
+ tracking,
244
+ kerningBefore,
245
+ isSpace,
246
+ isNewline: false,
247
+ resolved: hit.resolved,
248
+ fontSize: run.fontSize,
249
+ srcIndex: srcStart,
250
+ srcEnd
251
+ });
252
+ prevCp = cp;
253
+ prevMetrics = glyphMetrics;
254
+ prevFontSize = run.fontSize;
255
+ }
256
+ }
257
+ const lines = [];
258
+ let cur = { entries: [], width: 0, height: 0 };
259
+ function commitLine() {
260
+ lines.push(cur);
261
+ cur = { entries: [], width: 0, height: 0 };
262
+ }
263
+ let i = 0;
264
+ while (i < entries.length) {
265
+ const e = entries[i];
266
+ if (e.isNewline) {
267
+ if (cur.entries.length === 0) {
268
+ cur.height = Math.max(cur.height, e.fontSize * opts.lineHeight);
269
+ cur.blank = e;
270
+ }
271
+ commitLine();
272
+ i++;
273
+ continue;
274
+ }
275
+ if (e.isSpace) {
276
+ if (cur.entries.length > 0) {
277
+ cur.entries.push(e);
278
+ cur.width += e.kerningBefore + e.advance + e.tracking;
279
+ cur.height = Math.max(cur.height, e.fontSize * opts.lineHeight);
280
+ }
281
+ i++;
282
+ continue;
283
+ }
284
+ let j = i;
285
+ let wordWidth = 0;
286
+ while (j < entries.length && !entries[j].isSpace && !entries[j].isNewline) {
287
+ const w = entries[j];
288
+ wordWidth += w.kerningBefore + w.advance + w.tracking;
289
+ j++;
290
+ }
291
+ if (Number.isFinite(opts.maxWidth) && cur.width + wordWidth > opts.maxWidth && cur.entries.length > 0) {
292
+ commitLine();
293
+ }
294
+ for (let k = i; k < j; k++) {
295
+ const w = entries[k];
296
+ const kerningBefore = cur.entries.length === 0 ? 0 : w.kerningBefore;
297
+ cur.entries.push({ ...w, kerningBefore });
298
+ cur.width += kerningBefore + w.advance + w.tracking;
299
+ cur.height = Math.max(cur.height, w.fontSize * opts.lineHeight);
300
+ }
301
+ i = j;
302
+ }
303
+ if (cur.entries.length > 0) commitLine();
304
+ function outlineFor(e) {
305
+ const r0 = e.resolved.resolved;
306
+ if (e.resolved.source === "outline") {
307
+ return glyphOutline(r0.family, r0.weight, r0.style, e.cp);
308
+ }
309
+ const min = opts.outlineMinSize;
310
+ if (min === void 0) return null;
311
+ if (e.fontSize < min && !strokePaints(e.run.stroke)) return null;
312
+ if (e.resolved.synthetic.bold) return null;
313
+ const r = e.resolved.resolved;
314
+ return glyphOutline(r.family, r.weight, r.style, e.cp);
315
+ }
316
+ const decorations = [];
317
+ let span = null;
318
+ function flushSpan() {
319
+ const s = span;
320
+ span = null;
321
+ if (!s || s.x1 <= s.x0) return;
322
+ const thickness = s.fontSize * DECORATION_THICKNESS;
323
+ if (s.underline) {
324
+ const y0 = s.baselineY + s.fontSize * UNDERLINE_OFFSET;
325
+ decorations.push({ kind: "underline", x0: s.x0, y0, x1: s.x1, y1: y0 + thickness, fill: s.fill });
326
+ }
327
+ if (s.strikethrough) {
328
+ const y0 = s.baselineY + s.fontSize * STRIKETHROUGH_OFFSET;
329
+ decorations.push({ kind: "strikethrough", x0: s.x0, y0, x1: s.x1, y1: y0 + thickness, fill: s.fill });
330
+ }
331
+ if (s.overline) {
332
+ const y0 = s.baselineY + s.fontSize * OVERLINE_OFFSET;
333
+ decorations.push({ kind: "overline", x0: s.x0, y0, x1: s.x1, y1: y0 + thickness, fill: s.fill });
334
+ }
335
+ }
336
+ const lineBoxes = [];
337
+ let penY = 0;
338
+ let maxLineWidth = 0;
339
+ const finiteWidth = Number.isFinite(opts.maxWidth) ? opts.maxWidth : 0;
340
+ for (const line of lines) {
341
+ const alignShift = (() => {
342
+ if (opts.align === "left") return 0;
343
+ if (!Number.isFinite(opts.maxWidth)) {
344
+ return opts.align === "center" ? -line.width / 2 : -line.width;
345
+ }
346
+ const slack = finiteWidth - line.width;
347
+ return opts.align === "center" ? slack / 2 : slack;
348
+ })();
349
+ const lineX0 = alignShift;
350
+ let lineAscent = 0;
351
+ for (const e of line.entries) {
352
+ lineAscent = Math.max(lineAscent, e.metrics.base * (e.fontSize / e.metrics.size));
353
+ }
354
+ if (line.entries.length === 0 && line.blank) {
355
+ lineAscent = line.blank.metrics.base * (line.blank.fontSize / line.blank.metrics.size);
356
+ }
357
+ const lineBaselineY = penY + lineAscent;
358
+ const caretXs = [];
359
+ const caretIndices = [];
360
+ let penX = lineX0;
361
+ for (const e of line.entries) {
362
+ penX += e.kerningBefore;
363
+ caretXs.push(penX);
364
+ caretIndices.push(e.srcIndex);
365
+ const step = e.advance + e.tracking;
366
+ const scale = e.fontSize / e.metrics.size;
367
+ const baselineY = lineBaselineY - e.run.baselineShift;
368
+ if (e.run.underline || e.run.strikethrough || e.run.overline) {
369
+ if (span !== null && span.underline === e.run.underline && span.strikethrough === e.run.strikethrough && span.overline === e.run.overline && span.fontSize === e.fontSize && span.baselineY === baselineY && sameFill(span.fill, e.run.fill)) {
370
+ span.x1 = penX + step;
371
+ } else {
372
+ flushSpan();
373
+ span = {
374
+ underline: e.run.underline,
375
+ strikethrough: e.run.strikethrough,
376
+ overline: e.run.overline,
377
+ fill: e.run.fill,
378
+ fontSize: e.fontSize,
379
+ baselineY,
380
+ x0: penX,
381
+ x1: penX + step
382
+ };
383
+ }
384
+ } else {
385
+ flushSpan();
386
+ }
387
+ const outlineD = outlineFor(e);
388
+ if (outlineD !== null) {
389
+ const group2 = getOrCreateGroup(ctx, e.run, e.resolved, 0, "outline");
390
+ group2.glyphs.push({
391
+ d: outlineD,
392
+ key: `${e.resolved.resolved.family}|${e.resolved.resolved.weight}|${e.resolved.resolved.style}|${e.cp}`,
393
+ x: penX,
394
+ baselineY,
395
+ // Em space is unit-scale, so world units per em is just the size.
396
+ scale: e.fontSize
397
+ });
398
+ penX += step;
399
+ continue;
400
+ }
401
+ if (e.font === null || e.glyph === null || e.advance === 0 || e.glyph.width === 0 || e.glyph.page < 0) {
402
+ penX += step;
403
+ continue;
404
+ }
405
+ const group = getOrCreateGroup(ctx, e.run, e.resolved, e.glyph.page);
406
+ const atlasW = e.font.common.scaleW;
407
+ const atlasH = e.font.common.scaleH;
408
+ const qx0 = penX + e.glyph.xoffset * scale;
409
+ const qy0 = baselineY + (e.glyph.yoffset - e.metrics.base) * scale;
410
+ const qx1 = qx0 + e.glyph.width * scale;
411
+ const qy1 = qy0 + e.glyph.height * scale;
412
+ const u0 = e.glyph.x / atlasW;
413
+ const v0 = e.glyph.y / atlasH;
414
+ const u1 = (e.glyph.x + e.glyph.width) / atlasW;
415
+ const v1 = (e.glyph.y + e.glyph.height) / atlasH;
416
+ group.quads.push({ x0: qx0, y0: qy0, x1: qx1, y1: qy1, u0, v0, u1, v1, baselineY });
417
+ penX += step;
418
+ }
419
+ flushSpan();
420
+ const lastCell = line.entries[line.entries.length - 1];
421
+ caretXs.push(penX);
422
+ caretIndices.push(lastCell ? lastCell.srcEnd : line.blank?.srcIndex ?? 0);
423
+ lineBoxes.push({
424
+ x0: lineX0,
425
+ y0: penY,
426
+ x1: lineX0 + line.width,
427
+ y1: penY + line.height,
428
+ baselineY: lineBaselineY,
429
+ caretXs,
430
+ caretIndices
431
+ });
432
+ maxLineWidth = Math.max(maxLineWidth, line.width);
433
+ penY += line.height;
434
+ }
435
+ return {
436
+ groups: [...ctx.groups.values()],
437
+ decorations,
438
+ lines: lineBoxes,
439
+ bounds: { width: maxLineWidth, height: penY }
440
+ };
441
+ }
442
+ var LAYOUT_CACHE_VARIANT_LIMIT = 8;
443
+ var LAYOUT_CACHE_STRUCTURAL_LIMIT = 64;
444
+ var cache = /* @__PURE__ */ new WeakMap();
445
+ var structural = /* @__PURE__ */ new Map();
446
+ var structuralGeneration = -1;
447
+ function outlineBucket(runs, min) {
448
+ if (min === void 0) return -1;
449
+ const sizes = /* @__PURE__ */ new Set();
450
+ for (const r of runs) sizes.add(r.fontSize);
451
+ let n = 0;
452
+ for (const size of sizes) if (size >= min) n++;
453
+ return n;
454
+ }
455
+ function variantKey(runs, opts) {
456
+ return `${opts.maxWidth}|${opts.lineHeight}|${opts.align}|${outlineBucket(runs, opts.outlineMinSize)}`;
457
+ }
458
+ var nextRefId = 1;
459
+ var refIds = /* @__PURE__ */ new WeakMap();
460
+ function refId(o) {
461
+ let id = refIds.get(o);
462
+ if (id === void 0) {
463
+ id = nextRefId++;
464
+ refIds.set(o, id);
465
+ }
466
+ return id;
467
+ }
468
+ function paintKey(p) {
469
+ return "color" in p ? `s${p.color}:${p.opacity ?? 1}` : `#${refId(p)}`;
470
+ }
471
+ function strokeKey2(s) {
472
+ if (!s) return "-";
473
+ const width = s.width ?? 1;
474
+ return [
475
+ paintKey(s.paint),
476
+ typeof width === "number" ? width : `px${width.px}`,
477
+ s.join ?? "miter",
478
+ s.cap ?? "butt",
479
+ s.miterLimit ?? "",
480
+ s.align ?? "center",
481
+ (s.dash ?? []).join(",")
482
+ ].join(":");
483
+ }
484
+ function runsKey(runs) {
485
+ let out = "";
486
+ for (const r of runs) {
487
+ out += `${r.text.length}:${r.text}|${r.fontFamily.length}:${r.fontFamily}|${r.fontSize}|${r.fontWeight}|${r.fontStyle}|${r.letterSpacing}|${r.underline ? 1 : 0}${r.strikethrough ? 1 : 0}${r.overline ? 1 : 0}|${r.baselineShift}|${paintKey(r.fill)}|${strokeKey2(r.stroke)}|`;
488
+ }
489
+ return out;
490
+ }
491
+ function cachedLayoutRuns(runs, opts) {
492
+ const fonts = glyphGeneration();
493
+ let entry = cache.get(runs);
494
+ if (entry === void 0) {
495
+ entry = { generation: fonts, byVariant: /* @__PURE__ */ new Map() };
496
+ cache.set(runs, entry);
497
+ } else if (entry.generation !== fonts) {
498
+ entry.generation = fonts;
499
+ entry.byVariant.clear();
500
+ }
501
+ const key = variantKey(runs, opts);
502
+ const hit = entry.byVariant.get(key);
503
+ if (hit !== void 0) return hit;
504
+ if (structuralGeneration !== fonts) {
505
+ structural.clear();
506
+ structuralGeneration = fonts;
507
+ }
508
+ const structuralK = `${runsKey(runs)}\0${key}`;
509
+ let laid = structural.get(structuralK);
510
+ if (laid !== void 0) {
511
+ structural.delete(structuralK);
512
+ } else {
513
+ laid = layoutRuns(runs, opts);
514
+ if (structural.size >= LAYOUT_CACHE_STRUCTURAL_LIMIT) {
515
+ structural.delete(structural.keys().next().value);
516
+ }
517
+ }
518
+ structural.set(structuralK, laid);
519
+ if (entry.byVariant.size >= LAYOUT_CACHE_VARIANT_LIMIT) entry.byVariant.clear();
520
+ entry.byVariant.set(key, laid);
521
+ return laid;
522
+ }
523
+ function _resetLayoutCacheForTests() {
524
+ cache = /* @__PURE__ */ new WeakMap();
525
+ structural = /* @__PURE__ */ new Map();
526
+ structuralGeneration = -1;
527
+ }
528
+
529
+ export { LAYOUT_CACHE_STRUCTURAL_LIMIT, LAYOUT_CACHE_VARIANT_LIMIT, _resetLayoutCacheForTests, cachedLayoutRuns, layoutRuns };
530
+ //# sourceMappingURL=chunk-4OGXTESY.js.map
531
+ //# sourceMappingURL=chunk-4OGXTESY.js.map