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
package/src/index.js ADDED
@@ -0,0 +1,136 @@
1
+ /**
2
+ * MeshWriter - 3D Text Rendering for Babylon.js
3
+ *
4
+ * @example
5
+ * // ES Module usage
6
+ * import { MeshWriter, registerFont } from 'meshwriter';
7
+ * import helvetica from 'meshwriter/fonts/helvetica';
8
+ *
9
+ * registerFont('Helvetica', helvetica);
10
+ * const Writer = await MeshWriter.createAsync(scene);
11
+ * const text = new Writer("Hello World", { "letter-height": 20 });
12
+ */
13
+
14
+ // Core MeshWriter factory functions
15
+ export {
16
+ createMeshWriter,
17
+ createMeshWriterAsync,
18
+ isCSGReady,
19
+ getCSGVersion,
20
+ setCSGInitializer,
21
+ setCSGReadyCheck,
22
+ onCSGReady,
23
+ markCSGReady,
24
+ initCSGModule
25
+ } from './meshwriter.js';
26
+
27
+ // Font registry
28
+ export {
29
+ registerFont,
30
+ registerFontAliases,
31
+ getFont,
32
+ isFontRegistered,
33
+ getRegisteredFonts,
34
+ unregisterFont,
35
+ clearFonts,
36
+ codeList,
37
+ decodeList
38
+ } from './fontRegistry.js';
39
+
40
+ // Baked font loader (lightweight, no dependencies)
41
+ export {
42
+ loadBakedFont,
43
+ loadBakedFontsFromManifest,
44
+ findNearestWeight,
45
+ getBakedFontManifest
46
+ } from './bakedFontLoader.js';
47
+
48
+ // Utility exports (for advanced usage / font creation)
49
+ export { codeList as encodeFontData, decodeList as decodeFontData } from './fontCompression.js';
50
+
51
+ // Material plugins (for advanced usage)
52
+ export { TextFogPlugin } from './fogPlugin.js';
53
+
54
+ // Color contrast utilities (for accessibility)
55
+ export {
56
+ deriveEdgeColors,
57
+ adjustForContrast,
58
+ relativeLuminance,
59
+ contrastRatio,
60
+ hexToRgb,
61
+ rgbToHex,
62
+ rgbToHsl,
63
+ hslToRgb,
64
+ CONTRAST_LEVELS
65
+ } from './colorContrast.js';
66
+
67
+ /**
68
+ * MeshWriter namespace object (for convenience and backward compatibility patterns)
69
+ */
70
+ import {
71
+ createMeshWriter,
72
+ createMeshWriterAsync,
73
+ isCSGReady,
74
+ getCSGVersion,
75
+ setCSGInitializer,
76
+ setCSGReadyCheck,
77
+ onCSGReady,
78
+ markCSGReady,
79
+ initCSGModule
80
+ } from './meshwriter.js';
81
+
82
+ import {
83
+ registerFont,
84
+ registerFontAliases,
85
+ getFont,
86
+ isFontRegistered
87
+ } from './fontRegistry.js';
88
+
89
+ import { codeList, decodeList } from './fontCompression.js';
90
+
91
+ import {
92
+ loadBakedFont,
93
+ loadBakedFontsFromManifest,
94
+ findNearestWeight,
95
+ getBakedFontManifest
96
+ } from './bakedFontLoader.js';
97
+
98
+ export const MeshWriter = {
99
+ /**
100
+ * Create MeshWriter async (recommended for Babylon 8+)
101
+ */
102
+ createAsync: createMeshWriterAsync,
103
+
104
+ /**
105
+ * Create MeshWriter sync (for Babylon < 8 or when CSG2 is pre-initialized)
106
+ */
107
+ create: createMeshWriter,
108
+
109
+ // Static CSG methods
110
+ isReady: isCSGReady,
111
+ getCSGVersion,
112
+ setCSGInitializer,
113
+ setCSGReadyCheck,
114
+ onCSGReady,
115
+ markCSGReady,
116
+ initCSGModule,
117
+
118
+ // Font methods
119
+ registerFont,
120
+ registerFontAliases,
121
+ getFont,
122
+ isFontRegistered,
123
+
124
+ // Encoding utilities
125
+ codeList,
126
+ decodeList,
127
+
128
+ // Baked font methods (lightweight alternative to variable fonts)
129
+ loadBakedFont,
130
+ loadBakedFontsFromManifest,
131
+ findNearestWeight,
132
+ getBakedFontManifest
133
+ };
134
+
135
+ // Default export
136
+ export default MeshWriter;
@@ -0,0 +1,417 @@
1
+ /**
2
+ * MeshWriter Letter Mesh Construction
3
+ * Builds 3D letter meshes from font specifications
4
+ */
5
+
6
+ import { Path2, Vector2, Mesh, PolygonMeshBuilder } from './babylonImports.js';
7
+ import { splitMeshByFaceNormals } from './meshSplitter.js';
8
+ import { getCSGLib, getCSGVersion, isCSGReady } from './csg.js';
9
+ import { decodeList } from './fontCompression.js';
10
+ import {
11
+ isObject, isArray, isNumber, isBoolean,
12
+ isRelativeLength, round, weeid
13
+ } from './utils.js';
14
+ import earcutModule from 'earcut';
15
+
16
+ /** @typedef {import('@babylonjs/core/scene').Scene} Scene */
17
+ /** @typedef {import('@babylonjs/core/Materials/material').Material} Material */
18
+
19
+ // Handle both CJS default export and ESM module export
20
+ const earcut = earcutModule.default || earcutModule;
21
+
22
+ // Constants
23
+ const naturalLetterHeight = 1000;
24
+
25
+ /**
26
+ * Get kerning value between two characters
27
+ * @param {Object} fontSpec - Font specification object
28
+ * @param {string} left - Left character
29
+ * @param {string} right - Right character
30
+ * @returns {number} - Kerning adjustment value (0 if none)
31
+ */
32
+ function getKerning(fontSpec, left, right) {
33
+ if (!fontSpec.kern) return 0;
34
+ const key = `${left},${right}`;
35
+ return fontSpec.kern[key] || 0;
36
+ }
37
+
38
+ /**
39
+ * Decompress font letter on first use (lazy decompression)
40
+ * @param {Object} fontSpec - Font specification object
41
+ * @param {string} letter - Character to get spec for
42
+ * @returns {Object|undefined} - Letter specification
43
+ */
44
+ function makeLetterSpec(fontSpec, letter) {
45
+ const letterSpec = fontSpec[letter];
46
+
47
+ if (isObject(letterSpec)) {
48
+ // Decompress shape commands if compressed
49
+ if (!isArray(letterSpec.shapeCmds) && isArray(letterSpec.sC)) {
50
+ letterSpec.shapeCmds = letterSpec.sC.map(cmds => decodeList(cmds));
51
+ letterSpec.sC = null;
52
+ }
53
+ // Decompress hole commands if compressed
54
+ if (!isArray(letterSpec.holeCmds) && isArray(letterSpec.hC)) {
55
+ letterSpec.holeCmds = letterSpec.hC.map(cmdslists =>
56
+ isArray(cmdslists) ? cmdslists.map(cmds => decodeList(cmds)) : cmdslists
57
+ );
58
+ letterSpec.hC = null;
59
+ }
60
+ }
61
+ return letterSpec;
62
+ }
63
+
64
+ /**
65
+ * Convert point to Vector2
66
+ */
67
+ function point2Vector(point) {
68
+ return new Vector2(round(point.x), round(point.y));
69
+ }
70
+
71
+ /**
72
+ * Merge array of meshes
73
+ */
74
+ function merge(arrayOfMeshes) {
75
+ return arrayOfMeshes.length === 1
76
+ ? arrayOfMeshes[0]
77
+ : Mesh.MergeMeshes(arrayOfMeshes, true);
78
+ }
79
+
80
+ /**
81
+ * @typedef {Object} LetterPolygonsResult
82
+ * @property {number} xWidth - Total width of all letters
83
+ * @property {number} count - Number of valid letter meshes
84
+ */
85
+ /**
86
+ * @typedef {(any[] & LetterPolygonsResult) & { faceMeshes: Mesh[] }} LetterPolygonsCollection
87
+ */
88
+
89
+ /**
90
+ * Construct meshes for all letters in a string
91
+ * @param {string} letters - Text string
92
+ * @param {Object} fontSpec - Font specification
93
+ * @param {number} xOffset - X offset
94
+ * @param {number} yOffset - Y offset (unused, kept for API compatibility)
95
+ * @param {number} zOffset - Z offset
96
+ * @param {number} letterScale - Scale factor for letters
97
+ * @param {number} thickness - Letter thickness (depth)
98
+ * @param {Material} material - Material (unused in this function)
99
+ * @param {string} meshOrigin - "letterCenter" or "fontOrigin"
100
+ * @param {Scene} scene - Babylon scene
101
+ * @param {Object} [spacingOpts] - Optional spacing options
102
+ * @param {number} [spacingOpts.letterSpacing=0] - Extra spacing between letters (world units, added after kerning)
103
+ * @param {number} [spacingOpts.wordSpacing=0] - Extra spacing for spaces (world units, added to space width)
104
+ * @returns {any[] & LetterPolygonsResult} - [meshes, boxes, origins] with xWidth and count properties
105
+ */
106
+ export function constructLetterPolygons(
107
+ letters, fontSpec, xOffset, yOffset, zOffset,
108
+ letterScale, thickness, material, meshOrigin, scene, spacingOpts = {}
109
+ ) {
110
+ // Extract spacing options (already in world units)
111
+ const letterSpacing = isNumber(spacingOpts.letterSpacing) ? spacingOpts.letterSpacing : 0;
112
+ const wordSpacing = isNumber(spacingOpts.wordSpacing) ? spacingOpts.wordSpacing : 0;
113
+
114
+ let letterOffsetX = 0;
115
+ const lettersOrigins = new Array(letters.length);
116
+ const lettersBoxes = new Array(letters.length);
117
+ const lettersMeshes = new Array(letters.length);
118
+ const faceMeshes = new Array(letters.length);
119
+ let ix = 0;
120
+
121
+ for (let i = 0; i < letters.length; i++) {
122
+ const letter = letters[i];
123
+ const letterSpec = makeLetterSpec(fontSpec, letter);
124
+
125
+ if (isObject(letterSpec)) {
126
+ const lists = buildLetterMeshes(
127
+ letter, i, letterSpec, fontSpec.reverseShapes, fontSpec.reverseHoles,
128
+ meshOrigin, letterScale, xOffset, zOffset, letterOffsetX, thickness, scene
129
+ );
130
+
131
+ const shapesList = lists[0];
132
+ const holesList = lists[1];
133
+ const letterBox = lists[2];
134
+ const letterOrigins = lists[3];
135
+ let newOffsetX = lists[4];
136
+
137
+ // Apply kerning with next letter (if any)
138
+ if (i < letters.length - 1) {
139
+ const nextLetter = letters[i + 1];
140
+ const kernValue = getKerning(fontSpec, letter, nextLetter);
141
+ if (kernValue !== 0) {
142
+ newOffsetX += kernValue * letterScale;
143
+ }
144
+ // Apply letter spacing after kerning (not after last letter)
145
+ if (letterSpacing !== 0) {
146
+ newOffsetX += letterSpacing;
147
+ }
148
+ }
149
+
150
+ // Apply extra word spacing for space characters
151
+ if (letter === ' ' && wordSpacing !== 0) {
152
+ newOffsetX += wordSpacing;
153
+ }
154
+
155
+ letterOffsetX = newOffsetX;
156
+
157
+ const letterMeshes = punchHolesInShapes(shapesList, holesList, letter, i, scene);
158
+
159
+ if (letterMeshes.length) {
160
+ const merged = merge(letterMeshes);
161
+ const split = splitMeshByFaceNormals(merged, scene);
162
+
163
+ lettersMeshes[ix] = split.rimMesh;
164
+ faceMeshes[ix] = split.faceMesh;
165
+ lettersOrigins[ix] = letterOrigins;
166
+ lettersBoxes[ix] = letterBox;
167
+ ix++;
168
+ }
169
+ }
170
+ }
171
+
172
+ /** @type {LetterPolygonsCollection} */
173
+ const meshesAndBoxes = /** @type {any} */ ([lettersMeshes, lettersBoxes, lettersOrigins]);
174
+ meshesAndBoxes.faceMeshes = faceMeshes;
175
+ meshesAndBoxes.xWidth = round(letterOffsetX);
176
+ meshesAndBoxes.count = ix;
177
+ return meshesAndBoxes;
178
+ }
179
+
180
+ /**
181
+ * Build meshes for a single letter
182
+ * @returns {Array} - [shapesList, holesList, letterBox, letterOrigins, newOffsetX]
183
+ */
184
+ function buildLetterMeshes(
185
+ letter, index, spec, reverseShapes, reverseHoles,
186
+ meshOrigin, letterScale, xOffset, zOffset, letterOffsetX, thickness, scene
187
+ ) {
188
+ // Offset calculations
189
+ const balanced = meshOrigin === "letterCenter";
190
+ const centerX = (spec.xMin + spec.xMax) / 2;
191
+ const centerZ = (spec.yMin + spec.yMax) / 2;
192
+ const xFactor = isNumber(spec.xFactor) ? spec.xFactor : 1;
193
+ const zFactor = isNumber(spec.yFactor) ? spec.yFactor : 1;
194
+ const xShift = isNumber(spec.xShift) ? spec.xShift : 0;
195
+ const zShift = isNumber(spec.yShift) ? spec.yShift : 0;
196
+ const reverseShape = isBoolean(spec.reverseShape) ? spec.reverseShape : reverseShapes;
197
+ const reverseHole = isBoolean(spec.reverseHole) ? spec.reverseHole : reverseHoles;
198
+ const offX = xOffset - (balanced ? centerX : 0);
199
+ const offZ = zOffset - (balanced ? centerZ : 0);
200
+ const shapeCmdsLists = isArray(spec.shapeCmds) ? spec.shapeCmds : [];
201
+ const holeCmdsListsArray = isArray(spec.holeCmds) ? spec.holeCmds : [];
202
+
203
+ // Tracking for relative coordinates
204
+ let thisX, lastX, thisZ, lastZ;
205
+
206
+ // Scaling functions
207
+ const adjX = makeAdjust(letterScale, xFactor, offX, 0, false, true);
208
+ const adjZ = makeAdjust(letterScale, zFactor, offZ, 0, false, false);
209
+ const adjXfix = makeAdjust(letterScale, xFactor, offX, xShift, false, true);
210
+ const adjZfix = makeAdjust(letterScale, zFactor, offZ, zShift, false, false);
211
+ const adjXrel = makeAdjust(letterScale, xFactor, offX, xShift, true, true);
212
+ const adjZrel = makeAdjust(letterScale, zFactor, offZ, zShift, true, false);
213
+
214
+ const letterBox = [adjX(spec.xMin), adjX(spec.xMax), adjZ(spec.yMin), adjZ(spec.yMax)];
215
+ const letterOrigins = [round(letterOffsetX), -1 * adjX(0), -1 * adjZ(0)];
216
+
217
+ // Update letterOffsetX for next letter
218
+ const newOffsetX = letterOffsetX + spec.wdth * letterScale;
219
+
220
+ const shapesList = shapeCmdsLists.map(makeCmdsToMesh(reverseShape));
221
+ const holesList = holeCmdsListsArray.map(meshesFromCmdsListArray);
222
+
223
+ return [shapesList, holesList, letterBox, letterOrigins, newOffsetX];
224
+
225
+ function meshesFromCmdsListArray(cmdsListArray) {
226
+ return cmdsListArray.map(makeCmdsToMesh(reverseHole));
227
+ }
228
+
229
+ function makeCmdsToMesh(reverse) {
230
+ return function cmdsToMesh(cmdsList) {
231
+ let cmd = getCmd(cmdsList, 0);
232
+ /** @type {any} */
233
+ const path = new Path2(adjXfix(cmd[0]), adjZfix(cmd[1]));
234
+
235
+ // Process path commands
236
+ for (let j = 1; j < cmdsList.length; j++) {
237
+ cmd = getCmd(cmdsList, j);
238
+
239
+ // Line (2 coords = absolute, 3 = relative)
240
+ if (cmd.length === 2) {
241
+ path.addLineTo(adjXfix(cmd[0]), adjZfix(cmd[1]));
242
+ }
243
+ if (cmd.length === 3) {
244
+ path.addLineTo(adjXrel(cmd[1]), adjZrel(cmd[2]));
245
+ }
246
+
247
+ // Quadratic curve (4 = absolute, 5 = relative)
248
+ if (cmd.length === 4) {
249
+ path.addQuadraticCurveTo(
250
+ adjXfix(cmd[0]), adjZfix(cmd[1]),
251
+ adjXfix(cmd[2]), adjZfix(cmd[3])
252
+ );
253
+ }
254
+ if (cmd.length === 5) {
255
+ path.addQuadraticCurveTo(
256
+ adjXrel(cmd[1]), adjZrel(cmd[2]),
257
+ adjXrel(cmd[3]), adjZrel(cmd[4])
258
+ );
259
+ }
260
+
261
+ // Cubic curve (6 = absolute, 7 = relative)
262
+ if (cmd.length === 6) {
263
+ path.addCubicCurveTo(
264
+ adjXfix(cmd[0]), adjZfix(cmd[1]),
265
+ adjXfix(cmd[2]), adjZfix(cmd[3]),
266
+ adjXfix(cmd[4]), adjZfix(cmd[5])
267
+ );
268
+ }
269
+ if (cmd.length === 7) {
270
+ path.addCubicCurveTo(
271
+ adjXrel(cmd[1]), adjZrel(cmd[2]),
272
+ adjXrel(cmd[3]), adjZrel(cmd[4]),
273
+ adjXrel(cmd[5]), adjZrel(cmd[6])
274
+ );
275
+ }
276
+ }
277
+
278
+ // Convert path to array and process
279
+ let array = path.getPoints().map(point2Vector);
280
+
281
+ // Remove redundant start/end points
282
+ const first = 0;
283
+ const last = array.length - 1;
284
+ if (array[first].x === array[last].x && array[first].y === array[last].y) {
285
+ array = array.slice(1);
286
+ }
287
+ if (reverse) {
288
+ array.reverse();
289
+ }
290
+
291
+ const meshBuilder = new PolygonMeshBuilder(
292
+ "MeshWriter-" + letter + index + "-" + weeid(),
293
+ array,
294
+ scene,
295
+ earcut
296
+ );
297
+ return meshBuilder.build(true, thickness);
298
+ };
299
+ }
300
+
301
+ function getCmd(list, ix) {
302
+ lastX = thisX;
303
+ lastZ = thisZ;
304
+ const cmd = list[ix];
305
+ const len = cmd.length;
306
+ thisX = isRelativeLength(len)
307
+ ? round((cmd[len - 2] * xFactor) + thisX)
308
+ : round(cmd[len - 2] * xFactor);
309
+ thisZ = isRelativeLength(len)
310
+ ? round((cmd[len - 1] * zFactor) + thisZ)
311
+ : round(cmd[len - 1] * zFactor);
312
+ return cmd;
313
+ }
314
+
315
+ function makeAdjust(letterScale, factor, off, shift, relative, xAxis) {
316
+ if (relative) {
317
+ if (xAxis) {
318
+ return val => round(letterScale * ((val * factor) + shift + lastX + off));
319
+ } else {
320
+ return val => round(letterScale * ((val * factor) + shift + lastZ + off));
321
+ }
322
+ } else {
323
+ return val => round(letterScale * ((val * factor) + shift + off));
324
+ }
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Punch holes in letter shapes using CSG operations
330
+ * @param {Array} shapesList - Array of shape meshes
331
+ * @param {Array} holesList - Array of arrays of hole meshes
332
+ * @param {string} letter - Letter character (for naming)
333
+ * @param {number} letterIndex - Index of letter (for naming)
334
+ * @param {Scene} scene - Babylon scene
335
+ * @returns {Array} - Array of final letter meshes
336
+ */
337
+ function punchHolesInShapes(shapesList, holesList, letter, letterIndex, scene) {
338
+ const csgVersion = getCSGVersion();
339
+
340
+ // Validate CSG is available and initialized
341
+ if (csgVersion === 'CSG2' && !isCSGReady()) {
342
+ throw new Error(
343
+ "MeshWriter: CSG2 not initialized. " +
344
+ "Use 'await MeshWriter.createAsync(scene, prefs)', call " +
345
+ "'await BABYLON.InitializeCSG2Async()', or configure " +
346
+ "MeshWriter.setCSGInitializer before creating MeshWriter."
347
+ );
348
+ }
349
+ if (csgVersion === null) {
350
+ throw new Error(
351
+ "MeshWriter: No CSG implementation found. " +
352
+ "Ensure BABYLON.CSG or BABYLON.CSG2 is available."
353
+ );
354
+ }
355
+
356
+ const letterMeshes = [];
357
+ const csgLib = getCSGLib();
358
+
359
+ // Handle special case: single shape with multiple hole arrays
360
+ // (e.g., "B" has 1 shape but 2 holes - top and bottom)
361
+ // In this case, all holes should be punched into the single shape
362
+ if (shapesList.length === 1 && holesList.length > 1) {
363
+ const shape = shapesList[0];
364
+ // Flatten all hole arrays into a single array
365
+ const allHoles = holesList.flat();
366
+
367
+ if (allHoles.length > 0) {
368
+ letterMeshes.push(punchHolesInShape(shape, allHoles, letter, letterIndex, csgLib, csgVersion, scene));
369
+ } else {
370
+ letterMeshes.push(shape);
371
+ }
372
+ return letterMeshes;
373
+ }
374
+
375
+ // Standard case: 1:1 correspondence between shapes and hole arrays
376
+ for (let j = 0; j < shapesList.length; j++) {
377
+ const shape = shapesList[j];
378
+ const holes = holesList[j];
379
+
380
+ if (isArray(holes) && holes.length) {
381
+ letterMeshes.push(punchHolesInShape(shape, holes, letter, letterIndex, csgLib, csgVersion, scene));
382
+ } else {
383
+ // PolygonMeshBuilder creates meshes with correct normals by default
384
+ // No flipFaces needed for shapes without holes
385
+ letterMeshes.push(shape);
386
+ }
387
+ }
388
+
389
+ return letterMeshes;
390
+ }
391
+
392
+ /**
393
+ * Punch holes in a single shape
394
+ */
395
+ function punchHolesInShape(shape, holes, letter, letterIndex, csgLib, csgVersion, scene) {
396
+ const meshName = "Net-" + letter + letterIndex + "-" + weeid();
397
+
398
+ let csgShape = csgLib.FromMesh(shape);
399
+ for (let k = 0; k < holes.length; k++) {
400
+ csgShape = csgShape.subtract(csgLib.FromMesh(holes[k]));
401
+ }
402
+
403
+ const resultMesh = csgVersion === 'CSG2'
404
+ ? csgShape.toMesh(meshName, scene, { centerMesh: false, rebuildNormals: true })
405
+ : csgShape.toMesh(meshName, null, scene);
406
+
407
+ // CSG2 with rebuildNormals: true produces correct normals
408
+ // No flipFaces needed
409
+
410
+ // Cleanup
411
+ holes.forEach(h => h.dispose());
412
+ shape.dispose();
413
+
414
+ return resultMesh;
415
+ }
416
+
417
+ export { naturalLetterHeight };
@@ -0,0 +1,103 @@
1
+ /**
2
+ * MeshWriter Material Creation
3
+ * Creates StandardMaterial for text rendering
4
+ */
5
+
6
+ import { StandardMaterial, Color3 } from './babylonImports.js';
7
+ import { weeid } from './utils.js';
8
+ import { TextFogPlugin } from './fogPlugin.js';
9
+
10
+ /** @typedef {import('@babylonjs/core/scene').Scene} Scene */
11
+
12
+ const floor = Math.floor;
13
+
14
+ /**
15
+ * Convert hex color string to Babylon Color3
16
+ * @param {string} rgb - Hex color string (e.g., "#FF0000" or "FF0000")
17
+ * @returns {Color3}
18
+ */
19
+ export function rgb2Color3(rgb) {
20
+ rgb = rgb.replace("#", "");
21
+ return new Color3(
22
+ convert(rgb.substring(0, 2)),
23
+ convert(rgb.substring(2, 4)),
24
+ convert(rgb.substring(4, 6))
25
+ );
26
+
27
+ function convert(x) {
28
+ const parsed = parseInt(x, 16);
29
+ const val = isNaN(parsed) ? 0 : parsed;
30
+ return floor(1000 * Math.max(0, Math.min(val / 255, 1))) / 1000;
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Create a StandardMaterial for text
36
+ * @param {Scene} scene - Babylon scene
37
+ * @param {string} letters - Text string (used for material naming)
38
+ * @param {string} emissive - Hex color for emissive
39
+ * @param {string} ambient - Hex color for ambient
40
+ * @param {string} specular - Hex color for specular
41
+ * @param {string} diffuse - Hex color for diffuse
42
+ * @param {number} opac - Opacity (0-1)
43
+ * @param {boolean} [emissiveOnly=false] - If true, disables lighting (only emissive color shows)
44
+ * @param {boolean} [fogEnabled=true] - If true, the material is affected by scene fog
45
+ * @returns {StandardMaterial}
46
+ */
47
+ export function makeMaterial(scene, letters, emissive, ambient, specular, diffuse, opac, emissiveOnly = false, fogEnabled = true) {
48
+ const material = new StandardMaterial("mw-matl-" + letters + "-" + weeid(), scene);
49
+ material.diffuseColor = rgb2Color3(diffuse);
50
+ material.specularColor = rgb2Color3(specular);
51
+ material.ambientColor = rgb2Color3(ambient);
52
+ material.emissiveColor = rgb2Color3(emissive);
53
+ material.alpha = opac;
54
+
55
+ // When emissiveOnly is true, disable lighting so only emissive color shows
56
+ // This gives a "self-lit" appearance that ignores scene lights
57
+ if (emissiveOnly) {
58
+ material.disableLighting = true;
59
+ }
60
+
61
+ // Emissive-only materials should be self-lit and not affected by fog
62
+ if (emissiveOnly) {
63
+ material.fogEnabled = false;
64
+ } else {
65
+ material.fogEnabled = fogEnabled;
66
+ // Attach fog plugin to properly blend emissive color with fog
67
+ // Babylon's standard fog only affects diffuse/ambient, not emissive.
68
+ // The plugin re-fogs the entire fragment output so emissive fades properly.
69
+ // (The slight double-fog on diffuse/ambient is negligible since text is primarily emissive)
70
+ if (fogEnabled) {
71
+ material._textFogPlugin = new TextFogPlugin(material);
72
+ }
73
+ }
74
+
75
+ return material;
76
+ }
77
+
78
+ /**
79
+ * Create a dedicated emissive material for front faces.
80
+ * This keeps the face self-lit while still respecting fog settings.
81
+ * @param {Scene} scene
82
+ * @param {string} letters
83
+ * @param {string} emissive
84
+ * @param {number} opac
85
+ * @param {boolean} fogEnabled
86
+ * @returns {StandardMaterial}
87
+ */
88
+ export function makeFaceMaterial(scene, letters, emissive, opac, fogEnabled = true) {
89
+ const material = new StandardMaterial("mw-face-matl-" + letters + "-" + weeid(), scene);
90
+ const black = rgb2Color3("#000000");
91
+ material.diffuseColor = black;
92
+ material.specularColor = black;
93
+ material.ambientColor = black;
94
+ material.emissiveColor = rgb2Color3(emissive);
95
+ material.disableLighting = true;
96
+ material.alpha = opac;
97
+ material.backFaceCulling = false;
98
+ material.fogEnabled = fogEnabled;
99
+ if (fogEnabled) {
100
+ material._textFogPlugin = new TextFogPlugin(material);
101
+ }
102
+ return material;
103
+ }