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.
@@ -1,1074 +1,6 @@
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
- import * as THREE from 'three';
6
- import { Fn, uv, dFdx, dFdy, float, sqrt, clamp, Discard, vec4 } from 'three/tsl';
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 = 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 = 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
- }
1
+ import { Text as Text$1 } from './core.js';
2
+ import { createVectorMeshes } from './loopBlinnTSL.js';
3
+ export { createVectorMeshes, loopBlinnFragment } from './loopBlinnTSL.js';
1072
4
 
1073
5
  const CONTOUR_EPSILON = 0.001;
1074
6
  const CURVE_LINEARITY_EPSILON = 1e-5;
@@ -1246,173 +178,32 @@ function buildVectorGeometry(input) {
1246
178
  };
1247
179
  }
1248
180
 
1249
- // Loop-Blinn TSL adapter for Three.js WebGPURenderer.
1250
- // Creates meshes with TSL node materials for Loop-Blinn curve evaluation
1251
- // and Kokojima stencil fill. Works on WebGL (r170+) and WebGPU (r182+).
1252
- // Requires peer dependencies: three, three/tsl
1253
- // @ts-ignore - three is a peer dependency
1254
- // TSL fragment node: evaluates u^2 - v = 0 per fragment with
1255
- // screen-space derivative antialiasing. Discards outside fragments.
1256
- // UV convention per triangle: p0=(0,0), p1=(0.5,0), p2=(1,1)
1257
- const loopBlinnFragment = Fn(() => {
1258
- const curveUV = uv();
1259
- const px = dFdx(curveUV);
1260
- const py = dFdy(curveUV);
1261
- const fx = float(2.0).mul(curveUV.x).mul(px.x).sub(px.y);
1262
- const fy = float(2.0).mul(curveUV.x).mul(py.x).sub(py.y);
1263
- const denom = sqrt(fx.mul(fx).add(fy.mul(fy)));
1264
- const f = curveUV.x.mul(curveUV.x).sub(curveUV.y);
1265
- const sd = f.div(denom.max(1e-6));
1266
- const alpha = clamp(float(0.5).sub(sd), 0.0, 1.0);
1267
- Discard(alpha.lessThanEqual(0.0));
1268
- return vec4(1.0, 1.0, 1.0, alpha);
1269
- });
1270
- function setGlyphAttrsOnGeometry(geo, attrs, offsetX = 0, offsetY = 0) {
1271
- if (!attrs)
1272
- return;
1273
- const glyphCenter = new Float32Array(attrs.glyphCenter);
1274
- if (offsetX !== 0 || offsetY !== 0) {
1275
- for (let i = 0; i < glyphCenter.length; i += 3) {
1276
- glyphCenter[i] += offsetX;
1277
- glyphCenter[i + 1] += offsetY;
1278
- }
1279
- }
1280
- geo.setAttribute('glyphCenter', new THREE.Float32BufferAttribute(glyphCenter, 3));
1281
- geo.setAttribute('glyphIndex', new THREE.Float32BufferAttribute(attrs.glyphIndex, 1));
1282
- geo.setAttribute('glyphProgress', new THREE.Float32BufferAttribute(attrs.glyphProgress, 1));
1283
- geo.setAttribute('glyphLineIndex', new THREE.Float32BufferAttribute(attrs.glyphLineIndex, 1));
1284
- geo.setAttribute('glyphBaselineY', new THREE.Float32BufferAttribute(attrs.glyphBaselineY, 1));
1285
- }
1286
- function applyStencilXOR(mat) {
1287
- mat.depthTest = false;
1288
- mat.depthWrite = false;
1289
- mat.side = THREE.DoubleSide;
1290
- mat.stencilWrite = true;
1291
- mat.stencilFunc = THREE.AlwaysStencilFunc;
1292
- mat.stencilRef = 0;
1293
- mat.stencilFuncMask = 0xFF;
1294
- mat.stencilWriteMask = 0xFF;
1295
- mat.stencilFail = THREE.KeepStencilOp;
1296
- mat.stencilZFail = THREE.KeepStencilOp;
1297
- mat.stencilZPass = THREE.InvertStencilOp;
1298
- }
1299
- // Three meshes that must render in order (via renderOrder or sequential draws):
1300
- // 1. interiorMesh - stencil XOR (fan triangulated interiors, no color output)
1301
- // 2. curveMesh - stencil XOR (Loop-Blinn curve eval, alpha-to-coverage)
1302
- // 3. fillMesh - color where stencil != 0, then zeros stencil
1303
- function createVectorMeshes(data, color) {
1304
- const interiorGeo = new THREE.BufferGeometry();
1305
- interiorGeo.setAttribute('position', new THREE.Float32BufferAttribute(data.interiorPositions, 3));
1306
- interiorGeo.setIndex(new THREE.BufferAttribute(data.interiorIndices, 1));
1307
- setGlyphAttrsOnGeometry(interiorGeo, data.interiorGlyphAttrs);
1308
- const curveGeo = new THREE.BufferGeometry();
1309
- curveGeo.setAttribute('position', new THREE.Float32BufferAttribute(data.curvePositions, 3));
1310
- const curveVertCount = data.curvePositions.length / 3;
1311
- const curveUVs = new Float32Array(curveVertCount * 2);
1312
- for (let i = 0; i < curveVertCount; i += 3) {
1313
- curveUVs[i * 2] = 0;
1314
- curveUVs[i * 2 + 1] = 0; // p0
1315
- curveUVs[i * 2 + 2] = 0.5;
1316
- curveUVs[i * 2 + 3] = 0; // p1
1317
- curveUVs[i * 2 + 4] = 1;
1318
- curveUVs[i * 2 + 5] = 1; // p2
1319
- }
1320
- curveGeo.setAttribute('uv', new THREE.Float32BufferAttribute(curveUVs, 2));
1321
- setGlyphAttrsOnGeometry(curveGeo, data.curveGlyphAttrs);
1322
- const fillGeo = new THREE.BufferGeometry();
1323
- fillGeo.setAttribute('position', new THREE.Float32BufferAttribute(data.fillPositions, 3));
1324
- fillGeo.setIndex(new THREE.BufferAttribute(data.fillIndices, 1));
1325
- setGlyphAttrsOnGeometry(fillGeo, data.fillGlyphAttrs);
1326
- // 1) Interior stencil material - no color output
1327
- const stencilInteriorMat = new THREE.MeshBasicNodeMaterial();
1328
- applyStencilXOR(stencilInteriorMat);
1329
- stencilInteriorMat.colorWrite = false;
1330
- // 2) Curve stencil material - Loop-Blinn fragment evaluation
1331
- const stencilCurveMat = new THREE.MeshBasicNodeMaterial();
1332
- applyStencilXOR(stencilCurveMat);
1333
- stencilCurveMat.colorWrite = false;
1334
- stencilCurveMat.alphaToCoverage = true;
1335
- stencilCurveMat.fragmentNode = loopBlinnFragment();
1336
- // 3) Color fill material - renders where stencil != 0
1337
- const colorMat = new THREE.MeshBasicNodeMaterial();
1338
- colorMat.depthTest = false;
1339
- colorMat.depthWrite = false;
1340
- colorMat.side = THREE.DoubleSide;
1341
- colorMat.stencilWrite = true;
1342
- colorMat.stencilFunc = THREE.NotEqualStencilFunc;
1343
- colorMat.stencilRef = 0;
1344
- colorMat.stencilFuncMask = 0xFF;
1345
- colorMat.stencilWriteMask = 0xFF;
1346
- colorMat.stencilFail = THREE.KeepStencilOp;
1347
- colorMat.stencilZFail = THREE.KeepStencilOp;
1348
- colorMat.stencilZPass = THREE.ZeroStencilOp;
1349
- if (color !== undefined) {
1350
- colorMat.color = new THREE.Color(color);
1351
- }
1352
- const interiorMesh = new THREE.Mesh(interiorGeo, stencilInteriorMat);
1353
- const curveMesh = new THREE.Mesh(curveGeo, stencilCurveMat);
1354
- const fillMesh = new THREE.Mesh(fillGeo, colorMat);
181
+ function wrapResult(coreResult, opts) {
182
+ const meshes = createVectorMeshes(coreResult.geometryData, {
183
+ color: opts.color,
184
+ positionNode: opts.positionNode,
185
+ colorNode: opts.colorNode,
186
+ center: opts.center,
187
+ });
1355
188
  return {
1356
- interiorMesh,
1357
- curveMesh,
1358
- fillMesh,
1359
- setOffset(x, y, z = 0) {
1360
- interiorMesh.position.set(x, y, z);
1361
- curveMesh.position.set(x, y, z);
1362
- fillMesh.position.set(x, y, z);
189
+ glyphs: coreResult.glyphs,
190
+ geometryData: coreResult.geometryData,
191
+ group: meshes.group,
192
+ interiorGeometry: meshes.interiorGeometry,
193
+ curveGeometry: meshes.curveGeometry,
194
+ fillGeometry: meshes.fillGeometry,
195
+ query: coreResult.query,
196
+ getLoadedFont: coreResult.getLoadedFont,
197
+ measureTextWidth: coreResult.measureTextWidth,
198
+ updateMaterials: meshes.updateMaterials,
199
+ async update(newOptions) {
200
+ const newCore = await coreResult.update(newOptions);
201
+ return wrapResult(newCore, { ...opts, ...newOptions });
1363
202
  },
1364
203
  dispose() {
1365
- interiorGeo.dispose();
1366
- curveGeo.dispose();
1367
- fillGeo.dispose();
1368
- stencilInteriorMat.dispose();
1369
- stencilCurveMat.dispose();
1370
- colorMat.dispose();
1371
- }
1372
- };
1373
- }
1374
-
1375
- function buildVectorResult(layoutHandle, vectorBuilder, options) {
1376
- const scale = layoutHandle.layoutData.pixelsPerFontUnit;
1377
- const { loopBlinnInput, glyphs } = vectorBuilder.buildForLoopBlinn(layoutHandle.clustersByLine, scale);
1378
- const geometryData = buildVectorGeometry(loopBlinnInput);
1379
- let cachedQuery = null;
1380
- const update = async (newOptions) => {
1381
- const mergedOptions = { ...options };
1382
- for (const key in newOptions) {
1383
- const value = newOptions[key];
1384
- if (value !== undefined) {
1385
- mergedOptions[key] = value;
1386
- }
1387
- }
1388
- if (newOptions.font !== undefined ||
1389
- newOptions.fontVariations !== undefined ||
1390
- newOptions.fontFeatures !== undefined) {
1391
- const newLayout = await layoutHandle.update(mergedOptions);
1392
- const newBuilder = new GlyphVectorGeometryBuilder(newLayout.loadedFont, globalOutlineCache);
1393
- newBuilder.setFontId(newLayout.fontId);
1394
- layoutHandle = newLayout;
1395
- options = mergedOptions;
1396
- return buildVectorResult(layoutHandle, newBuilder, options);
204
+ meshes.dispose();
205
+ coreResult.dispose();
1397
206
  }
1398
- const newLayout = await layoutHandle.update(mergedOptions);
1399
- layoutHandle = newLayout;
1400
- options = mergedOptions;
1401
- return buildVectorResult(layoutHandle, vectorBuilder, options);
1402
- };
1403
- return {
1404
- glyphs,
1405
- geometryData,
1406
- query: (queryOptions) => {
1407
- if (!cachedQuery) {
1408
- cachedQuery = new TextRangeQuery(options.text, glyphs);
1409
- }
1410
- return cachedQuery.execute(queryOptions);
1411
- },
1412
- getLoadedFont: () => layoutHandle.getLoadedFont(),
1413
- measureTextWidth: (text, letterSpacing) => layoutHandle.measureTextWidth(text, letterSpacing),
1414
- update,
1415
- dispose: () => layoutHandle.dispose()
1416
207
  };
1417
208
  }
1418
209
  class Text {
@@ -1424,11 +215,9 @@ class Text {
1424
215
  static { this.setMaxFontCacheMemoryMB = Text$1.setMaxFontCacheMemoryMB; }
1425
216
  static { this.enableWoff2 = Text$1.enableWoff2; }
1426
217
  static async create(options) {
1427
- const layoutHandle = await Text$1.create(options);
1428
- const vectorBuilder = new GlyphVectorGeometryBuilder(layoutHandle.loadedFont, globalOutlineCache);
1429
- vectorBuilder.setFontId(layoutHandle.fontId);
1430
- return buildVectorResult(layoutHandle, vectorBuilder, options);
218
+ const coreResult = await Text$1.create(options);
219
+ return wrapResult(coreResult, options);
1431
220
  }
1432
221
  }
1433
222
 
1434
- export { Text, buildVectorGeometry, createVectorMeshes, extractContours };
223
+ export { Text, buildVectorGeometry, extractContours };