three-text 0.5.2 → 0.6.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 (52) hide show
  1. package/LICENSE_THIRD_PARTY +15 -0
  2. package/README.md +73 -42
  3. package/dist/index.cjs +1 -1
  4. package/dist/index.d.ts +2 -0
  5. package/dist/index.js +1 -1
  6. package/dist/index.min.cjs +1 -1
  7. package/dist/index.min.js +1 -1
  8. package/dist/index.umd.js +1 -1
  9. package/dist/index.umd.min.js +1 -1
  10. package/dist/three/react.d.ts +2 -0
  11. package/dist/types/core/types.d.ts +2 -33
  12. package/dist/types/vector/{core.d.ts → core/index.d.ts} +8 -7
  13. package/dist/types/vector/index.d.ts +22 -25
  14. package/dist/types/vector/react.d.ts +4 -3
  15. package/dist/types/vector/slug/SlugPacker.d.ts +2 -0
  16. package/dist/types/vector/slug/curveUtils.d.ts +6 -0
  17. package/dist/types/vector/slug/index.d.ts +8 -0
  18. package/dist/types/vector/slug/shaderStrings.d.ts +4 -0
  19. package/dist/types/vector/slug/slugGLSL.d.ts +21 -0
  20. package/dist/types/vector/slug/slugTSL.d.ts +13 -0
  21. package/dist/types/vector/slug/types.d.ts +30 -0
  22. package/dist/types/vector/slug/unpackVertices.d.ts +11 -0
  23. package/dist/types/vector/webgl/index.d.ts +7 -3
  24. package/dist/types/vector/webgpu/index.d.ts +4 -4
  25. package/dist/vector/core/index.cjs +856 -0
  26. package/dist/vector/core/index.d.ts +63 -0
  27. package/dist/vector/core/index.js +854 -0
  28. package/dist/vector/core.cjs +4419 -240
  29. package/dist/vector/core.d.ts +361 -71
  30. package/dist/vector/core.js +4406 -226
  31. package/dist/vector/index.cjs +5 -229
  32. package/dist/vector/index.d.ts +45 -396
  33. package/dist/vector/index.js +3 -223
  34. package/dist/vector/index2.cjs +287 -0
  35. package/dist/vector/index2.js +264 -0
  36. package/dist/vector/react.cjs +37 -8
  37. package/dist/vector/react.d.ts +6 -3
  38. package/dist/vector/react.js +18 -8
  39. package/dist/vector/slugTSL.cjs +252 -0
  40. package/dist/vector/slugTSL.js +231 -0
  41. package/dist/vector/webgl/index.cjs +131 -201
  42. package/dist/vector/webgl/index.d.ts +19 -44
  43. package/dist/vector/webgl/index.js +131 -201
  44. package/dist/vector/webgpu/index.cjs +100 -283
  45. package/dist/vector/webgpu/index.d.ts +16 -45
  46. package/dist/vector/webgpu/index.js +100 -283
  47. package/package.json +6 -1
  48. package/dist/types/vector/GlyphVectorGeometryBuilder.d.ts +0 -26
  49. package/dist/types/vector/LoopBlinnGeometry.d.ts +0 -68
  50. package/dist/types/vector/loopBlinnTSL.d.ts +0 -22
  51. package/dist/vector/loopBlinnTSL.cjs +0 -229
  52. package/dist/vector/loopBlinnTSL.js +0 -207
