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.
- package/README.md +15 -16
- package/dist/index.cjs +66 -20
- package/dist/index.d.ts +6 -0
- package/dist/index.js +66 -20
- package/dist/index.min.cjs +310 -307
- package/dist/index.min.js +265 -262
- package/dist/index.umd.js +68 -21
- package/dist/index.umd.min.js +268 -265
- package/dist/three/index.cjs +2 -1
- package/dist/three/index.d.ts +1 -0
- package/dist/three/index.js +2 -1
- package/dist/three/react.cjs +35 -17
- package/dist/three/react.d.ts +6 -0
- package/dist/three/react.js +35 -17
- package/dist/types/core/Text.d.ts +6 -0
- package/dist/types/three/index.d.ts +1 -0
- package/dist/types/vector/core.d.ts +27 -0
- package/dist/types/vector/index.d.ts +30 -24
- package/dist/types/vector/loopBlinnTSL.d.ts +14 -3
- package/dist/types/vector/react.d.ts +4 -6
- package/dist/vector/all.cjs +21 -0
- package/dist/vector/all.d.ts +134 -0
- package/dist/vector/all.js +2 -0
- package/dist/vector/core.cjs +1310 -0
- package/dist/vector/core.d.ts +112 -0
- package/dist/vector/core.js +1306 -0
- package/dist/vector/index.cjs +41 -1265
- package/dist/vector/index.d.ts +335 -20
- package/dist/vector/index.js +28 -1239
- package/dist/vector/loopBlinnTSL.cjs +229 -0
- package/dist/vector/loopBlinnTSL.d.ts +69 -0
- package/dist/vector/loopBlinnTSL.js +207 -0
- package/dist/vector/react.cjs +45 -60
- package/dist/vector/react.d.ts +9 -3
- package/dist/vector/react.js +46 -42
- package/dist/vector/webgl/index.cjs +3 -1
- package/dist/vector/webgl/index.js +3 -1
- package/package.json +1 -1
|
@@ -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;
|