three-text 0.6.1 → 0.6.3

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.1
3
+ * three-text v0.6.3
4
4
  * Copyright © 2025-2026 Jeremy Tribby, Countertype LLC
5
5
  * SPDX-License-Identifier: MIT
6
6
  */
package/dist/index.d.ts CHANGED
@@ -410,7 +410,6 @@ interface TextOptions {
410
410
  geometryOptimization?: GeometryOptimizationOptions;
411
411
  layout?: LayoutOptions;
412
412
  color?: [number, number, number] | ColorOptions;
413
- /** Enable rotated RGSS-4 adaptive supersampling (4 samples per pixel). Takes effect when the GLSL rendering path is active. */
414
413
  adaptiveSupersampling?: boolean;
415
414
  }
416
415
  interface HyphenationPatternsMap {
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /*!
2
2
  * @license
3
- * three-text v0.6.1
3
+ * three-text v0.6.3
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.1
3
+ * three-text v0.6.3
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.1
3
+ * three-text v0.6.3
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.1
3
+ * three-text v0.6.3
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.1
3
+ * three-text v0.6.3
4
4
  * Copyright © 2025-2026 Jeremy Tribby, Countertype LLC
5
5
  * SPDX-License-Identifier: MIT
6
6
  */
@@ -208,7 +208,6 @@ interface TextOptions {
208
208
  geometryOptimization?: GeometryOptimizationOptions;
209
209
  layout?: LayoutOptions;
210
210
  color?: [number, number, number] | ColorOptions;
211
- /** Enable rotated RGSS-4 adaptive supersampling (4 samples per pixel). Takes effect when the GLSL rendering path is active. */
212
211
  adaptiveSupersampling?: boolean;
213
212
  }
214
213
  interface HyphenationPatternsMap {
@@ -370,7 +370,6 @@ export interface TextOptions {
370
370
  geometryOptimization?: GeometryOptimizationOptions;
371
371
  layout?: LayoutOptions;
372
372
  color?: [number, number, number] | ColorOptions;
373
- /** Enable rotated RGSS-4 adaptive supersampling (4 samples per pixel). Takes effect when the GLSL rendering path is active. */
374
373
  adaptiveSupersampling?: boolean;
375
374
  }
376
375
  export interface HyphenationPatternsMap {
@@ -12,6 +12,7 @@ export interface VectorTextResult {
12
12
  query(options: TextQueryOptions): TextRange[];
13
13
  getLoadedFont(): LoadedFont | undefined;
14
14
  measureTextWidth(text: string, letterSpacing?: number): number;
15
+ setColor(r: number, g: number, b: number): void;
15
16
  update(options: Partial<TextOptions>): Promise<VectorTextResult>;
16
17
  dispose(): void;
17
18
  }
@@ -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;
@@ -285,13 +282,12 @@ function packSlugData(shapes, options) {
285
282
  const entries = [];
286
283
  const [ox, oy] = shape.bounds;
287
284
  for (const curve of shape.curves) {
288
- // Don't let a curve span across row boundary (needs 2 consecutive texels)
289
285
  if (curveX >= TEX_WIDTH - 1) {
290
286
  curveX = 0;
291
287
  curveY++;
292
288
  }
293
- // Store control points in glyph-local space (relative to bounds min)
294
- // so the fragment shader's renderCoord subtraction stays near zero
289
+ // Glyph-local space (relative to bounds min) keeps renderCoord
290
+ // subtraction near zero, preserving float32 precision in the solver
295
291
  const lp1x = curve.p1[0] - ox, lp1y = curve.p1[1] - oy;
296
292
  const lp2x = curve.p2[0] - ox, lp2y = curve.p2[1] - oy;
297
293
  const lp3x = curve.p3[0] - ox, lp3y = curve.p3[1] - oy;
@@ -320,12 +316,10 @@ function packSlugData(shapes, options) {
320
316
  allCurves.push(entries);
321
317
  }
322
318
  const actualCurveTexHeight = curveY + 1;
323
- // Build band data for each shape and pack into bandTexture
324
- // Layout per shape in bandTexture (relative to glyphLoc):
325
- // [0 .. hBandMax] : h-band headers
326
- // [hBandMax+1 .. hBandMax+1+vBandMax] : v-band headers
327
- // [hBandMax+vBandMax+2 .. ] : curve index lists
328
- // 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
329
323
  const shapeBandData = [];
330
324
  let totalBandTexels = 0;
331
325
  for (let si = 0; si < shapes.length; si++) {
@@ -345,21 +339,19 @@ function packSlugData(shapes, options) {
345
339
  const vBandCount = Math.min(bandCount, 255);
346
340
  const bandMaxY = hBandCount - 1;
347
341
  const bandMaxX = vBandCount - 1;
348
- // Build horizontal bands (partition y-axis, glyph-local coords)
342
+ // Horizontal bands (partition y-axis)
349
343
  const hBands = [];
350
344
  const hLists = [];
351
345
  const bandH = h / hBandCount;
352
346
  for (let bi = 0; bi < hBandCount; bi++) {
353
347
  const bandMinY = bi * bandH;
354
348
  const bandMaxYCoord = bandMinY + bandH;
355
- // Collect curves whose y-range overlaps this band
356
349
  const list = [];
357
350
  for (const c of curves) {
358
351
  if (c.maxY >= bandMinY && c.minY <= bandMaxYCoord) {
359
352
  list.push({ curve: c, sortKey: c.maxX });
360
353
  }
361
354
  }
362
- // Sort by descending max-x for early exit
363
355
  list.sort((a, b) => b.sortKey - a.sortKey);
364
356
  const flatList = [];
365
357
  for (const item of list) {
@@ -368,7 +360,7 @@ function packSlugData(shapes, options) {
368
360
  hBands.push({ curveCount: list.length, listOffset: 0 });
369
361
  hLists.push(flatList);
370
362
  }
371
- // Build vertical bands (partition x-axis, glyph-local coords)
363
+ // Vertical bands (partition x-axis)
372
364
  const vBands = [];
373
365
  const vLists = [];
374
366
  const bandW = w / vBandCount;
@@ -381,7 +373,6 @@ function packSlugData(shapes, options) {
381
373
  list.push({ curve: c, sortKey: c.maxY });
382
374
  }
383
375
  }
384
- // Sort by descending max-y for early exit
385
376
  list.sort((a, b) => b.sortKey - a.sortKey);
386
377
  const flatList = [];
387
378
  for (const item of list) {
@@ -390,7 +381,6 @@ function packSlugData(shapes, options) {
390
381
  vBands.push({ curveCount: list.length, listOffset: 0 });
391
382
  vLists.push(flatList);
392
383
  }
393
- // Total texels for this shape: band headers + curve lists
394
384
  const headerTexels = hBandCount + vBandCount;
395
385
  let listTexels = 0;
396
386
  for (const l of hLists)
@@ -404,10 +394,8 @@ function packSlugData(shapes, options) {
404
394
  });
405
395
  totalBandTexels += total;
406
396
  }
407
- // Allocate bandTexture (extra rows for row-alignment padding of curve lists)
408
397
  const bandTexHeight = Math.max(1, Math.ceil(totalBandTexels / TEX_WIDTH) + shapes.length * 2);
409
398
  const bandData = new Uint32Array(TEX_WIDTH * bandTexHeight * 4);
410
- // Pack band data per shape
411
399
  let bandX = 0;
412
400
  let bandY = 0;
413
401
  const glyphLocs = [];
@@ -417,11 +405,8 @@ function packSlugData(shapes, options) {
417
405
  glyphLocs.push({ x: 0, y: 0 });
418
406
  continue;
419
407
  }
420
- // Ensure glyph data doesn't start too close to row end
421
- // (need at least headerTexels contiguous... actually wrapping is handled by CalcBandLoc)
422
- // But the initial band header reads don't use CalcBandLoc, so glyphLoc.x + bandMax.y + 1 + bandMaxX
423
- // must be reachable. CalcBandLoc handles wrapping for curve lists.
424
- // 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
425
410
  const minContiguous = sd.hBands.length + sd.vBands.length;
426
411
  if (bandX + minContiguous > TEX_WIDTH) {
427
412
  bandX = 0;
@@ -430,11 +415,8 @@ function packSlugData(shapes, options) {
430
415
  const glyphLocX = bandX;
431
416
  const glyphLocY = bandY;
432
417
  glyphLocs.push({ x: glyphLocX, y: glyphLocY });
433
- // Curve lists start after all headers
434
418
  let listStartOffset = sd.hBands.length + sd.vBands.length;
435
- // The shader reads curve list entries at (hbandLoc.x + curveIndex, hbandLoc.y)
436
- // with NO row wrapping. Each list must fit entirely within a single texture row.
437
- // 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
438
420
  const ensureListFits = (listLen) => {
439
421
  if (listLen === 0)
440
422
  return;
@@ -443,21 +425,18 @@ function packSlugData(shapes, options) {
443
425
  listStartOffset += (TEX_WIDTH - startX);
444
426
  }
445
427
  };
446
- // Assign list offsets for h-bands
447
428
  for (let bi = 0; bi < sd.hBands.length; bi++) {
448
429
  const listLen = sd.hLists[bi].length / 2;
449
430
  ensureListFits(listLen);
450
431
  sd.hBands[bi].listOffset = listStartOffset;
451
432
  listStartOffset += listLen;
452
433
  }
453
- // Assign list offsets for v-bands
454
434
  for (let bi = 0; bi < sd.vBands.length; bi++) {
455
435
  const listLen = sd.vLists[bi].length / 2;
456
436
  ensureListFits(listLen);
457
437
  sd.vBands[bi].listOffset = listStartOffset;
458
438
  listStartOffset += listLen;
459
439
  }
460
- // Write h-band headers
461
440
  for (let bi = 0; bi < sd.hBands.length; bi++) {
462
441
  const tx = glyphLocX + bi;
463
442
  const ty = glyphLocY;
@@ -467,7 +446,6 @@ function packSlugData(shapes, options) {
467
446
  bandData[idx + 2] = 0;
468
447
  bandData[idx + 3] = 0;
469
448
  }
470
- // Write v-band headers (after h-bands)
471
449
  const vBandStart = glyphLocX + sd.hBands.length;
472
450
  for (let bi = 0; bi < sd.vBands.length; bi++) {
473
451
  const tx = vBandStart + bi;
@@ -478,9 +456,7 @@ function packSlugData(shapes, options) {
478
456
  bandData[idx + 2] = 0;
479
457
  bandData[idx + 3] = 0;
480
458
  }
481
- // Write curve lists using CalcBandLoc-style wrapping
482
459
  const texWidthMask = (1 << LOG_TEX_WIDTH) - 1;
483
- // Write h-band curve lists
484
460
  for (let bi = 0; bi < sd.hBands.length; bi++) {
485
461
  const list = sd.hLists[bi];
486
462
  const baseOffset = sd.hBands[bi].listOffset;
@@ -489,13 +465,12 @@ function packSlugData(shapes, options) {
489
465
  const by = glyphLocY + (bx >> LOG_TEX_WIDTH);
490
466
  bx &= texWidthMask;
491
467
  const idx = (by * TEX_WIDTH + bx) * 4;
492
- bandData[idx + 0] = list[ci]; // curveTexX
493
- bandData[idx + 1] = list[ci + 1]; // curveTexY
468
+ bandData[idx + 0] = list[ci];
469
+ bandData[idx + 1] = list[ci + 1];
494
470
  bandData[idx + 2] = 0;
495
471
  bandData[idx + 3] = 0;
496
472
  }
497
473
  }
498
- // Write v-band curve lists
499
474
  for (let bi = 0; bi < sd.vBands.length; bi++) {
500
475
  const list = sd.vLists[bi];
501
476
  const baseOffset = sd.vBands[bi].listOffset;
@@ -510,24 +485,20 @@ function packSlugData(shapes, options) {
510
485
  bandData[idx + 3] = 0;
511
486
  }
512
487
  }
513
- // Advance band cursor past this shape's data
514
488
  let endBx = glyphLocX + listStartOffset;
515
489
  bandY = glyphLocY + (endBx >> LOG_TEX_WIDTH);
516
490
  bandX = endBx & texWidthMask;
517
491
  }
518
492
  const actualBandTexHeight = bandY + 1;
519
- // Build vertex attributes
520
- // 5 attribs x 4 floats x 4 vertices per shape = 80 floats per shape
521
493
  const FLOATS_PER_VERTEX = 20; // 5 attribs * 4 components
522
494
  const VERTS_PER_SHAPE = 4;
523
495
  const vertices = new Float32Array(shapes.length * VERTS_PER_SHAPE * FLOATS_PER_VERTEX);
524
496
  const indices = new Uint16Array(shapes.length * 6);
525
- // Corner normals (outward-pointing, un-normalized; SlugDilate normalizes)
526
497
  const cornerNormals = [
527
- [-1, -1], // bottom-left
528
- [1, -1], // bottom-right
529
- [1, 1], // top-right
530
- [-1, 1], // top-left
498
+ [-1, -1],
499
+ [1, -1],
500
+ [1, 1],
501
+ [-1, 1],
531
502
  ];
532
503
  for (let si = 0; si < shapes.length; si++) {
533
504
  const shape = shapes[si];
@@ -536,28 +507,23 @@ function packSlugData(shapes, options) {
536
507
  const [bMinX, bMinY, bMaxX, bMaxY] = shape.bounds;
537
508
  const w = bMaxX - bMinX;
538
509
  const h = bMaxY - bMinY;
539
- // Corner positions in object-space
540
510
  const corners = [
541
511
  [bMinX, bMinY],
542
512
  [bMaxX, bMinY],
543
513
  [bMaxX, bMaxY],
544
514
  [bMinX, bMaxY],
545
515
  ];
546
- // Em-space sample coords in glyph-local space (origin at bounds min)
547
516
  const emCorners = [
548
517
  [0, 0],
549
518
  [w, 0],
550
519
  [w, h],
551
520
  [0, h],
552
521
  ];
553
- // Pack tex.z: glyph location in band texture
554
522
  const texZ = uintAsFloat((glyph.x & 0xFFFF) | ((glyph.y & 0xFFFF) << 16));
555
- // Pack tex.w: band max + flags
556
523
  let texWBits = (sd.bandMaxX & 0xFF) | ((sd.bandMaxY & 0xFF) << 16);
557
524
  if (evenOdd)
558
525
  texWBits |= 0x10000000; // E flag at bit 28
559
526
  const texW = uintAsFloat(texWBits);
560
- // Band transform: scale to map glyph-local em-coords to band indices
561
527
  const bandScaleX = w > 0 ? sd.vBands.length / w : 0;
562
528
  const bandScaleY = h > 0 ? sd.hBands.length / h : 0;
563
529
  for (let vi = 0; vi < 4; vi++) {
@@ -572,23 +538,22 @@ function packSlugData(shapes, options) {
572
538
  vertices[base + 5] = emCorners[vi][1];
573
539
  vertices[base + 6] = texZ;
574
540
  vertices[base + 7] = texW;
575
- // jac: identity Jacobian (em-space is a pure translation of object-space)
541
+ // jac: identity (em-space is a translation of object-space)
576
542
  vertices[base + 8] = 1.0;
577
543
  vertices[base + 9] = 0.0;
578
544
  vertices[base + 10] = 0.0;
579
545
  vertices[base + 11] = 1.0;
580
- // bnd: band scale (offset is zero in glyph-local space)
546
+ // bnd: band scale (offset zero in glyph-local space)
581
547
  vertices[base + 12] = bandScaleX;
582
548
  vertices[base + 13] = bandScaleY;
583
549
  vertices[base + 14] = 0;
584
550
  vertices[base + 15] = 0;
585
- // col: white with full alpha (caller overrides via uniform or attribute)
551
+ // col
586
552
  vertices[base + 16] = 1.0;
587
553
  vertices[base + 17] = 1.0;
588
554
  vertices[base + 18] = 1.0;
589
555
  vertices[base + 19] = 1.0;
590
556
  }
591
- // Indices: two triangles per quad
592
557
  const vBase = si * 4;
593
558
  const iBase = si * 6;
594
559
  indices[iBase + 0] = vBase + 0;
@@ -797,7 +762,6 @@ function computePlaneBounds(glyphInfos) {
797
762
  }
798
763
  return { min: { x: minX, y: minY, z: 0 }, max: { x: maxX, y: maxY, z: 0 } };
799
764
  }
800
- // Public API
801
765
  function buildVectorResult(layoutHandle, ctx, options) {
802
766
  const scale = layoutHandle.layoutData.pixelsPerFontUnit;
803
767
  let cachedQuery = null;