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.
@@ -256,7 +256,6 @@ class GlyphOutlineCollector {
256
256
  // GPU-ready packed textures + vertex attribute buffers
257
257
  const TEX_WIDTH = 4096;
258
258
  const LOG_TEX_WIDTH = 12;
259
- // Float/Uint32 reinterpretation helpers
260
259
  const _f32 = new Float32Array(1);
261
260
  const _u32 = new Uint32Array(_f32.buffer);
262
261
  function uintAsFloat(u) {
@@ -268,9 +267,7 @@ function uintAsFloat(u) {
268
267
  function packSlugData(shapes, options) {
269
268
  const bandCount = options?.bandCount ?? 16;
270
269
  const evenOdd = options?.evenOdd ?? false;
271
- // Pack all curves into curveTexture
272
270
  const allCurves = [];
273
- // Estimate max texels needed
274
271
  let totalCurves = 0;
275
272
  for (const shape of shapes) {
276
273
  totalCurves += shape.curves.length;
@@ -283,13 +280,12 @@ function packSlugData(shapes, options) {
283
280
  const entries = [];
284
281
  const [ox, oy] = shape.bounds;
285
282
  for (const curve of shape.curves) {
286
- // Don't let a curve span across row boundary (needs 2 consecutive texels)
287
283
  if (curveX >= TEX_WIDTH - 1) {
288
284
  curveX = 0;
289
285
  curveY++;
290
286
  }
291
- // Store control points in glyph-local space (relative to bounds min)
292
- // so the fragment shader's renderCoord subtraction stays near zero
287
+ // Glyph-local space (relative to bounds min) keeps renderCoord
288
+ // subtraction near zero, preserving float32 precision in the solver
293
289
  const lp1x = curve.p1[0] - ox, lp1y = curve.p1[1] - oy;
294
290
  const lp2x = curve.p2[0] - ox, lp2y = curve.p2[1] - oy;
295
291
  const lp3x = curve.p3[0] - ox, lp3y = curve.p3[1] - oy;
@@ -318,12 +314,10 @@ function packSlugData(shapes, options) {
318
314
  allCurves.push(entries);
319
315
  }
320
316
  const actualCurveTexHeight = curveY + 1;
321
- // Build band data for each shape and pack into bandTexture
322
- // Layout per shape in bandTexture (relative to glyphLoc):
323
- // [0 .. hBandMax] : h-band headers
324
- // [hBandMax+1 .. hBandMax+1+vBandMax] : v-band headers
325
- // [hBandMax+vBandMax+2 .. ] : curve index lists
326
- // First pass: compute total band texels needed
317
+ // Band texture layout per shape (relative to glyphLoc):
318
+ // [0 .. hBandMax] h-band headers
319
+ // [hBandMax+1 .. hBandMax+1+vBandMax] v-band headers
320
+ // [hBandMax+vBandMax+2 .. ] curve index lists
327
321
  const shapeBandData = [];
328
322
  let totalBandTexels = 0;
329
323
  for (let si = 0; si < shapes.length; si++) {
@@ -343,21 +337,19 @@ function packSlugData(shapes, options) {
343
337
  const vBandCount = Math.min(bandCount, 255);
344
338
  const bandMaxY = hBandCount - 1;
345
339
  const bandMaxX = vBandCount - 1;
346
- // Build horizontal bands (partition y-axis, glyph-local coords)
340
+ // Horizontal bands (partition y-axis)
347
341
  const hBands = [];
348
342
  const hLists = [];
349
343
  const bandH = h / hBandCount;
350
344
  for (let bi = 0; bi < hBandCount; bi++) {
351
345
  const bandMinY = bi * bandH;
352
346
  const bandMaxYCoord = bandMinY + bandH;
353
- // Collect curves whose y-range overlaps this band
354
347
  const list = [];
355
348
  for (const c of curves) {
356
349
  if (c.maxY >= bandMinY && c.minY <= bandMaxYCoord) {
357
350
  list.push({ curve: c, sortKey: c.maxX });
358
351
  }
359
352
  }
360
- // Sort by descending max-x for early exit
361
353
  list.sort((a, b) => b.sortKey - a.sortKey);
362
354
  const flatList = [];
363
355
  for (const item of list) {
@@ -366,7 +358,7 @@ function packSlugData(shapes, options) {
366
358
  hBands.push({ curveCount: list.length, listOffset: 0 });
367
359
  hLists.push(flatList);
368
360
  }
369
- // Build vertical bands (partition x-axis, glyph-local coords)
361
+ // Vertical bands (partition x-axis)
370
362
  const vBands = [];
371
363
  const vLists = [];
372
364
  const bandW = w / vBandCount;
@@ -379,7 +371,6 @@ function packSlugData(shapes, options) {
379
371
  list.push({ curve: c, sortKey: c.maxY });
380
372
  }
381
373
  }
382
- // Sort by descending max-y for early exit
383
374
  list.sort((a, b) => b.sortKey - a.sortKey);
384
375
  const flatList = [];
385
376
  for (const item of list) {
@@ -388,7 +379,6 @@ function packSlugData(shapes, options) {
388
379
  vBands.push({ curveCount: list.length, listOffset: 0 });
389
380
  vLists.push(flatList);
390
381
  }
391
- // Total texels for this shape: band headers + curve lists
392
382
  const headerTexels = hBandCount + vBandCount;
393
383
  let listTexels = 0;
394
384
  for (const l of hLists)
@@ -402,10 +392,8 @@ function packSlugData(shapes, options) {
402
392
  });
403
393
  totalBandTexels += total;
404
394
  }
405
- // Allocate bandTexture (extra rows for row-alignment padding of curve lists)
406
395
  const bandTexHeight = Math.max(1, Math.ceil(totalBandTexels / TEX_WIDTH) + shapes.length * 2);
407
396
  const bandData = new Uint32Array(TEX_WIDTH * bandTexHeight * 4);
408
- // Pack band data per shape
409
397
  let bandX = 0;
410
398
  let bandY = 0;
411
399
  const glyphLocs = [];
@@ -415,11 +403,8 @@ function packSlugData(shapes, options) {
415
403
  glyphLocs.push({ x: 0, y: 0 });
416
404
  continue;
417
405
  }
418
- // Ensure glyph data doesn't start too close to row end
419
- // (need at least headerTexels contiguous... actually wrapping is handled by CalcBandLoc)
420
- // But the initial band header reads don't use CalcBandLoc, so glyphLoc.x + bandMax.y + 1 + bandMaxX
421
- // must be reachable. CalcBandLoc handles wrapping for curve lists.
422
- // To be safe, start each glyph at the beginning of a row if remaining space is tight.
406
+ // Band headers are read without CalcBandLoc wrapping, so all
407
+ // headers for a glyph must fit within a single texture row
423
408
  const minContiguous = sd.hBands.length + sd.vBands.length;
424
409
  if (bandX + minContiguous > TEX_WIDTH) {
425
410
  bandX = 0;
@@ -428,11 +413,8 @@ function packSlugData(shapes, options) {
428
413
  const glyphLocX = bandX;
429
414
  const glyphLocY = bandY;
430
415
  glyphLocs.push({ x: glyphLocX, y: glyphLocY });
431
- // Curve lists start after all headers
432
416
  let listStartOffset = sd.hBands.length + sd.vBands.length;
433
- // The shader reads curve list entries at (hbandLoc.x + curveIndex, hbandLoc.y)
434
- // with NO row wrapping. Each list must fit entirely within a single texture row.
435
- // Pad the offset to the next row start when a list would cross a row boundary.
417
+ // Curve lists aren't row-wrapped by the shader, so pad to avoid crossing
436
418
  const ensureListFits = (listLen) => {
437
419
  if (listLen === 0)
438
420
  return;
@@ -441,21 +423,18 @@ function packSlugData(shapes, options) {
441
423
  listStartOffset += (TEX_WIDTH - startX);
442
424
  }
443
425
  };
444
- // Assign list offsets for h-bands
445
426
  for (let bi = 0; bi < sd.hBands.length; bi++) {
446
427
  const listLen = sd.hLists[bi].length / 2;
447
428
  ensureListFits(listLen);
448
429
  sd.hBands[bi].listOffset = listStartOffset;
449
430
  listStartOffset += listLen;
450
431
  }
451
- // Assign list offsets for v-bands
452
432
  for (let bi = 0; bi < sd.vBands.length; bi++) {
453
433
  const listLen = sd.vLists[bi].length / 2;
454
434
  ensureListFits(listLen);
455
435
  sd.vBands[bi].listOffset = listStartOffset;
456
436
  listStartOffset += listLen;
457
437
  }
458
- // Write h-band headers
459
438
  for (let bi = 0; bi < sd.hBands.length; bi++) {
460
439
  const tx = glyphLocX + bi;
461
440
  const ty = glyphLocY;
@@ -465,7 +444,6 @@ function packSlugData(shapes, options) {
465
444
  bandData[idx + 2] = 0;
466
445
  bandData[idx + 3] = 0;
467
446
  }
468
- // Write v-band headers (after h-bands)
469
447
  const vBandStart = glyphLocX + sd.hBands.length;
470
448
  for (let bi = 0; bi < sd.vBands.length; bi++) {
471
449
  const tx = vBandStart + bi;
@@ -476,9 +454,7 @@ function packSlugData(shapes, options) {
476
454
  bandData[idx + 2] = 0;
477
455
  bandData[idx + 3] = 0;
478
456
  }
479
- // Write curve lists using CalcBandLoc-style wrapping
480
457
  const texWidthMask = (1 << LOG_TEX_WIDTH) - 1;
481
- // Write h-band curve lists
482
458
  for (let bi = 0; bi < sd.hBands.length; bi++) {
483
459
  const list = sd.hLists[bi];
484
460
  const baseOffset = sd.hBands[bi].listOffset;
@@ -487,13 +463,12 @@ function packSlugData(shapes, options) {
487
463
  const by = glyphLocY + (bx >> LOG_TEX_WIDTH);
488
464
  bx &= texWidthMask;
489
465
  const idx = (by * TEX_WIDTH + bx) * 4;
490
- bandData[idx + 0] = list[ci]; // curveTexX
491
- bandData[idx + 1] = list[ci + 1]; // curveTexY
466
+ bandData[idx + 0] = list[ci];
467
+ bandData[idx + 1] = list[ci + 1];
492
468
  bandData[idx + 2] = 0;
493
469
  bandData[idx + 3] = 0;
494
470
  }
495
471
  }
496
- // Write v-band curve lists
497
472
  for (let bi = 0; bi < sd.vBands.length; bi++) {
498
473
  const list = sd.vLists[bi];
499
474
  const baseOffset = sd.vBands[bi].listOffset;
@@ -508,24 +483,20 @@ function packSlugData(shapes, options) {
508
483
  bandData[idx + 3] = 0;
509
484
  }
510
485
  }
511
- // Advance band cursor past this shape's data
512
486
  let endBx = glyphLocX + listStartOffset;
513
487
  bandY = glyphLocY + (endBx >> LOG_TEX_WIDTH);
514
488
  bandX = endBx & texWidthMask;
515
489
  }
516
490
  const actualBandTexHeight = bandY + 1;
517
- // Build vertex attributes
518
- // 5 attribs x 4 floats x 4 vertices per shape = 80 floats per shape
519
491
  const FLOATS_PER_VERTEX = 20; // 5 attribs * 4 components
520
492
  const VERTS_PER_SHAPE = 4;
521
493
  const vertices = new Float32Array(shapes.length * VERTS_PER_SHAPE * FLOATS_PER_VERTEX);
522
494
  const indices = new Uint16Array(shapes.length * 6);
523
- // Corner normals (outward-pointing, un-normalized; SlugDilate normalizes)
524
495
  const cornerNormals = [
525
- [-1, -1], // bottom-left
526
- [1, -1], // bottom-right
527
- [1, 1], // top-right
528
- [-1, 1], // top-left
496
+ [-1, -1],
497
+ [1, -1],
498
+ [1, 1],
499
+ [-1, 1],
529
500
  ];
530
501
  for (let si = 0; si < shapes.length; si++) {
531
502
  const shape = shapes[si];
@@ -534,28 +505,23 @@ function packSlugData(shapes, options) {
534
505
  const [bMinX, bMinY, bMaxX, bMaxY] = shape.bounds;
535
506
  const w = bMaxX - bMinX;
536
507
  const h = bMaxY - bMinY;
537
- // Corner positions in object-space
538
508
  const corners = [
539
509
  [bMinX, bMinY],
540
510
  [bMaxX, bMinY],
541
511
  [bMaxX, bMaxY],
542
512
  [bMinX, bMaxY],
543
513
  ];
544
- // Em-space sample coords in glyph-local space (origin at bounds min)
545
514
  const emCorners = [
546
515
  [0, 0],
547
516
  [w, 0],
548
517
  [w, h],
549
518
  [0, h],
550
519
  ];
551
- // Pack tex.z: glyph location in band texture
552
520
  const texZ = uintAsFloat((glyph.x & 0xFFFF) | ((glyph.y & 0xFFFF) << 16));
553
- // Pack tex.w: band max + flags
554
521
  let texWBits = (sd.bandMaxX & 0xFF) | ((sd.bandMaxY & 0xFF) << 16);
555
522
  if (evenOdd)
556
523
  texWBits |= 0x10000000; // E flag at bit 28
557
524
  const texW = uintAsFloat(texWBits);
558
- // Band transform: scale to map glyph-local em-coords to band indices
559
525
  const bandScaleX = w > 0 ? sd.vBands.length / w : 0;
560
526
  const bandScaleY = h > 0 ? sd.hBands.length / h : 0;
561
527
  for (let vi = 0; vi < 4; vi++) {
@@ -570,23 +536,22 @@ function packSlugData(shapes, options) {
570
536
  vertices[base + 5] = emCorners[vi][1];
571
537
  vertices[base + 6] = texZ;
572
538
  vertices[base + 7] = texW;
573
- // jac: identity Jacobian (em-space is a pure translation of object-space)
539
+ // jac: identity (em-space is a translation of object-space)
574
540
  vertices[base + 8] = 1.0;
575
541
  vertices[base + 9] = 0.0;
576
542
  vertices[base + 10] = 0.0;
577
543
  vertices[base + 11] = 1.0;
578
- // bnd: band scale (offset is zero in glyph-local space)
544
+ // bnd: band scale (offset zero in glyph-local space)
579
545
  vertices[base + 12] = bandScaleX;
580
546
  vertices[base + 13] = bandScaleY;
581
547
  vertices[base + 14] = 0;
582
548
  vertices[base + 15] = 0;
583
- // col: white with full alpha (caller overrides via uniform or attribute)
549
+ // col
584
550
  vertices[base + 16] = 1.0;
585
551
  vertices[base + 17] = 1.0;
586
552
  vertices[base + 18] = 1.0;
587
553
  vertices[base + 19] = 1.0;
588
554
  }
589
- // Indices: two triangles per quad
590
555
  const vBase = si * 4;
591
556
  const iBase = si * 6;
592
557
  indices[iBase + 0] = vBase + 0;
@@ -795,7 +760,6 @@ function computePlaneBounds(glyphInfos) {
795
760
  }
796
761
  return { min: { x: minX, y: minY, z: 0 }, max: { x: maxX, y: maxY, z: 0 } };
797
762
  }
798
- // Public API
799
763
  function buildVectorResult(layoutHandle, ctx, options) {
800
764
  const scale = layoutHandle.layoutData.pixelsPerFontUnit;
801
765
  let cachedQuery = null;
@@ -69,6 +69,7 @@ interface VectorTextResult {
69
69
  query(options: TextQueryOptions): TextRange[];
70
70
  getLoadedFont(): LoadedFont | undefined;
71
71
  measureTextWidth(text: string, letterSpacing?: number): number;
72
+ setColor(r: number, g: number, b: number): void;
72
73
  update(options: Partial<TextOptions>): Promise<VectorTextResult>;
73
74
  dispose(): void;
74
75
  }
@@ -22,8 +22,6 @@ function _interopNamespaceDefault(e) {
22
22
 
23
23
  var THREE__namespace = /*#__PURE__*/_interopNamespaceDefault(THREE);
24
24
 
25
- // Restructures the tightly-packed vertex buffer from SlugPacker into
26
- // separate per-attribute arrays for GPU attribute binding
27
25
  const FLOATS_PER_VERT = 20;
28
26
  function unpackSlugVertices(gpuData) {
29
27
  const vertCount = gpuData.shapeCount * 4;
@@ -45,7 +43,6 @@ function unpackSlugVertices(gpuData) {
45
43
  bandings[i * 4 + 1] = srcF[s + 13];
46
44
  bandings[i * 4 + 2] = srcF[s + 14];
47
45
  bandings[i * 4 + 3] = srcF[s + 15];
48
- // Unpack glyph location and band metadata from bit-packed fields
49
46
  const g0 = srcU[s + 6];
50
47
  const g1 = srcU[s + 7];
51
48
  glyphData[i * 4] = g0 & 0xFFFF;
@@ -57,7 +54,6 @@ function unpackSlugVertices(gpuData) {
57
54
  colors[i * 4 + 2] = srcF[s + 18];
58
55
  colors[i * 4 + 3] = srcF[s + 19];
59
56
  }
60
- // Per-glyph center: average of quad corner positions
61
57
  const glyphCenters = new Float32Array(vertCount * 3);
62
58
  const glyphIndices = new Float32Array(vertCount);
63
59
  for (let g = 0; g < gpuData.shapeCount; g++) {
@@ -82,23 +78,14 @@ function unpackSlugVertices(gpuData) {
82
78
 
83
79
  var fragGLSL = "#version 300 es\n// GLSL 300 es port of the Slug fragment shader\n// Eric Lengyel, MIT License, 2017\n\nprecision highp float;\nprecision highp int;\n\n#define kLogBandTextureWidth 12\n\nin vec4 v_color;\nin vec2 v_texcoord;\nflat in vec4 v_banding;\nflat in ivec4 v_glyph;\n\nuniform sampler2D curveTexture; // RGBA32F control points\nuniform highp usampler2D bandTexture; // RGBA32UI band data\n\nlayout(location = 0) out vec4 outColor;\n\nuint CalcRootCode(float y1, float y2, float y3) {\n uint i1 = floatBitsToUint(y1) >> 31u;\n uint i2 = floatBitsToUint(y2) >> 30u;\n uint i3 = floatBitsToUint(y3) >> 29u;\n\n uint shift = (i2 & 2u) | (i1 & ~2u);\n shift = (i3 & 4u) | (shift & ~4u);\n\n return (0x2E74u >> shift) & 0x0101u;\n}\n\nvec2 SolveHorizPoly(vec4 p12, vec2 p3) {\n vec2 a = p12.xy - p12.zw * 2.0 + p3;\n vec2 b = p12.xy - p12.zw;\n float ra = 1.0 / a.y;\n float rb = 0.5 / b.y;\n\n float d = sqrt(max(b.y * b.y - a.y * p12.y, 0.0));\n float t1 = (b.y - d) * ra;\n float t2 = (b.y + d) * ra;\n\n if (abs(a.y) < 1.0 / 65536.0) t1 = t2 = p12.y * rb;\n\n return vec2((a.x * t1 - b.x * 2.0) * t1 + p12.x, (a.x * t2 - b.x * 2.0) * t2 + p12.x);\n}\n\nvec2 SolveVertPoly(vec4 p12, vec2 p3) {\n vec2 a = p12.xy - p12.zw * 2.0 + p3;\n vec2 b = p12.xy - p12.zw;\n float ra = 1.0 / a.x;\n float rb = 0.5 / b.x;\n\n float d = sqrt(max(b.x * b.x - a.x * p12.x, 0.0));\n float t1 = (b.x - d) * ra;\n float t2 = (b.x + d) * ra;\n\n if (abs(a.x) < 1.0 / 65536.0) t1 = t2 = p12.x * rb;\n\n return vec2((a.y * t1 - b.y * 2.0) * t1 + p12.y, (a.y * t2 - b.y * 2.0) * t2 + p12.y);\n}\n\nivec2 CalcBandLoc(ivec2 glyphLoc, uint offset) {\n ivec2 bandLoc = ivec2(glyphLoc.x + int(offset), glyphLoc.y);\n bandLoc.y += bandLoc.x >> kLogBandTextureWidth;\n bandLoc.x &= (1 << kLogBandTextureWidth) - 1;\n return bandLoc;\n}\n\nfloat CalcCoverage(float xcov, float ycov, float xwgt, float ywgt, int flags) {\n float coverage = max(abs(xcov * xwgt + ycov * ywgt) / max(xwgt + ywgt, 1.0 / 65536.0), min(abs(xcov), abs(ycov)));\n\n#if defined(SLUG_EVENODD)\n if ((flags & 0x1000) == 0) {\n#endif\n coverage = clamp(coverage, 0.0, 1.0);\n#if defined(SLUG_EVENODD)\n } else {\n coverage = 1.0 - abs(1.0 - fract(coverage * 0.5) * 2.0);\n }\n#endif\n\n#if defined(SLUG_WEIGHT)\n coverage = sqrt(coverage);\n#endif\n\n return coverage;\n}\n\nfloat SlugRenderSingle(vec2 renderCoord, vec2 emsPerPixel, vec4 bandTransform, ivec4 glyphData) {\n int curveIndex;\n\n vec2 pixelsPerEm = 1.0 / emsPerPixel;\n\n ivec2 bandMax = glyphData.zw;\n bandMax.y &= 0x00FF;\n\n ivec2 bandIndex = clamp(ivec2(renderCoord * bandTransform.xy + bandTransform.zw), ivec2(0, 0), bandMax);\n ivec2 glyphLoc = glyphData.xy;\n\n float xcov = 0.0;\n float xwgt = 0.0;\n\n uvec2 hbandData = texelFetch(bandTexture, ivec2(glyphLoc.x + bandIndex.y, glyphLoc.y), 0).xy;\n ivec2 hbandLoc = CalcBandLoc(glyphLoc, hbandData.y);\n\n for (curveIndex = 0; curveIndex < int(hbandData.x); curveIndex++) {\n ivec2 curveLoc = ivec2(texelFetch(bandTexture, ivec2(hbandLoc.x + curveIndex, hbandLoc.y), 0).xy);\n\n vec4 p12 = texelFetch(curveTexture, curveLoc, 0) - vec4(renderCoord, renderCoord);\n vec2 p3 = texelFetch(curveTexture, ivec2(curveLoc.x + 1, curveLoc.y), 0).xy - renderCoord;\n\n if (max(max(p12.x, p12.z), p3.x) * pixelsPerEm.x < -0.5) break;\n\n uint code = CalcRootCode(p12.y, p12.w, p3.y);\n if (code != 0u) {\n vec2 r = SolveHorizPoly(p12, p3) * pixelsPerEm.x;\n\n if ((code & 1u) != 0u) {\n xcov += clamp(r.x + 0.5, 0.0, 1.0);\n xwgt = max(xwgt, clamp(1.0 - abs(r.x) * 2.0, 0.0, 1.0));\n }\n\n if (code > 1u) {\n xcov -= clamp(r.y + 0.5, 0.0, 1.0);\n xwgt = max(xwgt, clamp(1.0 - abs(r.y) * 2.0, 0.0, 1.0));\n }\n }\n }\n\n float ycov = 0.0;\n float ywgt = 0.0;\n\n uvec2 vbandData = texelFetch(bandTexture, ivec2(glyphLoc.x + bandMax.y + 1 + bandIndex.x, glyphLoc.y), 0).xy;\n ivec2 vbandLoc = CalcBandLoc(glyphLoc, vbandData.y);\n\n for (curveIndex = 0; curveIndex < int(vbandData.x); curveIndex++) {\n ivec2 curveLoc = ivec2(texelFetch(bandTexture, ivec2(vbandLoc.x + curveIndex, vbandLoc.y), 0).xy);\n vec4 p12 = texelFetch(curveTexture, curveLoc, 0) - vec4(renderCoord, renderCoord);\n vec2 p3 = texelFetch(curveTexture, ivec2(curveLoc.x + 1, curveLoc.y), 0).xy - renderCoord;\n\n if (max(max(p12.y, p12.w), p3.y) * pixelsPerEm.y < -0.5) break;\n\n uint code = CalcRootCode(p12.x, p12.z, p3.x);\n if (code != 0u) {\n vec2 r = SolveVertPoly(p12, p3) * pixelsPerEm.y;\n\n if ((code & 1u) != 0u) {\n ycov -= clamp(r.x + 0.5, 0.0, 1.0);\n ywgt = max(ywgt, clamp(1.0 - abs(r.x) * 2.0, 0.0, 1.0));\n }\n\n if (code > 1u) {\n ycov += clamp(r.y + 0.5, 0.0, 1.0);\n ywgt = max(ywgt, clamp(1.0 - abs(r.y) * 2.0, 0.0, 1.0));\n }\n }\n }\n\n return CalcCoverage(xcov, ycov, xwgt, ywgt, glyphData.w);\n}\n\nfloat SlugRender(vec2 renderCoord, vec4 bandTransform, ivec4 glyphData) {\n vec2 emsPerPixel = fwidth(renderCoord);\n\n#if defined(SLUG_ADAPTIVE_SUPERSAMPLE)\n // Per-pixel rotated RGSS-4. The base RGSS offsets are rotated by a\n // unique angle per fragment (interleaved gradient noise). This converts\n // structured aliasing shimmer into uncorrelated grain that the eye\n // naturally filters out, much closer to how hardware MSAA on many small\n // triangles behaves perceptually.\n float noise = fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715))));\n float angle = noise * 6.2831853;\n float ca = cos(angle), sa = sin(angle);\n\n // Base RGSS offsets rotated by per-pixel angle\n vec2 o0 = vec2(ca * -0.375 - sa * 0.125, sa * -0.375 + ca * 0.125) * emsPerPixel;\n vec2 o1 = vec2(ca * 0.125 - sa * 0.375, sa * 0.125 + ca * 0.375) * emsPerPixel;\n vec2 o2 = vec2(ca * 0.375 - sa * -0.125, sa * 0.375 + ca * -0.125) * emsPerPixel;\n vec2 o3 = vec2(ca * -0.125 - sa * -0.375, sa * -0.125 + ca * -0.375) * emsPerPixel;\n\n float coverage =\n SlugRenderSingle(renderCoord + o0, emsPerPixel, bandTransform, glyphData) +\n SlugRenderSingle(renderCoord + o1, emsPerPixel, bandTransform, glyphData) +\n SlugRenderSingle(renderCoord + o2, emsPerPixel, bandTransform, glyphData) +\n SlugRenderSingle(renderCoord + o3, emsPerPixel, bandTransform, glyphData);\n return coverage * 0.25;\n#else\n return SlugRenderSingle(renderCoord, emsPerPixel, bandTransform, glyphData);\n#endif\n}\n\nvoid main() {\n float coverage = SlugRender(v_texcoord, v_banding, v_glyph);\n outColor = v_color * coverage;\n}\n";
84
80
 
85
- // Slug shader source re-exports
86
- // The .glsl/.wgsl files are the single source of truth, imported as strings
87
- // at build time via the glslPlugin in rollup.config.js
88
81
  // @ts-ignore - resolved by rollup glslPlugin
89
82
  const fragmentShaderGLSL300 = fragGLSL;
90
83
 
91
- // Slug GLSL adapter for Three.js, using RawShaderMaterial with the
92
- // reference GLSL shaders. Works with both WebGLRenderer and
93
- // WebGPURenderer (via GLSL-to-WGSL transpilation)
84
+ // Slug adapter using Three.js RawShaderMaterial (GLSL)
85
+ // Works with any Three.js renderer (WebGL or WebGPU via transpilation)
94
86
  //
95
- // Compared to the TSL adapter (slugTSL.ts):
96
- // - Works with any Three.js renderer (no node-material dependency)
97
- // - Uses native Uint32 band texture (no float conversion)
98
- // - Supports GLSL animation injection via animationDeclarations/animationBody
99
- // - Same tradeoff: no vertex dilation (may cause sub-pixel edge clipping at extreme zoom)
100
- //
101
- // Requires peer dependency: three
87
+ // Supports GLSL animation injection and native Uint32 band textures
88
+ // No vertex dilation; may clip at sub-pixel edges under extreme zoom
102
89
  // @ts-ignore - three is a peer dependency
103
90
  // Three.js GLSL3 mode prepends #version 300 es, so strip it from the raw shader
104
91
  const fragShader = fragmentShaderGLSL300.replace(/^#version\s+300\s+es\s*\n/, '');
@@ -149,13 +136,12 @@ function createSlugGLSLMesh(gpuData, options) {
149
136
  geo.setAttribute('glyphCenter', new THREE__namespace.Float32BufferAttribute(attrs.glyphCenters, 3));
150
137
  geo.setAttribute('glyphIndex', new THREE__namespace.Float32BufferAttribute(attrs.glyphIndices, 1));
151
138
  geo.setIndex(new THREE__namespace.BufferAttribute(gpuData.indices, 1));
152
- // Curve texture: RGBA32F
153
139
  const curveTex = new THREE__namespace.DataTexture(gpuData.curveTexture.data, gpuData.curveTexture.width, gpuData.curveTexture.height, THREE__namespace.RGBAFormat, THREE__namespace.FloatType);
154
140
  curveTex.minFilter = THREE__namespace.NearestFilter;
155
141
  curveTex.magFilter = THREE__namespace.NearestFilter;
156
142
  curveTex.generateMipmaps = false;
157
143
  curveTex.needsUpdate = true;
158
- // Band texture: native RGBA32UI (no float conversion needed)
144
+ // RGBA32UI (unlike TSL, GLSL supports integer textures natively)
159
145
  const bandTex = new THREE__namespace.DataTexture(gpuData.bandTexture.data, gpuData.bandTexture.width, gpuData.bandTexture.height);
160
146
  bandTex.format = THREE__namespace.RGBAIntegerFormat;
161
147
  bandTex.type = THREE__namespace.UnsignedIntType;
@@ -249,6 +235,7 @@ async function wrapCoreResult(coreResult, options) {
249
235
  glyphs: coreResult.glyphs,
250
236
  planeBounds: coreResult.planeBounds,
251
237
  query: (queryOptions) => coreResult.query(queryOptions),
238
+ setColor: (r, g, b) => slugMesh.setColor(r, g, b),
252
239
  getLoadedFont: () => coreResult.getLoadedFont(),
253
240
  measureTextWidth: (text, letterSpacing) => coreResult.measureTextWidth(text, letterSpacing),
254
241
  update: async (newOptions) => {
@@ -1,8 +1,6 @@
1
1
  import { Text as Text$1 } from './core/index.js';
2
2
  import * as THREE from 'three';
3
3
 
4
- // Restructures the tightly-packed vertex buffer from SlugPacker into
5
- // separate per-attribute arrays for GPU attribute binding
6
4
  const FLOATS_PER_VERT = 20;
7
5
  function unpackSlugVertices(gpuData) {
8
6
  const vertCount = gpuData.shapeCount * 4;
@@ -24,7 +22,6 @@ function unpackSlugVertices(gpuData) {
24
22
  bandings[i * 4 + 1] = srcF[s + 13];
25
23
  bandings[i * 4 + 2] = srcF[s + 14];
26
24
  bandings[i * 4 + 3] = srcF[s + 15];
27
- // Unpack glyph location and band metadata from bit-packed fields
28
25
  const g0 = srcU[s + 6];
29
26
  const g1 = srcU[s + 7];
30
27
  glyphData[i * 4] = g0 & 0xFFFF;
@@ -36,7 +33,6 @@ function unpackSlugVertices(gpuData) {
36
33
  colors[i * 4 + 2] = srcF[s + 18];
37
34
  colors[i * 4 + 3] = srcF[s + 19];
38
35
  }
39
- // Per-glyph center: average of quad corner positions
40
36
  const glyphCenters = new Float32Array(vertCount * 3);
41
37
  const glyphIndices = new Float32Array(vertCount);
42
38
  for (let g = 0; g < gpuData.shapeCount; g++) {
@@ -61,23 +57,14 @@ function unpackSlugVertices(gpuData) {
61
57
 
62
58
  var fragGLSL = "#version 300 es\n// GLSL 300 es port of the Slug fragment shader\n// Eric Lengyel, MIT License, 2017\n\nprecision highp float;\nprecision highp int;\n\n#define kLogBandTextureWidth 12\n\nin vec4 v_color;\nin vec2 v_texcoord;\nflat in vec4 v_banding;\nflat in ivec4 v_glyph;\n\nuniform sampler2D curveTexture; // RGBA32F control points\nuniform highp usampler2D bandTexture; // RGBA32UI band data\n\nlayout(location = 0) out vec4 outColor;\n\nuint CalcRootCode(float y1, float y2, float y3) {\n uint i1 = floatBitsToUint(y1) >> 31u;\n uint i2 = floatBitsToUint(y2) >> 30u;\n uint i3 = floatBitsToUint(y3) >> 29u;\n\n uint shift = (i2 & 2u) | (i1 & ~2u);\n shift = (i3 & 4u) | (shift & ~4u);\n\n return (0x2E74u >> shift) & 0x0101u;\n}\n\nvec2 SolveHorizPoly(vec4 p12, vec2 p3) {\n vec2 a = p12.xy - p12.zw * 2.0 + p3;\n vec2 b = p12.xy - p12.zw;\n float ra = 1.0 / a.y;\n float rb = 0.5 / b.y;\n\n float d = sqrt(max(b.y * b.y - a.y * p12.y, 0.0));\n float t1 = (b.y - d) * ra;\n float t2 = (b.y + d) * ra;\n\n if (abs(a.y) < 1.0 / 65536.0) t1 = t2 = p12.y * rb;\n\n return vec2((a.x * t1 - b.x * 2.0) * t1 + p12.x, (a.x * t2 - b.x * 2.0) * t2 + p12.x);\n}\n\nvec2 SolveVertPoly(vec4 p12, vec2 p3) {\n vec2 a = p12.xy - p12.zw * 2.0 + p3;\n vec2 b = p12.xy - p12.zw;\n float ra = 1.0 / a.x;\n float rb = 0.5 / b.x;\n\n float d = sqrt(max(b.x * b.x - a.x * p12.x, 0.0));\n float t1 = (b.x - d) * ra;\n float t2 = (b.x + d) * ra;\n\n if (abs(a.x) < 1.0 / 65536.0) t1 = t2 = p12.x * rb;\n\n return vec2((a.y * t1 - b.y * 2.0) * t1 + p12.y, (a.y * t2 - b.y * 2.0) * t2 + p12.y);\n}\n\nivec2 CalcBandLoc(ivec2 glyphLoc, uint offset) {\n ivec2 bandLoc = ivec2(glyphLoc.x + int(offset), glyphLoc.y);\n bandLoc.y += bandLoc.x >> kLogBandTextureWidth;\n bandLoc.x &= (1 << kLogBandTextureWidth) - 1;\n return bandLoc;\n}\n\nfloat CalcCoverage(float xcov, float ycov, float xwgt, float ywgt, int flags) {\n float coverage = max(abs(xcov * xwgt + ycov * ywgt) / max(xwgt + ywgt, 1.0 / 65536.0), min(abs(xcov), abs(ycov)));\n\n#if defined(SLUG_EVENODD)\n if ((flags & 0x1000) == 0) {\n#endif\n coverage = clamp(coverage, 0.0, 1.0);\n#if defined(SLUG_EVENODD)\n } else {\n coverage = 1.0 - abs(1.0 - fract(coverage * 0.5) * 2.0);\n }\n#endif\n\n#if defined(SLUG_WEIGHT)\n coverage = sqrt(coverage);\n#endif\n\n return coverage;\n}\n\nfloat SlugRenderSingle(vec2 renderCoord, vec2 emsPerPixel, vec4 bandTransform, ivec4 glyphData) {\n int curveIndex;\n\n vec2 pixelsPerEm = 1.0 / emsPerPixel;\n\n ivec2 bandMax = glyphData.zw;\n bandMax.y &= 0x00FF;\n\n ivec2 bandIndex = clamp(ivec2(renderCoord * bandTransform.xy + bandTransform.zw), ivec2(0, 0), bandMax);\n ivec2 glyphLoc = glyphData.xy;\n\n float xcov = 0.0;\n float xwgt = 0.0;\n\n uvec2 hbandData = texelFetch(bandTexture, ivec2(glyphLoc.x + bandIndex.y, glyphLoc.y), 0).xy;\n ivec2 hbandLoc = CalcBandLoc(glyphLoc, hbandData.y);\n\n for (curveIndex = 0; curveIndex < int(hbandData.x); curveIndex++) {\n ivec2 curveLoc = ivec2(texelFetch(bandTexture, ivec2(hbandLoc.x + curveIndex, hbandLoc.y), 0).xy);\n\n vec4 p12 = texelFetch(curveTexture, curveLoc, 0) - vec4(renderCoord, renderCoord);\n vec2 p3 = texelFetch(curveTexture, ivec2(curveLoc.x + 1, curveLoc.y), 0).xy - renderCoord;\n\n if (max(max(p12.x, p12.z), p3.x) * pixelsPerEm.x < -0.5) break;\n\n uint code = CalcRootCode(p12.y, p12.w, p3.y);\n if (code != 0u) {\n vec2 r = SolveHorizPoly(p12, p3) * pixelsPerEm.x;\n\n if ((code & 1u) != 0u) {\n xcov += clamp(r.x + 0.5, 0.0, 1.0);\n xwgt = max(xwgt, clamp(1.0 - abs(r.x) * 2.0, 0.0, 1.0));\n }\n\n if (code > 1u) {\n xcov -= clamp(r.y + 0.5, 0.0, 1.0);\n xwgt = max(xwgt, clamp(1.0 - abs(r.y) * 2.0, 0.0, 1.0));\n }\n }\n }\n\n float ycov = 0.0;\n float ywgt = 0.0;\n\n uvec2 vbandData = texelFetch(bandTexture, ivec2(glyphLoc.x + bandMax.y + 1 + bandIndex.x, glyphLoc.y), 0).xy;\n ivec2 vbandLoc = CalcBandLoc(glyphLoc, vbandData.y);\n\n for (curveIndex = 0; curveIndex < int(vbandData.x); curveIndex++) {\n ivec2 curveLoc = ivec2(texelFetch(bandTexture, ivec2(vbandLoc.x + curveIndex, vbandLoc.y), 0).xy);\n vec4 p12 = texelFetch(curveTexture, curveLoc, 0) - vec4(renderCoord, renderCoord);\n vec2 p3 = texelFetch(curveTexture, ivec2(curveLoc.x + 1, curveLoc.y), 0).xy - renderCoord;\n\n if (max(max(p12.y, p12.w), p3.y) * pixelsPerEm.y < -0.5) break;\n\n uint code = CalcRootCode(p12.x, p12.z, p3.x);\n if (code != 0u) {\n vec2 r = SolveVertPoly(p12, p3) * pixelsPerEm.y;\n\n if ((code & 1u) != 0u) {\n ycov -= clamp(r.x + 0.5, 0.0, 1.0);\n ywgt = max(ywgt, clamp(1.0 - abs(r.x) * 2.0, 0.0, 1.0));\n }\n\n if (code > 1u) {\n ycov += clamp(r.y + 0.5, 0.0, 1.0);\n ywgt = max(ywgt, clamp(1.0 - abs(r.y) * 2.0, 0.0, 1.0));\n }\n }\n }\n\n return CalcCoverage(xcov, ycov, xwgt, ywgt, glyphData.w);\n}\n\nfloat SlugRender(vec2 renderCoord, vec4 bandTransform, ivec4 glyphData) {\n vec2 emsPerPixel = fwidth(renderCoord);\n\n#if defined(SLUG_ADAPTIVE_SUPERSAMPLE)\n // Per-pixel rotated RGSS-4. The base RGSS offsets are rotated by a\n // unique angle per fragment (interleaved gradient noise). This converts\n // structured aliasing shimmer into uncorrelated grain that the eye\n // naturally filters out, much closer to how hardware MSAA on many small\n // triangles behaves perceptually.\n float noise = fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715))));\n float angle = noise * 6.2831853;\n float ca = cos(angle), sa = sin(angle);\n\n // Base RGSS offsets rotated by per-pixel angle\n vec2 o0 = vec2(ca * -0.375 - sa * 0.125, sa * -0.375 + ca * 0.125) * emsPerPixel;\n vec2 o1 = vec2(ca * 0.125 - sa * 0.375, sa * 0.125 + ca * 0.375) * emsPerPixel;\n vec2 o2 = vec2(ca * 0.375 - sa * -0.125, sa * 0.375 + ca * -0.125) * emsPerPixel;\n vec2 o3 = vec2(ca * -0.125 - sa * -0.375, sa * -0.125 + ca * -0.375) * emsPerPixel;\n\n float coverage =\n SlugRenderSingle(renderCoord + o0, emsPerPixel, bandTransform, glyphData) +\n SlugRenderSingle(renderCoord + o1, emsPerPixel, bandTransform, glyphData) +\n SlugRenderSingle(renderCoord + o2, emsPerPixel, bandTransform, glyphData) +\n SlugRenderSingle(renderCoord + o3, emsPerPixel, bandTransform, glyphData);\n return coverage * 0.25;\n#else\n return SlugRenderSingle(renderCoord, emsPerPixel, bandTransform, glyphData);\n#endif\n}\n\nvoid main() {\n float coverage = SlugRender(v_texcoord, v_banding, v_glyph);\n outColor = v_color * coverage;\n}\n";
63
59
 
64
- // Slug shader source re-exports
65
- // The .glsl/.wgsl files are the single source of truth, imported as strings
66
- // at build time via the glslPlugin in rollup.config.js
67
60
  // @ts-ignore - resolved by rollup glslPlugin
68
61
  const fragmentShaderGLSL300 = fragGLSL;
69
62
 
70
- // Slug GLSL adapter for Three.js, using RawShaderMaterial with the
71
- // reference GLSL shaders. Works with both WebGLRenderer and
72
- // WebGPURenderer (via GLSL-to-WGSL transpilation)
63
+ // Slug adapter using Three.js RawShaderMaterial (GLSL)
64
+ // Works with any Three.js renderer (WebGL or WebGPU via transpilation)
73
65
  //
74
- // Compared to the TSL adapter (slugTSL.ts):
75
- // - Works with any Three.js renderer (no node-material dependency)
76
- // - Uses native Uint32 band texture (no float conversion)
77
- // - Supports GLSL animation injection via animationDeclarations/animationBody
78
- // - Same tradeoff: no vertex dilation (may cause sub-pixel edge clipping at extreme zoom)
79
- //
80
- // Requires peer dependency: three
66
+ // Supports GLSL animation injection and native Uint32 band textures
67
+ // No vertex dilation; may clip at sub-pixel edges under extreme zoom
81
68
  // @ts-ignore - three is a peer dependency
82
69
  // Three.js GLSL3 mode prepends #version 300 es, so strip it from the raw shader
83
70
  const fragShader = fragmentShaderGLSL300.replace(/^#version\s+300\s+es\s*\n/, '');
@@ -128,13 +115,12 @@ function createSlugGLSLMesh(gpuData, options) {
128
115
  geo.setAttribute('glyphCenter', new THREE.Float32BufferAttribute(attrs.glyphCenters, 3));
129
116
  geo.setAttribute('glyphIndex', new THREE.Float32BufferAttribute(attrs.glyphIndices, 1));
130
117
  geo.setIndex(new THREE.BufferAttribute(gpuData.indices, 1));
131
- // Curve texture: RGBA32F
132
118
  const curveTex = new THREE.DataTexture(gpuData.curveTexture.data, gpuData.curveTexture.width, gpuData.curveTexture.height, THREE.RGBAFormat, THREE.FloatType);
133
119
  curveTex.minFilter = THREE.NearestFilter;
134
120
  curveTex.magFilter = THREE.NearestFilter;
135
121
  curveTex.generateMipmaps = false;
136
122
  curveTex.needsUpdate = true;
137
- // Band texture: native RGBA32UI (no float conversion needed)
123
+ // RGBA32UI (unlike TSL, GLSL supports integer textures natively)
138
124
  const bandTex = new THREE.DataTexture(gpuData.bandTexture.data, gpuData.bandTexture.width, gpuData.bandTexture.height);
139
125
  bandTex.format = THREE.RGBAIntegerFormat;
140
126
  bandTex.type = THREE.UnsignedIntType;
@@ -228,6 +214,7 @@ async function wrapCoreResult(coreResult, options) {
228
214
  glyphs: coreResult.glyphs,
229
215
  planeBounds: coreResult.planeBounds,
230
216
  query: (queryOptions) => coreResult.query(queryOptions),
217
+ setColor: (r, g, b) => slugMesh.setColor(r, g, b),
231
218
  getLoadedFont: () => coreResult.getLoadedFont(),
232
219
  measureTextWidth: (text, letterSpacing) => coreResult.measureTextWidth(text, letterSpacing),
233
220
  update: async (newOptions) => {
@@ -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 {
@@ -25,20 +25,11 @@ function _interopNamespaceDefault(e) {
25
25
 
26
26
  var THREE__namespace = /*#__PURE__*/_interopNamespaceDefault(THREE);
27
27
 
28
- // Slug TSL adapter for Three.js WebGPURenderer (and WebGL via r170+)
28
+ // Slug adapter using Three.js TSL (node materials)
29
+ // Works on both WebGPU and WebGL backends (r170+)
29
30
  //
30
- // Creates a single Three.js Mesh with a NodeMaterial that implements
31
- // the Slug algorithm: per-fragment winding number evaluation via
32
- // band-accelerated ray-curve intersection
33
- //
34
- // Works on both WebGPU and WebGL backends via Three.js TSL
35
- //
36
- // Compared to the raw GLSL/WGSL standalone renderers, this adapter
37
- // trades some features for Three.js integration:
38
- // - No vertex dilation (may cause sub-pixel edge clipping at extreme zoom)
39
- // - No adaptive supersampling (single-sample per fragment)
40
- //
41
- // Requires peer dependencies: three, three/tsl
31
+ // No vertex dilation or adaptive supersampling (unlike the raw
32
+ // GLSL/WGSL renderers); may clip at sub-pixel edges under extreme zoom
42
33
  // @ts-ignore - three is a peer dependency
43
34
  const LOG_BAND_TEX_W = 12;
44
35
  const BAND_TEX_W_MASK = (1 << LOG_BAND_TEX_W) - 1;
@@ -100,10 +91,6 @@ const calcBandLoc = tsl.Fn(([glyphX, glyphY, offset]) => {
100
91
  bx.assign(bx.bitAnd(BAND_TEX_W_MASK));
101
92
  return tsl.ivec2(bx, by);
102
93
  });
103
- // Create a Three.js Mesh from SlugGPUData using TSL node materials.
104
- // Returns a single transparent mesh suitable for any Three.js scene.
105
- // The Slug algorithm evaluates per-fragment coverage analytically,
106
- // so no stencil buffer or multi-pass rendering is required
107
94
  function createSlugTSLMesh(gpuData, color) {
108
95
  const attrs = index.unpackSlugVertices(gpuData);
109
96
  const geo = new THREE__namespace.BufferGeometry();
@@ -130,16 +117,11 @@ function createSlugTSLMesh(gpuData, color) {
130
117
  bandTex.magFilter = THREE__namespace.NearestFilter;
131
118
  bandTex.generateMipmaps = false;
132
119
  bandTex.needsUpdate = true;
133
- // Varyings: vertex attributes interpolated to fragment stage
134
120
  const vTexcoord = tsl.varying(tsl.attribute('slugTexcoord', 'vec2'), 'v_texcoord');
135
121
  const vBanding = tsl.varying(tsl.attribute('slugBanding', 'vec4'), 'v_banding');
136
122
  const vGlyph = tsl.varying(tsl.attribute('slugGlyph', 'vec4'), 'v_glyph');
137
123
  const vColor = tsl.varying(tsl.attribute('slugColor', 'vec4'), 'v_color');
138
- // Color uniform (allows dynamic color updates)
139
124
  const textColor = tsl.uniform(new THREE__namespace.Color(color?.r ?? 1, color?.g ?? 1, color?.b ?? 1));
140
- // Main per-fragment evaluation: SlugRenderSingle ported to TSL
141
- // Evaluates horizontal and vertical band loops to compute
142
- // analytic winding-number coverage
143
125
  const slugRenderSingle = tsl.Fn(([renderCoord, emsPerPixel, bandTransform, glyphData]) => {
144
126
  const pixelsPerEm = tsl.vec2(tsl.float(1).div(emsPerPixel.x), tsl.float(1).div(emsPerPixel.y));
145
127
  const glyphLocX = glyphData.x.toInt();
@@ -148,7 +130,6 @@ function createSlugTSLMesh(gpuData, color) {
148
130
  const bandMaxY = glyphData.w.toInt().bitAnd(0xFF);
149
131
  const bandIdxX = tsl.max(tsl.min(renderCoord.x.mul(bandTransform.x).add(bandTransform.z).toInt(), bandMaxX), tsl.int(0));
150
132
  const bandIdxY = tsl.max(tsl.min(renderCoord.y.mul(bandTransform.y).add(bandTransform.w).toInt(), bandMaxY), tsl.int(0));
151
- // Horizontal band loop
152
133
  const xcov = tsl.float(0).toVar();
153
134
  const xwgt = tsl.float(0).toVar();
154
135
  const hbandData = tsl.textureLoad(bandTex, tsl.ivec2(glyphLocX.add(bandIdxY), glyphLocY));
@@ -181,7 +162,6 @@ function createSlugTSLMesh(gpuData, color) {
181
162
  });
182
163
  hIdx.addAssign(1);
183
164
  });
184
- // Vertical band loop
185
165
  const ycov = tsl.float(0).toVar();
186
166
  const ywgt = tsl.float(0).toVar();
187
167
  const vbandOffset = bandMaxY.add(1).add(bandIdxX);
@@ -215,17 +195,14 @@ function createSlugTSLMesh(gpuData, color) {
215
195
  });
216
196
  vIdx.addAssign(1);
217
197
  });
218
- // CalcCoverage (nonzero winding rule)
219
198
  const coverage = tsl.max(tsl.abs(xcov.mul(xwgt).add(ycov.mul(ywgt))).div(tsl.max(xwgt.add(ywgt), tsl.float(1.0 / 65536.0))), tsl.min(tsl.abs(xcov), tsl.abs(ycov)));
220
199
  return tsl.clamp(coverage, 0, 1);
221
200
  });
222
- // Top-level fragment node
223
201
  const fragmentNode = tsl.Fn(() => {
224
202
  const emsPerPixel = tsl.fwidth(vTexcoord);
225
203
  const coverage = slugRenderSingle(vTexcoord, emsPerPixel, vBanding, vGlyph);
226
204
  return tsl.vec4(textColor.x, textColor.y, textColor.z, vColor.w.mul(coverage));
227
205
  })();
228
- // Material & mesh
229
206
  const material = new webgpu.MeshBasicNodeMaterial();
230
207
  material.fragmentNode = fragmentNode;
231
208
  material.transparent = true;
@@ -4,20 +4,11 @@ import { Fn, select, uint, float, sqrt, max, If, abs, vec2, ivec2, varying, attr
4
4
  import { u as unpackSlugVertices } from './index2.js';
5
5
  import './core/index.js';
6
6
 
7
- // Slug TSL adapter for Three.js WebGPURenderer (and WebGL via r170+)
7
+ // Slug adapter using Three.js TSL (node materials)
8
+ // Works on both WebGPU and WebGL backends (r170+)
8
9
  //
9
- // Creates a single Three.js Mesh with a NodeMaterial that implements
10
- // the Slug algorithm: per-fragment winding number evaluation via
11
- // band-accelerated ray-curve intersection
12
- //
13
- // Works on both WebGPU and WebGL backends via Three.js TSL
14
- //
15
- // Compared to the raw GLSL/WGSL standalone renderers, this adapter
16
- // trades some features for Three.js integration:
17
- // - No vertex dilation (may cause sub-pixel edge clipping at extreme zoom)
18
- // - No adaptive supersampling (single-sample per fragment)
19
- //
20
- // Requires peer dependencies: three, three/tsl
10
+ // No vertex dilation or adaptive supersampling (unlike the raw
11
+ // GLSL/WGSL renderers); may clip at sub-pixel edges under extreme zoom
21
12
  // @ts-ignore - three is a peer dependency
22
13
  const LOG_BAND_TEX_W = 12;
23
14
  const BAND_TEX_W_MASK = (1 << LOG_BAND_TEX_W) - 1;
@@ -79,10 +70,6 @@ const calcBandLoc = Fn(([glyphX, glyphY, offset]) => {
79
70
  bx.assign(bx.bitAnd(BAND_TEX_W_MASK));
80
71
  return ivec2(bx, by);
81
72
  });
82
- // Create a Three.js Mesh from SlugGPUData using TSL node materials.
83
- // Returns a single transparent mesh suitable for any Three.js scene.
84
- // The Slug algorithm evaluates per-fragment coverage analytically,
85
- // so no stencil buffer or multi-pass rendering is required
86
73
  function createSlugTSLMesh(gpuData, color) {
87
74
  const attrs = unpackSlugVertices(gpuData);
88
75
  const geo = new THREE.BufferGeometry();
@@ -109,16 +96,11 @@ function createSlugTSLMesh(gpuData, color) {
109
96
  bandTex.magFilter = THREE.NearestFilter;
110
97
  bandTex.generateMipmaps = false;
111
98
  bandTex.needsUpdate = true;
112
- // Varyings: vertex attributes interpolated to fragment stage
113
99
  const vTexcoord = varying(attribute('slugTexcoord', 'vec2'), 'v_texcoord');
114
100
  const vBanding = varying(attribute('slugBanding', 'vec4'), 'v_banding');
115
101
  const vGlyph = varying(attribute('slugGlyph', 'vec4'), 'v_glyph');
116
102
  const vColor = varying(attribute('slugColor', 'vec4'), 'v_color');
117
- // Color uniform (allows dynamic color updates)
118
103
  const textColor = uniform(new THREE.Color(color?.r ?? 1, color?.g ?? 1, color?.b ?? 1));
119
- // Main per-fragment evaluation: SlugRenderSingle ported to TSL
120
- // Evaluates horizontal and vertical band loops to compute
121
- // analytic winding-number coverage
122
104
  const slugRenderSingle = Fn(([renderCoord, emsPerPixel, bandTransform, glyphData]) => {
123
105
  const pixelsPerEm = vec2(float(1).div(emsPerPixel.x), float(1).div(emsPerPixel.y));
124
106
  const glyphLocX = glyphData.x.toInt();
@@ -127,7 +109,6 @@ function createSlugTSLMesh(gpuData, color) {
127
109
  const bandMaxY = glyphData.w.toInt().bitAnd(0xFF);
128
110
  const bandIdxX = max(min(renderCoord.x.mul(bandTransform.x).add(bandTransform.z).toInt(), bandMaxX), int(0));
129
111
  const bandIdxY = max(min(renderCoord.y.mul(bandTransform.y).add(bandTransform.w).toInt(), bandMaxY), int(0));
130
- // Horizontal band loop
131
112
  const xcov = float(0).toVar();
132
113
  const xwgt = float(0).toVar();
133
114
  const hbandData = textureLoad(bandTex, ivec2(glyphLocX.add(bandIdxY), glyphLocY));
@@ -160,7 +141,6 @@ function createSlugTSLMesh(gpuData, color) {
160
141
  });
161
142
  hIdx.addAssign(1);
162
143
  });
163
- // Vertical band loop
164
144
  const ycov = float(0).toVar();
165
145
  const ywgt = float(0).toVar();
166
146
  const vbandOffset = bandMaxY.add(1).add(bandIdxX);
@@ -194,17 +174,14 @@ function createSlugTSLMesh(gpuData, color) {
194
174
  });
195
175
  vIdx.addAssign(1);
196
176
  });
197
- // CalcCoverage (nonzero winding rule)
198
177
  const coverage = max(abs(xcov.mul(xwgt).add(ycov.mul(ywgt))).div(max(xwgt.add(ywgt), float(1.0 / 65536.0))), min(abs(xcov), abs(ycov)));
199
178
  return clamp(coverage, 0, 1);
200
179
  });
201
- // Top-level fragment node
202
180
  const fragmentNode = Fn(() => {
203
181
  const emsPerPixel = fwidth(vTexcoord);
204
182
  const coverage = slugRenderSingle(vTexcoord, emsPerPixel, vBanding, vGlyph);
205
183
  return vec4(textColor.x, textColor.y, textColor.z, vColor.w.mul(coverage));
206
184
  })();
207
- // Material & mesh
208
185
  const material = new MeshBasicNodeMaterial();
209
186
  material.fragmentNode = fragmentNode;
210
187
  material.transparent = true;