meshwriter-cudu 3.0.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.md +11 -0
- package/README.md +349 -0
- package/dist/fonts/comic-sans.d.ts +1105 -0
- package/dist/fonts/helvetica.d.ts +1208 -0
- package/dist/fonts/hiruko-pro.d.ts +658 -0
- package/dist/fonts/jura.d.ts +750 -0
- package/dist/fonts/webgl-dings.d.ts +109 -0
- package/dist/index.d.ts +295 -0
- package/dist/meshwriter.cjs.js +2645 -0
- package/dist/meshwriter.cjs.js.map +1 -0
- package/dist/meshwriter.esm.js +2606 -0
- package/dist/meshwriter.esm.js.map +1 -0
- package/dist/meshwriter.min.js +2 -0
- package/dist/meshwriter.min.js.map +1 -0
- package/dist/meshwriter.umd.js +7146 -0
- package/dist/meshwriter.umd.js.map +1 -0
- package/dist/src/babylonImports.d.ts +11 -0
- package/dist/src/bakedFontLoader.d.ts +43 -0
- package/dist/src/colorContrast.d.ts +117 -0
- package/dist/src/csg.d.ts +55 -0
- package/dist/src/curves.d.ts +20 -0
- package/dist/src/fogPlugin.d.ts +32 -0
- package/dist/src/fontCompression.d.ts +12 -0
- package/dist/src/fontRegistry.d.ts +54 -0
- package/dist/src/index.d.ts +47 -0
- package/dist/src/letterMesh.d.ts +46 -0
- package/dist/src/material.d.ts +34 -0
- package/dist/src/meshSplitter.d.ts +10 -0
- package/dist/src/meshwriter.d.ts +46 -0
- package/dist/src/sps.d.ts +27 -0
- package/dist/src/umd-entry.d.ts +3 -0
- package/dist/src/utils.d.ts +12 -0
- package/dist/src/variableFontCache.d.ts +56 -0
- package/dist/src/variableFontConverter.d.ts +21 -0
- package/dist/src/variableFontLoader.d.ts +99 -0
- package/fonts/Figure1.png +0 -0
- package/fonts/LICENSE-OFL.txt +93 -0
- package/fonts/README.md +174 -0
- package/fonts/atkinson-hyperlegible-next.d.ts +8 -0
- package/fonts/atkinson-hyperlegible-next.js +6576 -0
- package/fonts/atkinson-hyperlegible.js +3668 -0
- package/fonts/baked/atkinson-hyperlegible-next-200.json +1 -0
- package/fonts/baked/atkinson-hyperlegible-next-250.json +1 -0
- package/fonts/baked/atkinson-hyperlegible-next-300.json +1 -0
- package/fonts/baked/atkinson-hyperlegible-next-350.json +1 -0
- package/fonts/baked/atkinson-hyperlegible-next-400.json +1 -0
- package/fonts/baked/atkinson-hyperlegible-next-450.json +1 -0
- package/fonts/baked/atkinson-hyperlegible-next-500.json +1 -0
- package/fonts/baked/atkinson-hyperlegible-next-550.json +1 -0
- package/fonts/baked/atkinson-hyperlegible-next-600.json +1 -0
- package/fonts/baked/atkinson-hyperlegible-next-650.json +1 -0
- package/fonts/baked/atkinson-hyperlegible-next-700.json +1 -0
- package/fonts/baked/atkinson-hyperlegible-next-750.json +1 -0
- package/fonts/baked/atkinson-hyperlegible-next-800.json +1 -0
- package/fonts/baked/manifest.json +41 -0
- package/fonts/comic-sans.js +1532 -0
- package/fonts/helvetica.js +1695 -0
- package/fonts/hiruko-pro.js +838 -0
- package/fonts/index.js +16 -0
- package/fonts/jura.js +994 -0
- package/fonts/variable/atkinson-hyperlegible-next-variable.ttf +0 -0
- package/fonts/webgl-dings.js +113 -0
- package/package.json +76 -0
- package/src/babylonImports.js +29 -0
- package/src/bakedFontLoader.js +125 -0
- package/src/colorContrast.js +528 -0
- package/src/csg.js +220 -0
- package/src/curves.js +67 -0
- package/src/fogPlugin.js +98 -0
- package/src/fontCompression.js +141 -0
- package/src/fontRegistry.js +98 -0
- package/src/globals.d.ts +20 -0
- package/src/index.js +136 -0
- package/src/letterMesh.js +417 -0
- package/src/material.js +103 -0
- package/src/meshSplitter.js +337 -0
- package/src/meshwriter.js +303 -0
- package/src/sps.js +106 -0
- package/src/types.d.ts +551 -0
- package/src/umd-entry.js +130 -0
- package/src/utils.js +57 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { Mesh, VertexData } from './babylonImports.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Split a mesh into two meshes: emissive face geometry and lit rim geometry.
|
|
5
|
+
* @param {import('@babylonjs/core').Mesh} mesh
|
|
6
|
+
* @param {import('@babylonjs/core').Scene} scene
|
|
7
|
+
* @returns {{ rimMesh: import('@babylonjs/core').Mesh, faceMesh: import('@babylonjs/core').Mesh | null }}
|
|
8
|
+
*/
|
|
9
|
+
export function splitMeshByFaceNormals(mesh, scene) {
|
|
10
|
+
var geometry = mesh.geometry
|
|
11
|
+
if (!geometry) {
|
|
12
|
+
return { rimMesh: mesh, faceMesh: null }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
var positions = geometry.getVerticesData('position')
|
|
16
|
+
var normals = geometry.getVerticesData('normal')
|
|
17
|
+
var uvs = geometry.getVerticesData('uv')
|
|
18
|
+
var indices = geometry.getIndices()
|
|
19
|
+
|
|
20
|
+
if (!positions || !normals || !indices || positions.length === 0) {
|
|
21
|
+
return { rimMesh: mesh, faceMesh: null }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
var faceData = createEmptyData()
|
|
25
|
+
var rimData = createEmptyData()
|
|
26
|
+
|
|
27
|
+
var axisInfo = detectFaceAxisFromGeometry(positions, normals, indices)
|
|
28
|
+
if (!axisInfo) {
|
|
29
|
+
axisInfo = detectFaceAxis(normals)
|
|
30
|
+
}
|
|
31
|
+
if (!axisInfo) {
|
|
32
|
+
axisInfo = detectExtrudeAxisFromPositions(positions)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
for (var i = 0; i < indices.length; i += 3) {
|
|
36
|
+
var i0 = indices[i]
|
|
37
|
+
var i1 = indices[i + 1]
|
|
38
|
+
var i2 = indices[i + 2]
|
|
39
|
+
var isFace = triangleIsFrontFace(i0, i1, i2, positions, normals, axisInfo)
|
|
40
|
+
var target = isFace ? faceData : rimData
|
|
41
|
+
var v0 = appendVertex(target, i0, positions, normals, uvs)
|
|
42
|
+
var v1 = appendVertex(target, i1, positions, normals, uvs)
|
|
43
|
+
var v2 = appendVertex(target, i2, positions, normals, uvs)
|
|
44
|
+
target.indices.push(v0, v1, v2)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
mesh.dispose()
|
|
48
|
+
|
|
49
|
+
var rimMesh = buildMesh(mesh.name + '_rim', rimData, scene)
|
|
50
|
+
var faceMesh = buildMesh(mesh.name + '_face', faceData, scene)
|
|
51
|
+
|
|
52
|
+
if (!faceMesh) {
|
|
53
|
+
return { rimMesh: rimMesh || mesh, faceMesh: null }
|
|
54
|
+
}
|
|
55
|
+
if (!rimMesh) {
|
|
56
|
+
return { rimMesh: faceMesh, faceMesh: null }
|
|
57
|
+
}
|
|
58
|
+
return { rimMesh, faceMesh }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function detectFaceAxisFromGeometry(positions, normals, indices) {
|
|
62
|
+
if (!positions || !indices) return null
|
|
63
|
+
|
|
64
|
+
var min = [Infinity, Infinity, Infinity]
|
|
65
|
+
var max = [-Infinity, -Infinity, -Infinity]
|
|
66
|
+
for (var i = 0; i < positions.length; i += 3) {
|
|
67
|
+
var x = positions[i]
|
|
68
|
+
var y = positions[i + 1]
|
|
69
|
+
var z = positions[i + 2]
|
|
70
|
+
if (x < min[0]) min[0] = x
|
|
71
|
+
if (x > max[0]) max[0] = x
|
|
72
|
+
if (y < min[1]) min[1] = y
|
|
73
|
+
if (y > max[1]) max[1] = y
|
|
74
|
+
if (z < min[2]) min[2] = z
|
|
75
|
+
if (z > max[2]) max[2] = z
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
var epsilons = [
|
|
79
|
+
Math.max((max[0] - min[0]) * 0.15, 0.001),
|
|
80
|
+
Math.max((max[1] - min[1]) * 0.15, 0.001),
|
|
81
|
+
Math.max((max[2] - min[2]) * 0.15, 0.001)
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
var counts = [
|
|
85
|
+
{ min: 0, max: 0, sumMin: 0, sumMax: 0 },
|
|
86
|
+
{ min: 0, max: 0, sumMin: 0, sumMax: 0 },
|
|
87
|
+
{ min: 0, max: 0, sumMin: 0, sumMax: 0 }
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
for (var idx = 0; idx < indices.length; idx += 3) {
|
|
91
|
+
var i0 = indices[idx]
|
|
92
|
+
var i1 = indices[idx + 1]
|
|
93
|
+
var i2 = indices[idx + 2]
|
|
94
|
+
|
|
95
|
+
for (var axis = 0; axis < 3; axis++) {
|
|
96
|
+
var epsilon = epsilons[axis]
|
|
97
|
+
if (epsilon <= 0) continue
|
|
98
|
+
var minVal = min[axis]
|
|
99
|
+
var maxVal = max[axis]
|
|
100
|
+
|
|
101
|
+
var c0 = positions[i0 * 3 + axis]
|
|
102
|
+
var c1 = positions[i1 * 3 + axis]
|
|
103
|
+
var c2 = positions[i2 * 3 + axis]
|
|
104
|
+
|
|
105
|
+
var nearMin = Math.abs(c0 - minVal) < epsilon &&
|
|
106
|
+
Math.abs(c1 - minVal) < epsilon &&
|
|
107
|
+
Math.abs(c2 - minVal) < epsilon
|
|
108
|
+
var nearMax = Math.abs(c0 - maxVal) < epsilon &&
|
|
109
|
+
Math.abs(c1 - maxVal) < epsilon &&
|
|
110
|
+
Math.abs(c2 - maxVal) < epsilon
|
|
111
|
+
|
|
112
|
+
if (nearMin) {
|
|
113
|
+
counts[axis].min++
|
|
114
|
+
if (normals) {
|
|
115
|
+
counts[axis].sumMin += normals[i0 * 3 + axis] + normals[i1 * 3 + axis] + normals[i2 * 3 + axis]
|
|
116
|
+
}
|
|
117
|
+
} else if (nearMax) {
|
|
118
|
+
counts[axis].max++
|
|
119
|
+
if (normals) {
|
|
120
|
+
counts[axis].sumMax += normals[i0 * 3 + axis] + normals[i1 * 3 + axis] + normals[i2 * 3 + axis]
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
var bestAxis = -1
|
|
127
|
+
var bestCount = 0
|
|
128
|
+
for (var axisIdx = 0; axisIdx < 3; axisIdx++) {
|
|
129
|
+
var total = counts[axisIdx].min + counts[axisIdx].max
|
|
130
|
+
if (total > bestCount) {
|
|
131
|
+
bestCount = total
|
|
132
|
+
bestAxis = axisIdx
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (bestAxis === -1 || bestCount === 0) {
|
|
137
|
+
return null
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// For 3D text, extrusion is typically along Y axis (axis=1).
|
|
141
|
+
// If Y axis has any face triangles and the detected axis is different,
|
|
142
|
+
// prefer Y unless the detected axis has significantly more triangles.
|
|
143
|
+
var yTotal = counts[1].min + counts[1].max
|
|
144
|
+
if (bestAxis !== 1 && yTotal > 0) {
|
|
145
|
+
// Only switch away from Y if another axis has 2x more triangles
|
|
146
|
+
if (bestCount < yTotal * 2) {
|
|
147
|
+
bestAxis = 1
|
|
148
|
+
bestCount = yTotal
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
var chosen = counts[bestAxis]
|
|
153
|
+
var frontSide
|
|
154
|
+
// For PolygonMeshBuilder extrusions: geometry goes from Y=0 (base) to Y=-depth (extruded end)
|
|
155
|
+
// After rotation -PI/2 around X: Y=0 → Z=0 (front, facing camera), Y=-depth → Z=+depth (back)
|
|
156
|
+
// So Y=max (Y=0) is the FRONT face, Y=min (Y=-depth) is the BACK face
|
|
157
|
+
var maxHasOutwardNormals = chosen.sumMax > 0
|
|
158
|
+
var minHasOutwardNormals = chosen.sumMin < 0
|
|
159
|
+
|
|
160
|
+
// Select MAX as front (the base at Y=0 which faces camera after rotation)
|
|
161
|
+
if (maxHasOutwardNormals) {
|
|
162
|
+
frontSide = 'max'
|
|
163
|
+
} else if (minHasOutwardNormals) {
|
|
164
|
+
frontSide = 'min'
|
|
165
|
+
} else {
|
|
166
|
+
// Fallback to max (the base)
|
|
167
|
+
frontSide = 'max'
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
axis: bestAxis,
|
|
172
|
+
strategy: 'positions',
|
|
173
|
+
min: min[bestAxis],
|
|
174
|
+
max: max[bestAxis],
|
|
175
|
+
epsilon: epsilons[bestAxis],
|
|
176
|
+
frontSide
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function detectFaceAxis(normals) {
|
|
181
|
+
if (!normals || normals.length === 0) {
|
|
182
|
+
return null
|
|
183
|
+
}
|
|
184
|
+
var sumsAbs = [0, 0, 0]
|
|
185
|
+
var sumsSigned = [0, 0, 0]
|
|
186
|
+
for (var i = 0; i < normals.length; i += 3) {
|
|
187
|
+
var nx = normals[i]
|
|
188
|
+
var ny = normals[i + 1]
|
|
189
|
+
var nz = normals[i + 2]
|
|
190
|
+
sumsAbs[0] += Math.abs(nx)
|
|
191
|
+
sumsAbs[1] += Math.abs(ny)
|
|
192
|
+
sumsAbs[2] += Math.abs(nz)
|
|
193
|
+
sumsSigned[0] += nx
|
|
194
|
+
sumsSigned[1] += ny
|
|
195
|
+
sumsSigned[2] += nz
|
|
196
|
+
}
|
|
197
|
+
var axis = 0
|
|
198
|
+
var maxSum = sumsAbs[0]
|
|
199
|
+
for (var j = 1; j < 3; j++) {
|
|
200
|
+
if (sumsAbs[j] > maxSum) {
|
|
201
|
+
maxSum = sumsAbs[j]
|
|
202
|
+
axis = j
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (maxSum === 0) {
|
|
206
|
+
return null
|
|
207
|
+
}
|
|
208
|
+
var frontSign = sumsSigned[axis] >= 0 ? 1 : -1
|
|
209
|
+
return {
|
|
210
|
+
axis,
|
|
211
|
+
frontSign,
|
|
212
|
+
strategy: 'normals',
|
|
213
|
+
min: 0,
|
|
214
|
+
max: 0,
|
|
215
|
+
epsilon: 0,
|
|
216
|
+
frontSide: 'max'
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function detectExtrudeAxisFromPositions(positions) {
|
|
221
|
+
if (!positions || positions.length === 0) {
|
|
222
|
+
return null
|
|
223
|
+
}
|
|
224
|
+
var min = [Infinity, Infinity, Infinity]
|
|
225
|
+
var max = [-Infinity, -Infinity, -Infinity]
|
|
226
|
+
for (var i = 0; i < positions.length; i += 3) {
|
|
227
|
+
var x = positions[i]
|
|
228
|
+
var y = positions[i + 1]
|
|
229
|
+
var z = positions[i + 2]
|
|
230
|
+
if (x < min[0]) min[0] = x
|
|
231
|
+
if (x > max[0]) max[0] = x
|
|
232
|
+
if (y < min[1]) min[1] = y
|
|
233
|
+
if (y > max[1]) max[1] = y
|
|
234
|
+
if (z < min[2]) min[2] = z
|
|
235
|
+
if (z > max[2]) max[2] = z
|
|
236
|
+
}
|
|
237
|
+
var ranges = [
|
|
238
|
+
max[0] - min[0],
|
|
239
|
+
max[1] - min[1],
|
|
240
|
+
max[2] - min[2]
|
|
241
|
+
]
|
|
242
|
+
var axis = 0
|
|
243
|
+
var minRange = ranges[0]
|
|
244
|
+
for (var j = 1; j < 3; j++) {
|
|
245
|
+
if (ranges[j] < minRange) {
|
|
246
|
+
minRange = ranges[j]
|
|
247
|
+
axis = j
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
var epsilon = Math.max(minRange * 0.05, 0.0001)
|
|
251
|
+
return {
|
|
252
|
+
axis,
|
|
253
|
+
strategy: 'positions',
|
|
254
|
+
min: min[axis],
|
|
255
|
+
max: max[axis],
|
|
256
|
+
epsilon,
|
|
257
|
+
frontSide: 'max'
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function triangleIsFrontFace(i0, i1, i2, positions, normals, axisInfo) {
|
|
262
|
+
if (!axisInfo) return false
|
|
263
|
+
var axis = axisInfo.axis || 1
|
|
264
|
+
|
|
265
|
+
if (axisInfo.strategy === 'normals' && normals) {
|
|
266
|
+
var frontSign = axisInfo.frontSign || 1
|
|
267
|
+
var threshold = 0.5
|
|
268
|
+
var n0 = normals[i0 * 3 + axis] * frontSign
|
|
269
|
+
var n1 = normals[i1 * 3 + axis] * frontSign
|
|
270
|
+
var n2 = normals[i2 * 3 + axis] * frontSign
|
|
271
|
+
return (n0 > threshold && n1 > threshold && n2 > threshold)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (axisInfo.strategy === 'positions' && positions) {
|
|
275
|
+
var epsilon = axisInfo.epsilon
|
|
276
|
+
var limitVal = axisInfo.frontSide === 'min' ? axisInfo.min : axisInfo.max
|
|
277
|
+
var c0 = positions[i0 * 3 + axis]
|
|
278
|
+
var c1 = positions[i1 * 3 + axis]
|
|
279
|
+
var c2 = positions[i2 * 3 + axis]
|
|
280
|
+
return (
|
|
281
|
+
Math.abs(c0 - limitVal) < epsilon &&
|
|
282
|
+
Math.abs(c1 - limitVal) < epsilon &&
|
|
283
|
+
Math.abs(c2 - limitVal) < epsilon
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return false
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function createEmptyData() {
|
|
291
|
+
return {
|
|
292
|
+
positions: [],
|
|
293
|
+
normals: [],
|
|
294
|
+
uvs: [],
|
|
295
|
+
indices: [],
|
|
296
|
+
nextIndex: 0
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function appendVertex(target, originalIndex, positions, normals, uvs) {
|
|
301
|
+
var posOffset = originalIndex * 3
|
|
302
|
+
var uvOffset = originalIndex * 2
|
|
303
|
+
|
|
304
|
+
target.positions.push(
|
|
305
|
+
positions[posOffset],
|
|
306
|
+
positions[posOffset + 1],
|
|
307
|
+
positions[posOffset + 2]
|
|
308
|
+
)
|
|
309
|
+
target.normals.push(
|
|
310
|
+
normals[posOffset],
|
|
311
|
+
normals[posOffset + 1],
|
|
312
|
+
normals[posOffset + 2]
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
if (uvs && uvs.length) {
|
|
316
|
+
target.uvs.push(uvs[uvOffset], uvs[uvOffset + 1])
|
|
317
|
+
} else {
|
|
318
|
+
target.uvs.push(0, 0)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
var newIndex = target.nextIndex
|
|
322
|
+
target.nextIndex += 1
|
|
323
|
+
return newIndex
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function buildMesh(name, data, scene) {
|
|
327
|
+
if (!data.positions.length) return null
|
|
328
|
+
var newMesh = new Mesh(name, scene)
|
|
329
|
+
var vertexData = new VertexData()
|
|
330
|
+
vertexData.positions = data.positions
|
|
331
|
+
vertexData.normals = data.normals
|
|
332
|
+
vertexData.indices = data.indices
|
|
333
|
+
vertexData.uvs = data.uvs
|
|
334
|
+
vertexData.applyToMesh(newMesh, true)
|
|
335
|
+
newMesh.refreshBoundingInfo()
|
|
336
|
+
return newMesh
|
|
337
|
+
}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MeshWriter Core Class
|
|
3
|
+
* Main MeshWriter implementation for 3D text rendering in Babylon.js
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Vector2 } from './babylonImports.js';
|
|
7
|
+
import { getFont, isFontRegistered } from './fontRegistry.js';
|
|
8
|
+
import {
|
|
9
|
+
initCSGModule, isCSGReady, getCSGVersion, initializeCSG2,
|
|
10
|
+
setCSGInitializer, setCSGReadyCheck, onCSGReady, markCSGInitialized
|
|
11
|
+
} from './csg.js';
|
|
12
|
+
import { makeMaterial, makeFaceMaterial, rgb2Color3 } from './material.js';
|
|
13
|
+
import { makeSPS } from './sps.js';
|
|
14
|
+
import { constructLetterPolygons, naturalLetterHeight } from './letterMesh.js';
|
|
15
|
+
import { installCurveExtensions } from './curves.js';
|
|
16
|
+
import {
|
|
17
|
+
isObject, isNumber, isBoolean, isString, isAmplitude,
|
|
18
|
+
isPositiveNumber, round, setOption
|
|
19
|
+
} from './utils.js';
|
|
20
|
+
|
|
21
|
+
/** @typedef {import('@babylonjs/core/scene').Scene} Scene */
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {Object} MeshWriterPreferences
|
|
25
|
+
* @property {string} [defaultFont] - Default font family
|
|
26
|
+
* @property {number} [scale=1] - Scale factor
|
|
27
|
+
* @property {string} [meshOrigin="letterCenter"] - "letterCenter" or "fontOrigin"
|
|
28
|
+
* @property {boolean} [debug=false] - Enable debug logging
|
|
29
|
+
* @property {Object} [babylon] - Babylon.js namespace object with CSG classes (for ES module builds)
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
// Constants
|
|
33
|
+
const defaultColor = "#808080";
|
|
34
|
+
const defaultOpac = 1;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Create a MeshWriter factory configured for a scene
|
|
38
|
+
* @param {Scene} scene - Babylon.js scene
|
|
39
|
+
* @param {MeshWriterPreferences} [preferences={}] - Configuration options
|
|
40
|
+
* @returns {Function} - MeshWriter constructor
|
|
41
|
+
*/
|
|
42
|
+
export function createMeshWriter(scene, preferences = {}) {
|
|
43
|
+
// Install curve extensions for Path2
|
|
44
|
+
installCurveExtensions();
|
|
45
|
+
|
|
46
|
+
const defaultFont = isFontRegistered(preferences.defaultFont)
|
|
47
|
+
? preferences.defaultFont
|
|
48
|
+
: (isFontRegistered("Helvetica") ? "Helvetica" : "HelveticaNeue-Medium");
|
|
49
|
+
const meshOrigin = preferences.meshOrigin === "fontOrigin"
|
|
50
|
+
? "fontOrigin"
|
|
51
|
+
: "letterCenter";
|
|
52
|
+
const scale = isNumber(preferences.scale) ? preferences.scale : 1;
|
|
53
|
+
const debug = isBoolean(preferences.debug) ? preferences.debug : false;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* MeshWriter constructor - creates 3D text
|
|
57
|
+
* @param {string} lttrs - Text to render
|
|
58
|
+
* @param {Object} opt - Options
|
|
59
|
+
*/
|
|
60
|
+
function MeshWriter(lttrs, opt) {
|
|
61
|
+
const options = isObject(opt) ? opt : {};
|
|
62
|
+
const position = setOption(options, "position", isObject, {});
|
|
63
|
+
const colors = setOption(options, "colors", isObject, {});
|
|
64
|
+
const fontFamily = setOption(options, "font-family", isSupportedFont, defaultFont);
|
|
65
|
+
const anchor = setOption(options, "anchor", isSupportedAnchor, "left");
|
|
66
|
+
const rawheight = setOption(options, "letter-height", isPositiveNumber, 100);
|
|
67
|
+
const rawThickness = setOption(options, "letter-thickness", isPositiveNumber, 1);
|
|
68
|
+
const basicColor = setOption(options, "color", isString, defaultColor);
|
|
69
|
+
const opac = setOption(options, "alpha", isAmplitude, defaultOpac);
|
|
70
|
+
const y = setOption(position, "y", isNumber, 0);
|
|
71
|
+
const x = setOption(position, "x", isNumber, 0);
|
|
72
|
+
const z = setOption(position, "z", isNumber, 0);
|
|
73
|
+
const diffuse = setOption(colors, "diffuse", isString, "#404040"); // Dark gray - lets emissive show
|
|
74
|
+
const specular = setOption(colors, "specular", isString, "#000000");
|
|
75
|
+
const ambient = setOption(colors, "ambient", isString, "#202020"); // Very dark - minimal ambient response
|
|
76
|
+
const emissive = setOption(colors, "emissive", isString, basicColor);
|
|
77
|
+
const emissiveOnly = setOption(options, "emissive-only", isBoolean, false);
|
|
78
|
+
const fogEnabled = setOption(options, "fog-enabled", isBoolean, true);
|
|
79
|
+
const letterSpacingRaw = setOption(options, "letter-spacing", isNumber, 0);
|
|
80
|
+
const wordSpacingRaw = setOption(options, "word-spacing", isNumber, 0);
|
|
81
|
+
const fontSpec = getFont(fontFamily);
|
|
82
|
+
const letterScale = round(scale * rawheight / naturalLetterHeight);
|
|
83
|
+
const thickness = round(scale * rawThickness);
|
|
84
|
+
const letters = isString(lttrs) ? lttrs : "";
|
|
85
|
+
// Scale spacing values to match letter scale
|
|
86
|
+
const letterSpacing = letterSpacingRaw * scale;
|
|
87
|
+
const wordSpacing = wordSpacingRaw * scale;
|
|
88
|
+
|
|
89
|
+
// Create material
|
|
90
|
+
const material = makeMaterial(scene, letters, emissive, ambient, specular, diffuse, opac, emissiveOnly, fogEnabled);
|
|
91
|
+
|
|
92
|
+
// Create letter meshes
|
|
93
|
+
const meshesAndBoxes = constructLetterPolygons(
|
|
94
|
+
letters, fontSpec, 0, 0, 0, letterScale, thickness, material, meshOrigin, scene,
|
|
95
|
+
{ letterSpacing, wordSpacing }
|
|
96
|
+
);
|
|
97
|
+
const meshes = meshesAndBoxes[0];
|
|
98
|
+
const lettersBoxes = meshesAndBoxes[1];
|
|
99
|
+
const lettersOrigins = meshesAndBoxes[2];
|
|
100
|
+
const xWidth = meshesAndBoxes.xWidth;
|
|
101
|
+
|
|
102
|
+
// Convert to SPS
|
|
103
|
+
const combo = makeSPS(scene, meshesAndBoxes, material);
|
|
104
|
+
const sps = combo[0];
|
|
105
|
+
const mesh = combo[1];
|
|
106
|
+
const faceCombo = combo.face || [];
|
|
107
|
+
const faceSps = faceCombo[0];
|
|
108
|
+
const faceMesh = faceCombo[1];
|
|
109
|
+
let faceMaterial;
|
|
110
|
+
|
|
111
|
+
if (faceMesh) {
|
|
112
|
+
faceMaterial = makeFaceMaterial(scene, letters, emissive, opac, fogEnabled);
|
|
113
|
+
faceMesh.material = faceMaterial;
|
|
114
|
+
if (mesh) {
|
|
115
|
+
faceMesh.parent = mesh;
|
|
116
|
+
faceMesh.layerMask = mesh.layerMask;
|
|
117
|
+
faceMesh.renderingGroupId = mesh.renderingGroupId;
|
|
118
|
+
}
|
|
119
|
+
// Tiny offset to prevent z-fighting without leaving a visible gap
|
|
120
|
+
// rotation.x=-PI/2 maps: +Y → -Z (toward camera), -Y → +Z (away)
|
|
121
|
+
// We want face IN FRONT of rim, so use POSITIVE Y
|
|
122
|
+
faceMesh.position.y = 0.001;
|
|
123
|
+
faceMesh.isPickable = false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Position mesh based on anchor
|
|
127
|
+
const offsetX = anchor === "right"
|
|
128
|
+
? (0 - xWidth)
|
|
129
|
+
: (anchor === "center" ? (0 - xWidth / 2) : 0);
|
|
130
|
+
|
|
131
|
+
if (mesh) {
|
|
132
|
+
mesh.position.x = scale * x + offsetX;
|
|
133
|
+
mesh.position.y = scale * y;
|
|
134
|
+
mesh.position.z = scale * z;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Instance methods
|
|
138
|
+
let color = basicColor;
|
|
139
|
+
this.getSPS = () => sps;
|
|
140
|
+
this.getMesh = () => mesh;
|
|
141
|
+
this.getMaterial = () => material;
|
|
142
|
+
this.getFaceMesh = () => faceMesh;
|
|
143
|
+
this.getFaceMaterial = () => faceMaterial;
|
|
144
|
+
this.getFaceSPS = () => faceSps;
|
|
145
|
+
this.getOffsetX = () => offsetX;
|
|
146
|
+
this.getLettersBoxes = () => lettersBoxes;
|
|
147
|
+
this.getLettersOrigins = () => lettersOrigins;
|
|
148
|
+
this.color = c => isString(c) ? color = c : color;
|
|
149
|
+
this.alpha = o => isAmplitude(o) ? opac : opac;
|
|
150
|
+
// Track disposed state to prevent double-disposal
|
|
151
|
+
let _disposed = false;
|
|
152
|
+
|
|
153
|
+
this.clearall = function() {
|
|
154
|
+
// Mark as disposed - getters will return null after this
|
|
155
|
+
_disposed = true;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
this.isDisposed = function() {
|
|
159
|
+
return _disposed;
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Prototype methods
|
|
164
|
+
MeshWriter.prototype.setColor = function(color) {
|
|
165
|
+
const material = this.getMaterial();
|
|
166
|
+
if (material && isString(color)) {
|
|
167
|
+
const next = rgb2Color3(this.color(color));
|
|
168
|
+
material.emissiveColor = next;
|
|
169
|
+
const faceMaterial = this.getFaceMaterial && this.getFaceMaterial();
|
|
170
|
+
if (faceMaterial) {
|
|
171
|
+
faceMaterial.emissiveColor = next;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
MeshWriter.prototype.setAlpha = function(alpha) {
|
|
177
|
+
const material = this.getMaterial();
|
|
178
|
+
if (material && isAmplitude(alpha)) {
|
|
179
|
+
const next = this.alpha(alpha);
|
|
180
|
+
material.alpha = next;
|
|
181
|
+
const faceMaterial = this.getFaceMaterial && this.getFaceMaterial();
|
|
182
|
+
if (faceMaterial) {
|
|
183
|
+
faceMaterial.alpha = next;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
MeshWriter.prototype.overrideAlpha = function(alpha) {
|
|
189
|
+
const material = this.getMaterial();
|
|
190
|
+
if (material && isAmplitude(alpha)) {
|
|
191
|
+
material.alpha = alpha;
|
|
192
|
+
const faceMaterial = this.getFaceMaterial && this.getFaceMaterial();
|
|
193
|
+
if (faceMaterial) {
|
|
194
|
+
faceMaterial.alpha = alpha;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
MeshWriter.prototype.resetAlpha = function() {
|
|
200
|
+
const material = this.getMaterial();
|
|
201
|
+
const alpha = this.alpha();
|
|
202
|
+
if (material) {
|
|
203
|
+
material.alpha = alpha;
|
|
204
|
+
}
|
|
205
|
+
const faceMaterial = this.getFaceMaterial && this.getFaceMaterial();
|
|
206
|
+
if (faceMaterial) {
|
|
207
|
+
faceMaterial.alpha = alpha;
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
MeshWriter.prototype.getLetterCenter = function(ix) {
|
|
212
|
+
return new Vector2(0, 0);
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
MeshWriter.prototype.dispose = function() {
|
|
216
|
+
// Prevent double-disposal
|
|
217
|
+
if (this.isDisposed && this.isDisposed()) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Dispose TextFogPlugin before materials to break circular references
|
|
222
|
+
const material = this.getMaterial();
|
|
223
|
+
if (material) {
|
|
224
|
+
if (material._textFogPlugin && typeof material._textFogPlugin.dispose === 'function') {
|
|
225
|
+
material._textFogPlugin.dispose();
|
|
226
|
+
}
|
|
227
|
+
material.dispose();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const faceMaterial = this.getFaceMaterial && this.getFaceMaterial();
|
|
231
|
+
if (faceMaterial) {
|
|
232
|
+
if (faceMaterial._textFogPlugin && typeof faceMaterial._textFogPlugin.dispose === 'function') {
|
|
233
|
+
faceMaterial._textFogPlugin.dispose();
|
|
234
|
+
}
|
|
235
|
+
faceMaterial.dispose();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Dispose SolidParticleSystem (which also disposes its mesh)
|
|
239
|
+
const sps = this.getSPS();
|
|
240
|
+
if (sps) {
|
|
241
|
+
sps.dispose();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Dispose face mesh (merged mesh, not SPS-based)
|
|
245
|
+
const faceMesh = this.getFaceMesh && this.getFaceMesh();
|
|
246
|
+
if (faceMesh && typeof faceMesh.dispose === 'function') {
|
|
247
|
+
faceMesh.dispose();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Mark as disposed
|
|
251
|
+
this.clearall();
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
return MeshWriter;
|
|
255
|
+
|
|
256
|
+
// Helper functions
|
|
257
|
+
function isSupportedFont(ff) {
|
|
258
|
+
return isFontRegistered(ff);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function isSupportedAnchor(a) {
|
|
262
|
+
return a === "left" || a === "right" || a === "center";
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Async factory for Babylon 8+ with CSG2
|
|
268
|
+
* Handles CSG2 initialization automatically
|
|
269
|
+
* @param {Scene} scene - Babylon.js scene
|
|
270
|
+
* @param {MeshWriterPreferences} [preferences={}] - Configuration options
|
|
271
|
+
* @returns {Promise<Function>} - MeshWriter constructor
|
|
272
|
+
*/
|
|
273
|
+
export async function createMeshWriterAsync(scene, preferences = {}) {
|
|
274
|
+
// Initialize CSG module with Babylon methods
|
|
275
|
+
if (preferences.babylon) {
|
|
276
|
+
initCSGModule(preferences.babylon);
|
|
277
|
+
} else {
|
|
278
|
+
// Check for BABYLON global (UMD bundle usage)
|
|
279
|
+
/** @type {any} */
|
|
280
|
+
const globalBabylon = typeof globalThis !== 'undefined' ? globalThis.BABYLON : undefined;
|
|
281
|
+
if (globalBabylon) {
|
|
282
|
+
initCSGModule(globalBabylon);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Initialize CSG2 if needed
|
|
287
|
+
if (getCSGVersion() === 'CSG2' && !isCSGReady()) {
|
|
288
|
+
await initializeCSG2();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return createMeshWriter(scene, preferences);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Re-export CSG control functions for static API
|
|
295
|
+
export {
|
|
296
|
+
isCSGReady,
|
|
297
|
+
getCSGVersion,
|
|
298
|
+
setCSGInitializer,
|
|
299
|
+
setCSGReadyCheck,
|
|
300
|
+
onCSGReady,
|
|
301
|
+
markCSGInitialized as markCSGReady,
|
|
302
|
+
initCSGModule
|
|
303
|
+
};
|