three-text 0.5.0 → 0.5.2

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.
@@ -0,0 +1,1310 @@
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
+ function ceilDiv(a, b) {
255
+ return Math.floor((a + b - 1) / b);
256
+ }
257
+ function createPackedTexture(texelCount, width) {
258
+ const height = Math.max(1, ceilDiv(texelCount, width));
259
+ return { width, height, data: new Float32Array(width * height * 4) };
260
+ }
261
+ // All cubic-to-quadratic work uses scalar x/y to avoid per-recursion
262
+ // Vec2 allocations. Only the final emitted QuadSegs create Vec2s
263
+ function cubicToQuadraticsAdaptive(contourId, p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y, tol2, depth, out) {
264
+ // Approximate quad control points
265
+ const c0x = p0x + 0.75 * (p1x - p0x);
266
+ const c0y = p0y + 0.75 * (p1y - p0y);
267
+ const c1x = p3x + 0.75 * (p2x - p3x);
268
+ const c1y = p3y + 0.75 * (p2y - p3y);
269
+ const mx = (c0x + c1x) * 0.5;
270
+ const my = (c0y + c1y) * 0.5;
271
+ // Error: sample cubic vs piecewise quads at t=0.25, 0.5, 0.75
272
+ let maxErr2 = 0;
273
+ for (let si = 0; si < 3; si++) {
274
+ const t = 0.25 + si * 0.25;
275
+ const u = 1 - t;
276
+ const uu = u * u;
277
+ const uuu = uu * u;
278
+ const tt = t * t;
279
+ const ttt = tt * t;
280
+ const cx = uuu * p0x + 3 * uu * t * p1x + 3 * u * tt * p2x + ttt * p3x;
281
+ const cy = uuu * p0y + 3 * uu * t * p1y + 3 * u * tt * p2y + ttt * p3y;
282
+ let qx, qy;
283
+ if (t < 0.5) {
284
+ const qt = t * 2;
285
+ const qu = 1 - qt;
286
+ qx = qu * qu * p0x + 2 * qu * qt * c0x + qt * qt * mx;
287
+ qy = qu * qu * p0y + 2 * qu * qt * c0y + qt * qt * my;
288
+ }
289
+ else {
290
+ const qt = (t - 0.5) * 2;
291
+ const qu = 1 - qt;
292
+ qx = qu * qu * mx + 2 * qu * qt * c1x + qt * qt * p3x;
293
+ qy = qu * qu * my + 2 * qu * qt * c1y + qt * qt * p3y;
294
+ }
295
+ const dx = cx - qx, dy = cy - qy;
296
+ const e2 = dx * dx + dy * dy;
297
+ if (e2 > maxErr2)
298
+ maxErr2 = e2;
299
+ }
300
+ if (maxErr2 <= tol2 || depth <= 0) {
301
+ out.push({ type: 1, contourId, p0: new Vec2(p0x, p0y), p1: new Vec2(c0x, c0y), p2: new Vec2(mx, my) }, { type: 1, contourId, p0: new Vec2(mx, my), p1: new Vec2(c1x, c1y), p2: new Vec2(p3x, p3y) });
302
+ return;
303
+ }
304
+ // De Casteljau split at t=0.5, pure scalar
305
+ const p01x = (p0x + p1x) * 0.5;
306
+ const p01y = (p0y + p1y) * 0.5;
307
+ const p12x = (p1x + p2x) * 0.5;
308
+ const p12y = (p1y + p2y) * 0.5;
309
+ const p23x = (p2x + p3x) * 0.5;
310
+ const p23y = (p2y + p3y) * 0.5;
311
+ const p012x = (p01x + p12x) * 0.5;
312
+ const p012y = (p01y + p12y) * 0.5;
313
+ const p123x = (p12x + p23x) * 0.5;
314
+ const p123y = (p12y + p23y) * 0.5;
315
+ const p0123x = (p012x + p123x) * 0.5;
316
+ const p0123y = (p012y + p123y) * 0.5;
317
+ cubicToQuadraticsAdaptive(contourId, p0x, p0y, p01x, p01y, p012x, p012y, p0123x, p0123y, tol2, depth - 1, out);
318
+ cubicToQuadraticsAdaptive(contourId, p0123x, p0123y, p123x, p123y, p23x, p23y, p3x, p3y, tol2, depth - 1, out);
319
+ }
320
+ function toQuadratics(segments) {
321
+ const out = [];
322
+ // tol=0.25 font units; pre-squared for the scalar subdivision
323
+ const tol2 = 0.25 * 0.25;
324
+ const maxDepth = 4;
325
+ for (let i = 0; i < segments.length; i++) {
326
+ const s = segments[i];
327
+ const type = s.type;
328
+ if (type === 0) {
329
+ const p0 = s.p0;
330
+ const p2 = s.p1;
331
+ out.push({
332
+ type: 1,
333
+ contourId: s.contourId,
334
+ p0,
335
+ p1: new Vec2((p0.x + p2.x) * 0.5, (p0.y + p2.y) * 0.5),
336
+ p2
337
+ });
338
+ continue;
339
+ }
340
+ if (type === 1) {
341
+ out.push({
342
+ type: 1,
343
+ contourId: s.contourId,
344
+ p0: s.p0,
345
+ p1: s.p1,
346
+ p2: s.p2
347
+ });
348
+ continue;
349
+ }
350
+ cubicToQuadraticsAdaptive(s.contourId, s.p0.x, s.p0.y, s.p1.x, s.p1.y, s.p2.x, s.p2.y, s.p3.x, s.p3.y, tol2, maxDepth, out);
351
+ }
352
+ return out;
353
+ }
354
+ // Packs glyph outlines into GPU-friendly textures and instance
355
+ // attributes for vector rendering without tessellation
356
+ class GlyphVectorGeometryBuilder {
357
+ constructor(loadedFont, cache = sharedCaches.globalOutlineCache) {
358
+ this.fontId = 'default';
359
+ this.cacheKeyPrefix = 'default';
360
+ this.emptyGlyphs = new Set();
361
+ this.loadedFont = loadedFont;
362
+ this.outlineCache = cache;
363
+ this.collector = new GlyphOutlineCollector();
364
+ this.drawCallbacks = DrawCallbacks.getSharedDrawCallbackHandler(this.loadedFont);
365
+ this.drawCallbacks.createDrawFuncs(this.loadedFont, this.collector);
366
+ }
367
+ setFontId(fontId) {
368
+ this.fontId = fontId;
369
+ this.cacheKeyPrefix = `${this.fontId}__outline`;
370
+ }
371
+ clearCache() {
372
+ this.outlineCache.clear();
373
+ this.emptyGlyphs.clear();
374
+ }
375
+ getCacheStats() {
376
+ return this.outlineCache.getStats();
377
+ }
378
+ collectUniqueGlyphIds(clustersByLine) {
379
+ const ids = [];
380
+ const seen = new Set();
381
+ for (const line of clustersByLine) {
382
+ for (const cluster of line) {
383
+ for (const g of cluster.glyphs) {
384
+ if (!seen.has(g.g)) {
385
+ seen.add(g.g);
386
+ ids.push(g.g);
387
+ }
388
+ }
389
+ }
390
+ }
391
+ return ids;
392
+ }
393
+ computePlaneBounds(glyphInfos) {
394
+ if (glyphInfos.length === 0) {
395
+ return { min: { x: 0, y: 0, z: 0 }, max: { x: 0, y: 0, z: 0 } };
396
+ }
397
+ let minX = Infinity, minY = Infinity;
398
+ let maxX = -Infinity, maxY = -Infinity;
399
+ for (const g of glyphInfos) {
400
+ if (g.bounds.min.x < minX)
401
+ minX = g.bounds.min.x;
402
+ if (g.bounds.min.y < minY)
403
+ minY = g.bounds.min.y;
404
+ if (g.bounds.max.x > maxX)
405
+ maxX = g.bounds.max.x;
406
+ if (g.bounds.max.y > maxY)
407
+ maxY = g.bounds.max.y;
408
+ }
409
+ return { min: { x: minX, y: minY, z: 0 }, max: { x: maxX, y: maxY, z: 0 } };
410
+ }
411
+ buildForLoopBlinn(clustersByLine, scale) {
412
+ const uniqueGlyphIds = this.collectUniqueGlyphIds(clustersByLine);
413
+ const scaledSegsByGlyph = new Map();
414
+ const glyphBoundsByGlyph = new Map();
415
+ for (const glyphId of uniqueGlyphIds) {
416
+ const outline = this.getOutlineForGlyph(glyphId);
417
+ const quadSegs = toQuadratics(outline.segments);
418
+ const scaledSegs = [];
419
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
420
+ for (const seg of quadSegs) {
421
+ const p0x = seg.p0.x * scale, p0y = seg.p0.y * scale;
422
+ const p1x = seg.p1.x * scale, p1y = seg.p1.y * scale;
423
+ const p2x = seg.p2.x * scale, p2y = seg.p2.y * scale;
424
+ scaledSegs.push({ p0x, p0y, p1x, p1y, p2x, p2y });
425
+ if (p0x < minX)
426
+ minX = p0x;
427
+ if (p0y < minY)
428
+ minY = p0y;
429
+ if (p0x > maxX)
430
+ maxX = p0x;
431
+ if (p0y > maxY)
432
+ maxY = p0y;
433
+ if (p1x < minX)
434
+ minX = p1x;
435
+ if (p1y < minY)
436
+ minY = p1y;
437
+ if (p1x > maxX)
438
+ maxX = p1x;
439
+ if (p1y > maxY)
440
+ maxY = p1y;
441
+ if (p2x < minX)
442
+ minX = p2x;
443
+ if (p2y < minY)
444
+ minY = p2y;
445
+ if (p2x > maxX)
446
+ maxX = p2x;
447
+ if (p2y > maxY)
448
+ maxY = p2y;
449
+ }
450
+ scaledSegsByGlyph.set(glyphId, scaledSegs);
451
+ glyphBoundsByGlyph.set(glyphId, scaledSegs.length > 0
452
+ ? { minX, minY, maxX, maxY }
453
+ : { minX: 0, minY: 0, maxX: 0, maxY: 0 });
454
+ }
455
+ const loopBlinnGlyphs = [];
456
+ const glyphInfos = [];
457
+ for (const line of clustersByLine) {
458
+ for (const cluster of line) {
459
+ for (const g of cluster.glyphs) {
460
+ const segments = scaledSegsByGlyph.get(g.g);
461
+ if (!segments || segments.length === 0)
462
+ continue;
463
+ const bounds = glyphBoundsByGlyph.get(g.g);
464
+ const px = (cluster.position.x + (g.x ?? 0)) * scale;
465
+ const py = (cluster.position.y + (g.y ?? 0)) * scale;
466
+ loopBlinnGlyphs.push({
467
+ offsetX: px,
468
+ offsetY: py,
469
+ segments,
470
+ bounds,
471
+ lineIndex: g.lineIndex,
472
+ baselineY: (cluster.position.y + (g.y ?? 0)) * scale
473
+ });
474
+ glyphInfos.push({
475
+ textIndex: g.absoluteTextIndex,
476
+ lineIndex: g.lineIndex,
477
+ vertexStart: 0,
478
+ vertexCount: 0,
479
+ segmentStart: 0,
480
+ segmentCount: segments.length,
481
+ bounds: {
482
+ min: { x: px + bounds.minX, y: py + bounds.minY, z: 0 },
483
+ max: { x: px + bounds.maxX, y: py + bounds.maxY, z: 0 }
484
+ }
485
+ });
486
+ }
487
+ }
488
+ }
489
+ const planeBounds = this.computePlaneBounds(glyphInfos);
490
+ return {
491
+ loopBlinnInput: { glyphs: loopBlinnGlyphs, planeBounds },
492
+ glyphs: glyphInfos
493
+ };
494
+ }
495
+ getOutlineForGlyph(glyphId) {
496
+ if (this.emptyGlyphs.has(glyphId)) {
497
+ return {
498
+ glyphId,
499
+ textIndex: 0,
500
+ segments: [],
501
+ bounds: { min: { x: 0, y: 0 }, max: { x: 0, y: 0 } }
502
+ };
503
+ }
504
+ const key = `${this.cacheKeyPrefix}_${glyphId}`;
505
+ const cached = this.outlineCache.get(key);
506
+ if (cached)
507
+ return cached;
508
+ this.drawCallbacks.setCollector(this.collector);
509
+ this.collector.reset();
510
+ this.collector.beginGlyph(glyphId, 0);
511
+ this.loadedFont.module.exports.hb_font_draw_glyph(this.loadedFont.font.ptr, glyphId, this.drawCallbacks.getDrawFuncsPtr(), 0);
512
+ this.collector.finishGlyph();
513
+ const collected = this.collector.getCollectedGlyphs()[0];
514
+ const outline = collected ?? {
515
+ glyphId,
516
+ textIndex: 0,
517
+ segments: [],
518
+ bounds: { min: { x: 0, y: 0 }, max: { x: 0, y: 0 } }
519
+ };
520
+ if (outline.segments.length === 0) {
521
+ this.emptyGlyphs.add(glyphId);
522
+ }
523
+ this.outlineCache.set(key, outline);
524
+ return outline;
525
+ }
526
+ writeSegmentToTexture(tex, texelIndex, a, b, c, d) {
527
+ const i = texelIndex * 4;
528
+ tex.data[i] = a;
529
+ tex.data[i + 1] = b;
530
+ tex.data[i + 2] = c;
531
+ tex.data[i + 3] = d;
532
+ }
533
+ segmentAABB(seg) {
534
+ let minX = seg.p0.x;
535
+ let minY = seg.p0.y;
536
+ let maxX = seg.p0.x;
537
+ let maxY = seg.p0.y;
538
+ const p1 = seg.p1;
539
+ if (p1.x < minX)
540
+ minX = p1.x;
541
+ if (p1.y < minY)
542
+ minY = p1.y;
543
+ if (p1.x > maxX)
544
+ maxX = p1.x;
545
+ if (p1.y > maxY)
546
+ maxY = p1.y;
547
+ const p2 = seg.p2;
548
+ if (p2) {
549
+ if (p2.x < minX)
550
+ minX = p2.x;
551
+ if (p2.y < minY)
552
+ minY = p2.y;
553
+ if (p2.x > maxX)
554
+ maxX = p2.x;
555
+ if (p2.y > maxY)
556
+ maxY = p2.y;
557
+ }
558
+ const p3 = seg.p3;
559
+ if (p3) {
560
+ if (p3.x < minX)
561
+ minX = p3.x;
562
+ if (p3.y < minY)
563
+ minY = p3.y;
564
+ if (p3.x > maxX)
565
+ maxX = p3.x;
566
+ if (p3.y > maxY)
567
+ maxY = p3.y;
568
+ }
569
+ return { minX, minY, maxX, maxY };
570
+ }
571
+ buildVectorGeometry(clustersByLine, scale, segmentTextureWidth = 1024, bandCount = 0, tileCountX = 0, tileCountY = 0) {
572
+ // Base quad: positions in [-1,1] with UV in [0,1]
573
+ const quadVertices = new Float32Array([
574
+ -1, -1, 0, 0,
575
+ -1, 1, 0, 1,
576
+ 1, 1, 1, 1,
577
+ 1, -1, 1, 0
578
+ ]);
579
+ const quadIndices = new Uint16Array([0, 1, 2, 0, 2, 3]);
580
+ const uniqueGlyphIds = [];
581
+ const seen = new Set();
582
+ for (const line of clustersByLine) {
583
+ for (const cluster of line) {
584
+ for (const g of cluster.glyphs) {
585
+ if (!seen.has(g.g)) {
586
+ seen.add(g.g);
587
+ uniqueGlyphIds.push(g.g);
588
+ }
589
+ }
590
+ }
591
+ }
592
+ const outlinesByGlyph = new Map();
593
+ const rangesByGlyph = new Map();
594
+ const glyphDataIndexByGlyphId = new Map();
595
+ const quadSegsByGlyph = new Map();
596
+ let totalSegments = 0;
597
+ for (let gi = 0; gi < uniqueGlyphIds.length; gi++) {
598
+ const glyphId = uniqueGlyphIds[gi];
599
+ const outline = this.getOutlineForGlyph(glyphId);
600
+ outlinesByGlyph.set(glyphId, outline);
601
+ glyphDataIndexByGlyphId.set(glyphId, gi);
602
+ const quadSegs = toQuadratics(outline.segments);
603
+ quadSegsByGlyph.set(glyphId, quadSegs);
604
+ totalSegments += quadSegs.length;
605
+ }
606
+ // Pack segments into RGBA32F texels: 3 texels per segment (12 floats)
607
+ const segmentTexelsPerSegment = 3;
608
+ const segmentTexelCount = totalSegments * segmentTexelsPerSegment;
609
+ const segmentsTex = createPackedTexture(segmentTexelCount, segmentTextureWidth);
610
+ // Segment bounds: 1 texel per segment (minX,minY,maxX,maxY), conservative hull AABB
611
+ const boundsTex = createPackedTexture(totalSegments, segmentTextureWidth);
612
+ // Keep bounds in CPU arrays for banding/tiling calculations (scaled, local coords)
613
+ const segMinX = new Float32Array(totalSegments);
614
+ const segMaxX = new Float32Array(totalSegments);
615
+ const segMinY = new Float32Array(totalSegments);
616
+ const segMaxY = new Float32Array(totalSegments);
617
+ let segmentCursor = 0;
618
+ for (const glyphId of uniqueGlyphIds) {
619
+ outlinesByGlyph.get(glyphId);
620
+ const start = segmentCursor;
621
+ const quadSegs = quadSegsByGlyph.get(glyphId);
622
+ const count = quadSegs.length;
623
+ rangesByGlyph.set(glyphId, { start, count });
624
+ for (let i = 0; i < count; i++) {
625
+ const seg = quadSegs[i];
626
+ // texel base for this segment
627
+ const tBase = (segmentCursor + i) * segmentTexelsPerSegment;
628
+ // Scale to match the mesh pipeline's output units
629
+ const p0x = seg.p0.x * scale;
630
+ const p0y = seg.p0.y * scale;
631
+ const p1x = seg.p1.x * scale;
632
+ const p1y = seg.p1.y * scale;
633
+ const p2x = seg.p2.x * scale;
634
+ const p2y = seg.p2.y * scale;
635
+ const p3x = 0;
636
+ const p3y = 0;
637
+ // Texel 0: p0.xy, p1.xy
638
+ this.writeSegmentToTexture(segmentsTex, tBase + 0, p0x, p0y, p1x, p1y);
639
+ // Texel 1: p2.xy, p3.xy
640
+ this.writeSegmentToTexture(segmentsTex, tBase + 1, p2x, p2y, p3x, p3y);
641
+ // Texel 2: type, contourId, reserved, reserved
642
+ this.writeSegmentToTexture(segmentsTex, tBase + 2, 1, // quadratic-only
643
+ seg.contourId, 0, 0);
644
+ // Bounds texel (conservative AABB from control points, scaled)
645
+ const aabb = this.segmentAABB(seg);
646
+ const sMinX = aabb.minX * scale;
647
+ const sMaxX = aabb.maxX * scale;
648
+ const sMinY = aabb.minY * scale;
649
+ const sMaxY = aabb.maxY * scale;
650
+ this.writeSegmentToTexture(boundsTex, segmentCursor + i, sMinX, sMinY, sMaxX, sMaxY);
651
+ segMinX[segmentCursor + i] = sMinX;
652
+ segMaxX[segmentCursor + i] = sMaxX;
653
+ segMinY[segmentCursor + i] = sMinY;
654
+ segMaxY[segmentCursor + i] = sMaxY;
655
+ }
656
+ segmentCursor += count;
657
+ }
658
+ // Per-glyph bounds from the packed segments, not the original outline,
659
+ // since cubic->quadratic approximation can slightly overshoot
660
+ const glyphMinX = new Float32Array(uniqueGlyphIds.length);
661
+ const glyphMinY = new Float32Array(uniqueGlyphIds.length);
662
+ const glyphMaxX = new Float32Array(uniqueGlyphIds.length);
663
+ const glyphMaxY = new Float32Array(uniqueGlyphIds.length);
664
+ for (let gi = 0; gi < uniqueGlyphIds.length; gi++) {
665
+ const glyphId = uniqueGlyphIds[gi];
666
+ const range = rangesByGlyph.get(glyphId);
667
+ if (range.count === 0) {
668
+ glyphMinX[gi] = 0;
669
+ glyphMinY[gi] = 0;
670
+ glyphMaxX[gi] = 0;
671
+ glyphMaxY[gi] = 0;
672
+ continue;
673
+ }
674
+ let minX = Infinity;
675
+ let minY = Infinity;
676
+ let maxX = -Infinity;
677
+ let maxY = -Infinity;
678
+ for (let si = 0; si < range.count; si++) {
679
+ const segIndex = range.start + si;
680
+ const x0 = segMinX[segIndex];
681
+ const x1 = segMaxX[segIndex];
682
+ const y0 = segMinY[segIndex];
683
+ const y1 = segMaxY[segIndex];
684
+ if (x0 < minX)
685
+ minX = x0;
686
+ if (y0 < minY)
687
+ minY = y0;
688
+ if (x1 > maxX)
689
+ maxX = x1;
690
+ if (y1 > maxY)
691
+ maxY = y1;
692
+ }
693
+ glyphMinX[gi] = minX;
694
+ glyphMinY[gi] = minY;
695
+ glyphMaxX[gi] = maxX;
696
+ glyphMaxY[gi] = maxY;
697
+ }
698
+ // Y-bands: partition each glyph into horizontal bands so the shader
699
+ // only tests segments overlapping the current fragment's Y range
700
+ const useBands = bandCount > 0;
701
+ const bandRangesTex = useBands
702
+ ? createPackedTexture(uniqueGlyphIds.length * bandCount, segmentTextureWidth)
703
+ : undefined;
704
+ let bandIndexRefsTotal = 0;
705
+ let bandListsByGlyph = [];
706
+ if (useBands) {
707
+ bandListsByGlyph = new Array(uniqueGlyphIds.length);
708
+ for (let gi = 0; gi < uniqueGlyphIds.length; gi++) {
709
+ const glyphId = uniqueGlyphIds[gi];
710
+ const range = rangesByGlyph.get(glyphId);
711
+ const gMinY = glyphMinY[gi];
712
+ const gMaxY = glyphMaxY[gi];
713
+ const height = gMaxY - gMinY;
714
+ const invBandH = height > 1e-8 ? bandCount / height : 0;
715
+ const bands = new Array(bandCount);
716
+ for (let bi = 0; bi < bandCount; bi++)
717
+ bands[bi] = [];
718
+ for (let si = 0; si < range.count; si++) {
719
+ const segIndex = range.start + si;
720
+ const s0 = segMinY[segIndex];
721
+ const s1 = segMaxY[segIndex];
722
+ let b0 = 0;
723
+ let b1 = bandCount - 1;
724
+ if (invBandH > 0) {
725
+ b0 = Math.floor((s0 - gMinY) * invBandH);
726
+ b1 = Math.floor((s1 - gMinY) * invBandH);
727
+ if (b0 < 0)
728
+ b0 = 0;
729
+ if (b1 < 0)
730
+ b1 = 0;
731
+ if (b0 >= bandCount)
732
+ b0 = bandCount - 1;
733
+ if (b1 >= bandCount)
734
+ b1 = bandCount - 1;
735
+ }
736
+ if (b1 < b0) {
737
+ const tmp = b0;
738
+ b0 = b1;
739
+ b1 = tmp;
740
+ }
741
+ for (let bi = b0; bi <= b1; bi++) {
742
+ bands[bi].push(segIndex);
743
+ }
744
+ }
745
+ // Sort by maxX descending so the shader can early-exit leftward
746
+ for (let bi = 0; bi < bandCount; bi++) {
747
+ bands[bi].sort((a, b) => segMaxX[b] - segMaxX[a]);
748
+ }
749
+ bandListsByGlyph[gi] = bands;
750
+ for (let bi = 0; bi < bandCount; bi++) {
751
+ bandIndexRefsTotal += bands[bi].length;
752
+ }
753
+ }
754
+ }
755
+ const bandIndicesTex = useBands
756
+ ? createPackedTexture(bandIndexRefsTotal, segmentTextureWidth)
757
+ : undefined;
758
+ if (useBands && bandRangesTex && bandIndicesTex) {
759
+ let bandIndexCursor = 0;
760
+ for (let gi = 0; gi < uniqueGlyphIds.length; gi++) {
761
+ const bands = bandListsByGlyph[gi];
762
+ for (let bi = 0; bi < bandCount; bi++) {
763
+ const list = bands[bi];
764
+ const start = bandIndexCursor;
765
+ const count = list.length;
766
+ // bandRanges texel: (start, count, 0, 0)
767
+ this.writeSegmentToTexture(bandRangesTex, gi * bandCount + bi, start, count, 0, 0);
768
+ for (let k = 0; k < count; k++) {
769
+ const segIndex = list[k];
770
+ // bandIndices texel.x = segIndex
771
+ this.writeSegmentToTexture(bandIndicesTex, bandIndexCursor++, segIndex, 0, 0, 0);
772
+ }
773
+ }
774
+ }
775
+ }
776
+ // X-bands: same idea along the horizontal axis for vertical ray tests
777
+ const xBandCount = bandCount; // Use same count for both axes
778
+ const xBandRangesTex = useBands
779
+ ? createPackedTexture(uniqueGlyphIds.length * xBandCount, segmentTextureWidth)
780
+ : undefined;
781
+ let xBandIndexRefsTotal = 0;
782
+ let xBandListsByGlyph = [];
783
+ if (useBands) {
784
+ xBandListsByGlyph = new Array(uniqueGlyphIds.length);
785
+ for (let gi = 0; gi < uniqueGlyphIds.length; gi++) {
786
+ const glyphId = uniqueGlyphIds[gi];
787
+ const range = rangesByGlyph.get(glyphId);
788
+ const gMinX = glyphMinX[gi];
789
+ const gMaxX = glyphMaxX[gi];
790
+ const width = gMaxX - gMinX;
791
+ const invBandW = width > 1e-8 ? xBandCount / width : 0;
792
+ const bands = new Array(xBandCount);
793
+ for (let bi = 0; bi < xBandCount; bi++)
794
+ bands[bi] = [];
795
+ for (let si = 0; si < range.count; si++) {
796
+ const segIndex = range.start + si;
797
+ const s0 = segMinX[segIndex];
798
+ const s1 = segMaxX[segIndex];
799
+ let b0 = 0;
800
+ let b1 = xBandCount - 1;
801
+ if (invBandW > 0) {
802
+ b0 = Math.floor((s0 - gMinX) * invBandW);
803
+ b1 = Math.floor((s1 - gMinX) * invBandW);
804
+ if (b0 < 0)
805
+ b0 = 0;
806
+ if (b1 < 0)
807
+ b1 = 0;
808
+ if (b0 >= xBandCount)
809
+ b0 = xBandCount - 1;
810
+ if (b1 >= xBandCount)
811
+ b1 = xBandCount - 1;
812
+ }
813
+ if (b1 < b0) {
814
+ const tmp = b0;
815
+ b0 = b1;
816
+ b1 = tmp;
817
+ }
818
+ for (let bi = b0; bi <= b1; bi++) {
819
+ bands[bi].push(segIndex);
820
+ }
821
+ }
822
+ // Sort by maxY descending so the shader can early-exit downward
823
+ for (let bi = 0; bi < xBandCount; bi++) {
824
+ bands[bi].sort((a, b) => segMaxY[b] - segMaxY[a]);
825
+ }
826
+ xBandListsByGlyph[gi] = bands;
827
+ for (let bi = 0; bi < xBandCount; bi++) {
828
+ xBandIndexRefsTotal += bands[bi].length;
829
+ }
830
+ }
831
+ }
832
+ const xBandIndicesTex = useBands
833
+ ? createPackedTexture(xBandIndexRefsTotal, segmentTextureWidth)
834
+ : undefined;
835
+ if (useBands && xBandRangesTex && xBandIndicesTex) {
836
+ let xBandIndexCursor = 0;
837
+ for (let gi = 0; gi < uniqueGlyphIds.length; gi++) {
838
+ const bands = xBandListsByGlyph[gi];
839
+ for (let bi = 0; bi < xBandCount; bi++) {
840
+ const list = bands[bi];
841
+ const start = xBandIndexCursor;
842
+ const count = list.length;
843
+ // xBandRanges texel: (start, count, 0, 0)
844
+ this.writeSegmentToTexture(xBandRangesTex, gi * xBandCount + bi, start, count, 0, 0);
845
+ for (let k = 0; k < count; k++) {
846
+ const segIndex = list[k];
847
+ // xBandIndices texel.x = segIndex
848
+ this.writeSegmentToTexture(xBandIndicesTex, xBandIndexCursor++, segIndex, 0, 0, 0);
849
+ }
850
+ }
851
+ }
852
+ }
853
+ // Build 2D tile grid (optional acceleration, Pathfinder-style)
854
+ const useTiles = tileCountX > 0 && tileCountY > 0;
855
+ const tileCount = useTiles ? tileCountX * tileCountY : 0;
856
+ const tileRangesTex = useTiles
857
+ ? createPackedTexture(uniqueGlyphIds.length * tileCount, segmentTextureWidth)
858
+ : undefined;
859
+ let tileIndexRefsTotal = 0;
860
+ let tileListsByGlyph = [];
861
+ if (useTiles) {
862
+ tileListsByGlyph = new Array(uniqueGlyphIds.length);
863
+ for (let gi = 0; gi < uniqueGlyphIds.length; gi++) {
864
+ const glyphId = uniqueGlyphIds[gi];
865
+ const range = rangesByGlyph.get(glyphId);
866
+ const gMinX = glyphMinX[gi];
867
+ const gMaxX = glyphMaxX[gi];
868
+ const gMinY = glyphMinY[gi];
869
+ const gMaxY = glyphMaxY[gi];
870
+ const width = gMaxX - gMinX;
871
+ const height = gMaxY - gMinY;
872
+ const invTileW = width > 1e-8 ? tileCountX / width : 0;
873
+ const invTileH = height > 1e-8 ? tileCountY / height : 0;
874
+ const tiles = new Array(tileCount);
875
+ for (let ti = 0; ti < tileCount; ti++)
876
+ tiles[ti] = [];
877
+ for (let si = 0; si < range.count; si++) {
878
+ const segIndex = range.start + si;
879
+ const x0 = segMinX[segIndex];
880
+ const x1 = segMaxX[segIndex];
881
+ const y0 = segMinY[segIndex];
882
+ const y1 = segMaxY[segIndex];
883
+ let tx0 = 0;
884
+ let tx1 = tileCountX - 1;
885
+ let ty0 = 0;
886
+ let ty1 = tileCountY - 1;
887
+ if (invTileW > 0) {
888
+ tx0 = Math.floor((x0 - gMinX) * invTileW);
889
+ tx1 = Math.floor((x1 - gMinX) * invTileW);
890
+ if (tx0 < 0)
891
+ tx0 = 0;
892
+ if (tx1 < 0)
893
+ tx1 = 0;
894
+ if (tx0 >= tileCountX)
895
+ tx0 = tileCountX - 1;
896
+ if (tx1 >= tileCountX)
897
+ tx1 = tileCountX - 1;
898
+ }
899
+ if (invTileH > 0) {
900
+ ty0 = Math.floor((y0 - gMinY) * invTileH);
901
+ ty1 = Math.floor((y1 - gMinY) * invTileH);
902
+ if (ty0 < 0)
903
+ ty0 = 0;
904
+ if (ty1 < 0)
905
+ ty1 = 0;
906
+ if (ty0 >= tileCountY)
907
+ ty0 = tileCountY - 1;
908
+ if (ty1 >= tileCountY)
909
+ ty1 = tileCountY - 1;
910
+ }
911
+ if (tx1 < tx0) {
912
+ const t = tx0;
913
+ tx0 = tx1;
914
+ tx1 = t;
915
+ }
916
+ if (ty1 < ty0) {
917
+ const t = ty0;
918
+ ty0 = ty1;
919
+ ty1 = t;
920
+ }
921
+ for (let ty = ty0; ty <= ty1; ty++) {
922
+ const rowBase = ty * tileCountX;
923
+ for (let tx = tx0; tx <= tx1; tx++) {
924
+ tiles[rowBase + tx].push(segIndex);
925
+ }
926
+ }
927
+ }
928
+ tileListsByGlyph[gi] = tiles;
929
+ for (let ti = 0; ti < tileCount; ti++) {
930
+ tileIndexRefsTotal += tiles[ti].length;
931
+ }
932
+ }
933
+ }
934
+ const tileIndicesTex = useTiles
935
+ ? createPackedTexture(tileIndexRefsTotal, segmentTextureWidth)
936
+ : undefined;
937
+ if (useTiles && tileRangesTex && tileIndicesTex) {
938
+ let tileIndexCursor = 0;
939
+ for (let gi = 0; gi < uniqueGlyphIds.length; gi++) {
940
+ const tiles = tileListsByGlyph[gi];
941
+ for (let ti = 0; ti < tileCount; ti++) {
942
+ const list = tiles[ti];
943
+ const start = tileIndexCursor;
944
+ const count = list.length;
945
+ // tileRanges texel: (start, count, 0, 0)
946
+ this.writeSegmentToTexture(tileRangesTex, gi * tileCount + ti, start, count, 0, 0);
947
+ for (let k = 0; k < count; k++) {
948
+ const segIndex = list[k];
949
+ // tileIndices texel.x = segIndex
950
+ this.writeSegmentToTexture(tileIndicesTex, tileIndexCursor++, segIndex, 0, 0, 0);
951
+ }
952
+ }
953
+ }
954
+ }
955
+ let glyphInstanceCount = 0;
956
+ for (const line of clustersByLine) {
957
+ for (const cluster of line) {
958
+ for (const g of cluster.glyphs) {
959
+ const range = rangesByGlyph.get(g.g);
960
+ if (range && range.count > 0)
961
+ glyphInstanceCount++;
962
+ }
963
+ }
964
+ }
965
+ const instances = {
966
+ position: new Float32Array(glyphInstanceCount * 3),
967
+ bounds: new Float32Array(glyphInstanceCount * 4),
968
+ segmentRange: new Uint32Array(glyphInstanceCount * 2),
969
+ glyphDataIndex: new Uint32Array(glyphInstanceCount),
970
+ glyphIndex: new Uint32Array(glyphInstanceCount),
971
+ textIndex: new Uint32Array(glyphInstanceCount),
972
+ lineIndex: new Uint32Array(glyphInstanceCount)
973
+ };
974
+ const glyphInfos = [];
975
+ const planeBounds = {
976
+ min: { x: Infinity, y: Infinity, z: 0 },
977
+ max: { x: -Infinity, y: -Infinity, z: 0 }
978
+ };
979
+ let instanceCursor = 0;
980
+ for (const line of clustersByLine) {
981
+ for (const cluster of line) {
982
+ const clusterX = cluster.position.x;
983
+ const clusterY = cluster.position.y;
984
+ const clusterZ = cluster.position.z;
985
+ for (const g of cluster.glyphs) {
986
+ const range = rangesByGlyph.get(g.g);
987
+ if (!range || range.count === 0)
988
+ continue;
989
+ const px = (clusterX + (g.x ?? 0)) * scale;
990
+ const py = (clusterY + (g.y ?? 0)) * scale;
991
+ const pz = clusterZ * scale;
992
+ const gi = glyphDataIndexByGlyphId.get(g.g) ?? 0;
993
+ const bMinX = glyphMinX[gi];
994
+ const bMinY = glyphMinY[gi];
995
+ const bMaxX = glyphMaxX[gi];
996
+ const bMaxY = glyphMaxY[gi];
997
+ // Instance attributes
998
+ instances.position[instanceCursor * 3 + 0] = px;
999
+ instances.position[instanceCursor * 3 + 1] = py;
1000
+ instances.position[instanceCursor * 3 + 2] = pz;
1001
+ instances.bounds[instanceCursor * 4 + 0] = bMinX;
1002
+ instances.bounds[instanceCursor * 4 + 1] = bMinY;
1003
+ instances.bounds[instanceCursor * 4 + 2] = bMaxX;
1004
+ instances.bounds[instanceCursor * 4 + 3] = bMaxY;
1005
+ instances.segmentRange[instanceCursor * 2 + 0] = range.start;
1006
+ instances.segmentRange[instanceCursor * 2 + 1] = range.count;
1007
+ instances.glyphDataIndex[instanceCursor] =
1008
+ glyphDataIndexByGlyphId.get(g.g) ?? 0;
1009
+ instances.glyphIndex[instanceCursor] = g.g;
1010
+ instances.textIndex[instanceCursor] = g.absoluteTextIndex;
1011
+ instances.lineIndex[instanceCursor] = g.lineIndex;
1012
+ // Glyph info for querying/selection (world bounds)
1013
+ const glyphInfo = {
1014
+ textIndex: g.absoluteTextIndex,
1015
+ lineIndex: g.lineIndex,
1016
+ vertexStart: 0,
1017
+ vertexCount: 0,
1018
+ segmentStart: range.start,
1019
+ segmentCount: range.count,
1020
+ bounds: {
1021
+ min: { x: px + bMinX, y: py + bMinY, z: pz },
1022
+ max: { x: px + bMaxX, y: py + bMaxY, z: pz }
1023
+ }
1024
+ };
1025
+ glyphInfos.push(glyphInfo);
1026
+ if (glyphInfo.bounds.min.x < planeBounds.min.x)
1027
+ planeBounds.min.x = glyphInfo.bounds.min.x;
1028
+ if (glyphInfo.bounds.min.y < planeBounds.min.y)
1029
+ planeBounds.min.y = glyphInfo.bounds.min.y;
1030
+ if (glyphInfo.bounds.min.z < planeBounds.min.z)
1031
+ planeBounds.min.z = glyphInfo.bounds.min.z;
1032
+ if (glyphInfo.bounds.max.x > planeBounds.max.x)
1033
+ planeBounds.max.x = glyphInfo.bounds.max.x;
1034
+ if (glyphInfo.bounds.max.y > planeBounds.max.y)
1035
+ planeBounds.max.y = glyphInfo.bounds.max.y;
1036
+ if (glyphInfo.bounds.max.z > planeBounds.max.z)
1037
+ planeBounds.max.z = glyphInfo.bounds.max.z;
1038
+ instanceCursor++;
1039
+ }
1040
+ }
1041
+ }
1042
+ if (glyphInfos.length === 0) {
1043
+ planeBounds.min.x = 0;
1044
+ planeBounds.min.y = 0;
1045
+ planeBounds.min.z = 0;
1046
+ planeBounds.max.x = 0;
1047
+ planeBounds.max.y = 0;
1048
+ planeBounds.max.z = 0;
1049
+ }
1050
+ return {
1051
+ quadVertices,
1052
+ quadIndices,
1053
+ instances,
1054
+ segmentTexelsPerSegment,
1055
+ segments: segmentsTex,
1056
+ segmentBounds: boundsTex,
1057
+ bandCount: useBands ? bandCount : undefined,
1058
+ bandRanges: bandRangesTex,
1059
+ bandIndices: bandIndicesTex,
1060
+ xBandCount: useBands ? xBandCount : undefined,
1061
+ xBandRanges: xBandRangesTex,
1062
+ xBandIndices: xBandIndicesTex,
1063
+ tileCountX: useTiles ? tileCountX : undefined,
1064
+ tileCountY: useTiles ? tileCountY : undefined,
1065
+ tileRanges: tileRangesTex,
1066
+ tileIndices: tileIndicesTex,
1067
+ glyphs: glyphInfos,
1068
+ planeBounds
1069
+ };
1070
+ }
1071
+ }
1072
+
1073
+ const CONTOUR_EPSILON = 0.001;
1074
+ const CURVE_LINEARITY_EPSILON = 1e-5;
1075
+ function nearlyEqual(a, b, epsilon) {
1076
+ return Math.abs(a - b) < epsilon;
1077
+ }
1078
+ function extractContours(segments) {
1079
+ const contours = [];
1080
+ let contourVertices = [];
1081
+ let contourSegments = [];
1082
+ const pushCurrentContour = () => {
1083
+ if (contourVertices.length < 3) {
1084
+ contourVertices = [];
1085
+ contourSegments = [];
1086
+ return;
1087
+ }
1088
+ const first = contourVertices[0];
1089
+ const last = contourVertices[contourVertices.length - 1];
1090
+ if (nearlyEqual(first.x, last.x, CONTOUR_EPSILON) &&
1091
+ nearlyEqual(first.y, last.y, CONTOUR_EPSILON)) {
1092
+ contourVertices.pop();
1093
+ }
1094
+ if (contourVertices.length >= 3) {
1095
+ contours.push({
1096
+ vertices: contourVertices,
1097
+ segments: contourSegments
1098
+ });
1099
+ }
1100
+ contourVertices = [];
1101
+ contourSegments = [];
1102
+ };
1103
+ for (const segment of segments) {
1104
+ if (contourVertices.length === 0) {
1105
+ contourVertices.push({ x: segment.p0x, y: segment.p0y }, { x: segment.p2x, y: segment.p2y });
1106
+ contourSegments.push(segment);
1107
+ continue;
1108
+ }
1109
+ const previousEnd = contourVertices[contourVertices.length - 1];
1110
+ if (nearlyEqual(segment.p0x, previousEnd.x, CONTOUR_EPSILON) &&
1111
+ nearlyEqual(segment.p0y, previousEnd.y, CONTOUR_EPSILON)) {
1112
+ contourVertices.push({ x: segment.p2x, y: segment.p2y });
1113
+ contourSegments.push(segment);
1114
+ continue;
1115
+ }
1116
+ pushCurrentContour();
1117
+ contourVertices.push({ x: segment.p0x, y: segment.p0y }, { x: segment.p2x, y: segment.p2y });
1118
+ contourSegments.push(segment);
1119
+ }
1120
+ pushCurrentContour();
1121
+ return contours;
1122
+ }
1123
+ function triangulateContourFan(vertices, offsetX, offsetY, positions, indices) {
1124
+ const baseIndex = positions.length / 3;
1125
+ for (const vertex of vertices) {
1126
+ positions.push(vertex.x + offsetX, vertex.y + offsetY, 0);
1127
+ }
1128
+ if (vertices.length < 3)
1129
+ return 0;
1130
+ for (let index = 1; index < vertices.length - 1; index++) {
1131
+ indices.push(baseIndex, baseIndex + index, baseIndex + index + 1);
1132
+ }
1133
+ return vertices.length - 2;
1134
+ }
1135
+ function shouldSkipCurveSegment(segment) {
1136
+ const dx = segment.p2x - segment.p0x;
1137
+ const dy = segment.p2y - segment.p0y;
1138
+ const lenSq = dx * dx + dy * dy;
1139
+ if (lenSq <= 0)
1140
+ return true;
1141
+ const cross = (segment.p1x - segment.p0x) * dy - (segment.p1y - segment.p0y) * dx;
1142
+ return Math.abs(cross) < CURVE_LINEARITY_EPSILON * lenSq;
1143
+ }
1144
+ function fillGlyphAttrs(center, idx, progress, lineIdx, baselineY, vertCount, gc, gi, gp, gl, gb) {
1145
+ for (let v = 0; v < vertCount; v++) {
1146
+ gc.push(center[0], center[1], center[2]);
1147
+ gi.push(idx);
1148
+ gp.push(progress);
1149
+ gl.push(lineIdx);
1150
+ gb.push(baselineY);
1151
+ }
1152
+ }
1153
+ function packAttrs(gc, gi, gp, gl, gb) {
1154
+ return {
1155
+ glyphCenter: new Float32Array(gc),
1156
+ glyphIndex: new Float32Array(gi),
1157
+ glyphProgress: new Float32Array(gp),
1158
+ glyphLineIndex: new Float32Array(gl),
1159
+ glyphBaselineY: new Float32Array(gb)
1160
+ };
1161
+ }
1162
+ function buildVectorGeometry(input) {
1163
+ const interiorPositions = [];
1164
+ const interiorIndices = [];
1165
+ const curvePositions = [];
1166
+ const iGC = [], iGI = [], iGP = [], iGL = [], iGB = [];
1167
+ const cGC = [], cGI = [], cGP = [], cGL = [], cGB = [];
1168
+ const fGC = [], fGI = [], fGP = [], fGL = [], fGB = [];
1169
+ const glyphCount = input.glyphs.length;
1170
+ let contourCount = 0;
1171
+ let interiorTriangleCount = 0;
1172
+ let curveTriangleCount = 0;
1173
+ const glyphRanges = [];
1174
+ const fillPositions = new Float32Array(glyphCount * 12);
1175
+ const fillIndices = new Uint32Array(glyphCount * 6);
1176
+ for (let i = 0; i < glyphCount; i++) {
1177
+ const glyph = input.glyphs[i];
1178
+ const { minX, minY, maxX, maxY } = glyph.bounds;
1179
+ const cx = glyph.offsetX + (minX + maxX) * 0.5;
1180
+ const cy = glyph.offsetY + (minY + maxY) * 0.5;
1181
+ const center = [cx, cy, 0];
1182
+ const progress = glyphCount > 1 ? i / (glyphCount - 1) : 0;
1183
+ const intIdxStart = interiorIndices.length;
1184
+ const curveVertStart = curvePositions.length / 3;
1185
+ const contours = extractContours(glyph.segments);
1186
+ contourCount += contours.length;
1187
+ for (const contour of contours) {
1188
+ const intVertsBefore = interiorPositions.length / 3;
1189
+ interiorTriangleCount += triangulateContourFan(contour.vertices, glyph.offsetX, glyph.offsetY, interiorPositions, interiorIndices);
1190
+ const intVertsAfter = interiorPositions.length / 3;
1191
+ fillGlyphAttrs(center, i, progress, glyph.lineIndex, glyph.baselineY, intVertsAfter - intVertsBefore, iGC, iGI, iGP, iGL, iGB);
1192
+ for (const segment of contour.segments) {
1193
+ if (shouldSkipCurveSegment(segment))
1194
+ continue;
1195
+ curvePositions.push(glyph.offsetX + segment.p0x, glyph.offsetY + segment.p0y, 0, glyph.offsetX + segment.p1x, glyph.offsetY + segment.p1y, 0, glyph.offsetX + segment.p2x, glyph.offsetY + segment.p2y, 0);
1196
+ curveTriangleCount++;
1197
+ fillGlyphAttrs(center, i, progress, glyph.lineIndex, glyph.baselineY, 3, cGC, cGI, cGP, cGL, cGB);
1198
+ }
1199
+ }
1200
+ glyphRanges.push({
1201
+ interiorIndexStart: intIdxStart,
1202
+ interiorIndexCount: interiorIndices.length - intIdxStart,
1203
+ curveVertexStart: curveVertStart,
1204
+ curveVertexCount: (curvePositions.length / 3) - curveVertStart
1205
+ });
1206
+ const fp = i * 12;
1207
+ fillPositions[fp] = glyph.offsetX + minX;
1208
+ fillPositions[fp + 1] = glyph.offsetY + minY;
1209
+ fillPositions[fp + 2] = 0;
1210
+ fillPositions[fp + 3] = glyph.offsetX + maxX;
1211
+ fillPositions[fp + 4] = glyph.offsetY + minY;
1212
+ fillPositions[fp + 5] = 0;
1213
+ fillPositions[fp + 6] = glyph.offsetX + maxX;
1214
+ fillPositions[fp + 7] = glyph.offsetY + maxY;
1215
+ fillPositions[fp + 8] = 0;
1216
+ fillPositions[fp + 9] = glyph.offsetX + minX;
1217
+ fillPositions[fp + 10] = glyph.offsetY + maxY;
1218
+ fillPositions[fp + 11] = 0;
1219
+ fillGlyphAttrs(center, i, progress, glyph.lineIndex, glyph.baselineY, 4, fGC, fGI, fGP, fGL, fGB);
1220
+ const fi = i * 6;
1221
+ const fv = i * 4;
1222
+ fillIndices[fi] = fv;
1223
+ fillIndices[fi + 1] = fv + 1;
1224
+ fillIndices[fi + 2] = fv + 2;
1225
+ fillIndices[fi + 3] = fv;
1226
+ fillIndices[fi + 4] = fv + 2;
1227
+ fillIndices[fi + 5] = fv + 3;
1228
+ }
1229
+ return {
1230
+ interiorPositions: new Float32Array(interiorPositions),
1231
+ interiorIndices: new Uint32Array(interiorIndices),
1232
+ curvePositions: new Float32Array(curvePositions),
1233
+ fillPositions,
1234
+ fillIndices,
1235
+ glyphRanges,
1236
+ interiorGlyphAttrs: packAttrs(iGC, iGI, iGP, iGL, iGB),
1237
+ curveGlyphAttrs: packAttrs(cGC, cGI, cGP, cGL, cGB),
1238
+ fillGlyphAttrs: packAttrs(fGC, fGI, fGP, fGL, fGB),
1239
+ planeBounds: input.planeBounds,
1240
+ stats: {
1241
+ glyphCount,
1242
+ contourCount,
1243
+ interiorTriangleCount,
1244
+ curveTriangleCount
1245
+ }
1246
+ };
1247
+ }
1248
+
1249
+ function buildVectorResult(layoutHandle, vectorBuilder, options) {
1250
+ const scale = layoutHandle.layoutData.pixelsPerFontUnit;
1251
+ const { loopBlinnInput, glyphs } = vectorBuilder.buildForLoopBlinn(layoutHandle.clustersByLine, scale);
1252
+ const geometryData = buildVectorGeometry(loopBlinnInput);
1253
+ let cachedQuery = null;
1254
+ const update = async (newOptions) => {
1255
+ const mergedOptions = { ...options };
1256
+ for (const key in newOptions) {
1257
+ const value = newOptions[key];
1258
+ if (value !== undefined) {
1259
+ mergedOptions[key] = value;
1260
+ }
1261
+ }
1262
+ if (newOptions.font !== undefined ||
1263
+ newOptions.fontVariations !== undefined ||
1264
+ newOptions.fontFeatures !== undefined) {
1265
+ const newLayout = await layoutHandle.update(mergedOptions);
1266
+ const newBuilder = new GlyphVectorGeometryBuilder(newLayout.loadedFont, sharedCaches.globalOutlineCache);
1267
+ newBuilder.setFontId(newLayout.fontId);
1268
+ layoutHandle = newLayout;
1269
+ options = mergedOptions;
1270
+ return buildVectorResult(layoutHandle, newBuilder, options);
1271
+ }
1272
+ const newLayout = await layoutHandle.update(mergedOptions);
1273
+ layoutHandle = newLayout;
1274
+ options = mergedOptions;
1275
+ return buildVectorResult(layoutHandle, vectorBuilder, options);
1276
+ };
1277
+ return {
1278
+ glyphs,
1279
+ geometryData,
1280
+ query: (queryOptions) => {
1281
+ if (!cachedQuery) {
1282
+ cachedQuery = new TextRangeQuery.TextRangeQuery(options.text, glyphs);
1283
+ }
1284
+ return cachedQuery.execute(queryOptions);
1285
+ },
1286
+ getLoadedFont: () => layoutHandle.getLoadedFont(),
1287
+ measureTextWidth: (text, letterSpacing) => layoutHandle.measureTextWidth(text, letterSpacing),
1288
+ update,
1289
+ dispose: () => layoutHandle.dispose()
1290
+ };
1291
+ }
1292
+ class Text {
1293
+ static { this.setHarfBuzzPath = Text$1.Text.setHarfBuzzPath; }
1294
+ static { this.setHarfBuzzBuffer = Text$1.Text.setHarfBuzzBuffer; }
1295
+ static { this.init = Text$1.Text.init; }
1296
+ static { this.registerPattern = Text$1.Text.registerPattern; }
1297
+ static { this.preloadPatterns = Text$1.Text.preloadPatterns; }
1298
+ static { this.setMaxFontCacheMemoryMB = Text$1.Text.setMaxFontCacheMemoryMB; }
1299
+ static { this.enableWoff2 = Text$1.Text.enableWoff2; }
1300
+ static async create(options) {
1301
+ const layoutHandle = await Text$1.Text.create(options);
1302
+ const vectorBuilder = new GlyphVectorGeometryBuilder(layoutHandle.loadedFont, sharedCaches.globalOutlineCache);
1303
+ vectorBuilder.setFontId(layoutHandle.fontId);
1304
+ return buildVectorResult(layoutHandle, vectorBuilder, options);
1305
+ }
1306
+ }
1307
+
1308
+ exports.Text = Text;
1309
+ exports.buildVectorGeometry = buildVectorGeometry;
1310
+ exports.extractContours = extractContours;