three-text 0.6.0 → 0.6.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 CHANGED
@@ -19,8 +19,6 @@ High fidelity 3D text rendering and layout for the web
19
19
 
20
20
  The library has a framework-agnostic core with lightweight adapters for [Three.js](https://threejs.org), [React Three Fiber](https://docs.pmnd.rs/react-three-fiber), [p5.js](https://p5js.org), [WebGL](https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API), and [WebGPU](https://developer.mozilla.org/en-US/docs/Web/API/WebGPU_API)
21
21
 
22
- Under the hood, three-text relies on a core of [harfbuzzjs](https://github.com/harfbuzz/harfbuzzjs) (based on [HarfBuzz](https://github.com/harfbuzz/harfbuzz) by Behdad Esfahbod et al) for text shaping, [Knuth-Plass](http://www.eprg.org/G53DOC/pdfs/knuth-plass-breaking.pdf) line breaking (with [SILE](https://github.com/sile-typesetter/sile/blob/master/core/break.lua) and LuaTex being the closest modern references), [Liang](https://tug.org/docs/liang/liang-thesis.pdf) hyphenation and the [TeX hyphenation patterns](https://github.com/hyphenation/tex-hyphen), and [woff-lib](https://github.com/countertype/woff-lib) for optional WOFF2 support. The mesh text pipeline uses [libtess-ts](https://github.com/countertype/libtess-ts) (a port of the [GLU tessellator](https://www.songho.ca/opengl/gl_tessellation.html) by Eric Veach) for removing overlaps and triangulation, adaptive curve polygonization from Maxim Shemanarev's [Anti-Grain Geometry](https://web.archive.org/web/20060128212843/http://www.antigrain.com/research/adaptive_bezier/index.html), and [Visvalingam-Whyatt](https://hull-repository.worktribe.com/preview/376364/000870493786962263.pdf) [line simplification](https://bost.ocks.org/mike/simplify/). The vector pipeline uses [Slug](https://github.com/EricLengyel/Slug) by Eric Lengyel for resolution-independent curve rendering
23
-
24
22
  ## Table of contents
25
23
 
26
24
  - [Overview](#overview)
@@ -41,6 +39,7 @@ Under the hood, three-text relies on a core of [harfbuzzjs](https://github.com/h
41
39
  - [Browser compatibility](#browser-compatibility)
42
40
  - [Testing](#testing)
43
41
  - [Build system](#build-system)
42
+ - [Entry points](#entry-points)
44
43
  - [Build outputs](#build-outputs)
45
44
  - [Acknowledgements](#acknowledgements)
46
45
  - [License](#license)
@@ -60,34 +59,14 @@ npm install three
60
59
 
61
60
  ## Architecture
62
61
 
63
- three-text has a framework-agnostic core that processes fonts and generates geometry data. Lightweight adapters convert this data to framework-specific formats:
64
-
65
- - **`three-text`** - Three.js adapter (default export, returns `BufferGeometry`)
66
- - **`three-text/mesh`** - Same as above (explicit alias)
67
- - **`three-text/mesh/react`** - React Three Fiber component for extruded mesh text
68
- - **`three-text/three`** - Deprecated, use `three-text/mesh`
69
- - **`three-text/three/react`** - Deprecated, use `three-text/mesh/react`
70
- - **`three-text/mesh/webgl`** - WebGL mesh buffer utility
71
- - **`three-text/mesh/webgpu`** - WebGPU mesh buffer utility
72
- - **`three-text/mesh/p5`** - p5.js adapter
73
- - **`three-text/core`** - Framework-agnostic core (returns raw arrays)
74
- - **`three-text/vector`** - Vector rendering (Slug per-fragment curve evaluation), `Text.create()` returns a `THREE.Group`
75
- - **`three-text/vector/react`** - React Three Fiber component for vector text
76
- - **`three-text/vector/core`** - Framework-agnostic vector core (returns raw `SlugGPUData`, no Three.js dependency)
77
- - **`three-text/vector/webgl`** - Raw WebGL2 vector renderer (no Three.js dependency)
78
- - **`three-text/vector/webgpu`** - Raw WebGPU vector renderer (no Three.js dependency)
79
- - **`three-text/webgl`** - Deprecated, use `three-text/mesh/webgl`
80
- - **`three-text/webgpu`** - Deprecated, use `three-text/mesh/webgpu`
81
- - **`three-text/p5`** - Deprecated, use `three-text/mesh/p5`
82
-
83
- Most users will just `import { Text } from 'three-text'` for Three.js projects with mesh, or `import { Text } from 'three-text/vector'` for vector text
62
+ three-text has a framework-agnostic core that processes fonts and generates geometry data. Lightweight adapters convert this data to framework-specific formats. Most users will just `import { Text } from 'three-text'` for Three.js projects with mesh, or `import { Text } from 'three-text/vector'` for vector text. See [Entry points](#entry-points) for the full list of adapters
84
63
 
85
64
  ### Mesh vs vector
86
65
 
87
66
  The library offers two rendering modes that share the same core (HarfBuzz shaping, Knuth-Plass justification, glyph caching):
88
67
 
89
68
  - **Mesh** (`three-text` (default) / `three-text/mesh`): triangulated geometry you can extrude, light, and shade. Use for 3D text, text in a scene graph, or anywhere you need depth
90
- - **Vector** (`three-text/vector`): resolution-independent rendering on the GPU via per-fragment curve evaluation. No tessellation, no stencil buffer. Use for text that needs to stay sharp at arbitrary zoom
69
+ - **Vector** (`three-text/vector`): resolution-independent rendering on the GPU via per-fragment curve evaluation. Use for flat text that needs to stay sharp at arbitrary zoom
91
70
 
92
71
  Both can be used in the same project from separate entry points
93
72
 
@@ -483,6 +462,8 @@ Existing solutions take different approaches:
483
462
 
484
463
  three-text produces actual geometry from font files, sharper at close distances than bitmap approaches, with control over typesetting and paragraph justification via TeX-based parameters
485
464
 
465
+ Under the hood, three-text relies on a core of [harfbuzzjs](https://github.com/harfbuzz/harfbuzzjs) (based on [HarfBuzz](https://github.com/harfbuzz/harfbuzz) by Behdad Esfahbod et al) for text shaping, [Knuth-Plass](http://www.eprg.org/G53DOC/pdfs/knuth-plass-breaking.pdf) line breaking (with [SILE](https://github.com/sile-typesetter/sile/blob/master/core/break.lua) and LuaTex being the closest modern references), [Liang](https://tug.org/docs/liang/liang-thesis.pdf) hyphenation and the [TeX hyphenation patterns](https://github.com/hyphenation/tex-hyphen), and [woff-lib](https://github.com/countertype/woff-lib) for optional WOFF2 support. The mesh text pipeline uses [libtess-ts](https://github.com/countertype/libtess-ts) (a port of the [GLU tessellator](https://www.songho.ca/opengl/gl_tessellation.html) by Eric Veach) for removing overlaps and triangulation, adaptive curve polygonization from Maxim Shemanarev's [Anti-Grain Geometry](https://web.archive.org/web/20060128212843/http://www.antigrain.com/research/adaptive_bezier/index.html), and [Visvalingam-Whyatt](https://hull-repository.worktribe.com/preview/376364/000870493786962263.pdf) [line simplification](https://bost.ocks.org/mike/simplify/). The vector pipeline uses [Slug](https://github.com/EricLengyel/Slug) by Eric Lengyel for resolution-independent curve rendering
466
+
486
467
  ### Why Slug
487
468
 
488
469
  The vector path uses the Slug algorithm by Eric Lengyel. Each glyph is a single quad; the fragment shader evaluates curve coverage analytically to compute a winding number. Because it operates on winding rather than geometry, it naturally handles the self-intersecting contours that variable fonts produce when axes like weight push outlines into each other
@@ -1256,6 +1237,22 @@ git submodule update --init --recursive
1256
1237
 
1257
1238
  The script then processes the TeX hyphenation data into optimized trie structures. The process is slow for the complete set of languages (~1 minute on an M2 Max), so using `--languages` for development is recommended
1258
1239
 
1240
+ ## Entry points
1241
+
1242
+ - **`three-text`** - Three.js adapter (default export, returns mesh `BufferGeometry`)
1243
+ - **`three-text/mesh`** - Same as above (explicit alias)
1244
+ - **`three-text/mesh/react`** - React Three Fiber component for extruded mesh text
1245
+ - **`three-text/three/react`** - Deprecated, use `three-text/mesh/react`
1246
+ - **`three-text/mesh/webgl`** - WebGL mesh buffer utility
1247
+ - **`three-text/mesh/webgpu`** - WebGPU mesh buffer utility
1248
+ - **`three-text/mesh/p5`** - p5.js adapter
1249
+ - **`three-text/core`** - Framework-agnostic core (returns raw arrays)
1250
+ - **`three-text/vector`** - Vector rendering (Slug per-fragment curve evaluation), `Text.create()` returns a `THREE.Group`
1251
+ - **`three-text/vector/react`** - React Three Fiber component for vector text
1252
+ - **`three-text/vector/core`** - Framework-agnostic vector core (returns raw `SlugGPUData`, no Three.js dependency)
1253
+ - **`three-text/vector/webgl`** - Raw WebGL2 vector renderer (no Three.js dependency)
1254
+ - **`three-text/vector/webgpu`** - Raw WebGPU vector renderer (no Three.js dependency)
1255
+
1259
1256
  ## Build outputs
1260
1257
 
1261
1258
  The build generates multiple module formats for core and all adapters:
@@ -1289,4 +1286,4 @@ The build generates multiple module formats for core and all adapters:
1289
1286
 
1290
1287
  `three-text` was written by Jeremy Tribby ([@jpt](https://github.com/jpt)) and is licensed under the MIT License. See the [LICENSE](LICENSE) file for details
1291
1288
 
1292
- This software includes code from third-party libraries under compatible permissive licenses. For full license details, see the [LICENSE_THIRD_PARTY](LICENSE_THIRD_PARTY) file
1289
+ This software includes code from third-party libraries under compatible permissive licenses. For full license details, see the [LICENSE_THIRD_PARTY](LICENSE_THIRD_PARTY) file
package/dist/index.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  /*!
2
2
  * @license
3
- * three-text v0.6.0
3
+ * three-text v0.6.2
4
4
  * Copyright © 2025-2026 Jeremy Tribby, Countertype LLC
5
5
  * SPDX-License-Identifier: MIT
6
6
  */
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /*!
2
2
  * @license
3
- * three-text v0.6.0
3
+ * three-text v0.6.2
4
4
  * Copyright © 2025-2026 Jeremy Tribby, Countertype LLC
5
5
  * SPDX-License-Identifier: MIT
6
6
  */
@@ -1,6 +1,6 @@
1
1
  /*!
2
2
  * @license
3
- * three-text v0.6.0
3
+ * three-text v0.6.2
4
4
  * Copyright © 2025-2026 Jeremy Tribby, Countertype LLC
5
5
  * SPDX-License-Identifier: MIT
6
6
  */
package/dist/index.min.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /*!
2
2
  * @license
3
- * three-text v0.6.0
3
+ * three-text v0.6.2
4
4
  * Copyright © 2025-2026 Jeremy Tribby, Countertype LLC
5
5
  * SPDX-License-Identifier: MIT
6
6
  */
package/dist/index.umd.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /*!
2
2
  * @license
3
- * three-text v0.6.0
3
+ * three-text v0.6.2
4
4
  * Copyright © 2025-2026 Jeremy Tribby, Countertype LLC
5
5
  * SPDX-License-Identifier: MIT
6
6
  */
@@ -1,6 +1,6 @@
1
1
  /*!
2
2
  * @license
3
- * three-text v0.6.0
3
+ * three-text v0.6.2
4
4
  * Copyright © 2025-2026 Jeremy Tribby, Countertype LLC
5
5
  * SPDX-License-Identifier: MIT
6
6
  */
@@ -258,7 +258,6 @@ class GlyphOutlineCollector {
258
258
  // GPU-ready packed textures + vertex attribute buffers
259
259
  const TEX_WIDTH = 4096;
260
260
  const LOG_TEX_WIDTH = 12;
261
- // Float/Uint32 reinterpretation helpers
262
261
  const _f32 = new Float32Array(1);
263
262
  const _u32 = new Uint32Array(_f32.buffer);
264
263
  function uintAsFloat(u) {
@@ -270,9 +269,7 @@ function uintAsFloat(u) {
270
269
  function packSlugData(shapes, options) {
271
270
  const bandCount = options?.bandCount ?? 16;
272
271
  const evenOdd = options?.evenOdd ?? false;
273
- // Pack all curves into curveTexture
274
272
  const allCurves = [];
275
- // Estimate max texels needed
276
273
  let totalCurves = 0;
277
274
  for (const shape of shapes) {
278
275
  totalCurves += shape.curves.length;
@@ -283,28 +280,33 @@ function packSlugData(shapes, options) {
283
280
  let curveY = 0;
284
281
  for (const shape of shapes) {
285
282
  const entries = [];
283
+ const [ox, oy] = shape.bounds;
286
284
  for (const curve of shape.curves) {
287
- // Don't let a curve span across row boundary (needs 2 consecutive texels)
288
285
  if (curveX >= TEX_WIDTH - 1) {
289
286
  curveX = 0;
290
287
  curveY++;
291
288
  }
289
+ // Glyph-local space (relative to bounds min) keeps renderCoord
290
+ // subtraction near zero, preserving float32 precision in the solver
291
+ const lp1x = curve.p1[0] - ox, lp1y = curve.p1[1] - oy;
292
+ const lp2x = curve.p2[0] - ox, lp2y = curve.p2[1] - oy;
293
+ const lp3x = curve.p3[0] - ox, lp3y = curve.p3[1] - oy;
292
294
  const base = (curveY * TEX_WIDTH + curveX) * 4;
293
- curveData[base + 0] = curve.p1[0];
294
- curveData[base + 1] = curve.p1[1];
295
- curveData[base + 2] = curve.p2[0];
296
- curveData[base + 3] = curve.p2[1];
295
+ curveData[base + 0] = lp1x;
296
+ curveData[base + 1] = lp1y;
297
+ curveData[base + 2] = lp2x;
298
+ curveData[base + 3] = lp2y;
297
299
  const base2 = base + 4;
298
- curveData[base2 + 0] = curve.p3[0];
299
- curveData[base2 + 1] = curve.p3[1];
300
- const minX = Math.min(curve.p1[0], curve.p2[0], curve.p3[0]);
301
- const minY = Math.min(curve.p1[1], curve.p2[1], curve.p3[1]);
302
- const maxX = Math.max(curve.p1[0], curve.p2[0], curve.p3[0]);
303
- const maxY = Math.max(curve.p1[1], curve.p2[1], curve.p3[1]);
300
+ curveData[base2 + 0] = lp3x;
301
+ curveData[base2 + 1] = lp3y;
302
+ const minX = Math.min(lp1x, lp2x, lp3x);
303
+ const minY = Math.min(lp1y, lp2y, lp3y);
304
+ const maxX = Math.max(lp1x, lp2x, lp3x);
305
+ const maxY = Math.max(lp1y, lp2y, lp3y);
304
306
  entries.push({
305
- p1x: curve.p1[0], p1y: curve.p1[1],
306
- p2x: curve.p2[0], p2y: curve.p2[1],
307
- p3x: curve.p3[0], p3y: curve.p3[1],
307
+ p1x: lp1x, p1y: lp1y,
308
+ p2x: lp2x, p2y: lp2y,
309
+ p3x: lp3x, p3y: lp3y,
308
310
  minX, minY, maxX, maxY,
309
311
  curveTexX: curveX,
310
312
  curveTexY: curveY
@@ -314,12 +316,10 @@ function packSlugData(shapes, options) {
314
316
  allCurves.push(entries);
315
317
  }
316
318
  const actualCurveTexHeight = curveY + 1;
317
- // Build band data for each shape and pack into bandTexture
318
- // Layout per shape in bandTexture (relative to glyphLoc):
319
- // [0 .. hBandMax] : h-band headers
320
- // [hBandMax+1 .. hBandMax+1+vBandMax] : v-band headers
321
- // [hBandMax+vBandMax+2 .. ] : curve index lists
322
- // First pass: compute total band texels needed
319
+ // Band texture layout per shape (relative to glyphLoc):
320
+ // [0 .. hBandMax] h-band headers
321
+ // [hBandMax+1 .. hBandMax+1+vBandMax] v-band headers
322
+ // [hBandMax+vBandMax+2 .. ] curve index lists
323
323
  const shapeBandData = [];
324
324
  let totalBandTexels = 0;
325
325
  for (let si = 0; si < shapes.length; si++) {
@@ -339,21 +339,19 @@ function packSlugData(shapes, options) {
339
339
  const vBandCount = Math.min(bandCount, 255);
340
340
  const bandMaxY = hBandCount - 1;
341
341
  const bandMaxX = vBandCount - 1;
342
- // Build horizontal bands (partition y-axis)
342
+ // Horizontal bands (partition y-axis)
343
343
  const hBands = [];
344
344
  const hLists = [];
345
345
  const bandH = h / hBandCount;
346
346
  for (let bi = 0; bi < hBandCount; bi++) {
347
- const bandMinY = bMinY + bi * bandH;
347
+ const bandMinY = bi * bandH;
348
348
  const bandMaxYCoord = bandMinY + bandH;
349
- // Collect curves whose y-range overlaps this band
350
349
  const list = [];
351
350
  for (const c of curves) {
352
351
  if (c.maxY >= bandMinY && c.minY <= bandMaxYCoord) {
353
352
  list.push({ curve: c, sortKey: c.maxX });
354
353
  }
355
354
  }
356
- // Sort by descending max-x for early exit
357
355
  list.sort((a, b) => b.sortKey - a.sortKey);
358
356
  const flatList = [];
359
357
  for (const item of list) {
@@ -362,12 +360,12 @@ function packSlugData(shapes, options) {
362
360
  hBands.push({ curveCount: list.length, listOffset: 0 });
363
361
  hLists.push(flatList);
364
362
  }
365
- // Build vertical bands (partition x-axis)
363
+ // Vertical bands (partition x-axis)
366
364
  const vBands = [];
367
365
  const vLists = [];
368
366
  const bandW = w / vBandCount;
369
367
  for (let bi = 0; bi < vBandCount; bi++) {
370
- const bandMinX = bMinX + bi * bandW;
368
+ const bandMinX = bi * bandW;
371
369
  const bandMaxXCoord = bandMinX + bandW;
372
370
  const list = [];
373
371
  for (const c of curves) {
@@ -375,7 +373,6 @@ function packSlugData(shapes, options) {
375
373
  list.push({ curve: c, sortKey: c.maxY });
376
374
  }
377
375
  }
378
- // Sort by descending max-y for early exit
379
376
  list.sort((a, b) => b.sortKey - a.sortKey);
380
377
  const flatList = [];
381
378
  for (const item of list) {
@@ -384,7 +381,6 @@ function packSlugData(shapes, options) {
384
381
  vBands.push({ curveCount: list.length, listOffset: 0 });
385
382
  vLists.push(flatList);
386
383
  }
387
- // Total texels for this shape: band headers + curve lists
388
384
  const headerTexels = hBandCount + vBandCount;
389
385
  let listTexels = 0;
390
386
  for (const l of hLists)
@@ -398,10 +394,8 @@ function packSlugData(shapes, options) {
398
394
  });
399
395
  totalBandTexels += total;
400
396
  }
401
- // Allocate bandTexture (extra rows for row-alignment padding of curve lists)
402
397
  const bandTexHeight = Math.max(1, Math.ceil(totalBandTexels / TEX_WIDTH) + shapes.length * 2);
403
398
  const bandData = new Uint32Array(TEX_WIDTH * bandTexHeight * 4);
404
- // Pack band data per shape
405
399
  let bandX = 0;
406
400
  let bandY = 0;
407
401
  const glyphLocs = [];
@@ -411,11 +405,8 @@ function packSlugData(shapes, options) {
411
405
  glyphLocs.push({ x: 0, y: 0 });
412
406
  continue;
413
407
  }
414
- // Ensure glyph data doesn't start too close to row end
415
- // (need at least headerTexels contiguous... actually wrapping is handled by CalcBandLoc)
416
- // But the initial band header reads don't use CalcBandLoc, so glyphLoc.x + bandMax.y + 1 + bandMaxX
417
- // must be reachable. CalcBandLoc handles wrapping for curve lists.
418
- // To be safe, start each glyph at the beginning of a row if remaining space is tight.
408
+ // Band headers are read without CalcBandLoc wrapping, so all
409
+ // headers for a glyph must fit within a single texture row
419
410
  const minContiguous = sd.hBands.length + sd.vBands.length;
420
411
  if (bandX + minContiguous > TEX_WIDTH) {
421
412
  bandX = 0;
@@ -424,11 +415,8 @@ function packSlugData(shapes, options) {
424
415
  const glyphLocX = bandX;
425
416
  const glyphLocY = bandY;
426
417
  glyphLocs.push({ x: glyphLocX, y: glyphLocY });
427
- // Curve lists start after all headers
428
418
  let listStartOffset = sd.hBands.length + sd.vBands.length;
429
- // The shader reads curve list entries at (hbandLoc.x + curveIndex, hbandLoc.y)
430
- // with NO row wrapping. Each list must fit entirely within a single texture row.
431
- // Pad the offset to the next row start when a list would cross a row boundary.
419
+ // Curve lists aren't row-wrapped by the shader, so pad to avoid crossing
432
420
  const ensureListFits = (listLen) => {
433
421
  if (listLen === 0)
434
422
  return;
@@ -437,21 +425,18 @@ function packSlugData(shapes, options) {
437
425
  listStartOffset += (TEX_WIDTH - startX);
438
426
  }
439
427
  };
440
- // Assign list offsets for h-bands
441
428
  for (let bi = 0; bi < sd.hBands.length; bi++) {
442
429
  const listLen = sd.hLists[bi].length / 2;
443
430
  ensureListFits(listLen);
444
431
  sd.hBands[bi].listOffset = listStartOffset;
445
432
  listStartOffset += listLen;
446
433
  }
447
- // Assign list offsets for v-bands
448
434
  for (let bi = 0; bi < sd.vBands.length; bi++) {
449
435
  const listLen = sd.vLists[bi].length / 2;
450
436
  ensureListFits(listLen);
451
437
  sd.vBands[bi].listOffset = listStartOffset;
452
438
  listStartOffset += listLen;
453
439
  }
454
- // Write h-band headers
455
440
  for (let bi = 0; bi < sd.hBands.length; bi++) {
456
441
  const tx = glyphLocX + bi;
457
442
  const ty = glyphLocY;
@@ -461,7 +446,6 @@ function packSlugData(shapes, options) {
461
446
  bandData[idx + 2] = 0;
462
447
  bandData[idx + 3] = 0;
463
448
  }
464
- // Write v-band headers (after h-bands)
465
449
  const vBandStart = glyphLocX + sd.hBands.length;
466
450
  for (let bi = 0; bi < sd.vBands.length; bi++) {
467
451
  const tx = vBandStart + bi;
@@ -472,9 +456,7 @@ function packSlugData(shapes, options) {
472
456
  bandData[idx + 2] = 0;
473
457
  bandData[idx + 3] = 0;
474
458
  }
475
- // Write curve lists using CalcBandLoc-style wrapping
476
459
  const texWidthMask = (1 << LOG_TEX_WIDTH) - 1;
477
- // Write h-band curve lists
478
460
  for (let bi = 0; bi < sd.hBands.length; bi++) {
479
461
  const list = sd.hLists[bi];
480
462
  const baseOffset = sd.hBands[bi].listOffset;
@@ -483,13 +465,12 @@ function packSlugData(shapes, options) {
483
465
  const by = glyphLocY + (bx >> LOG_TEX_WIDTH);
484
466
  bx &= texWidthMask;
485
467
  const idx = (by * TEX_WIDTH + bx) * 4;
486
- bandData[idx + 0] = list[ci]; // curveTexX
487
- bandData[idx + 1] = list[ci + 1]; // curveTexY
468
+ bandData[idx + 0] = list[ci];
469
+ bandData[idx + 1] = list[ci + 1];
488
470
  bandData[idx + 2] = 0;
489
471
  bandData[idx + 3] = 0;
490
472
  }
491
473
  }
492
- // Write v-band curve lists
493
474
  for (let bi = 0; bi < sd.vBands.length; bi++) {
494
475
  const list = sd.vLists[bi];
495
476
  const baseOffset = sd.vBands[bi].listOffset;
@@ -504,24 +485,20 @@ function packSlugData(shapes, options) {
504
485
  bandData[idx + 3] = 0;
505
486
  }
506
487
  }
507
- // Advance band cursor past this shape's data
508
488
  let endBx = glyphLocX + listStartOffset;
509
489
  bandY = glyphLocY + (endBx >> LOG_TEX_WIDTH);
510
490
  bandX = endBx & texWidthMask;
511
491
  }
512
492
  const actualBandTexHeight = bandY + 1;
513
- // Build vertex attributes
514
- // 5 attribs x 4 floats x 4 vertices per shape = 80 floats per shape
515
493
  const FLOATS_PER_VERTEX = 20; // 5 attribs * 4 components
516
494
  const VERTS_PER_SHAPE = 4;
517
495
  const vertices = new Float32Array(shapes.length * VERTS_PER_SHAPE * FLOATS_PER_VERTEX);
518
496
  const indices = new Uint16Array(shapes.length * 6);
519
- // Corner normals (outward-pointing, un-normalized; SlugDilate normalizes)
520
497
  const cornerNormals = [
521
- [-1, -1], // bottom-left
522
- [1, -1], // bottom-right
523
- [1, 1], // top-right
524
- [-1, 1], // top-left
498
+ [-1, -1],
499
+ [1, -1],
500
+ [1, 1],
501
+ [-1, 1],
525
502
  ];
526
503
  for (let si = 0; si < shapes.length; si++) {
527
504
  const shape = shapes[si];
@@ -530,32 +507,25 @@ function packSlugData(shapes, options) {
530
507
  const [bMinX, bMinY, bMaxX, bMaxY] = shape.bounds;
531
508
  const w = bMaxX - bMinX;
532
509
  const h = bMaxY - bMinY;
533
- // Corner positions in object-space
534
510
  const corners = [
535
511
  [bMinX, bMinY],
536
512
  [bMaxX, bMinY],
537
513
  [bMaxX, bMaxY],
538
514
  [bMinX, bMaxY],
539
515
  ];
540
- // Em-space sample coords at corners (same as object-space for 1:1 mapping)
541
516
  const emCorners = [
542
- [bMinX, bMinY],
543
- [bMaxX, bMinY],
544
- [bMaxX, bMaxY],
545
- [bMinX, bMaxY],
517
+ [0, 0],
518
+ [w, 0],
519
+ [w, h],
520
+ [0, h],
546
521
  ];
547
- // Pack tex.z: glyph location in band texture
548
522
  const texZ = uintAsFloat((glyph.x & 0xFFFF) | ((glyph.y & 0xFFFF) << 16));
549
- // Pack tex.w: band max + flags
550
523
  let texWBits = (sd.bandMaxX & 0xFF) | ((sd.bandMaxY & 0xFF) << 16);
551
524
  if (evenOdd)
552
525
  texWBits |= 0x10000000; // E flag at bit 28
553
526
  const texW = uintAsFloat(texWBits);
554
- // Band transform: scale and offset to map em-coords to band indices
555
527
  const bandScaleX = w > 0 ? sd.vBands.length / w : 0;
556
528
  const bandScaleY = h > 0 ? sd.hBands.length / h : 0;
557
- const bandOffsetX = -bMinX * bandScaleX;
558
- const bandOffsetY = -bMinY * bandScaleY;
559
529
  for (let vi = 0; vi < 4; vi++) {
560
530
  const base = (si * 4 + vi) * FLOATS_PER_VERTEX;
561
531
  // pos: .xy = position, .zw = normal
@@ -568,23 +538,22 @@ function packSlugData(shapes, options) {
568
538
  vertices[base + 5] = emCorners[vi][1];
569
539
  vertices[base + 6] = texZ;
570
540
  vertices[base + 7] = texW;
571
- // jac: identity Jacobian (em-space = object-space)
541
+ // jac: identity (em-space is a translation of object-space)
572
542
  vertices[base + 8] = 1.0;
573
543
  vertices[base + 9] = 0.0;
574
544
  vertices[base + 10] = 0.0;
575
545
  vertices[base + 11] = 1.0;
576
- // bnd: band scale and offset
546
+ // bnd: band scale (offset zero in glyph-local space)
577
547
  vertices[base + 12] = bandScaleX;
578
548
  vertices[base + 13] = bandScaleY;
579
- vertices[base + 14] = bandOffsetX;
580
- vertices[base + 15] = bandOffsetY;
581
- // col: white with full alpha (caller overrides via uniform or attribute)
549
+ vertices[base + 14] = 0;
550
+ vertices[base + 15] = 0;
551
+ // col
582
552
  vertices[base + 16] = 1.0;
583
553
  vertices[base + 17] = 1.0;
584
554
  vertices[base + 18] = 1.0;
585
555
  vertices[base + 19] = 1.0;
586
556
  }
587
- // Indices: two triangles per quad
588
557
  const vBase = si * 4;
589
558
  const iBase = si * 6;
590
559
  indices[iBase + 0] = vBase + 0;
@@ -793,7 +762,6 @@ function computePlaneBounds(glyphInfos) {
793
762
  }
794
763
  return { min: { x: minX, y: minY, z: 0 }, max: { x: maxX, y: maxY, z: 0 } };
795
764
  }
796
- // Public API
797
765
  function buildVectorResult(layoutHandle, ctx, options) {
798
766
  const scale = layoutHandle.layoutData.pixelsPerFontUnit;
799
767
  let cachedQuery = null;