three-text 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/LICENSE_THIRD_PARTY +15 -0
  2. package/README.md +73 -42
  3. package/dist/index.cjs +1 -1
  4. package/dist/index.d.ts +2 -0
  5. package/dist/index.js +1 -1
  6. package/dist/index.min.cjs +1 -1
  7. package/dist/index.min.js +1 -1
  8. package/dist/index.umd.js +1 -1
  9. package/dist/index.umd.min.js +1 -1
  10. package/dist/three/react.d.ts +2 -0
  11. package/dist/types/core/types.d.ts +2 -33
  12. package/dist/types/vector/{core.d.ts → core/index.d.ts} +8 -7
  13. package/dist/types/vector/index.d.ts +22 -25
  14. package/dist/types/vector/react.d.ts +4 -3
  15. package/dist/types/vector/slug/SlugPacker.d.ts +2 -0
  16. package/dist/types/vector/slug/curveUtils.d.ts +6 -0
  17. package/dist/types/vector/slug/index.d.ts +8 -0
  18. package/dist/types/vector/slug/shaderStrings.d.ts +4 -0
  19. package/dist/types/vector/slug/slugGLSL.d.ts +21 -0
  20. package/dist/types/vector/slug/slugTSL.d.ts +13 -0
  21. package/dist/types/vector/slug/types.d.ts +30 -0
  22. package/dist/types/vector/slug/unpackVertices.d.ts +11 -0
  23. package/dist/types/vector/webgl/index.d.ts +7 -3
  24. package/dist/types/vector/webgpu/index.d.ts +4 -4
  25. package/dist/vector/core/index.cjs +856 -0
  26. package/dist/vector/core/index.d.ts +63 -0
  27. package/dist/vector/core/index.js +854 -0
  28. package/dist/vector/core.cjs +4419 -240
  29. package/dist/vector/core.d.ts +361 -71
  30. package/dist/vector/core.js +4406 -226
  31. package/dist/vector/index.cjs +5 -229
  32. package/dist/vector/index.d.ts +45 -396
  33. package/dist/vector/index.js +3 -223
  34. package/dist/vector/index2.cjs +287 -0
  35. package/dist/vector/index2.js +264 -0
  36. package/dist/vector/react.cjs +37 -8
  37. package/dist/vector/react.d.ts +6 -3
  38. package/dist/vector/react.js +18 -8
  39. package/dist/vector/slugTSL.cjs +252 -0
  40. package/dist/vector/slugTSL.js +231 -0
  41. package/dist/vector/webgl/index.cjs +131 -201
  42. package/dist/vector/webgl/index.d.ts +19 -44
  43. package/dist/vector/webgl/index.js +131 -201
  44. package/dist/vector/webgpu/index.cjs +100 -283
  45. package/dist/vector/webgpu/index.d.ts +16 -45
  46. package/dist/vector/webgpu/index.js +100 -283
  47. package/package.json +6 -1
  48. package/dist/types/vector/GlyphVectorGeometryBuilder.d.ts +0 -26
  49. package/dist/types/vector/LoopBlinnGeometry.d.ts +0 -68
  50. package/dist/types/vector/loopBlinnTSL.d.ts +0 -22
  51. package/dist/vector/loopBlinnTSL.cjs +0 -229
  52. package/dist/vector/loopBlinnTSL.js +0 -207
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import * as THREE from 'three';
3
- import { VectorTextOptions, VectorResult, Text as Text$2 } from './index';
3
+ import { TextOptions as TextOptions$1, VectorTextResult, Text as Text$2 } from './index';
4
4
 
5
5
  interface HyphenationTrieNode {
6
6
  patterns: number[] | null;
@@ -208,6 +208,8 @@ 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
+ adaptiveSupersampling?: boolean;
211
213
  }
