three-slug 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,399 @@
1
+ // SlugGenerator.js
2
+ // Port of the Sluggish C++ Generator using opentype.js
3
+ import opentype from 'opentype.js';
4
+ import * as THREE from 'three';
5
+
6
+ const TEXTURE_WIDTH = 4096;
7
+ const SLUGGISH_HEADER_DATA = "SLUGGISH";
8
+ const SLUGGISH_HEADER_LEN = 8;
9
+
10
+ export class SlugGenerator {
11
+ constructor(parameters = {}) {
12
+ this.bandCount = 16;
13
+ this.fullRange = parameters.fullRange !== undefined ? parameters.fullRange : false;
14
+ this.whitelist = parameters.whitelist || null; // Array of codepoints to include
15
+ }
16
+
17
+ async generateFromUrl(url) {
18
+ const font = await opentype.load(url);
19
+ return this.generate(font);
20
+ }
21
+
22
+ async generateFromFile(file) {
23
+ const buffer = await file.arrayBuffer();
24
+ return this.generateFromBuffer(buffer);
25
+ }
26
+
27
+ generateFromBuffer(buffer) {
28
+ const font = opentype.parse(buffer);
29
+ return this.generate(font);
30
+ }
31
+
32
+ generate(font) {
33
+ let ignoredCodePoints = 0;
34
+ let curvesTexData = []; // Flat array of [x, y]
35
+ let bandsTexBandOffsets = []; // 2 per band [curveCount, bandOffset]
36
+ let bandsTexCurveOffsets = []; // 2 per curve [curveX, curveY]
37
+ let codePointsData = [];
38
+
39
+ for (let i = 0; i < font.glyphs.length; i++) {
40
+ const glyph = font.glyphs.get(i);
41
+ let cp = glyph.unicode;
42
+ if (cp === undefined) {
43
+ if (i === 0) cp = -1; // .notdef fallback glyph
44
+ else continue;
45
+ }
46
+
47
+ // Optional range filtering to control .sluggish file bloat
48
+ if (!this.fullRange && i !== 0) {
49
+ if (this.whitelist) {
50
+ if (!this.whitelist.includes(cp)) continue;
51
+ } else if (cp < 32 || cp > 126) {
52
+ continue; // Default to basic printable ASCII only
53
+ }
54
+ }
55
+ const path = glyph.path; // ONLY use raw font-space unstretched paths to match getBoundingBox() perfectly!
56
+
57
+ // Determine visible bounding box in font units
58
+ const bbox = glyph.getBoundingBox();
59
+ if (bbox.x1 === bbox.x2 || bbox.y1 === bbox.y2) {
60
+ // Empty glyph (e.g. space) or missing - keep it to preserve advanceWidth!
61
+ codePointsData.push({
62
+ codePoint: cp,
63
+ width: 0,
64
+ height: 0,
65
+ advanceWidth: Math.floor(glyph.advanceWidth || 0),
66
+ bearingX: 0,
67
+ bearingY: 0,
68
+ bandCount: 0,
69
+ bandDimX: 0,
70
+ bandDimY: 0,
71
+ bandsTexCoordX: 0,
72
+ bandsTexCoordY: 0
73
+ });
74
+ continue;
75
+ }
76
+
77
+ const gx1 = bbox.x1;
78
+ const gy1 = bbox.y1;
79
+ const gx2 = bbox.x2;
80
+ const gy2 = bbox.y2;
81
+
82
+ // 1. Build Temporary Curve List
83
+ let curves = [];
84
+ let currentX = 0, currentY = 0;
85
+ let firstCurve = false;
86
+ let startOfShapeX = 0, startOfShapeY = 0;
87
+
88
+ for (let i = 0; i < path.commands.length; i++) {
89
+ const cmd = path.commands[i];
90
+ if (cmd.type === 'M') {
91
+ firstCurve = true;
92
+ currentX = cmd.x - gx1;
93
+ currentY = cmd.y - gy1;
94
+ startOfShapeX = currentX;
95
+ startOfShapeY = currentY;
96
+ } else if (cmd.type === 'L') {
97
+ let nextX = cmd.x - gx1;
98
+ let nextY = cmd.y - gy1;
99
+ let c = {
100
+ first: firstCurve,
101
+ x1: currentX, y1: currentY,
102
+ x3: nextX, y3: nextY,
103
+ x2: (currentX + nextX) / 2.0,
104
+ y2: (currentY + nextY) / 2.0
105
+ };
106
+ curves.push(c);
107
+ firstCurve = false;
108
+ currentX = nextX;
109
+ currentY = nextY;
110
+ } else if (cmd.type === 'Q') {
111
+ let nextX = cmd.x - gx1;
112
+ let nextY = cmd.y - gy1;
113
+ let c = {
114
+ first: firstCurve,
115
+ x1: currentX, y1: currentY,
116
+ x2: cmd.x1 - gx1, y2: cmd.y1 - gy1,
117
+ x3: nextX, y3: nextY
118
+ };
119
+ curves.push(c);
120
+ firstCurve = false;
121
+ currentX = nextX;
122
+ currentY = nextY;
123
+ } else if (cmd.type === 'C') {
124
+ console.warn(`U+${cp.toString(16)} has bicubic curves. Slug requires quadratic.`);
125
+ ignoredCodePoints++;
126
+ curves = null; // Mark to skip
127
+ break;
128
+ } else if (cmd.type === 'Z') {
129
+ if (currentX !== startOfShapeX || currentY !== startOfShapeY) {
130
+ let c = {
131
+ first: firstCurve,
132
+ x1: currentX, y1: currentY,
133
+ x3: startOfShapeX, y3: startOfShapeY,
134
+ x2: (currentX + startOfShapeX) / 2.0,
135
+ y2: (currentY + startOfShapeY) / 2.0
136
+ };
137
+ curves.push(c);
138
+ firstCurve = false;
139
+ }
140
+ currentX = startOfShapeX;
141
+ currentY = startOfShapeY;
142
+ }
143
+ }
144
+
145
+ if (!curves || curves.length === 0) continue;
146
+
147
+ // Fix up curves where the control point is one of the endpoints
148
+ for (let c of curves) {
149
+ if ((c.x2 === c.x1 && c.y2 === c.y1) || (c.x2 === c.x3 && c.y2 === c.y3)) {
150
+ c.x2 = (c.x1 + c.x3) / 2.0;
151
+ c.y2 = (c.y1 + c.y3) / 2.0;
152
+ }
153
+ }
154
+
155
+ const bandsTexelIndex = Math.floor(bandsTexBandOffsets.length / 2);
156
+
157
+ // 2. Write Curves Texture
158
+ for (let c of curves) {
159
+ // make sure we start a curve at a texel's boundary (4 values = 2 points = 1 texel RGBA)
160
+ // wait, C++ code: g_curvesTexture.size() % 4
161
+ // each push_back pushed 1 float. so 4 floats = 1 texel.
162
+ // a curve is 3 points -> 6 floats.
163
+ if (c.first && curvesTexData.length % 4 !== 0) {
164
+ const toAdd = 4 - (curvesTexData.length % 4);
165
+ for (let i = 0; i < toAdd; i++) curvesTexData.push(-1.0);
166
+ }
167
+
168
+ // make sure a curve doesn't cross a row boundary
169
+ let texelCount = Math.floor(curvesTexData.length / 4);
170
+ let col = texelCount % TEXTURE_WIDTH;
171
+ const newRow = col === TEXTURE_WIDTH - 1;
172
+ if (newRow) {
173
+ const toAdd = 8 - (curvesTexData.length % 4); // C++ used 8
174
+ for (let i = 0; i < toAdd; i++) curvesTexData.push(-1.0);
175
+ }
176
+
177
+ if (c.first || newRow) {
178
+ c.texelIndex = Math.floor(curvesTexData.length / 4);
179
+ curvesTexData.push(c.x1, c.y1);
180
+ } else {
181
+ c.texelIndex = Math.floor((Math.floor(curvesTexData.length / 2) - 1) / 2);
182
+ }
183
+
184
+ curvesTexData.push(c.x2, c.y2);
185
+ curvesTexData.push(c.x3, c.y3);
186
+ }
187
+
188
+ const sizeX = 1 + (gx2 - gx1);
189
+ const sizeY = 1 + (gy2 - gy1);
190
+ let bCount = this.bandCount; // Re-enable spatial banding optimizations!
191
+ if (sizeX < bCount || sizeY < bCount) {
192
+ bCount = Math.floor(Math.min(sizeX, sizeY) / 2);
193
+ if(bCount < 1) bCount = 1;
194
+ }
195
+
196
+ const bandDimY = Math.ceil(sizeY / bCount);
197
+ let bandMinY = 0;
198
+ let bandMaxY = bandDimY;
199
+
200
+ // Sort curves by highest Y (max of y1,y2,y3) ascending/descending?
201
+ // C++: Max(a.x1, a.x2, a.x3) > Max(b.x1, b.x2, b.x3) for H-bands, wait!
202
+ // C++ for H-bands sorted by max X descending.
203
+ curves.sort((a, b) => Math.max(b.x1, b.x2, b.x3) - Math.max(a.x1, a.x2, a.x3));
204
+
205
+ for (let b = 0; b < bCount; b++) {
206
+ let bandTexelOffset = Math.floor(bandsTexCurveOffsets.length / 2);
207
+ let curveCount = 0;
208
+
209
+ for (let c of curves) {
210
+ if (c.y1 === c.y2 && c.y2 === c.y3) continue; // perfectly horizontal
211
+ let curveMinY = Math.min(c.y1, c.y2, c.y3);
212
+ let curveMaxY = Math.max(c.y1, c.y2, c.y3);
213
+ if (curveMinY > bandMaxY || curveMaxY < bandMinY) continue; // doesn't cross band
214
+
215
+ let texelIndex = c.texelIndex;
216
+ let curveOffsetX = texelIndex % TEXTURE_WIDTH;
217
+ let curveOffsetY = Math.floor(texelIndex / TEXTURE_WIDTH);
218
+ bandsTexCurveOffsets.push(curveOffsetX, curveOffsetY);
219
+ curveCount++;
220
+ }
221
+ bandsTexBandOffsets.push(curveCount, bandTexelOffset);
222
+ bandMinY += bandDimY;
223
+ bandMaxY += bandDimY;
224
+ }
225
+
226
+ // For vertical bands, sort by max Y descending
227
+ const bandDimX = Math.ceil(sizeX / bCount);
228
+ let bandMinX = 0;
229
+ let bandMaxX = bandDimX;
230
+
231
+ curves.sort((a, b) => Math.max(b.y1, b.y2, b.y3) - Math.max(a.y1, a.y2, a.y3));
232
+
233
+ for (let b = 0; b < bCount; b++) {
234
+ let bandTexelOffset = Math.floor(bandsTexCurveOffsets.length / 2);
235
+ let curveCount = 0;
236
+
237
+ for (let c of curves) {
238
+ if (c.x1 === c.x2 && c.x2 === c.x3) continue; // perfectly vertical
239
+ let curveMinX = Math.min(c.x1, c.x2, c.x3);
240
+ let curveMaxX = Math.max(c.x1, c.x2, c.x3);
241
+ if (curveMinX > bandMaxX || curveMaxX < bandMinX) continue; // doesn't cross band
242
+
243
+ let texelIndex = c.texelIndex;
244
+ let curveOffsetX = texelIndex % TEXTURE_WIDTH;
245
+ let curveOffsetY = Math.floor(texelIndex / TEXTURE_WIDTH);
246
+ bandsTexCurveOffsets.push(curveOffsetX, curveOffsetY);
247
+ curveCount++;
248
+ }
249
+ bandsTexBandOffsets.push(curveCount, bandTexelOffset);
250
+ bandMinX += bandDimX;
251
+ bandMaxX += bandDimX;
252
+ }
253
+
254
+ codePointsData.push({
255
+ codePoint: cp,
256
+ width: Math.floor(gx2 - gx1),
257
+ height: Math.floor(gy2 - gy1),
258
+ advanceWidth: Math.floor(glyph.advanceWidth || 0),
259
+ bearingX: Math.floor(gx1),
260
+ bearingY: Math.floor(gy1),
261
+ bandCount: bCount,
262
+ bandDimX: bandDimX,
263
+ bandDimY: bandDimY,
264
+ bandsTexCoordX: bandsTexelIndex % TEXTURE_WIDTH,
265
+ bandsTexCoordY: Math.floor(bandsTexelIndex / TEXTURE_WIDTH)
266
+ });
267
+ }
268
+
269
+ // Post-processing
270
+ const bandHeaderTexels = Math.floor(bandsTexBandOffsets.length / 2);
271
+ for (let i = 1; i < bandsTexBandOffsets.length; i += 2) {
272
+ bandsTexBandOffsets[i] += bandHeaderTexels;
273
+ }
274
+
275
+ return this.buildOutput(codePointsData, curvesTexData, bandsTexBandOffsets, bandsTexCurveOffsets, font);
276
+ }
277
+
278
+ buildOutput(codePoints, curvesList, bandOffsets, curveOffsets, font) {
279
+ // Build DataTextures and codePoint Map
280
+ const map = new Map();
281
+ codePoints.forEach(cp => map.set(cp.codePoint, cp));
282
+
283
+ const curvesTexels = Math.ceil(curvesList.length / 4);
284
+ const curvesTexHeight = Math.ceil(curvesTexels / TEXTURE_WIDTH);
285
+
286
+ let curvesFloatArray = new Float32Array(TEXTURE_WIDTH * curvesTexHeight * 4);
287
+ curvesFloatArray.fill(-1.0);
288
+ curvesFloatArray.set(curvesList);
289
+
290
+ const curvesTex = new THREE.DataTexture(curvesFloatArray, TEXTURE_WIDTH, curvesTexHeight, THREE.RGBAFormat, THREE.FloatType);
291
+ curvesTex.internalFormat = 'RGBA32F';
292
+ curvesTex.minFilter = THREE.NearestFilter;
293
+ curvesTex.magFilter = THREE.NearestFilter;
294
+ curvesTex.needsUpdate = true;
295
+
296
+ const bandsTexels = Math.floor(bandOffsets.length / 2) + Math.floor(curveOffsets.length / 2);
297
+ const bandsTexHeight = Math.ceil(bandsTexels / TEXTURE_WIDTH);
298
+
299
+ let bandsUintArray = new Uint32Array(TEXTURE_WIDTH * bandsTexHeight * 2);
300
+ bandsUintArray.set(bandOffsets, 0);
301
+ bandsUintArray.set(curveOffsets, bandOffsets.length);
302
+
303
+ const bandsTex = new THREE.DataTexture(bandsUintArray, TEXTURE_WIDTH, bandsTexHeight, THREE.RGIntegerFormat, THREE.UnsignedIntType);
304
+ bandsTex.internalFormat = 'RG32UI';
305
+ bandsTex.minFilter = THREE.NearestFilter;
306
+ bandsTex.magFilter = THREE.NearestFilter;
307
+ bandsTex.needsUpdate = true;
308
+
309
+ // Also return the raw arrays for export
310
+ return {
311
+ codePoints: map,
312
+ curvesTex: curvesTex,
313
+ bandsTex: bandsTex,
314
+ ascender: font.ascender || 0,
315
+ descender: font.descender || 0,
316
+ lineGap: font.lineGap || 0,
317
+ unitsPerEm: font.unitsPerEm || 0,
318
+ _raw: { codePoints, curvesList, bandOffsets, curveOffsets, metrics: {
319
+ ascender: font.ascender || 0,
320
+ descender: font.descender || 0,
321
+ lineGap: font.lineGap || 0,
322
+ unitsPerEm: font.unitsPerEm || 0
323
+ } }
324
+ };
325
+ }
326
+
327
+ exportSluggish(generatedData) {
328
+ // Binary pack into ArrayBuffer
329
+ const { codePoints, curvesList, bandOffsets, curveOffsets } = generatedData._raw;
330
+
331
+ const curvesTexels = Math.ceil(curvesList.length / 4);
332
+ const curvesTexHeight = Math.ceil(curvesTexels / TEXTURE_WIDTH);
333
+ const curvesFloatArray = new Float32Array(TEXTURE_WIDTH * curvesTexHeight * 4);
334
+ curvesFloatArray.fill(0); // Optional, filling with 0 or -1. Let's use 0 to match normal behavior
335
+ curvesFloatArray.set(curvesList);
336
+
337
+ const bandsTexels = Math.floor(bandOffsets.length / 2) + Math.floor(curveOffsets.length / 2);
338
+ const bandsTexHeight = Math.ceil(bandsTexels / TEXTURE_WIDTH);
339
+ const bandsUintArray = new Uint32Array(TEXTURE_WIDTH * bandsTexHeight * 2);
340
+ bandsUintArray.set(bandOffsets, 0);
341
+ bandsUintArray.set(curveOffsets, bandOffsets.length);
342
+
343
+ // Calculate total size
344
+ // Header: 8 bytes
345
+ // CP Count: 2 bytes
346
+ // CPs: 28 bytes * N
347
+ // Curves: Width(2) + Height(2) + Bytes(4) = 8 bytes + CurvesBytes
348
+ // Bands: Width(2) + Height(2) + Bytes(4) = 8 bytes + BandsBytes
349
+ // Calculate total size: 40 bytes per code point
350
+ const curvesBytes = curvesFloatArray.byteLength;
351
+ const bandsBytes = bandsUintArray.byteLength;
352
+ const metrics = generatedData._raw.metrics || { ascender: 0, descender: 0, lineGap: 0, unitsPerEm: 0 };
353
+
354
+ const totalBytes = 8 + 2 + (codePoints.length * 40) + 8 + curvesBytes + 8 + bandsBytes + 16;
355
+ const buffer = new ArrayBuffer(totalBytes);
356
+ const view = new DataView(buffer);
357
+ let offset = 0;
358
+
359
+ for (let i = 0; i < 8; i++) {
360
+ view.setUint8(offset++, SLUGGISH_HEADER_DATA.charCodeAt(i));
361
+ }
362
+
363
+ view.setUint16(offset, codePoints.length, true); offset += 2;
364
+
365
+ for (let cp of codePoints) {
366
+ view.setUint32(offset, cp.codePoint, true); offset += 4;
367
+ view.setUint32(offset, cp.width, true); offset += 4;
368
+ view.setUint32(offset, cp.height, true); offset += 4;
369
+ view.setUint32(offset, cp.advanceWidth, true); offset += 4;
370
+ view.setInt32(offset, cp.bearingX, true); offset += 4; // Signed
371
+ view.setInt32(offset, cp.bearingY, true); offset += 4; // Signed
372
+ view.setUint32(offset, cp.bandCount, true); offset += 4;
373
+ view.setUint32(offset, cp.bandDimX, true); offset += 4;
374
+ view.setUint32(offset, cp.bandDimY, true); offset += 4;
375
+ view.setUint16(offset, cp.bandsTexCoordX, true); offset += 2;
376
+ view.setUint16(offset, cp.bandsTexCoordY, true); offset += 2;
377
+ }
378
+
379
+ view.setUint16(offset, TEXTURE_WIDTH, true); offset += 2;
380
+ view.setUint16(offset, curvesTexHeight, true); offset += 2;
381
+ view.setUint32(offset, curvesBytes, true); offset += 4;
382
+ new Uint8Array(buffer).set(new Uint8Array(curvesFloatArray.buffer), offset);
383
+ offset += curvesBytes;
384
+
385
+ view.setUint16(offset, TEXTURE_WIDTH, true); offset += 2;
386
+ view.setUint16(offset, bandsTexHeight, true); offset += 2;
387
+ view.setUint32(offset, bandsBytes, true); offset += 4;
388
+ new Uint8Array(buffer).set(new Uint8Array(bandsUintArray.buffer), offset);
389
+ offset += bandsBytes;
390
+
391
+ // Footer Metadata Table (Backward compatible fallback)
392
+ view.setInt32(offset, metrics.ascender || 0, true); offset += 4;
393
+ view.setInt32(offset, metrics.descender || 0, true); offset += 4;
394
+ view.setInt32(offset, metrics.lineGap || 0, true); offset += 4;
395
+ view.setInt32(offset, metrics.unitsPerEm || 0, true); offset += 4;
396
+
397
+ return buffer;
398
+ }
399
+ }
@@ -0,0 +1,211 @@
1
+ import * as THREE from 'three';
2
+
3
+ export class SlugGeometry extends THREE.InstancedBufferGeometry {
4
+ constructor(maxGlyphs = 1024) {
5
+ super();
6
+
7
+ // 1. Base Quad Geometry (0 to 1, or -1 to 1 based on shader assumptions)
8
+ // Shader expects position * scaleBias.xy + scaleBias.zw.
9
+ // In the C++ code, they use vertices from -1 to 1:
10
+ // -1, -1 | -1, 1 | 1, 1 | 1, -1
11
+ const vertices = new Float32Array([
12
+ -1.0, -1.0,
13
+ -1.0, 1.0,
14
+ 1.0, 1.0,
15
+ 1.0, -1.0
16
+ ]);
17
+
18
+ const uvs = new Float32Array([
19
+ 0.0, 0.0,
20
+ 0.0, 1.0,
21
+ 1.0, 1.0,
22
+ 1.0, 0.0
23
+ ]);
24
+
25
+ const normals = new Float32Array([
26
+ 0.0, 0.0, 1.0,
27
+ 0.0, 0.0, 1.0,
28
+ 0.0, 0.0, 1.0,
29
+ 0.0, 0.0, 1.0
30
+ ]);
31
+
32
+ const indices = new Uint16Array([
33
+ 0, 2, 1,
34
+ 0, 3, 2
35
+ ]);
36
+
37
+ this.setIndex(new THREE.BufferAttribute(indices, 1));
38
+ this.setAttribute('position', new THREE.BufferAttribute(vertices, 2));
39
+ this.setAttribute('uv', new THREE.BufferAttribute(uvs, 2));
40
+ this.setAttribute('normal', new THREE.BufferAttribute(normals, 3));
41
+
42
+ // 2. Instanced Attributes
43
+ this.maxGlyphs = maxGlyphs;
44
+ this.glyphCount = 0;
45
+
46
+ this.aScaleBias = new Float32Array(maxGlyphs * 4);
47
+ this.aGlyphBandScale = new Float32Array(maxGlyphs * 4);
48
+ this.aBandMaxTexCoords = new Float32Array(maxGlyphs * 4);
49
+
50
+ const attrScaleBias = new THREE.InstancedBufferAttribute(this.aScaleBias, 4);
51
+ attrScaleBias.setUsage(THREE.DynamicDrawUsage);
52
+ this.setAttribute('aScaleBias', attrScaleBias);
53
+
54
+ const attrGlyphBandScale = new THREE.InstancedBufferAttribute(this.aGlyphBandScale, 4);
55
+ attrGlyphBandScale.setUsage(THREE.DynamicDrawUsage);
56
+ this.setAttribute('aGlyphBandScale', attrGlyphBandScale);
57
+
58
+ // WebGL2 requires InterleavedBuffer or Int32/Uint32 for int attributes in shader (uvec4)
59
+ const attrBandMaxTexCoords = new THREE.InstancedBufferAttribute(this.aBandMaxTexCoords, 4);
60
+ attrBandMaxTexCoords.setUsage(THREE.DynamicDrawUsage);
61
+ this.setAttribute('aBandMaxTexCoords', attrBandMaxTexCoords);
62
+
63
+ this.instanceCount = 0;
64
+
65
+ this.boundingBox = new THREE.Box3();
66
+ this.boundingSphere = new THREE.Sphere();
67
+ }
68
+
69
+ clear() {
70
+ this.glyphCount = 0;
71
+ this.instanceCount = 0;
72
+ this.boundingBox.makeEmpty();
73
+ this.boundingSphere.radius = 0;
74
+ }
75
+
76
+ computeBoundingSphere() {
77
+ if (!this.boundingBox || this.boundingBox.isEmpty()) {
78
+ this.boundingSphere.set(new THREE.Vector3(), 0);
79
+ } else {
80
+ this.boundingBox.getBoundingSphere(this.boundingSphere);
81
+ }
82
+ }
83
+
84
+ addGlyph(codePointData, x, y, width, height, displayWidth, displayHeight) {
85
+
86
+ // Based on C++ GL_RenderGlyph
87
+ if (this.glyphCount >= this.maxGlyphs) {
88
+ console.warn("Max glyphs reached");
89
+ return false;
90
+ }
91
+
92
+ const i = this.glyphCount;
93
+
94
+ // Quads go from -1 to 1. Multiply by half width/height to get correct size.
95
+ // Bias translates the center position.
96
+ const sx = width / 2.0;
97
+ const sy = height / 2.0;
98
+ const cx = x + sx;
99
+ const cy = y + sy;
100
+
101
+ this.aScaleBias[i * 4 + 0] = sx;
102
+ this.aScaleBias[i * 4 + 1] = sy;
103
+ this.aScaleBias[i * 4 + 2] = cx;
104
+ this.aScaleBias[i * 4 + 3] = cy;
105
+
106
+ // Dynamically compute the exact bounding box of the packed structure for frustum culling and native Shadow Map projections
107
+ this.boundingBox.expandByPoint(new THREE.Vector3(cx - sx, cy - sy, 0));
108
+ this.boundingBox.expandByPoint(new THREE.Vector3(cx + sx, cy + sy, 0));
109
+
110
+ // Glyph and Band Scale
111
+
112
+ this.aGlyphBandScale[i * 4 + 0] = codePointData.width;
113
+ this.aGlyphBandScale[i * 4 + 1] = codePointData.height;
114
+ this.aGlyphBandScale[i * 4 + 2] = codePointData.width / codePointData.bandDimX;
115
+ this.aGlyphBandScale[i * 4 + 3] = codePointData.height / codePointData.bandDimY;
116
+
117
+ // Band Max Tex Coords (Uint32)
118
+ this.aBandMaxTexCoords[i * 4 + 0] = codePointData.bandCount - 1;
119
+ this.aBandMaxTexCoords[i * 4 + 1] = codePointData.bandCount - 1;
120
+ this.aBandMaxTexCoords[i * 4 + 2] = codePointData.bandsTexCoordX;
121
+ this.aBandMaxTexCoords[i * 4 + 3] = codePointData.bandsTexCoordY;
122
+
123
+ this.glyphCount++;
124
+ this.instanceCount = this.glyphCount;
125
+
126
+ return true;
127
+ }
128
+
129
+ updateBuffers() {
130
+ this.attributes.aScaleBias.needsUpdate = true;
131
+ this.attributes.aGlyphBandScale.needsUpdate = true;
132
+ this.attributes.aBandMaxTexCoords.needsUpdate = true;
133
+
134
+ // Automatically sync the bounding sphere to the aggressively tracked bounding box for native physics implementations
135
+ this.computeBoundingSphere();
136
+ }
137
+
138
+ clear() {
139
+ this.glyphCount = 0;
140
+ this.instanceCount = 0;
141
+ if (this.boundingBox) this.boundingBox.makeEmpty();
142
+ if (this.boundingSphere) this.boundingSphere.radius = 0;
143
+ }
144
+
145
+ addText(text, slugData, options = {}) {
146
+ const ascender = slugData.ascender || 0;
147
+ const descender = slugData.descender || 0;
148
+ const lineGap = slugData.lineGap || 0;
149
+
150
+ const defaultLineHeight = slugData.unitsPerEm > 0
151
+ ? (ascender - descender + lineGap)
152
+ : 2000;
153
+
154
+ const {
155
+ fontScale = 0.5,
156
+ lineHeight = defaultLineHeight * fontScale,
157
+ startX = 0,
158
+ startY = 0,
159
+ justify = 'left' // 'left', 'center', 'right'
160
+ } = options;
161
+
162
+ const lines = text.split('\n');
163
+ let currentY = startY;
164
+
165
+ for (const line of lines) {
166
+ let lineWidth = 0;
167
+
168
+ // First pass: Measure exact physical width of the line for justification offsets
169
+ let j = 0;
170
+ while (j < line.length) {
171
+ const charCode = line.codePointAt(j);
172
+ j += charCode > 0xFFFF ? 2 : 1;
173
+ let data = slugData.codePoints.get(charCode) || slugData.codePoints.get(-1);
174
+ if (data) {
175
+ lineWidth += data.advanceWidth * fontScale;
176
+ } else if (line[j-1] === ' ') {
177
+ lineWidth += 600 * fontScale; // Fallback space width
178
+ }
179
+ }
180
+
181
+ let currentX = startX;
182
+ if (justify === 'center') currentX -= lineWidth / 2.0;
183
+ else if (justify === 'right') currentX -= lineWidth;
184
+
185
+ // Second pass: Inject the geometric layout frames into the instanced buffers
186
+ let k = 0;
187
+ while (k < line.length) {
188
+ const charCode = line.codePointAt(k);
189
+ k += charCode > 0xFFFF ? 2 : 1;
190
+ let data = slugData.codePoints.get(charCode) || slugData.codePoints.get(-1);
191
+
192
+ if (data) {
193
+ if (data.width > 0 && data.height > 0) {
194
+ const quadW = data.width * fontScale;
195
+ const quadH = data.height * fontScale;
196
+ const px = currentX + data.bearingX * fontScale;
197
+ const py = currentY + data.bearingY * fontScale;
198
+
199
+ this.addGlyph(data, px, py, quadW, quadH, 0, 0); // Display size dropped in PBR pass
200
+ }
201
+ currentX += data.advanceWidth * fontScale;
202
+ } else if (line[k-1] === ' ') {
203
+ currentX += 600 * fontScale;
204
+ }
205
+ }
206
+ currentY -= lineHeight;
207
+ }
208
+
209
+ this.updateBuffers();
210
+ }
211
+ }