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