212
214
  interface HyphenationPatternsMap {
213
215
  [language: string]: HyphenationTrieNode;
@@ -299,14 +301,15 @@ declare class Text$1 {
299
301
  destroy(): void;
300
302
  }
301
303
 
302
- interface TextProps extends Omit<VectorTextOptions, 'text' | 'color'> {
304
+ interface TextProps extends Omit<TextOptions$1, 'text' | 'color'> {
303
305
  children: string;
304
306
  font: string | ArrayBuffer;
305
307
  fillColor?: THREE.ColorRepresentation;
306
308
  position?: [number, number, number];
307
309
  rotation?: [number, number, number];
308
310
  scale?: [number, number, number];
309
- onLoad?: (result: VectorResult) => void;
311
+ positionNode?: any;
312
+ onLoad?: (result: VectorTextResult) => void;
310
313
  onError?: (error: Error) => void;
311
314
  }
312
315
  declare const Text: react.ForwardRefExoticComponent<TextProps & react.RefAttributes<THREE.Group<THREE.Object3DEventMap>>> & {
@@ -1,5 +1,6 @@
1
1
  import { jsx } from 'react/jsx-runtime';
2
2
  import { useRef, forwardRef, useState, useEffect } from 'react';
3
+ import * as THREE from 'three';
3
4
  import { Text as Text$1 } from './index.js';
4
5
 
5
6
  function deepEqual(a, b) {
@@ -44,7 +45,7 @@ function useDeepCompareMemo(value) {
44
45
  }
45
46
 
46
47
  const TextInner = forwardRef(function TextInner(props, ref) {
47
- const { children, font, fillColor = '#ffffff', positionNode, colorNode, position = [0, 0, 0], rotation = [0, 0, 0], scale = [1, 1, 1], onLoad, onError, ...restOptions } = props;
48
+ const { children, font, fillColor = '#ffffff', position = [0, 0, 0], rotation = [0, 0, 0], scale = [1, 1, 1], positionNode, onLoad, onError, ...restOptions } = props;
48
49
  const memoizedTextOptions = useDeepCompareMemo(restOptions);
49
50
  const [group, setGroup] = useState(null);
50
51
  const [error, setError] = useState(null);
@@ -59,24 +60,22 @@ const TextInner = forwardRef(function TextInner(props, ref) {
59
60
  async function setup() {
60
61
  try {
61
62
  setError(null);
63
+ const color = new THREE.Color(fillColor);
62
64
  const resultPromise = opRef.current.catch(() => null).then(() => {
63
65
  if (cancelled)
64
66
  return null;
67
+ const colorArr = [color.r, color.g, color.b];
65
68
  return resultRef.current
66
69
  ? resultRef.current.update({
67
70
  text: children,
68
71
  font,
69
- color: fillColor,
70
- positionNode,
71
- colorNode,
72
+ color: colorArr,
72
73
  ...memoizedTextOptions
73
74
  })
74
75
  : Text$1.create({
75
76
  text: children,
76
77
  font,
77
- color: fillColor,
78
- positionNode,
79
- colorNode,
78
+ color: colorArr,
80
79
  ...memoizedTextOptions
81
80
  });
82
81
  });
@@ -88,6 +87,10 @@ const TextInner = forwardRef(function TextInner(props, ref) {
88
87
  result.dispose();
89
88
  return;
90
89
  }
90
+ if (positionNode && result.mesh?.material) {
91
+ result.mesh.material.positionNode = positionNode;
92
+ result.mesh.material.needsUpdate = true;
93
+ }
91
94
  const prev = resultRef.current;
92
95
  resultRef.current = result;
93
96
  setGroup(result.group);
@@ -110,7 +113,14 @@ const TextInner = forwardRef(function TextInner(props, ref) {
110
113
  return () => {
111
114
  cancelled = true;
112
115
  };
113
- }, [children, font, memoizedTextOptions, fillColor, positionNode, colorNode]);
116
+ }, [children, font, memoizedTextOptions, fillColor]);
117
+ useEffect(() => {
118
+ const result = resultRef.current;
119
+ if (result?.mesh?.material) {
120
+ result.mesh.material.positionNode = positionNode ?? null;
121
+ result.mesh.material.needsUpdate = true;
122
+ }
123
+ }, [positionNode]);
114
124
  useEffect(() => {
115
125
  return () => {
116
126
  resultRef.current?.dispose();
@@ -0,0 +1,252 @@
1
+ 'use strict';
2
+
3
+ var THREE = require('three');
4
+ var webgpu = require('three/webgpu');
5
+ var tsl = require('three/tsl');
6
+ var index = require('./index2.cjs');
7
+ require('./core/index.cjs');
8
+
9
+ function _interopNamespaceDefault(e) {
10
+ var n = Object.create(null);
11
+ if (e) {
12
+ Object.keys(e).forEach(function (k) {
13
+ if (k !== 'default') {
14
+ var d = Object.getOwnPropertyDescriptor(e, k);
15
+ Object.defineProperty(n, k, d.get ? d : {
16
+ enumerable: true,
17
+ get: function () { return e[k]; }
18
+ });
19
+ }
20
+ });
21
+ }
22
+ n.default = e;
23
+ return Object.freeze(n);
24
+ }
25
+
26
+ var THREE__namespace = /*#__PURE__*/_interopNamespaceDefault(THREE);
27
+
28
+ // Slug TSL adapter for Three.js WebGPURenderer (and WebGL via r170+)
29
+ //
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
42
+ // @ts-ignore - three is a peer dependency
43
+ const LOG_BAND_TEX_W = 12;
44
+ const BAND_TEX_W_MASK = (1 << LOG_BAND_TEX_W) - 1;
45
+ // Determines which quadratic roots to evaluate from the signs of
46
+ // control-point y-coordinates, using direct sign comparisons instead
47
+ // of the original floatBitsToUint encoding.
48
+ // Returns a 16-bit value where bit 0 = evaluate root 1,
49
+ // bit 8 = evaluate root 2 (matching the 0x0101 mask convention)
50
+ const calcRootCode = tsl.Fn(([y1, y2, y3]) => {
51
+ const s1 = tsl.select(y1.lessThan(0), tsl.uint(1), tsl.uint(0));
52
+ const s2 = tsl.select(y2.lessThan(0), tsl.uint(1), tsl.uint(0));
53
+ const s3 = tsl.select(y3.lessThan(0), tsl.uint(1), tsl.uint(0));
54
+ const shift = s1.bitOr(s2.shiftLeft(1)).bitOr(s3.shiftLeft(2));
55
+ return tsl.uint(0x2E74).shiftRight(shift).bitAnd(tsl.uint(0x0101));
56
+ });
57
+ // Solve horizontal quadratic: finds x-intercepts where the curve
58
+ // crosses the fragment's y = 0 line
59
+ const solveHorizPoly = tsl.Fn(([p12, p3]) => {
60
+ const ax = p12.x.sub(p12.z.mul(2)).add(p3.x);
61
+ const ay = p12.y.sub(p12.w.mul(2)).add(p3.y);
62
+ const bx = p12.x.sub(p12.z);
63
+ const by = p12.y.sub(p12.w);
64
+ const ra = tsl.float(1).div(ay);
65
+ const rb = tsl.float(0.5).div(by);
66
+ const d = tsl.sqrt(tsl.max(by.mul(by).sub(ay.mul(p12.y)), 0));
67
+ const t1 = by.sub(d).mul(ra).toVar();
68
+ const t2 = by.add(d).mul(ra).toVar();
69
+ tsl.If(tsl.abs(ay).lessThan(tsl.float(1.0 / 65536.0)), () => {
70
+ const fb = p12.y.mul(rb);
71
+ t1.assign(fb);
72
+ t2.assign(fb);
73
+ });
74
+ return tsl.vec2(ax.mul(t1).sub(bx.mul(2)).mul(t1).add(p12.x), ax.mul(t2).sub(bx.mul(2)).mul(t2).add(p12.x));
75
+ });
76
+ // Solve vertical quadratic: finds y-intercepts where the curve
77
+ // crosses the fragment's x = 0 line
78
+ const solveVertPoly = tsl.Fn(([p12, p3]) => {
79
+ const ax = p12.x.sub(p12.z.mul(2)).add(p3.x);
80
+ const ay = p12.y.sub(p12.w.mul(2)).add(p3.y);
81
+ const bx = p12.x.sub(p12.z);
82
+ const by = p12.y.sub(p12.w);
83
+ const ra = tsl.float(1).div(ax);
84
+ const rb = tsl.float(0.5).div(bx);
85
+ const d = tsl.sqrt(tsl.max(bx.mul(bx).sub(ax.mul(p12.x)), 0));
86
+ const t1 = bx.sub(d).mul(ra).toVar();
87
+ const t2 = bx.add(d).mul(ra).toVar();
88
+ tsl.If(tsl.abs(ax).lessThan(tsl.float(1.0 / 65536.0)), () => {
89
+ const fb = p12.x.mul(rb);
90
+ t1.assign(fb);
91
+ t2.assign(fb);
92
+ });
93
+ return tsl.vec2(ay.mul(t1).sub(by.mul(2)).mul(t1).add(p12.y), ay.mul(t2).sub(by.mul(2)).mul(t2).add(p12.y));
94
+ });
95
+ // Compute a band-texture coordinate with row wrapping
96
+ // Equivalent to CalcBandLoc in the GLSL reference
97
+ const calcBandLoc = tsl.Fn(([glyphX, glyphY, offset]) => {
98
+ const bx = glyphX.add(offset).toVar();
99
+ const by = glyphY.add(bx.shiftRight(LOG_BAND_TEX_W)).toVar();
100
+ bx.assign(bx.bitAnd(BAND_TEX_W_MASK));
101
+ return tsl.ivec2(bx, by);
102
+ });
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
+ function createSlugTSLMesh(gpuData, color) {
108
+ const attrs = index.unpackSlugVertices(gpuData);
109
+ const geo = new THREE__namespace.BufferGeometry();
110
+ geo.setAttribute('position', new THREE__namespace.Float32BufferAttribute(attrs.positions, 3));
111
+ geo.setAttribute('slugTexcoord', new THREE__namespace.Float32BufferAttribute(attrs.texcoords, 2));
112
+ geo.setAttribute('slugBanding', new THREE__namespace.Float32BufferAttribute(attrs.bandings, 4));
113
+ geo.setAttribute('slugGlyph', new THREE__namespace.Float32BufferAttribute(attrs.glyphData, 4));
114
+ geo.setAttribute('slugColor', new THREE__namespace.Float32BufferAttribute(attrs.colors, 4));
115
+ geo.setAttribute('glyphCenter', new THREE__namespace.Float32BufferAttribute(attrs.glyphCenters, 3));
116
+ geo.setAttribute('glyphIndex', new THREE__namespace.Float32BufferAttribute(attrs.glyphIndices, 1));
117
+ geo.setIndex(new THREE__namespace.BufferAttribute(gpuData.indices, 1));
118
+ const curveTex = new THREE__namespace.DataTexture(gpuData.curveTexture.data, gpuData.curveTexture.width, gpuData.curveTexture.height, THREE__namespace.RGBAFormat, THREE__namespace.FloatType);
119
+ curveTex.minFilter = THREE__namespace.NearestFilter;
120
+ curveTex.magFilter = THREE__namespace.NearestFilter;
121
+ curveTex.generateMipmaps = false;
122
+ curveTex.needsUpdate = true;
123
+ // Band texture: convert Uint32 to Float32 (values are small ints, exact in f32)
124
+ const bandFloat = new Float32Array(gpuData.bandTexture.data.length);
125
+ for (let i = 0; i < bandFloat.length; i++) {
126
+ bandFloat[i] = gpuData.bandTexture.data[i];
127
+ }
128
+ const bandTex = new THREE__namespace.DataTexture(bandFloat, gpuData.bandTexture.width, gpuData.bandTexture.height, THREE__namespace.RGBAFormat, THREE__namespace.FloatType);
129
+ bandTex.minFilter = THREE__namespace.NearestFilter;
130
+ bandTex.magFilter = THREE__namespace.NearestFilter;
131
+ bandTex.generateMipmaps = false;
132
+ bandTex.needsUpdate = true;
133
+ // Varyings: vertex attributes interpolated to fragment stage
134
+ const vTexcoord = tsl.varying(tsl.attribute('slugTexcoord', 'vec2'), 'v_texcoord');
135
+ const vBanding = tsl.varying(tsl.attribute('slugBanding', 'vec4'), 'v_banding');
136
+ const vGlyph = tsl.varying(tsl.attribute('slugGlyph', 'vec4'), 'v_glyph');
137
+ const vColor = tsl.varying(tsl.attribute('slugColor', 'vec4'), 'v_color');
138
+ // Color uniform (allows dynamic color updates)
139
+ 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
+ const slugRenderSingle = tsl.Fn(([renderCoord, emsPerPixel, bandTransform, glyphData]) => {
144
+ const pixelsPerEm = tsl.vec2(tsl.float(1).div(emsPerPixel.x), tsl.float(1).div(emsPerPixel.y));
145
+ const glyphLocX = glyphData.x.toInt();
146
+ const glyphLocY = glyphData.y.toInt();
147
+ const bandMaxX = glyphData.z.toInt();
148
+ const bandMaxY = glyphData.w.toInt().bitAnd(0xFF);
149
+ const bandIdxX = tsl.max(tsl.min(renderCoord.x.mul(bandTransform.x).add(bandTransform.z).toInt(), bandMaxX), tsl.int(0));
150
+ const bandIdxY = tsl.max(tsl.min(renderCoord.y.mul(bandTransform.y).add(bandTransform.w).toInt(), bandMaxY), tsl.int(0));
151
+ // Horizontal band loop
152
+ const xcov = tsl.float(0).toVar();
153
+ const xwgt = tsl.float(0).toVar();
154
+ const hbandData = tsl.textureLoad(bandTex, tsl.ivec2(glyphLocX.add(bandIdxY), glyphLocY));
155
+ const hCurveCount = hbandData.x.toInt();
156
+ const hListOffset = hbandData.y.toInt();
157
+ const hbandLoc = calcBandLoc(glyphLocX, glyphLocY, hListOffset);
158
+ const hIdx = tsl.int(0).toVar();
159
+ tsl.Loop(hIdx.lessThan(hCurveCount), () => {
160
+ const clEntry = tsl.textureLoad(bandTex, tsl.ivec2(hbandLoc.x.add(hIdx), hbandLoc.y));
161
+ const cLocX = clEntry.x.toInt();
162
+ const cLocY = clEntry.y.toInt();
163
+ const rawP12 = tsl.textureLoad(curveTex, tsl.ivec2(cLocX, cLocY));
164
+ const rawP3 = tsl.textureLoad(curveTex, tsl.ivec2(cLocX.add(1), cLocY));
165
+ const p12 = tsl.vec4(rawP12.x.sub(renderCoord.x), rawP12.y.sub(renderCoord.y), rawP12.z.sub(renderCoord.x), rawP12.w.sub(renderCoord.y));
166
+ const p3 = tsl.vec2(rawP3.x.sub(renderCoord.x), rawP3.y.sub(renderCoord.y));
167
+ tsl.If(tsl.max(tsl.max(p12.x, p12.z), p3.x).mul(pixelsPerEm.x).lessThan(-0.5), () => {
168
+ tsl.Break();
169
+ });
170
+ const code = calcRootCode(p12.y, p12.w, p3.y);
171
+ tsl.If(code.notEqual(tsl.uint(0)), () => {
172
+ const r = solveHorizPoly(p12, p3).mul(pixelsPerEm.x);
173
+ tsl.If(code.bitAnd(tsl.uint(1)).notEqual(tsl.uint(0)), () => {
174
+ xcov.addAssign(tsl.clamp(r.x.add(0.5), 0, 1));
175
+ xwgt.assign(tsl.max(xwgt, tsl.clamp(tsl.float(1).sub(tsl.abs(r.x).mul(2)), 0, 1)));
176
+ });
177
+ tsl.If(code.greaterThan(tsl.uint(1)), () => {
178
+ xcov.subAssign(tsl.clamp(r.y.add(0.5), 0, 1));
179
+ xwgt.assign(tsl.max(xwgt, tsl.clamp(tsl.float(1).sub(tsl.abs(r.y).mul(2)), 0, 1)));
180
+ });
181
+ });
182
+ hIdx.addAssign(1);
183
+ });
184
+ // Vertical band loop
185
+ const ycov = tsl.float(0).toVar();
186
+ const ywgt = tsl.float(0).toVar();
187
+ const vbandOffset = bandMaxY.add(1).add(bandIdxX);
188
+ const vbandData = tsl.textureLoad(bandTex, tsl.ivec2(glyphLocX.add(vbandOffset), glyphLocY));
189
+ const vCurveCount = vbandData.x.toInt();
190
+ const vListOffset = vbandData.y.toInt();
191
+ const vbandLoc = calcBandLoc(glyphLocX, glyphLocY, vListOffset);
192
+ const vIdx = tsl.int(0).toVar();
193
+ tsl.Loop(vIdx.lessThan(vCurveCount), () => {
194
+ const clEntry = tsl.textureLoad(bandTex, tsl.ivec2(vbandLoc.x.add(vIdx), vbandLoc.y));
195
+ const cLocX = clEntry.x.toInt();
196
+ const cLocY = clEntry.y.toInt();
197
+ const rawP12 = tsl.textureLoad(curveTex, tsl.ivec2(cLocX, cLocY));
198
+ const rawP3 = tsl.textureLoad(curveTex, tsl.ivec2(cLocX.add(1), cLocY));
199
+ const p12 = tsl.vec4(rawP12.x.sub(renderCoord.x), rawP12.y.sub(renderCoord.y), rawP12.z.sub(renderCoord.x), rawP12.w.sub(renderCoord.y));
200
+ const p3 = tsl.vec2(rawP3.x.sub(renderCoord.x), rawP3.y.sub(renderCoord.y));
201
+ tsl.If(tsl.max(tsl.max(p12.y, p12.w), p3.y).mul(pixelsPerEm.y).lessThan(-0.5), () => {
202
+ tsl.Break();
203
+ });
204
+ const code = calcRootCode(p12.x, p12.z, p3.x);
205
+ tsl.If(code.notEqual(tsl.uint(0)), () => {
206
+ const r = solveVertPoly(p12, p3).mul(pixelsPerEm.y);
207
+ tsl.If(code.bitAnd(tsl.uint(1)).notEqual(tsl.uint(0)), () => {
208
+ ycov.subAssign(tsl.clamp(r.x.add(0.5), 0, 1));
209
+ ywgt.assign(tsl.max(ywgt, tsl.clamp(tsl.float(1).sub(tsl.abs(r.x).mul(2)), 0, 1)));
210
+ });
211
+ tsl.If(code.greaterThan(tsl.uint(1)), () => {
212
+ ycov.addAssign(tsl.clamp(r.y.add(0.5), 0, 1));
213
+ ywgt.assign(tsl.max(ywgt, tsl.clamp(tsl.float(1).sub(tsl.abs(r.y).mul(2)), 0, 1)));
214
+ });
215
+ });
216
+ vIdx.addAssign(1);
217
+ });
218
+ // CalcCoverage (nonzero winding rule)
219
+ 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
+ return tsl.clamp(coverage, 0, 1);
221
+ });
222
+ // Top-level fragment node
223
+ const fragmentNode = tsl.Fn(() => {
224
+ const emsPerPixel = tsl.fwidth(vTexcoord);
225
+ const coverage = slugRenderSingle(vTexcoord, emsPerPixel, vBanding, vGlyph);
226
+ return tsl.vec4(textColor.x, textColor.y, textColor.z, vColor.w.mul(coverage));
227
+ })();
228
+ // Material & mesh
229
+ const material = new webgpu.MeshBasicNodeMaterial();
230
+ material.fragmentNode = fragmentNode;
231
+ material.transparent = true;
232
+ material.depthWrite = false;
233
+ material.side = THREE__namespace.DoubleSide;
234
+ const mesh = new THREE__namespace.Mesh(geo, material);
235
+ return {
236
+ mesh,
237
+ setOffset(x, y, z = 0) {
238
+ mesh.position.set(x, y, z);
239
+ },
240
+ setColor(r, g, b) {
241
+ textColor.value.setRGB(r, g, b);
242
+ },
243
+ dispose() {
244
+ geo.dispose();
245
+ material.dispose();
246
+ curveTex.dispose();
247
+ bandTex.dispose();
248
+ }
249
+ };
250
+ }
251
+
252
+ exports.createSlugTSLMesh = createSlugTSLMesh;
@@ -0,0 +1,231 @@
1
+ import * as THREE from 'three';
2
+ import { MeshBasicNodeMaterial } from 'three/webgpu';
3
+ import { Fn, select, uint, float, sqrt, max, If, abs, vec2, ivec2, varying, attribute, uniform, min, int, textureLoad, Loop, vec4, Break, clamp, fwidth } from 'three/tsl';
4
+ import { u as unpackSlugVertices } from './index2.js';
5
+ import './core/index.js';
6
+
7
+ // Slug TSL adapter for Three.js WebGPURenderer (and WebGL via r170+)
8
+ //
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
21
+ // @ts-ignore - three is a peer dependency
22
+ const LOG_BAND_TEX_W = 12;
23
+ const BAND_TEX_W_MASK = (1 << LOG_BAND_TEX_W) - 1;
24
+ // Determines which quadratic roots to evaluate from the signs of
25
+ // control-point y-coordinates, using direct sign comparisons instead
26
+ // of the original floatBitsToUint encoding.
27
+ // Returns a 16-bit value where bit 0 = evaluate root 1,
28
+ // bit 8 = evaluate root 2 (matching the 0x0101 mask convention)
29
+ const calcRootCode = Fn(([y1, y2, y3]) => {
30
+ const s1 = select(y1.lessThan(0), uint(1), uint(0));
31
+ const s2 = select(y2.lessThan(0), uint(1), uint(0));
32
+ const s3 = select(y3.lessThan(0), uint(1), uint(0));
33
+ const shift = s1.bitOr(s2.shiftLeft(1)).bitOr(s3.shiftLeft(2));
34
+ return uint(0x2E74).shiftRight(shift).bitAnd(uint(0x0101));
35
+ });
36
+ // Solve horizontal quadratic: finds x-intercepts where the curve
37
+ // crosses the fragment's y = 0 line
38
+ const solveHorizPoly = Fn(([p12, p3]) => {
39
+ const ax = p12.x.sub(p12.z.mul(2)).add(p3.x);
40
+ const ay = p12.y.sub(p12.w.mul(2)).add(p3.y);
41
+ const bx = p12.x.sub(p12.z);
42
+ const by = p12.y.sub(p12.w);
43
+ const ra = float(1).div(ay);
44
+ const rb = float(0.5).div(by);
45
+ const d = sqrt(max(by.mul(by).sub(ay.mul(p12.y)), 0));
46
+ const t1 = by.sub(d).mul(ra).toVar();
47
+ const t2 = by.add(d).mul(ra).toVar();
48
+ If(abs(ay).lessThan(float(1.0 / 65536.0)), () => {
49
+ const fb = p12.y.mul(rb);
50
+ t1.assign(fb);
51
+ t2.assign(fb);
52
+ });
53
+ return vec2(ax.mul(t1).sub(bx.mul(2)).mul(t1).add(p12.x), ax.mul(t2).sub(bx.mul(2)).mul(t2).add(p12.x));
54
+ });
55
+ // Solve vertical quadratic: finds y-intercepts where the curve
56
+ // crosses the fragment's x = 0 line
57
+ const solveVertPoly = Fn(([p12, p3]) => {
58
+ const ax = p12.x.sub(p12.z.mul(2)).add(p3.x);
59
+ const ay = p12.y.sub(p12.w.mul(2)).add(p3.y);
60
+ const bx = p12.x.sub(p12.z);
61
+ const by = p12.y.sub(p12.w);
62
+ const ra = float(1).div(ax);
63
+ const rb = float(0.5).div(bx);
64
+ const d = sqrt(max(bx.mul(bx).sub(ax.mul(p12.x)), 0));
65
+ const t1 = bx.sub(d).mul(ra).toVar();
66
+ const t2 = bx.add(d).mul(ra).toVar();
67
+ If(abs(ax).lessThan(float(1.0 / 65536.0)), () => {
68
+ const fb = p12.x.mul(rb);
69
+ t1.assign(fb);
70
+ t2.assign(fb);
71
+ });
72
+ return vec2(ay.mul(t1).sub(by.mul(2)).mul(t1).add(p12.y), ay.mul(t2).sub(by.mul(2)).mul(t2).add(p12.y));
73
+ });
74
+ // Compute a band-texture coordinate with row wrapping
75
+ // Equivalent to CalcBandLoc in the GLSL reference
76
+ const calcBandLoc = Fn(([glyphX, glyphY, offset]) => {
77
+ const bx = glyphX.add(offset).toVar();
78
+ const by = glyphY.add(bx.shiftRight(LOG_BAND_TEX_W)).toVar();
79
+ bx.assign(bx.bitAnd(BAND_TEX_W_MASK));
80
+ return ivec2(bx, by);
81
+ });
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
+ function createSlugTSLMesh(gpuData, color) {
87
+ const attrs = unpackSlugVertices(gpuData);
88
+ const geo = new THREE.BufferGeometry();
89
+ geo.setAttribute('position', new THREE.Float32BufferAttribute(attrs.positions, 3));
90
+ geo.setAttribute('slugTexcoord', new THREE.Float32BufferAttribute(attrs.texcoords, 2));
91
+ geo.setAttribute('slugBanding', new THREE.Float32BufferAttribute(attrs.bandings, 4));
92
+ geo.setAttribute('slugGlyph', new THREE.Float32BufferAttribute(attrs.glyphData, 4));
93
+ geo.setAttribute('slugColor', new THREE.Float32BufferAttribute(attrs.colors, 4));
94
+ geo.setAttribute('glyphCenter', new THREE.Float32BufferAttribute(attrs.glyphCenters, 3));
95
+ geo.setAttribute('glyphIndex', new THREE.Float32BufferAttribute(attrs.glyphIndices, 1));
96
+ geo.setIndex(new THREE.BufferAttribute(gpuData.indices, 1));
97
+ const curveTex = new THREE.DataTexture(gpuData.curveTexture.data, gpuData.curveTexture.width, gpuData.curveTexture.height, THREE.RGBAFormat, THREE.FloatType);
98
+ curveTex.minFilter = THREE.NearestFilter;
99
+ curveTex.magFilter = THREE.NearestFilter;
100
+ curveTex.generateMipmaps = false;
101
+ curveTex.needsUpdate = true;
102
+ // Band texture: convert Uint32 to Float32 (values are small ints, exact in f32)
103
+ const bandFloat = new Float32Array(gpuData.bandTexture.data.length);
104
+ for (let i = 0; i < bandFloat.length; i++) {
105
+ bandFloat[i] = gpuData.bandTexture.data[i];
106
+ }
107
+ const bandTex = new THREE.DataTexture(bandFloat, gpuData.bandTexture.width, gpuData.bandTexture.height, THREE.RGBAFormat, THREE.FloatType);
108
+ bandTex.minFilter = THREE.NearestFilter;
109
+ bandTex.magFilter = THREE.NearestFilter;
110
+ bandTex.generateMipmaps = false;
111
+ bandTex.needsUpdate = true;
112
+ // Varyings: vertex attributes interpolated to fragment stage
113
+ const vTexcoord = varying(attribute('slugTexcoord', 'vec2'), 'v_texcoord');
114
+ const vBanding = varying(attribute('slugBanding', 'vec4'), 'v_banding');
115
+ const vGlyph = varying(attribute('slugGlyph', 'vec4'), 'v_glyph');
116
+ const vColor = varying(attribute('slugColor', 'vec4'), 'v_color');
117
+ // Color uniform (allows dynamic color updates)
118
+ 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
+ const slugRenderSingle = Fn(([renderCoord, emsPerPixel, bandTransform, glyphData]) => {
123
+ const pixelsPerEm = vec2(float(1).div(emsPerPixel.x), float(1).div(emsPerPixel.y));
124
+ const glyphLocX = glyphData.x.toInt();
125
+ const glyphLocY = glyphData.y.toInt();
126
+ const bandMaxX = glyphData.z.toInt();
127
+ const bandMaxY = glyphData.w.toInt().bitAnd(0xFF);
128
+ const bandIdxX = max(min(renderCoord.x.mul(bandTransform.x).add(bandTransform.z).toInt(), bandMaxX), int(0));
129
+ const bandIdxY = max(min(renderCoord.y.mul(bandTransform.y).add(bandTransform.w).toInt(), bandMaxY), int(0));
130
+ // Horizontal band loop
131
+ const xcov = float(0).toVar();
132
+ const xwgt = float(0).toVar();
133
+ const hbandData = textureLoad(bandTex, ivec2(glyphLocX.add(bandIdxY), glyphLocY));
134
+ const hCurveCount = hbandData.x.toInt();
135
+ const hListOffset = hbandData.y.toInt();
136
+ const hbandLoc = calcBandLoc(glyphLocX, glyphLocY, hListOffset);
137
+ const hIdx = int(0).toVar();
138
+ Loop(hIdx.lessThan(hCurveCount), () => {
139
+ const clEntry = textureLoad(bandTex, ivec2(hbandLoc.x.add(hIdx), hbandLoc.y));
140
+ const cLocX = clEntry.x.toInt();
141
+ const cLocY = clEntry.y.toInt();
142
+ const rawP12 = textureLoad(curveTex, ivec2(cLocX, cLocY));
143
+ const rawP3 = textureLoad(curveTex, ivec2(cLocX.add(1), cLocY));
144
+ const p12 = vec4(rawP12.x.sub(renderCoord.x), rawP12.y.sub(renderCoord.y), rawP12.z.sub(renderCoord.x), rawP12.w.sub(renderCoord.y));
145
+ const p3 = vec2(rawP3.x.sub(renderCoord.x), rawP3.y.sub(renderCoord.y));
146
+ If(max(max(p12.x, p12.z), p3.x).mul(pixelsPerEm.x).lessThan(-0.5), () => {
147
+ Break();
148
+ });
149
+ const code = calcRootCode(p12.y, p12.w, p3.y);
150
+ If(code.notEqual(uint(0)), () => {
151
+ const r = solveHorizPoly(p12, p3).mul(pixelsPerEm.x);
152
+ If(code.bitAnd(uint(1)).notEqual(uint(0)), () => {
153
+ xcov.addAssign(clamp(r.x.add(0.5), 0, 1));
154
+ xwgt.assign(max(xwgt, clamp(float(1).sub(abs(r.x).mul(2)), 0, 1)));
155
+ });
156
+ If(code.greaterThan(uint(1)), () => {
157
+ xcov.subAssign(clamp(r.y.add(0.5), 0, 1));
158
+ xwgt.assign(max(xwgt, clamp(float(1).sub(abs(r.y).mul(2)), 0, 1)));
159
+ });
160
+ });
161
+ hIdx.addAssign(1);
162
+ });
163
+ // Vertical band loop
164
+ const ycov = float(0).toVar();
165
+ const ywgt = float(0).toVar();
166
+ const vbandOffset = bandMaxY.add(1).add(bandIdxX);
167
+ const vbandData = textureLoad(bandTex, ivec2(glyphLocX.add(vbandOffset), glyphLocY));
168
+ const vCurveCount = vbandData.x.toInt();
169
+ const vListOffset = vbandData.y.toInt();
170
+ const vbandLoc = calcBandLoc(glyphLocX, glyphLocY, vListOffset);
171
+ const vIdx = int(0).toVar();
172
+ Loop(vIdx.lessThan(vCurveCount), () => {
173
+ const clEntry = textureLoad(bandTex, ivec2(vbandLoc.x.add(vIdx), vbandLoc.y));
174
+ const cLocX = clEntry.x.toInt();
175
+ const cLocY = clEntry.y.toInt();
176
+ const rawP12 = textureLoad(curveTex, ivec2(cLocX, cLocY));
177
+ const rawP3 = textureLoad(curveTex, ivec2(cLocX.add(1), cLocY));
178
+ const p12 = vec4(rawP12.x.sub(renderCoord.x), rawP12.y.sub(renderCoord.y), rawP12.z.sub(renderCoord.x), rawP12.w.sub(renderCoord.y));
179
+ const p3 = vec2(rawP3.x.sub(renderCoord.x), rawP3.y.sub(renderCoord.y));
180
+ If(max(max(p12.y, p12.w), p3.y).mul(pixelsPerEm.y).lessThan(-0.5), () => {
181
+ Break();
182
+ });
183
+ const code = calcRootCode(p12.x, p12.z, p3.x);
184
+ If(code.notEqual(uint(0)), () => {
185
+ const r = solveVertPoly(p12, p3).mul(pixelsPerEm.y);
186
+ If(code.bitAnd(uint(1)).notEqual(uint(0)), () => {
187
+ ycov.subAssign(clamp(r.x.add(0.5), 0, 1));
188
+ ywgt.assign(max(ywgt, clamp(float(1).sub(abs(r.x).mul(2)), 0, 1)));
189
+ });
190
+ If(code.greaterThan(uint(1)), () => {
191
+ ycov.addAssign(clamp(r.y.add(0.5), 0, 1));
192
+ ywgt.assign(max(ywgt, clamp(float(1).sub(abs(r.y).mul(2)), 0, 1)));
193
+ });
194
+ });
195
+ vIdx.addAssign(1);
196
+ });
197
+ // CalcCoverage (nonzero winding rule)
198
+ 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
+ return clamp(coverage, 0, 1);
200
+ });
201
+ // Top-level fragment node
202
+ const fragmentNode = Fn(() => {
203
+ const emsPerPixel = fwidth(vTexcoord);
204
+ const coverage = slugRenderSingle(vTexcoord, emsPerPixel, vBanding, vGlyph);
205
+ return vec4(textColor.x, textColor.y, textColor.z, vColor.w.mul(coverage));
206
+ })();
207
+ // Material & mesh
208
+ const material = new MeshBasicNodeMaterial();
209
+ material.fragmentNode = fragmentNode;
210
+ material.transparent = true;
211
+ material.depthWrite = false;
212
+ material.side = THREE.DoubleSide;
213
+ const mesh = new THREE.Mesh(geo, material);
214
+ return {
215
+ mesh,
216
+ setOffset(x, y, z = 0) {
217
+ mesh.position.set(x, y, z);
218
+ },
219
+ setColor(r, g, b) {
220
+ textColor.value.setRGB(r, g, b);
221
+ },
222
+ dispose() {
223
+ geo.dispose();
224
+ material.dispose();
225
+ curveTex.dispose();
226
+ bandTex.dispose();
227
+ }
228
+ };
229
+ }
230
+
231
+ export { createSlugTSLMesh };