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.
- package/LICENSE_THIRD_PARTY +15 -0
- package/README.md +73 -42
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -1
- package/dist/index.min.cjs +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/index.umd.min.js +1 -1
- package/dist/three/react.d.ts +2 -0
- package/dist/types/core/types.d.ts +2 -33
- package/dist/types/vector/{core.d.ts → core/index.d.ts} +8 -7
- package/dist/types/vector/index.d.ts +22 -25
- package/dist/types/vector/react.d.ts +4 -3
- package/dist/types/vector/slug/SlugPacker.d.ts +2 -0
- package/dist/types/vector/slug/curveUtils.d.ts +6 -0
- package/dist/types/vector/slug/index.d.ts +8 -0
- package/dist/types/vector/slug/shaderStrings.d.ts +4 -0
- package/dist/types/vector/slug/slugGLSL.d.ts +21 -0
- package/dist/types/vector/slug/slugTSL.d.ts +13 -0
- package/dist/types/vector/slug/types.d.ts +30 -0
- package/dist/types/vector/slug/unpackVertices.d.ts +11 -0
- package/dist/types/vector/webgl/index.d.ts +7 -3
- package/dist/types/vector/webgpu/index.d.ts +4 -4
- package/dist/vector/core/index.cjs +856 -0
- package/dist/vector/core/index.d.ts +63 -0
- package/dist/vector/core/index.js +854 -0
- package/dist/vector/core.cjs +4419 -240
- package/dist/vector/core.d.ts +361 -71
- package/dist/vector/core.js +4406 -226
- package/dist/vector/index.cjs +5 -229
- package/dist/vector/index.d.ts +45 -396
- package/dist/vector/index.js +3 -223
- package/dist/vector/index2.cjs +287 -0
- package/dist/vector/index2.js +264 -0
- package/dist/vector/react.cjs +37 -8
- package/dist/vector/react.d.ts +6 -3
- package/dist/vector/react.js +18 -8
- package/dist/vector/slugTSL.cjs +252 -0
- package/dist/vector/slugTSL.js +231 -0
- package/dist/vector/webgl/index.cjs +131 -201
- package/dist/vector/webgl/index.d.ts +19 -44
- package/dist/vector/webgl/index.js +131 -201
- package/dist/vector/webgpu/index.cjs +100 -283
- package/dist/vector/webgpu/index.d.ts +16 -45
- package/dist/vector/webgpu/index.js +100 -283
- package/package.json +6 -1
- package/dist/types/vector/GlyphVectorGeometryBuilder.d.ts +0 -26
- package/dist/types/vector/LoopBlinnGeometry.d.ts +0 -68
- package/dist/types/vector/loopBlinnTSL.d.ts +0 -22
- package/dist/vector/loopBlinnTSL.cjs +0 -229
- package/dist/vector/loopBlinnTSL.js +0 -207
package/dist/vector/react.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import * as THREE from 'three';
|
|
3
|
-
import {
|
|
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<
|
|
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
|
-
|
|
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>>> & {
|
package/dist/vector/react.js
CHANGED
|
@@ -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',
|
|
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:
|
|
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:
|
|
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
|
|
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 };
|