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.
Files changed (81) hide show
  1. package/LICENSE.md +11 -0
  2. package/README.md +349 -0
  3. package/dist/fonts/comic-sans.d.ts +1105 -0
  4. package/dist/fonts/helvetica.d.ts +1208 -0
  5. package/dist/fonts/hiruko-pro.d.ts +658 -0
  6. package/dist/fonts/jura.d.ts +750 -0
  7. package/dist/fonts/webgl-dings.d.ts +109 -0
  8. package/dist/index.d.ts +295 -0
  9. package/dist/meshwriter.cjs.js +2645 -0
  10. package/dist/meshwriter.cjs.js.map +1 -0
  11. package/dist/meshwriter.esm.js +2606 -0
  12. package/dist/meshwriter.esm.js.map +1 -0
  13. package/dist/meshwriter.min.js +2 -0
  14. package/dist/meshwriter.min.js.map +1 -0
  15. package/dist/meshwriter.umd.js +7146 -0
  16. package/dist/meshwriter.umd.js.map +1 -0
  17. package/dist/src/babylonImports.d.ts +11 -0
  18. package/dist/src/bakedFontLoader.d.ts +43 -0
  19. package/dist/src/colorContrast.d.ts +117 -0
  20. package/dist/src/csg.d.ts +55 -0
  21. package/dist/src/curves.d.ts +20 -0
  22. package/dist/src/fogPlugin.d.ts +32 -0
  23. package/dist/src/fontCompression.d.ts +12 -0
  24. package/dist/src/fontRegistry.d.ts +54 -0
  25. package/dist/src/index.d.ts +47 -0
  26. package/dist/src/letterMesh.d.ts +46 -0
  27. package/dist/src/material.d.ts +34 -0
  28. package/dist/src/meshSplitter.d.ts +10 -0
  29. package/dist/src/meshwriter.d.ts +46 -0
  30. package/dist/src/sps.d.ts +27 -0
  31. package/dist/src/umd-entry.d.ts +3 -0
  32. package/dist/src/utils.d.ts +12 -0
  33. package/dist/src/variableFontCache.d.ts +56 -0
  34. package/dist/src/variableFontConverter.d.ts +21 -0
  35. package/dist/src/variableFontLoader.d.ts +99 -0
  36. package/fonts/Figure1.png +0 -0
  37. package/fonts/LICENSE-OFL.txt +93 -0
  38. package/fonts/README.md +174 -0
  39. package/fonts/atkinson-hyperlegible-next.d.ts +8 -0
  40. package/fonts/atkinson-hyperlegible-next.js +6576 -0
  41. package/fonts/atkinson-hyperlegible.js +3668 -0
  42. package/fonts/baked/atkinson-hyperlegible-next-200.json +1 -0
  43. package/fonts/baked/atkinson-hyperlegible-next-250.json +1 -0
  44. package/fonts/baked/atkinson-hyperlegible-next-300.json +1 -0
  45. package/fonts/baked/atkinson-hyperlegible-next-350.json +1 -0
  46. package/fonts/baked/atkinson-hyperlegible-next-400.json +1 -0
  47. package/fonts/baked/atkinson-hyperlegible-next-450.json +1 -0
  48. package/fonts/baked/atkinson-hyperlegible-next-500.json +1 -0
  49. package/fonts/baked/atkinson-hyperlegible-next-550.json +1 -0
  50. package/fonts/baked/atkinson-hyperlegible-next-600.json +1 -0
  51. package/fonts/baked/atkinson-hyperlegible-next-650.json +1 -0
  52. package/fonts/baked/atkinson-hyperlegible-next-700.json +1 -0
  53. package/fonts/baked/atkinson-hyperlegible-next-750.json +1 -0
  54. package/fonts/baked/atkinson-hyperlegible-next-800.json +1 -0
  55. package/fonts/baked/manifest.json +41 -0
  56. package/fonts/comic-sans.js +1532 -0
  57. package/fonts/helvetica.js +1695 -0
  58. package/fonts/hiruko-pro.js +838 -0
  59. package/fonts/index.js +16 -0
  60. package/fonts/jura.js +994 -0
  61. package/fonts/variable/atkinson-hyperlegible-next-variable.ttf +0 -0
  62. package/fonts/webgl-dings.js +113 -0
  63. package/package.json +76 -0
  64. package/src/babylonImports.js +29 -0
  65. package/src/bakedFontLoader.js +125 -0
  66. package/src/colorContrast.js +528 -0
  67. package/src/csg.js +220 -0
  68. package/src/curves.js +67 -0
  69. package/src/fogPlugin.js +98 -0
  70. package/src/fontCompression.js +141 -0
  71. package/src/fontRegistry.js +98 -0
  72. package/src/globals.d.ts +20 -0
  73. package/src/index.js +136 -0
  74. package/src/letterMesh.js +417 -0
  75. package/src/material.js +103 -0
  76. package/src/meshSplitter.js +337 -0
  77. package/src/meshwriter.js +303 -0
  78. package/src/sps.js +106 -0
  79. package/src/types.d.ts +551 -0
  80. package/src/umd-entry.js +130 -0
  81. 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
+ };