@@ -0,0 +1,854 @@
1
+ import { Text as Text$1 } from '../../index.js';
2
+ import { globalOutlineCache } from '../../index.js';
3
+ import { getSharedDrawCallbackHandler } from '../../index.js';
4
+ import { TextRangeQuery } from '../../index.js';
5
+
6
+ // 2D Vector
7
+ class Vec2 {
8
+ constructor(x = 0, y = 0) {
9
+ this.x = x;
10
+ this.y = y;
11
+ }
12
+ set(x, y) {
13
+ this.x = x;
14
+ this.y = y;
15
+ return this;
16
+ }
17
+ clone() {
18
+ return new Vec2(this.x, this.y);
19
+ }
20
+ copy(v) {
21
+ this.x = v.x;
22
+ this.y = v.y;
23
+ return this;
24
+ }
25
+ add(v) {
26
+ this.x += v.x;
27
+ this.y += v.y;
28
+ return this;
29
+ }
30
+ sub(v) {
31
+ this.x -= v.x;
32
+ this.y -= v.y;
33
+ return this;
34
+ }
35
+ multiply(scalar) {
36
+ this.x *= scalar;
37
+ this.y *= scalar;
38
+ return this;
39
+ }
40
+ divide(scalar) {
41
+ this.x /= scalar;
42
+ this.y /= scalar;
43
+ return this;
44
+ }
45
+ length() {
46
+ return Math.sqrt(this.x * this.x + this.y * this.y);
47
+ }
48
+ lengthSq() {
49
+ return this.x * this.x + this.y * this.y;
50
+ }
51
+ normalize() {
52
+ const len = this.length();
53
+ if (len > 0) {
54
+ this.divide(len);
55
+ }
56
+ return this;
57
+ }
58
+ dot(v) {
59
+ return this.x * v.x + this.y * v.y;
60
+ }
61
+ distanceTo(v) {
62
+ const dx = this.x - v.x;
63
+ const dy = this.y - v.y;
64
+ return Math.sqrt(dx * dx + dy * dy);
65
+ }
66
+ distanceToSquared(v) {
67
+ const dx = this.x - v.x;
68
+ const dy = this.y - v.y;
69
+ return dx * dx + dy * dy;
70
+ }
71
+ equals(v) {
72
+ return this.x === v.x && this.y === v.y;
73
+ }
74
+ angle() {
75
+ return Math.atan2(this.y, this.x);
76
+ }
77
+ }
78
+
79
+ class GlyphOutlineCollector {
80
+ constructor() {
81
+ this.currentGlyphId = 0;
82
+ this.currentTextIndex = 0;
83
+ this.inGlyph = false;
84
+ this.currentSegments = [];
85
+ this.currentPoint = null;
86
+ this.contourStartPoint = null;
87
+ this.contourId = 0;
88
+ this.currentGlyphBounds = {
89
+ min: new Vec2(Infinity, Infinity),
90
+ max: new Vec2(-Infinity, -Infinity)
91
+ };
92
+ this.collectedGlyphs = [];
93
+ this.currentPosition = new Vec2(0, 0);
94
+ }
95
+ setPosition(x, y) {
96
+ this.currentPosition.set(x, y);
97
+ }
98
+ updatePosition(dx, dy) {
99
+ this.currentPosition.x += dx;
100
+ this.currentPosition.y += dy;
101
+ }
102
+ beginGlyph(glyphId, textIndex) {
103
+ if (this.currentSegments.length > 0) {
104
+ this.finishGlyph();
105
+ }
106
+ this.currentGlyphId = glyphId;
107
+ this.currentTextIndex = textIndex;
108
+ this.inGlyph = true;
109
+ this.currentSegments = [];
110
+ this.currentPoint = null;
111
+ this.contourStartPoint = null;
112
+ this.contourId = 0;
113
+ this.currentGlyphBounds.min.set(Infinity, Infinity);
114
+ this.currentGlyphBounds.max.set(-Infinity, -Infinity);
115
+ }
116
+ finishGlyph() {
117
+ if (this.currentSegments.length > 0) {
118
+ this.collectedGlyphs.push({
119
+ glyphId: this.currentGlyphId,
120
+ textIndex: this.currentTextIndex,
121
+ segments: this.currentSegments,
122
+ bounds: {
123
+ min: {
124
+ x: this.currentGlyphBounds.min.x,
125
+ y: this.currentGlyphBounds.min.y
126
+ },
127
+ max: {
128
+ x: this.currentGlyphBounds.max.x,
129
+ y: this.currentGlyphBounds.max.y
130
+ }
131
+ }
132
+ });
133
+ }
134
+ else {
135
+ this.collectedGlyphs.push({
136
+ glyphId: this.currentGlyphId,
137
+ textIndex: this.currentTextIndex,
138
+ segments: [],
139
+ bounds: {
140
+ min: { x: 0, y: 0 },
141
+ max: { x: 0, y: 0 }
142
+ }
143
+ });
144
+ }
145
+ this.currentSegments = [];
146
+ this.currentPoint = null;
147
+ this.contourStartPoint = null;
148
+ this.inGlyph = false;
149
+ this.currentGlyphId = 0;
150
+ this.currentTextIndex = 0;
151
+ }
152
+ onMoveTo(x, y) {
153
+ const p = new Vec2(x, y);
154
+ this.updateBounds(p);
155
+ this.currentPoint = p;
156
+ this.contourStartPoint = p;
157
+ this.contourId++;
158
+ }
159
+ onLineTo(x, y) {
160
+ if (!this.currentPoint)
161
+ return;
162
+ const p1 = new Vec2(x, y);
163
+ const p0 = this.currentPoint;
164
+ this.updateBounds(p1);
165
+ this.currentSegments.push({
166
+ type: 0,
167
+ contourId: this.contourId,
168
+ p0,
169
+ p1
170
+ });
171
+ this.currentPoint = p1;
172
+ }
173
+ onQuadTo(cx, cy, x, y) {
174
+ if (!this.currentPoint)
175
+ return;
176
+ const p0 = this.currentPoint;
177
+ const p1 = new Vec2(cx, cy);
178
+ const p2 = new Vec2(x, y);
179
+ this.updateBounds(p1);
180
+ this.updateBounds(p2);
181
+ this.currentSegments.push({
182
+ type: 1,
183
+ contourId: this.contourId,
184
+ p0,
185
+ p1,
186
+ p2
187
+ });
188
+ this.currentPoint = p2;
189
+ }
190
+ onCubicTo(c1x, c1y, c2x, c2y, x, y) {
191
+ if (!this.currentPoint)
192
+ return;
193
+ const p0 = this.currentPoint;
194
+ const p1 = new Vec2(c1x, c1y);
195
+ const p2 = new Vec2(c2x, c2y);
196
+ const p3 = new Vec2(x, y);
197
+ this.updateBounds(p1);
198
+ this.updateBounds(p2);
199
+ this.updateBounds(p3);
200
+ this.currentSegments.push({
201
+ type: 2,
202
+ contourId: this.contourId,
203
+ p0,
204
+ p1,
205
+ p2,
206
+ p3
207
+ });
208
+ this.currentPoint = p3;
209
+ }
210
+ onClosePath() {
211
+ if (!this.currentPoint || !this.contourStartPoint)
212
+ return;
213
+ const p0 = this.currentPoint;
214
+ const p1 = this.contourStartPoint;
215
+ if (p0.x !== p1.x || p0.y !== p1.y) {
216
+ this.currentSegments.push({
217
+ type: 0,
218
+ contourId: this.contourId,
219
+ p0,
220
+ p1
221
+ });
222
+ }
223
+ this.currentPoint = p1;
224
+ this.contourStartPoint = null;
225
+ }
226
+ getCollectedGlyphs() {
227
+ if (this.inGlyph) {
228
+ this.finishGlyph();
229
+ }
230
+ return this.collectedGlyphs;
231
+ }
232
+ reset() {
233
+ this.collectedGlyphs = [];
234
+ this.currentGlyphId = 0;
235
+ this.currentTextIndex = 0;
236
+ this.currentSegments = [];
237
+ this.currentPoint = null;
238
+ this.contourStartPoint = null;
239
+ this.contourId = 0;
240
+ this.currentPosition.set(0, 0);
241
+ this.currentGlyphBounds.min.set(Infinity, Infinity);
242
+ this.currentGlyphBounds.max.set(-Infinity, -Infinity);
243
+ }
244
+ updateBounds(p) {
245
+ this.currentGlyphBounds.min.x = Math.min(this.currentGlyphBounds.min.x, p.x);
246
+ this.currentGlyphBounds.min.y = Math.min(this.currentGlyphBounds.min.y, p.y);
247
+ this.currentGlyphBounds.max.x = Math.max(this.currentGlyphBounds.max.x, p.x);
248
+ this.currentGlyphBounds.max.y = Math.max(this.currentGlyphBounds.max.y, p.y);
249
+ }
250
+ }
251
+
252
+ // CPU-side data packer for the Slug algorithm
253
+ // Faithful to Eric Lengyel's reference layout (MIT License, 2017)
254
+ //
255
+ // Takes generic quadratic Bezier shapes (not text-specific) and produces
256
+ // GPU-ready packed textures + vertex attribute buffers
257
+ const TEX_WIDTH = 4096;
258
+ const LOG_TEX_WIDTH = 12;
259
+ // Float/Uint32 reinterpretation helpers
260
+ const _f32 = new Float32Array(1);
261
+ const _u32 = new Uint32Array(_f32.buffer);
262
+ function uintAsFloat(u) {
263
+ _u32[0] = u;
264
+ return _f32[0];
265
+ }
266
+ // Pack an array of shapes into Slug's GPU data layout
267
+ // Each shape is a closed region defined by quadratic Bezier curves
268
+ function packSlugData(shapes, options) {
269
+ const bandCount = options?.bandCount ?? 16;
270
+ const evenOdd = options?.evenOdd ?? false;
271
+ // Pack all curves into curveTexture
272
+ const allCurves = [];
273
+ // Estimate max texels needed
274
+ let totalCurves = 0;
275
+ for (const shape of shapes) {
276
+ totalCurves += shape.curves.length;
277
+ }
278
+ const curveTexHeight = Math.ceil((totalCurves * 2) / TEX_WIDTH) + 1;
279
+ const curveData = new Float32Array(TEX_WIDTH * curveTexHeight * 4);
280
+ let curveX = 0;
281
+ let curveY = 0;
282
+ for (const shape of shapes) {
283
+ const entries = [];
284
+ for (const curve of shape.curves) {
285
+ // Don't let a curve span across row boundary (needs 2 consecutive texels)
286
+ if (curveX >= TEX_WIDTH - 1) {
287
+ curveX = 0;
288
+ curveY++;
289
+ }
290
+ const base = (curveY * TEX_WIDTH + curveX) * 4;
291
+ curveData[base + 0] = curve.p1[0];
292
+ curveData[base + 1] = curve.p1[1];
293
+ curveData[base + 2] = curve.p2[0];
294
+ curveData[base + 3] = curve.p2[1];
295
+ const base2 = base + 4;
296
+ curveData[base2 + 0] = curve.p3[0];
297
+ curveData[base2 + 1] = curve.p3[1];
298
+ const minX = Math.min(curve.p1[0], curve.p2[0], curve.p3[0]);
299
+ const minY = Math.min(curve.p1[1], curve.p2[1], curve.p3[1]);
300
+ const maxX = Math.max(curve.p1[0], curve.p2[0], curve.p3[0]);
301
+ const maxY = Math.max(curve.p1[1], curve.p2[1], curve.p3[1]);
302
+ entries.push({
303
+ p1x: curve.p1[0], p1y: curve.p1[1],
304
+ p2x: curve.p2[0], p2y: curve.p2[1],
305
+ p3x: curve.p3[0], p3y: curve.p3[1],
306
+ minX, minY, maxX, maxY,
307
+ curveTexX: curveX,
308
+ curveTexY: curveY
309
+ });
310
+ curveX += 2;
311
+ }
312
+ allCurves.push(entries);
313
+ }
314
+ const actualCurveTexHeight = curveY + 1;
315
+ // Build band data for each shape and pack into bandTexture
316
+ // Layout per shape in bandTexture (relative to glyphLoc):
317
+ // [0 .. hBandMax] : h-band headers
318
+ // [hBandMax+1 .. hBandMax+1+vBandMax] : v-band headers
319
+ // [hBandMax+vBandMax+2 .. ] : curve index lists
320
+ // First pass: compute total band texels needed
321
+ const shapeBandData = [];
322
+ let totalBandTexels = 0;
323
+ for (let si = 0; si < shapes.length; si++) {
324
+ const shape = shapes[si];
325
+ const curves = allCurves[si];
326
+ if (curves.length === 0) {
327
+ shapeBandData.push({
328
+ hBands: [], vBands: [], hLists: [], vLists: [],
329
+ totalTexels: 0, bandMaxX: 0, bandMaxY: 0
330
+ });
331
+ continue;
332
+ }
333
+ const [bMinX, bMinY, bMaxX, bMaxY] = shape.bounds;
334
+ const w = bMaxX - bMinX;
335
+ const h = bMaxY - bMinY;
336
+ const hBandCount = Math.min(bandCount, 255); // max 255 (fits in 8 bits)
337
+ const vBandCount = Math.min(bandCount, 255);
338
+ const bandMaxY = hBandCount - 1;
339
+ const bandMaxX = vBandCount - 1;
340
+ // Build horizontal bands (partition y-axis)
341
+ const hBands = [];
342
+ const hLists = [];
343
+ const bandH = h / hBandCount;
344
+ for (let bi = 0; bi < hBandCount; bi++) {
345
+ const bandMinY = bMinY + bi * bandH;
346
+ const bandMaxYCoord = bandMinY + bandH;
347
+ // Collect curves whose y-range overlaps this band
348
+ const list = [];
349
+ for (const c of curves) {
350
+ if (c.maxY >= bandMinY && c.minY <= bandMaxYCoord) {
351
+ list.push({ curve: c, sortKey: c.maxX });
352
+ }
353
+ }
354
+ // Sort by descending max-x for early exit
355
+ list.sort((a, b) => b.sortKey - a.sortKey);
356
+ const flatList = [];
357
+ for (const item of list) {
358
+ flatList.push(item.curve.curveTexX, item.curve.curveTexY);
359
+ }
360
+ hBands.push({ curveCount: list.length, listOffset: 0 });
361
+ hLists.push(flatList);
362
+ }
363
+ // Build vertical bands (partition x-axis)
364
+ const vBands = [];
365
+ const vLists = [];
366
+ const bandW = w / vBandCount;
367
+ for (let bi = 0; bi < vBandCount; bi++) {
368
+ const bandMinX = bMinX + bi * bandW;
369
+ const bandMaxXCoord = bandMinX + bandW;
370
+ const list = [];
371
+ for (const c of curves) {
372
+ if (c.maxX >= bandMinX && c.minX <= bandMaxXCoord) {
373
+ list.push({ curve: c, sortKey: c.maxY });
374
+ }
375
+ }
376
+ // Sort by descending max-y for early exit
377
+ list.sort((a, b) => b.sortKey - a.sortKey);
378
+ const flatList = [];
379
+ for (const item of list) {
380
+ flatList.push(item.curve.curveTexX, item.curve.curveTexY);
381
+ }
382
+ vBands.push({ curveCount: list.length, listOffset: 0 });
383
+ vLists.push(flatList);
384
+ }
385
+ // Total texels for this shape: band headers + curve lists
386
+ const headerTexels = hBandCount + vBandCount;
387
+ let listTexels = 0;
388
+ for (const l of hLists)
389
+ listTexels += l.length / 2;
390
+ for (const l of vLists)
391
+ listTexels += l.length / 2;
392
+ const total = headerTexels + listTexels;
393
+ shapeBandData.push({
394
+ hBands, vBands, hLists, vLists,
395
+ totalTexels: total, bandMaxX, bandMaxY
396
+ });
397
+ totalBandTexels += total;
398
+ }
399
+ // Allocate bandTexture (extra rows for row-alignment padding of curve lists)
400
+ const bandTexHeight = Math.max(1, Math.ceil(totalBandTexels / TEX_WIDTH) + shapes.length * 2);
401
+ const bandData = new Uint32Array(TEX_WIDTH * bandTexHeight * 4);
402
+ // Pack band data per shape
403
+ let bandX = 0;
404
+ let bandY = 0;
405
+ const glyphLocs = [];
406
+ for (let si = 0; si < shapes.length; si++) {
407
+ const sd = shapeBandData[si];
408
+ if (sd.totalTexels === 0) {
409
+ glyphLocs.push({ x: 0, y: 0 });
410
+ continue;
411
+ }
412
+ // Ensure glyph data doesn't start too close to row end
413
+ // (need at least headerTexels contiguous... actually wrapping is handled by CalcBandLoc)
414
+ // But the initial band header reads don't use CalcBandLoc, so glyphLoc.x + bandMax.y + 1 + bandMaxX
415
+ // must be reachable. CalcBandLoc handles wrapping for curve lists.
416
+ // To be safe, start each glyph at the beginning of a row if remaining space is tight.
417
+ const minContiguous = sd.hBands.length + sd.vBands.length;
418
+ if (bandX + minContiguous > TEX_WIDTH) {
419
+ bandX = 0;
420
+ bandY++;
421
+ }
422
+ const glyphLocX = bandX;
423
+ const glyphLocY = bandY;
424
+ glyphLocs.push({ x: glyphLocX, y: glyphLocY });
425
+ // Curve lists start after all headers
426
+ let listStartOffset = sd.hBands.length + sd.vBands.length;
427
+ // The shader reads curve list entries at (hbandLoc.x + curveIndex, hbandLoc.y)
428
+ // with NO row wrapping. Each list must fit entirely within a single texture row.
429
+ // Pad the offset to the next row start when a list would cross a row boundary.
430
+ const ensureListFits = (listLen) => {
431
+ if (listLen === 0)
432
+ return;
433
+ const startX = (glyphLocX + listStartOffset) & ((1 << LOG_TEX_WIDTH) - 1);
434
+ if (startX + listLen > TEX_WIDTH) {
435
+ listStartOffset += (TEX_WIDTH - startX);
436
+ }
437
+ };
438
+ // Assign list offsets for h-bands
439
+ for (let bi = 0; bi < sd.hBands.length; bi++) {
440
+ const listLen = sd.hLists[bi].length / 2;
441
+ ensureListFits(listLen);
442
+ sd.hBands[bi].listOffset = listStartOffset;
443
+ listStartOffset += listLen;
444
+ }
445
+ // Assign list offsets for v-bands
446
+ for (let bi = 0; bi < sd.vBands.length; bi++) {
447
+ const listLen = sd.vLists[bi].length / 2;
448
+ ensureListFits(listLen);
449
+ sd.vBands[bi].listOffset = listStartOffset;
450
+ listStartOffset += listLen;
451
+ }
452
+ // Write h-band headers
453
+ for (let bi = 0; bi < sd.hBands.length; bi++) {
454
+ const tx = glyphLocX + bi;
455
+ const ty = glyphLocY;
456
+ const idx = (ty * TEX_WIDTH + tx) * 4;
457
+ bandData[idx + 0] = sd.hBands[bi].curveCount;
458
+ bandData[idx + 1] = sd.hBands[bi].listOffset;
459
+ bandData[idx + 2] = 0;
460
+ bandData[idx + 3] = 0;
461
+ }
462
+ // Write v-band headers (after h-bands)
463
+ const vBandStart = glyphLocX + sd.hBands.length;
464
+ for (let bi = 0; bi < sd.vBands.length; bi++) {
465
+ const tx = vBandStart + bi;
466
+ const ty = glyphLocY;
467
+ const idx = (ty * TEX_WIDTH + tx) * 4;
468
+ bandData[idx + 0] = sd.vBands[bi].curveCount;
469
+ bandData[idx + 1] = sd.vBands[bi].listOffset;
470
+ bandData[idx + 2] = 0;
471
+ bandData[idx + 3] = 0;
472
+ }
473
+ // Write curve lists using CalcBandLoc-style wrapping
474
+ const texWidthMask = (1 << LOG_TEX_WIDTH) - 1;
475
+ // Write h-band curve lists
476
+ for (let bi = 0; bi < sd.hBands.length; bi++) {
477
+ const list = sd.hLists[bi];
478
+ const baseOffset = sd.hBands[bi].listOffset;
479
+ for (let ci = 0; ci < list.length; ci += 2) {
480
+ let bx = glyphLocX + baseOffset + (ci >> 1);
481
+ const by = glyphLocY + (bx >> LOG_TEX_WIDTH);
482
+ bx &= texWidthMask;
483
+ const idx = (by * TEX_WIDTH + bx) * 4;
484
+ bandData[idx + 0] = list[ci]; // curveTexX
485
+ bandData[idx + 1] = list[ci + 1]; // curveTexY
486
+ bandData[idx + 2] = 0;
487
+ bandData[idx + 3] = 0;
488
+ }
489
+ }
490
+ // Write v-band curve lists
491
+ for (let bi = 0; bi < sd.vBands.length; bi++) {
492
+ const list = sd.vLists[bi];
493
+ const baseOffset = sd.vBands[bi].listOffset;
494
+ for (let ci = 0; ci < list.length; ci += 2) {
495
+ let bx = glyphLocX + baseOffset + (ci >> 1);
496
+ const by = glyphLocY + (bx >> LOG_TEX_WIDTH);
497
+ bx &= texWidthMask;
498
+ const idx = (by * TEX_WIDTH + bx) * 4;
499
+ bandData[idx + 0] = list[ci];
500
+ bandData[idx + 1] = list[ci + 1];
501
+ bandData[idx + 2] = 0;
502
+ bandData[idx + 3] = 0;
503
+ }
504
+ }
505
+ // Advance band cursor past this shape's data
506
+ let endBx = glyphLocX + listStartOffset;
507
+ bandY = glyphLocY + (endBx >> LOG_TEX_WIDTH);
508
+ bandX = endBx & texWidthMask;
509
+ }
510
+ const actualBandTexHeight = bandY + 1;
511
+ // Build vertex attributes
512
+ // 5 attribs x 4 floats x 4 vertices per shape = 80 floats per shape
513
+ const FLOATS_PER_VERTEX = 20; // 5 attribs * 4 components
514
+ const VERTS_PER_SHAPE = 4;
515
+ const vertices = new Float32Array(shapes.length * VERTS_PER_SHAPE * FLOATS_PER_VERTEX);
516
+ const indices = new Uint16Array(shapes.length * 6);
517
+ // Corner normals (outward-pointing, un-normalized; SlugDilate normalizes)
518
+ const cornerNormals = [
519
+ [-1, -1], // bottom-left
520
+ [1, -1], // bottom-right
521
+ [1, 1], // top-right
522
+ [-1, 1], // top-left
523
+ ];
524
+ for (let si = 0; si < shapes.length; si++) {
525
+ const shape = shapes[si];
526
+ const sd = shapeBandData[si];
527
+ const glyph = glyphLocs[si];
528
+ const [bMinX, bMinY, bMaxX, bMaxY] = shape.bounds;
529
+ const w = bMaxX - bMinX;
530
+ const h = bMaxY - bMinY;
531
+ // Corner positions in object-space
532
+ const corners = [
533
+ [bMinX, bMinY],
534
+ [bMaxX, bMinY],
535
+ [bMaxX, bMaxY],
536
+ [bMinX, bMaxY],
537
+ ];
538
+ // Em-space sample coords at corners (same as object-space for 1:1 mapping)
539
+ const emCorners = [
540
+ [bMinX, bMinY],
541
+ [bMaxX, bMinY],
542
+ [bMaxX, bMaxY],
543
+ [bMinX, bMaxY],
544
+ ];
545
+ // Pack tex.z: glyph location in band texture
546
+ const texZ = uintAsFloat((glyph.x & 0xFFFF) | ((glyph.y & 0xFFFF) << 16));
547
+ // Pack tex.w: band max + flags
548
+ let texWBits = (sd.bandMaxX & 0xFF) | ((sd.bandMaxY & 0xFF) << 16);
549
+ if (evenOdd)
550
+ texWBits |= 0x10000000; // E flag at bit 28
551
+ const texW = uintAsFloat(texWBits);
552
+ // Band transform: scale and offset to map em-coords to band indices
553
+ const bandScaleX = w > 0 ? sd.vBands.length / w : 0;
554
+ const bandScaleY = h > 0 ? sd.hBands.length / h : 0;
555
+ const bandOffsetX = -bMinX * bandScaleX;
556
+ const bandOffsetY = -bMinY * bandScaleY;
557
+ for (let vi = 0; vi < 4; vi++) {
558
+ const base = (si * 4 + vi) * FLOATS_PER_VERTEX;
559
+ // pos: .xy = position, .zw = normal
560
+ vertices[base + 0] = corners[vi][0];
561
+ vertices[base + 1] = corners[vi][1];
562
+ vertices[base + 2] = cornerNormals[vi][0];
563
+ vertices[base + 3] = cornerNormals[vi][1];
564
+ // tex: .xy = em-space coords, .z = packed glyph loc, .w = packed band max
565
+ vertices[base + 4] = emCorners[vi][0];
566
+ vertices[base + 5] = emCorners[vi][1];
567
+ vertices[base + 6] = texZ;
568
+ vertices[base + 7] = texW;
569
+ // jac: identity Jacobian (em-space = object-space)
570
+ vertices[base + 8] = 1.0;
571
+ vertices[base + 9] = 0.0;
572
+ vertices[base + 10] = 0.0;
573
+ vertices[base + 11] = 1.0;
574
+ // bnd: band scale and offset
575
+ vertices[base + 12] = bandScaleX;
576
+ vertices[base + 13] = bandScaleY;
577
+ vertices[base + 14] = bandOffsetX;
578
+ vertices[base + 15] = bandOffsetY;
579
+ // col: white with full alpha (caller overrides via uniform or attribute)
580
+ vertices[base + 16] = 1.0;
581
+ vertices[base + 17] = 1.0;
582
+ vertices[base + 18] = 1.0;
583
+ vertices[base + 19] = 1.0;
584
+ }
585
+ // Indices: two triangles per quad
586
+ const vBase = si * 4;
587
+ const iBase = si * 6;
588
+ indices[iBase + 0] = vBase + 0;
589
+ indices[iBase + 1] = vBase + 1;
590
+ indices[iBase + 2] = vBase + 2;
591
+ indices[iBase + 3] = vBase + 0;
592
+ indices[iBase + 4] = vBase + 2;
593
+ indices[iBase + 5] = vBase + 3;
594
+ }
595
+ return {
596
+ curveTexture: {
597
+ data: curveData.slice(0, TEX_WIDTH * actualCurveTexHeight * 4),
598
+ width: TEX_WIDTH,
599
+ height: actualCurveTexHeight
600
+ },
601
+ bandTexture: {
602
+ data: bandData.slice(0, TEX_WIDTH * actualBandTexHeight * 4),
603
+ width: TEX_WIDTH,
604
+ height: actualBandTexHeight
605
+ },
606
+ vertices,
607
+ indices,
608
+ shapeCount: shapes.length
609
+ };
610
+ }
611
+
612
+ const DEFAULT_MAX_ERROR = 0.01;
613
+ const DEFAULT_MAX_DEPTH = 8;
614
+ function cloneVec2(p) {
615
+ return [p[0], p[1]];
616
+ }
617
+ function cubicToQuadratics(p0, p1, p2, p3, options) {
618
+ const maxError = options?.maxError ?? DEFAULT_MAX_ERROR;
619
+ const maxDepth = options?.maxDepth ?? DEFAULT_MAX_DEPTH;
620
+ const out = [];
621
+ const recurse = (rp0, rp1, rp2, rp3, depth) => {
622
+ // Error bound for best-fit quadratic approximation of this cubic:
623
+ // |P3 - 3*P2 + 3*P1 - P0| / 6
624
+ const dx = rp3[0] - 3 * rp2[0] + 3 * rp1[0] - rp0[0];
625
+ const dy = rp3[1] - 3 * rp2[1] + 3 * rp1[1] - rp0[1];
626
+ const err = dx * dx + dy * dy;
627
+ const threshold = maxError * maxError * 36;
628
+ if (err <= threshold || depth >= maxDepth) {
629
+ const qx = (3 * (rp1[0] + rp2[0]) - rp0[0] - rp3[0]) * 0.25;
630
+ const qy = (3 * (rp1[1] + rp2[1]) - rp0[1] - rp3[1]) * 0.25;
631
+ out.push({ p1: cloneVec2(rp0), p2: [qx, qy], p3: cloneVec2(rp3) });
632
+ return;
633
+ }
634
+ const m01x = (rp0[0] + rp1[0]) * 0.5, m01y = (rp0[1] + rp1[1]) * 0.5;
635
+ const m12x = (rp1[0] + rp2[0]) * 0.5, m12y = (rp1[1] + rp2[1]) * 0.5;
636
+ const m23x = (rp2[0] + rp3[0]) * 0.5, m23y = (rp2[1] + rp3[1]) * 0.5;
637
+ const m012x = (m01x + m12x) * 0.5, m012y = (m01y + m12y) * 0.5;
638
+ const m123x = (m12x + m23x) * 0.5, m123y = (m12y + m23y) * 0.5;
639
+ const midx = (m012x + m123x) * 0.5, midy = (m012y + m123y) * 0.5;
640
+ recurse(rp0, [m01x, m01y], [m012x, m012y], [midx, midy], depth + 1);
641
+ recurse([midx, midy], [m123x, m123y], [m23x, m23y], rp3, depth + 1);
642
+ };
643
+ recurse(p0, p1, p2, p3, 0);
644
+ return out;
645
+ }
646
+
647
+ function createOutlineContext(loadedFont, fontId, cache = globalOutlineCache) {
648
+ const collector = new GlyphOutlineCollector();
649
+ const drawCallbacks = getSharedDrawCallbackHandler(loadedFont);
650
+ drawCallbacks.createDrawFuncs(loadedFont, collector);
651
+ return {
652
+ loadedFont,
653
+ outlineCache: cache,
654
+ drawCallbacks,
655
+ collector,
656
+ cacheKeyPrefix: `${fontId}__outline`,
657
+ emptyGlyphs: new Set(),
658
+ };
659
+ }
660
+ function getOutlineForGlyph(ctx, glyphId) {
661
+ if (ctx.emptyGlyphs.has(glyphId)) {
662
+ return {
663
+ glyphId,
664
+ textIndex: 0,
665
+ segments: [],
666
+ bounds: { min: { x: 0, y: 0 }, max: { x: 0, y: 0 } }
667
+ };
668
+ }
669
+ const key = `${ctx.cacheKeyPrefix}_${glyphId}`;
670
+ const cached = ctx.outlineCache.get(key);
671
+ if (cached)
672
+ return cached;
673
+ ctx.drawCallbacks.setCollector(ctx.collector);
674
+ ctx.collector.reset();
675
+ ctx.collector.beginGlyph(glyphId, 0);
676
+ ctx.loadedFont.module.exports.hb_font_draw_glyph(ctx.loadedFont.font.ptr, glyphId, ctx.drawCallbacks.getDrawFuncsPtr(), 0);
677
+ ctx.collector.finishGlyph();
678
+ const collected = ctx.collector.getCollectedGlyphs()[0];
679
+ const outline = collected ?? {
680
+ glyphId,
681
+ textIndex: 0,
682
+ segments: [],
683
+ bounds: { min: { x: 0, y: 0 }, max: { x: 0, y: 0 } }
684
+ };
685
+ if (outline.segments.length === 0) {
686
+ ctx.emptyGlyphs.add(glyphId);
687
+ }
688
+ ctx.outlineCache.set(key, outline);
689
+ return outline;
690
+ }
691
+ function collectForSlug(ctx, clustersByLine, scale) {
692
+ const seen = new Set();
693
+ const outlinesByGlyph = new Map();
694
+ for (const line of clustersByLine) {
695
+ for (const cluster of line) {
696
+ for (const g of cluster.glyphs) {
697
+ if (!seen.has(g.g)) {
698
+ seen.add(g.g);
699
+ outlinesByGlyph.set(g.g, getOutlineForGlyph(ctx, g.g));
700
+ }
701
+ }
702
+ }
703
+ }
704
+ const shapes = [];
705
+ const glyphInfos = [];
706
+ for (const line of clustersByLine) {
707
+ for (const cluster of line) {
708
+ for (const g of cluster.glyphs) {
709
+ const outline = outlinesByGlyph.get(g.g);
710
+ if (!outline || outline.segments.length === 0)
711
+ continue;
712
+ const px = (cluster.position.x + (g.x ?? 0)) * scale;
713
+ const py = (cluster.position.y + (g.y ?? 0)) * scale;
714
+ const curves = [];
715
+ for (const seg of outline.segments) {
716
+ switch (seg.type) {
717
+ // line to quadratic
718
+ case 0: {
719
+ const x0 = seg.p0.x * scale + px, y0 = seg.p0.y * scale + py;
720
+ const x1 = seg.p1.x * scale + px, y1 = seg.p1.y * scale + py;
721
+ curves.push({
722
+ p1: [x0, y0],
723
+ p2: [(x0 + x1) * 0.5, (y0 + y1) * 0.5],
724
+ p3: [x1, y1],
725
+ });
726
+ break;
727
+ }
728
+ // quadratic
729
+ case 1:
730
+ curves.push({
731
+ p1: [seg.p0.x * scale + px, seg.p0.y * scale + py],
732
+ p2: [seg.p1.x * scale + px, seg.p1.y * scale + py],
733
+ p3: [seg.p2.x * scale + px, seg.p2.y * scale + py],
734
+ });
735
+ break;
736
+ // cubic to quadratic
737
+ case 2: {
738
+ const quads = cubicToQuadratics([seg.p0.x, seg.p0.y], [seg.p1.x, seg.p1.y], [seg.p2.x, seg.p2.y], [seg.p3.x, seg.p3.y]);
739
+ for (const q of quads) {
740
+ curves.push({
741
+ p1: [q.p1[0] * scale + px, q.p1[1] * scale + py],
742
+ p2: [q.p2[0] * scale + px, q.p2[1] * scale + py],
743
+ p3: [q.p3[0] * scale + px, q.p3[1] * scale + py],
744
+ });
745
+ }
746
+ break;
747
+ }
748
+ }
749
+ }
750
+ const bounds = [
751
+ outline.bounds.min.x * scale + px,
752
+ outline.bounds.min.y * scale + py,
753
+ outline.bounds.max.x * scale + px,
754
+ outline.bounds.max.y * scale + py,
755
+ ];
756
+ glyphInfos.push({
757
+ textIndex: g.absoluteTextIndex,
758
+ lineIndex: g.lineIndex,
759
+ vertexStart: 0,
760
+ vertexCount: 0,
761
+ segmentStart: 0,
762
+ segmentCount: outline.segments.length,
763
+ bounds: {
764
+ min: { x: bounds[0], y: bounds[1], z: 0 },
765
+ max: { x: bounds[2], y: bounds[3], z: 0 },
766
+ },
767
+ });
768
+ shapes.push({ curves, bounds });
769
+ }
770
+ }
771
+ }
772
+ const gpuData = packSlugData(shapes);
773
+ const planeBounds = computePlaneBounds(glyphInfos);
774
+ return { gpuData, glyphs: glyphInfos, planeBounds };
775
+ }
776
+ function computePlaneBounds(glyphInfos) {
777
+ if (glyphInfos.length === 0) {
778
+ return { min: { x: 0, y: 0, z: 0 }, max: { x: 0, y: 0, z: 0 } };
779
+ }
780
+ let minX = Infinity, minY = Infinity;
781
+ let maxX = -Infinity, maxY = -Infinity;
782
+ for (const g of glyphInfos) {
783
+ if (g.bounds.min.x < minX)
784
+ minX = g.bounds.min.x;
785
+ if (g.bounds.min.y < minY)
786
+ minY = g.bounds.min.y;
787
+ if (g.bounds.max.x > maxX)
788
+ maxX = g.bounds.max.x;
789
+ if (g.bounds.max.y > maxY)
790
+ maxY = g.bounds.max.y;
791
+ }
792
+ return { min: { x: minX, y: minY, z: 0 }, max: { x: maxX, y: maxY, z: 0 } };
793
+ }
794
+ // Public API
795
+ function buildVectorResult(layoutHandle, ctx, options) {
796
+ const scale = layoutHandle.layoutData.pixelsPerFontUnit;
797
+ let cachedQuery = null;
798
+ const update = async (newOptions) => {
799
+ const mergedOptions = { ...options };
800
+ for (const key in newOptions) {
801
+ const value = newOptions[key];
802
+ if (value !== undefined) {
803
+ mergedOptions[key] = value;
804
+ }
805
+ }
806
+ if (newOptions.font !== undefined ||
807
+ newOptions.fontVariations !== undefined ||
808
+ newOptions.fontFeatures !== undefined) {
809
+ const newLayout = await layoutHandle.update(mergedOptions);
810
+ const newCtx = createOutlineContext(newLayout.loadedFont, newLayout.fontId, globalOutlineCache);
811
+ layoutHandle = newLayout;
812
+ options = mergedOptions;
813
+ return buildVectorResult(layoutHandle, newCtx, options);
814
+ }
815
+ const newLayout = await layoutHandle.update(mergedOptions);
816
+ layoutHandle = newLayout;
817
+ options = mergedOptions;
818
+ return buildVectorResult(layoutHandle, ctx, options);
819
+ };
820
+ const { gpuData, glyphs, planeBounds } = collectForSlug(ctx, layoutHandle.clustersByLine, scale);
821
+ return {
822
+ gpuData,
823
+ glyphs,
824
+ planeBounds,
825
+ query: (queryOptions) => {
826
+ if (!cachedQuery) {
827
+ cachedQuery = new TextRangeQuery(options.text, glyphs);
828
+ }
829
+ return cachedQuery.execute(queryOptions);
830
+ },
831
+ getLoadedFont: () => layoutHandle.getLoadedFont(),
832
+ measureTextWidth: (text, letterSpacing) => layoutHandle.measureTextWidth(text, letterSpacing),
833
+ update,
834
+ dispose: () => {
835
+ layoutHandle.dispose();
836
+ }
837
+ };
838
+ }
839
+ class Text {
840
+ static { this.setHarfBuzzPath = Text$1.setHarfBuzzPath; }
841
+ static { this.setHarfBuzzBuffer = Text$1.setHarfBuzzBuffer; }
842
+ static { this.init = Text$1.init; }
843
+ static { this.registerPattern = Text$1.registerPattern; }
844
+ static { this.preloadPatterns = Text$1.preloadPatterns; }
845
+ static { this.setMaxFontCacheMemoryMB = Text$1.setMaxFontCacheMemoryMB; }
846
+ static { this.enableWoff2 = Text$1.enableWoff2; }
847
+ static async create(options) {
848
+ const layoutHandle = await Text$1.create(options);
849
+ const ctx = createOutlineContext(layoutHandle.loadedFont, layoutHandle.fontId, globalOutlineCache);
850
+ return buildVectorResult(layoutHandle, ctx, options);
851
+ }
852
+ }
853
+
854
+ export { Text };