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,2645 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var math_vector = require('@babylonjs/core/Maths/math.vector');
6
+ var math_color = require('@babylonjs/core/Maths/math.color');
7
+ var math_path = require('@babylonjs/core/Maths/math.path');
8
+ var mesh = require('@babylonjs/core/Meshes/mesh');
9
+ var mesh_vertexData = require('@babylonjs/core/Meshes/mesh.vertexData');
10
+ var polygonMesh = require('@babylonjs/core/Meshes/polygonMesh');
11
+ var standardMaterial = require('@babylonjs/core/Materials/standardMaterial');
12
+ var materialPluginBase = require('@babylonjs/core/Materials/materialPluginBase');
13
+ var solidParticleSystem = require('@babylonjs/core/Particles/solidParticleSystem');
14
+ var csg = require('@babylonjs/core/Meshes/csg');
15
+ var csg2 = require('@babylonjs/core/Meshes/csg2');
16
+ var earcutModule = require('earcut');
17
+
18
+ /**
19
+ * MeshWriter Utility Functions
20
+ * Pure helper functions with no external dependencies
21
+ */
22
+
23
+ const floor$2 = Math.floor;
24
+
25
+ // Type checking functions
26
+ function isPositiveNumber(mn) {
27
+ return typeof mn === "number" && !isNaN(mn) ? 0 < mn : false;
28
+ }
29
+
30
+ function isNumber(mn) {
31
+ return typeof mn === "number";
32
+ }
33
+
34
+ function isBoolean(mn) {
35
+ return typeof mn === "boolean";
36
+ }
37
+
38
+ function isAmplitude(ma) {
39
+ return typeof ma === "number" && !isNaN(ma) ? 0 <= ma && ma <= 1 : false;
40
+ }
41
+
42
+ function isObject(mo) {
43
+ return mo != null && typeof mo === "object" || typeof mo === "function";
44
+ }
45
+
46
+ function isPromiseLike(mo) {
47
+ return isObject(mo) && typeof mo.then === "function";
48
+ }
49
+
50
+ function isArray(ma) {
51
+ return ma != null && typeof ma === "object" && ma.constructor === Array;
52
+ }
53
+
54
+ function isString(ms) {
55
+ return typeof ms === "string" ? ms.length > 0 : false;
56
+ }
57
+
58
+ function isRelativeLength(l) {
59
+ return l === 3 || l === 5 || l === 7;
60
+ }
61
+
62
+ // Math utilities
63
+ function round(n) {
64
+ return floor$2(0.3 + n * 1000000) / 1000000;
65
+ }
66
+
67
+ function weeid() {
68
+ return Math.floor(Math.random() * 1000000);
69
+ }
70
+
71
+ // Option handling
72
+ function setOption(opts, field, tst, defalt) {
73
+ return tst(opts[field]) ? opts[field] : defalt;
74
+ }
75
+
76
+ /**
77
+ * MeshWriter Font Compression/Decompression
78
+ * Base-128 encoding for font data compression (~50% size reduction)
79
+ */
80
+
81
+
82
+ const floor$1 = Math.floor;
83
+
84
+ // Encoding arrays (initialized once at module load)
85
+ let b128back;
86
+ let b128digits;
87
+
88
+ /**
89
+ * Initialize encoding arrays for base-128 conversion
90
+ * Called once when module loads
91
+ */
92
+ function prepArray() {
93
+ let pntr = -1;
94
+ let n;
95
+ b128back = new Uint8Array(256);
96
+ b128digits = new Array(128);
97
+
98
+ while (160 > pntr++) {
99
+ if (pntr < 128) {
100
+ n = fr128to256(pntr);
101
+ b128digits[pntr] = String.fromCharCode(n);
102
+ b128back[n] = pntr;
103
+ } else {
104
+ if (pntr === 128) {
105
+ b128back[32] = pntr;
106
+ } else {
107
+ b128back[pntr + 71] = pntr;
108
+ }
109
+ }
110
+ }
111
+
112
+ function fr128to256(n) {
113
+ if (n < 92) {
114
+ return n < 58 ? n < 6 ? n + 33 : n + 34 : n + 35;
115
+ } else {
116
+ return n + 69;
117
+ }
118
+ }
119
+ }
120
+
121
+ // Initialize on module load
122
+ prepArray();
123
+
124
+ /**
125
+ * Convert base-128 encoded string to number
126
+ */
127
+ function frB128(s) {
128
+ let result = 0;
129
+ let i = -1;
130
+ const l = s.length - 1;
131
+ while (i++ < l) {
132
+ result = result * 128 + b128back[s.charCodeAt(i)];
133
+ }
134
+ return result;
135
+ }
136
+
137
+ /**
138
+ * Convert number to base-128 encoded string
139
+ */
140
+ function toB128(i) {
141
+ let s = b128digits[(i % 128)];
142
+ i = floor$1(i / 128);
143
+ while (i > 0) {
144
+ s = b128digits[(i % 128)] + s;
145
+ i = floor$1(i / 128);
146
+ }
147
+ return s;
148
+ }
149
+
150
+ /**
151
+ * Decode a compressed command list string
152
+ * @param {string} str - Compressed string (space-separated encoded commands)
153
+ * @returns {Array} - Array of decoded command arrays
154
+ */
155
+ function decodeList(str) {
156
+ const split = str.split(" ");
157
+ const list = [];
158
+
159
+ split.forEach(function(cmds) {
160
+ if (cmds.length === 12) { list.push(decode6(cmds)); }
161
+ if (cmds.length === 8) { list.push(decode4(cmds)); }
162
+ if (cmds.length === 4) { list.push(decode2(cmds)); }
163
+ });
164
+
165
+ return list;
166
+
167
+ function decode6(s) {
168
+ return [
169
+ decode1(s, 0, 2), decode1(s, 2, 4), decode1(s, 4, 6),
170
+ decode1(s, 6, 8), decode1(s, 8, 10), decode1(s, 10, 12)
171
+ ];
172
+ }
173
+ function decode4(s) {
174
+ return [decode1(s, 0, 2), decode1(s, 2, 4), decode1(s, 4, 6), decode1(s, 6, 8)];
175
+ }
176
+ function decode2(s) {
177
+ return [decode1(s, 0, 2), decode1(s, 2, 4)];
178
+ }
179
+ function decode1(s, start, end) {
180
+ return (frB128(s.substring(start, end)) - 4000) / 2;
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Encode a command list to compressed string
186
+ * @param {Array} list - Array of command arrays
187
+ * @returns {string} - Compressed string
188
+ */
189
+ function codeList(list) {
190
+ let str = "";
191
+ let xtra = "";
192
+
193
+ if (isArray(list)) {
194
+ list.forEach(function(cmds) {
195
+ if (cmds.length === 6) { str += xtra + code6(cmds); xtra = " "; }
196
+ if (cmds.length === 4) { str += xtra + code4(cmds); xtra = " "; }
197
+ if (cmds.length === 2) { str += xtra + code2(cmds); xtra = " "; }
198
+ });
199
+ }
200
+
201
+ return str;
202
+
203
+ function code6(a) {
204
+ return code1(a[0]) + code1(a[1]) + code1(a[2]) + code1(a[3]) + code1(a[4]) + code1(a[5]);
205
+ }
206
+ function code4(a) {
207
+ return code1(a[0]) + code1(a[1]) + code1(a[2]) + code1(a[3]);
208
+ }
209
+ function code2(a) {
210
+ return code1(a[0]) + code1(a[1]);
211
+ }
212
+ function code1(n) {
213
+ return toB128((n + n) + 4000);
214
+ }
215
+ }
216
+
217
+ /**
218
+ * MeshWriter Font Registry
219
+ * Manages font registration and lookup
220
+ */
221
+
222
+
223
+ // Private font storage
224
+ const FONTS = {};
225
+
226
+ /**
227
+ * Register a font for use with MeshWriter
228
+ * @param {string} name - Font name (case-sensitive, used in "font-family" option)
229
+ * @param {Function|Object} fontData - Font factory function or pre-initialized font object
230
+ *
231
+ * @example
232
+ * // Register a font factory (receives codeList for encoding)
233
+ * import helvetica from 'meshwriter/fonts/helvetica';
234
+ * registerFont('Helvetica', helvetica);
235
+ *
236
+ * // Register with aliases
237
+ * registerFont('Arial', helvetica);
238
+ * registerFont('sans-serif', helvetica);
239
+ */
240
+ function registerFont(name, fontData) {
241
+ if (typeof fontData === 'function') {
242
+ // Font is a factory function expecting codeList
243
+ FONTS[name] = fontData(codeList);
244
+ } else if (isObject(fontData)) {
245
+ // Font is already initialized
246
+ FONTS[name] = fontData;
247
+ } else {
248
+ throw new Error(`MeshWriter: Invalid font data for "${name}"`);
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Register multiple font aliases pointing to the same font
254
+ * @param {string} targetName - Name of already-registered font
255
+ * @param {...string} aliases - Alias names to register
256
+ *
257
+ * @example
258
+ * registerFont('Helvetica', helveticaData);
259
+ * registerFontAliases('Helvetica', 'Arial', 'sans-serif');
260
+ */
261
+ function registerFontAliases(targetName, ...aliases) {
262
+ if (!FONTS[targetName]) {
263
+ throw new Error(`MeshWriter: Cannot create aliases: font "${targetName}" not registered`);
264
+ }
265
+ aliases.forEach(alias => {
266
+ FONTS[alias] = FONTS[targetName];
267
+ });
268
+ }
269
+
270
+ /**
271
+ * Get a registered font by name
272
+ * @param {string} name - Font name
273
+ * @returns {Object|undefined} - Font object or undefined if not found
274
+ */
275
+ function getFont(name) {
276
+ return FONTS[name];
277
+ }
278
+
279
+ /**
280
+ * Check if a font is registered
281
+ * @param {string} name - Font name
282
+ * @returns {boolean}
283
+ */
284
+ function isFontRegistered(name) {
285
+ return isObject(FONTS[name]);
286
+ }
287
+
288
+ /**
289
+ * Get list of all registered font names
290
+ * @returns {string[]}
291
+ */
292
+ function getRegisteredFonts() {
293
+ return Object.keys(FONTS);
294
+ }
295
+
296
+ /**
297
+ * Unregister a font (mainly for testing)
298
+ * @param {string} name - Font name to remove
299
+ */
300
+ function unregisterFont(name) {
301
+ delete FONTS[name];
302
+ }
303
+
304
+ /**
305
+ * Clear all registered fonts (mainly for testing)
306
+ */
307
+ function clearFonts() {
308
+ Object.keys(FONTS).forEach(key => delete FONTS[key]);
309
+ }
310
+
311
+ /**
312
+ * MeshWriter CSG Module
313
+ * Handles CSG version detection, async initialization, and ready state management
314
+ */
315
+
316
+
317
+ // CSG libraries - initialized from Babylon imports or external source
318
+ let CSG = csg.CSG;
319
+ let CSG2 = csg2.CSG2;
320
+ let InitializeCSG2Async = csg2.InitializeCSG2Async;
321
+ let IsCSG2Ready = csg2.IsCSG2Ready;
322
+
323
+ // State
324
+ let csgVersion = null; // 'CSG2', 'CSG', or null
325
+ let csgReady = false;
326
+ const csgReadyListeners = [];
327
+ let externalCSGInitializer = null;
328
+ let externalCSGReadyCheck = null;
329
+
330
+ /**
331
+ * Initialize CSG module with Babylon.js CSG classes
332
+ * Must be called before using any CSG functionality
333
+ * @param {Object} babylon - Object containing CSG, CSG2, InitializeCSG2Async, IsCSG2Ready
334
+ */
335
+ function initCSGModule(babylon) {
336
+ if (isObject(babylon)) {
337
+ CSG = babylon.CSG || null;
338
+ CSG2 = babylon.CSG2 || null;
339
+ InitializeCSG2Async = babylon.InitializeCSG2Async || null;
340
+ IsCSG2Ready = babylon.IsCSG2Ready || null;
341
+ }
342
+ csgVersion = detectCSGVersion();
343
+
344
+ // Legacy CSG is immediately ready
345
+ if (csgVersion === 'CSG') {
346
+ markCSGInitialized();
347
+ } else if (csgVersion === 'CSG2') {
348
+ // Check if CSG2 is already initialized
349
+ csgReady = false;
350
+ if (runCSGReadyCheck()) {
351
+ markCSGInitialized();
352
+ }
353
+ }
354
+ }
355
+
356
+ /**
357
+ * Detect which CSG implementation is available
358
+ * @returns {'CSG2'|'CSG'|null}
359
+ */
360
+ function detectCSGVersion() {
361
+ // Prefer CSG2 (Babylon 7.31+)
362
+ if (isObject(CSG2) && typeof InitializeCSG2Async === 'function') {
363
+ return 'CSG2';
364
+ }
365
+ // Fall back to legacy CSG
366
+ if (isObject(CSG) && typeof CSG.FromMesh === 'function') {
367
+ return 'CSG';
368
+ }
369
+ return null;
370
+ }
371
+
372
+ /**
373
+ * Mark CSG as ready and notify all listeners
374
+ */
375
+ function markCSGInitialized() {
376
+ if (csgReady) {
377
+ return;
378
+ }
379
+ csgReady = true;
380
+
381
+ // Notify all waiting listeners
382
+ if (csgReadyListeners.length) {
383
+ csgReadyListeners.splice(0).forEach(function(listener) {
384
+ try {
385
+ listener();
386
+ } catch (err) {
387
+ console.error("MeshWriter: onCSGReady listener failed", err);
388
+ }
389
+ });
390
+ }
391
+ }
392
+
393
+ /**
394
+ * Check if CSG is ready for use
395
+ * @returns {boolean}
396
+ */
397
+ function isCSGReady() {
398
+ if (csgVersion === 'CSG2') {
399
+ refreshCSGReadyState();
400
+ return csgReady;
401
+ }
402
+ return csgVersion === 'CSG';
403
+ }
404
+
405
+ /**
406
+ * Refresh CSG2 ready state from external checks
407
+ */
408
+ function refreshCSGReadyState() {
409
+ if (csgVersion !== 'CSG2' || csgReady) {
410
+ return;
411
+ }
412
+ if (runCSGReadyCheck()) {
413
+ markCSGInitialized();
414
+ }
415
+ }
416
+
417
+ /**
418
+ * Run CSG ready check (external or native)
419
+ */
420
+ function runCSGReadyCheck() {
421
+ // Try external ready check first
422
+ if (typeof externalCSGReadyCheck === "function") {
423
+ try {
424
+ if (externalCSGReadyCheck()) {
425
+ return true;
426
+ }
427
+ } catch (err) {
428
+ console.warn("MeshWriter: external CSG ready check failed", err);
429
+ }
430
+ }
431
+
432
+ // Check native IsCSG2Ready
433
+ if (typeof IsCSG2Ready === "function" && IsCSG2Ready()) {
434
+ return true;
435
+ }
436
+
437
+ return false;
438
+ }
439
+
440
+ /**
441
+ * Initialize CSG2 asynchronously
442
+ * @returns {Promise<void>}
443
+ */
444
+ async function initializeCSG2() {
445
+ if (csgVersion !== 'CSG2') {
446
+ return;
447
+ }
448
+ if (csgReady) {
449
+ return;
450
+ }
451
+
452
+ const initializer = externalCSGInitializer || InitializeCSG2Async;
453
+ if (typeof initializer !== "function") {
454
+ throw new Error(
455
+ "MeshWriter: No CSG2 initializer available. " +
456
+ "Use MeshWriter.setCSGInitializer() or ensure BABYLON.InitializeCSG2Async is available."
457
+ );
458
+ }
459
+
460
+ const result = initializer();
461
+ if (isPromiseLike(result)) {
462
+ await result;
463
+ }
464
+ markCSGInitialized();
465
+ }
466
+
467
+ // Public API
468
+
469
+ /**
470
+ * Get the current CSG version being used
471
+ * @returns {'CSG2'|'CSG'|null}
472
+ */
473
+ function getCSGVersion() {
474
+ return csgVersion;
475
+ }
476
+
477
+ /**
478
+ * Set an external CSG2 initializer function
479
+ * @param {Function} fn - Async function that initializes CSG2
480
+ */
481
+ function setCSGInitializer(fn) {
482
+ if (typeof fn === "function") {
483
+ externalCSGInitializer = fn;
484
+ }
485
+ }
486
+
487
+ /**
488
+ * Set an external CSG2 ready check function
489
+ * @param {Function} fn - Function that returns true when CSG2 is ready
490
+ */
491
+ function setCSGReadyCheck(fn) {
492
+ if (typeof fn === "function") {
493
+ externalCSGReadyCheck = fn;
494
+ refreshCSGReadyState();
495
+ }
496
+ }
497
+
498
+ /**
499
+ * Register a callback to be called when CSG is ready
500
+ * If CSG is already ready, callback is called immediately
501
+ * @param {Function} listener
502
+ */
503
+ function onCSGReady(listener) {
504
+ if (typeof listener !== "function") {
505
+ return;
506
+ }
507
+ if (isCSGReady()) {
508
+ listener();
509
+ } else {
510
+ csgReadyListeners.push(listener);
511
+ }
512
+ }
513
+
514
+ /**
515
+ * Get the CSG library to use for operations
516
+ * @returns {Object} - CSG or CSG2 class
517
+ */
518
+ function getCSGLib() {
519
+ return csgVersion === 'CSG2' ? CSG2 : CSG;
520
+ }
521
+
522
+ /**
523
+ * TextFogPlugin - MaterialPluginBase that applies fog to emissive color
524
+ *
525
+ * Babylon's standard fog only affects diffuse/ambient channels.
526
+ * This plugin recalculates fog blending for the final color output,
527
+ * ensuring emissive text fades properly with distance fog.
528
+ */
529
+
530
+
531
+ /**
532
+ * Plugin that applies scene fog to text materials by modifying
533
+ * the final fragment color before output.
534
+ */
535
+ class TextFogPlugin extends materialPluginBase.MaterialPluginBase {
536
+ /**
537
+ * @param {import('@babylonjs/core/Materials/material').Material} material
538
+ */
539
+ constructor(material) {
540
+ var priority = 300; // Run after standard material processing
541
+ var defines = { 'MESHWRITER_TEXT_FOG': false };
542
+ super(material, 'TextFogPlugin', priority, defines);
543
+ this._enable(true);
544
+ }
545
+
546
+ /**
547
+ * Set the define based on whether scene fog is enabled
548
+ * @param {object} defines
549
+ * @param {import('@babylonjs/core/scene').Scene} scene
550
+ * @param {import('@babylonjs/core/Meshes/mesh').Mesh} mesh
551
+ */
552
+ prepareDefines(defines, scene, mesh) {
553
+ // Enable when scene has any fog mode set (1=LINEAR, 2=EXP, 3=EXP2)
554
+ defines['MESHWRITER_TEXT_FOG'] = scene.fogMode !== 0;
555
+ }
556
+
557
+ getClassName() {
558
+ return 'TextFogPlugin';
559
+ }
560
+
561
+ getUniforms() {
562
+ // We use Babylon's built-in fog uniforms (vFogInfos, vFogColor)
563
+ // which are already available in the standard material
564
+ return { ubo: [] };
565
+ }
566
+
567
+ /**
568
+ * Clean up the plugin
569
+ */
570
+ dispose() {
571
+ super.dispose();
572
+ }
573
+
574
+ /**
575
+ * Inject shader code to apply fog to emissive color
576
+ * @param {string} shaderType - 'vertex' or 'fragment'
577
+ */
578
+ getCustomCode(shaderType) {
579
+ if (shaderType === 'fragment') {
580
+ return {
581
+ // This injection point runs just before gl_FragColor is finalized
582
+ // At this point, standard fog has been applied to diffuse/ambient
583
+ // but emissive contribution bypasses fog, so we re-apply fog
584
+ // to the entire output to properly fade text into fog
585
+ 'CUSTOM_FRAGMENT_BEFORE_FRAGCOLOR': `
586
+ #ifdef MESHWRITER_TEXT_FOG
587
+ #ifdef FOG
588
+ // Recalculate fog for the full fragment color including emissive
589
+ // vFogInfos: x=fogMode, y=fogStart, z=fogEnd, w=fogDensity
590
+ // vFogColor: fog RGB color
591
+ // vFogDistance: vec3 distance from camera (set by vertex shader)
592
+
593
+ float textFogFactor = 1.0;
594
+ float textFogDist = length(vFogDistance);
595
+
596
+ if (FOGMODE_LINEAR == vFogInfos.x) {
597
+ // Linear fog: factor = (end - dist) / (end - start)
598
+ textFogFactor = clamp((vFogInfos.z - textFogDist) / (vFogInfos.z - vFogInfos.y), 0.0, 1.0);
599
+ } else if (FOGMODE_EXP == vFogInfos.x) {
600
+ // Exponential fog: factor = exp(-dist * density)
601
+ textFogFactor = clamp(exp(-textFogDist * vFogInfos.w), 0.0, 1.0);
602
+ } else if (FOGMODE_EXP2 == vFogInfos.x) {
603
+ // Exponential squared fog: factor = exp(-(dist * density)^2)
604
+ float fogDistDensity = textFogDist * vFogInfos.w;
605
+ textFogFactor = clamp(exp(-fogDistDensity * fogDistDensity), 0.0, 1.0);
606
+ }
607
+
608
+ // Blend the entire fragment (including emissive) toward fog color
609
+ // textFogFactor: 1.0 = no fog (full color), 0.0 = full fog
610
+ color.rgb = mix(vFogColor, color.rgb, textFogFactor);
611
+ #endif
612
+ #endif
613
+ `,
614
+ };
615
+ }
616
+ return null;
617
+ }
618
+ }
619
+
620
+ /**
621
+ * MeshWriter Material Creation
622
+ * Creates StandardMaterial for text rendering
623
+ */
624
+
625
+
626
+ /** @typedef {import('@babylonjs/core/scene').Scene} Scene */
627
+
628
+ const floor = Math.floor;
629
+
630
+ /**
631
+ * Convert hex color string to Babylon Color3
632
+ * @param {string} rgb - Hex color string (e.g., "#FF0000" or "FF0000")
633
+ * @returns {Color3}
634
+ */
635
+ function rgb2Color3(rgb) {
636
+ rgb = rgb.replace("#", "");
637
+ return new math_color.Color3(
638
+ convert(rgb.substring(0, 2)),
639
+ convert(rgb.substring(2, 4)),
640
+ convert(rgb.substring(4, 6))
641
+ );
642
+
643
+ function convert(x) {
644
+ const parsed = parseInt(x, 16);
645
+ const val = isNaN(parsed) ? 0 : parsed;
646
+ return floor(1000 * Math.max(0, Math.min(val / 255, 1))) / 1000;
647
+ }
648
+ }
649
+
650
+ /**
651
+ * Create a StandardMaterial for text
652
+ * @param {Scene} scene - Babylon scene
653
+ * @param {string} letters - Text string (used for material naming)
654
+ * @param {string} emissive - Hex color for emissive
655
+ * @param {string} ambient - Hex color for ambient
656
+ * @param {string} specular - Hex color for specular
657
+ * @param {string} diffuse - Hex color for diffuse
658
+ * @param {number} opac - Opacity (0-1)
659
+ * @param {boolean} [emissiveOnly=false] - If true, disables lighting (only emissive color shows)
660
+ * @param {boolean} [fogEnabled=true] - If true, the material is affected by scene fog
661
+ * @returns {StandardMaterial}
662
+ */
663
+ function makeMaterial(scene, letters, emissive, ambient, specular, diffuse, opac, emissiveOnly = false, fogEnabled = true) {
664
+ const material = new standardMaterial.StandardMaterial("mw-matl-" + letters + "-" + weeid(), scene);
665
+ material.diffuseColor = rgb2Color3(diffuse);
666
+ material.specularColor = rgb2Color3(specular);
667
+ material.ambientColor = rgb2Color3(ambient);
668
+ material.emissiveColor = rgb2Color3(emissive);
669
+ material.alpha = opac;
670
+
671
+ // When emissiveOnly is true, disable lighting so only emissive color shows
672
+ // This gives a "self-lit" appearance that ignores scene lights
673
+ if (emissiveOnly) {
674
+ material.disableLighting = true;
675
+ }
676
+
677
+ // Emissive-only materials should be self-lit and not affected by fog
678
+ if (emissiveOnly) {
679
+ material.fogEnabled = false;
680
+ } else {
681
+ material.fogEnabled = fogEnabled;
682
+ // Attach fog plugin to properly blend emissive color with fog
683
+ // Babylon's standard fog only affects diffuse/ambient, not emissive.
684
+ // The plugin re-fogs the entire fragment output so emissive fades properly.
685
+ // (The slight double-fog on diffuse/ambient is negligible since text is primarily emissive)
686
+ if (fogEnabled) {
687
+ material._textFogPlugin = new TextFogPlugin(material);
688
+ }
689
+ }
690
+
691
+ return material;
692
+ }
693
+
694
+ /**
695
+ * Create a dedicated emissive material for front faces.
696
+ * This keeps the face self-lit while still respecting fog settings.
697
+ * @param {Scene} scene
698
+ * @param {string} letters
699
+ * @param {string} emissive
700
+ * @param {number} opac
701
+ * @param {boolean} fogEnabled
702
+ * @returns {StandardMaterial}
703
+ */
704
+ function makeFaceMaterial(scene, letters, emissive, opac, fogEnabled = true) {
705
+ const material = new standardMaterial.StandardMaterial("mw-face-matl-" + letters + "-" + weeid(), scene);
706
+ const black = rgb2Color3("#000000");
707
+ material.diffuseColor = black;
708
+ material.specularColor = black;
709
+ material.ambientColor = black;
710
+ material.emissiveColor = rgb2Color3(emissive);
711
+ material.disableLighting = true;
712
+ material.alpha = opac;
713
+ material.backFaceCulling = false;
714
+ material.fogEnabled = fogEnabled;
715
+ if (fogEnabled) {
716
+ material._textFogPlugin = new TextFogPlugin(material);
717
+ }
718
+ return material;
719
+ }
720
+
721
+ /**
722
+ * MeshWriter SPS (Solid Particle System) Helpers
723
+ * Converts letter meshes into an efficient SPS
724
+ */
725
+
726
+
727
+ /** @typedef {import('@babylonjs/core/scene').Scene} Scene */
728
+ /** @typedef {import('@babylonjs/core/Materials/material').Material} Material */
729
+ /** @typedef {import('@babylonjs/core/Meshes/mesh').Mesh} BabylonMesh */
730
+ /** @typedef {(any[] & { faceMeshes?: BabylonMesh[] })} MeshCollection */
731
+ /**
732
+ * @typedef {[SolidParticleSystem | undefined, BabylonMesh | undefined] & {
733
+ * face: [SolidParticleSystem | undefined, BabylonMesh | undefined];
734
+ * }} SPSCombo
735
+ */
736
+
737
+ /**
738
+ * Create an SPS from letter meshes
739
+ * @param {Scene} scene - Babylon scene
740
+ * @param {MeshCollection} meshesAndBoxes - [meshes, boxes, origins] with optional face geometry
741
+ * @param {Material} material - Material to apply to SPS mesh
742
+ * @returns {SPSCombo} - Combined SPS + emissive face SPS
743
+ */
744
+ function makeSPS(scene, meshesAndBoxes, material) {
745
+ const rimMeshes = meshesAndBoxes[0] || [];
746
+ const faceMeshes = meshesAndBoxes.faceMeshes || [];
747
+ const lettersOrigins = meshesAndBoxes[2] || [];
748
+
749
+ const rim = buildSystem("sps_rim", rimMeshes, lettersOrigins, scene, material);
750
+
751
+ // Use Mesh.MergeMeshes for face instead of SPS - SPS has issues with face geometry
752
+ const face = buildFaceMesh("sps_face", faceMeshes, lettersOrigins);
753
+
754
+ /** @type {SPSCombo} */
755
+ const combo = /** @type {any} */ ([rim.sps, rim.mesh]);
756
+ combo.face = [undefined, face.mesh]; // No SPS for face, just merged mesh
757
+ return combo;
758
+ }
759
+
760
+ /**
761
+ * Build face mesh using Mesh.MergeMeshes instead of SPS
762
+ * @param {string} name - Mesh name
763
+ * @param {BabylonMesh[]} meshes - Face meshes to merge
764
+ * @param {Array} lettersOrigins - Letter origin positions
765
+ * @param {Scene} scene - Babylon scene
766
+ * @returns {{ mesh: BabylonMesh | undefined }}
767
+ */
768
+ function buildFaceMesh(name, meshes, lettersOrigins, scene) {
769
+ const validMeshes = meshes.filter(m => m != null);
770
+ if (!validMeshes.length) {
771
+ return { mesh: undefined };
772
+ }
773
+
774
+ // Position each mesh according to letter origins before merging
775
+ validMeshes.forEach((mesh, ix) => {
776
+ if (lettersOrigins[ix]) {
777
+ mesh.position.x = lettersOrigins[ix][0] + lettersOrigins[ix][1];
778
+ mesh.position.z = lettersOrigins[ix][2];
779
+ }
780
+ });
781
+
782
+ // Merge all face meshes into one
783
+ const merged = mesh.Mesh.MergeMeshes(validMeshes, true, true, undefined, false, true);
784
+ if (merged) {
785
+ merged.name = name;
786
+ }
787
+
788
+ return { mesh: merged };
789
+ }
790
+
791
+ function buildSystem(name, meshes, lettersOrigins, scene, material) {
792
+ // Pre-filter null meshes to avoid repeated null checks in hot loop
793
+ const validMeshes = meshes
794
+ .map((mesh, ix) => mesh ? { mesh, origins: lettersOrigins[ix] } : null)
795
+ .filter(item => item !== null);
796
+
797
+ if (!validMeshes.length) {
798
+ return { sps: undefined, mesh: undefined };
799
+ }
800
+
801
+ const sps = new solidParticleSystem.SolidParticleSystem(name, scene, {});
802
+ validMeshes.forEach(function(item) {
803
+ sps.addShape(item.mesh, 1, {
804
+ positionFunction: makePositionParticle(item.origins)
805
+ });
806
+ item.mesh.dispose();
807
+ });
808
+
809
+ const spsMesh = sps.buildMesh();
810
+
811
+ if (spsMesh && material) {
812
+ spsMesh.material = material;
813
+ }
814
+ sps.setParticles();
815
+
816
+ return { sps, mesh: spsMesh };
817
+ }
818
+
819
+ function makePositionParticle(letterOrigins) {
820
+ return function positionParticle(particle) {
821
+ if (!letterOrigins) return;
822
+ particle.position.x = letterOrigins[0] + letterOrigins[1];
823
+ particle.position.z = letterOrigins[2];
824
+ };
825
+ }
826
+
827
+ /**
828
+ * Split a mesh into two meshes: emissive face geometry and lit rim geometry.
829
+ * @param {import('@babylonjs/core').Mesh} mesh
830
+ * @param {import('@babylonjs/core').Scene} scene
831
+ * @returns {{ rimMesh: import('@babylonjs/core').Mesh, faceMesh: import('@babylonjs/core').Mesh | null }}
832
+ */
833
+ function splitMeshByFaceNormals(mesh, scene) {
834
+ var geometry = mesh.geometry;
835
+ if (!geometry) {
836
+ return { rimMesh: mesh, faceMesh: null }
837
+ }
838
+
839
+ var positions = geometry.getVerticesData('position');
840
+ var normals = geometry.getVerticesData('normal');
841
+ var uvs = geometry.getVerticesData('uv');
842
+ var indices = geometry.getIndices();
843
+
844
+ if (!positions || !normals || !indices || positions.length === 0) {
845
+ return { rimMesh: mesh, faceMesh: null }
846
+ }
847
+
848
+ var faceData = createEmptyData();
849
+ var rimData = createEmptyData();
850
+
851
+ var axisInfo = detectFaceAxisFromGeometry(positions, normals, indices);
852
+ if (!axisInfo) {
853
+ axisInfo = detectFaceAxis(normals);
854
+ }
855
+ if (!axisInfo) {
856
+ axisInfo = detectExtrudeAxisFromPositions(positions);
857
+ }
858
+
859
+ for (var i = 0; i < indices.length; i += 3) {
860
+ var i0 = indices[i];
861
+ var i1 = indices[i + 1];
862
+ var i2 = indices[i + 2];
863
+ var isFace = triangleIsFrontFace(i0, i1, i2, positions, normals, axisInfo);
864
+ var target = isFace ? faceData : rimData;
865
+ var v0 = appendVertex(target, i0, positions, normals, uvs);
866
+ var v1 = appendVertex(target, i1, positions, normals, uvs);
867
+ var v2 = appendVertex(target, i2, positions, normals, uvs);
868
+ target.indices.push(v0, v1, v2);
869
+ }
870
+
871
+ mesh.dispose();
872
+
873
+ var rimMesh = buildMesh(mesh.name + '_rim', rimData, scene);
874
+ var faceMesh = buildMesh(mesh.name + '_face', faceData, scene);
875
+
876
+ if (!faceMesh) {
877
+ return { rimMesh: rimMesh || mesh, faceMesh: null }
878
+ }
879
+ if (!rimMesh) {
880
+ return { rimMesh: faceMesh, faceMesh: null }
881
+ }
882
+ return { rimMesh, faceMesh }
883
+ }
884
+
885
+ function detectFaceAxisFromGeometry(positions, normals, indices) {
886
+ if (!positions || !indices) return null
887
+
888
+ var min = [Infinity, Infinity, Infinity];
889
+ var max = [-Infinity, -Infinity, -Infinity];
890
+ for (var i = 0; i < positions.length; i += 3) {
891
+ var x = positions[i];
892
+ var y = positions[i + 1];
893
+ var z = positions[i + 2];
894
+ if (x < min[0]) min[0] = x;
895
+ if (x > max[0]) max[0] = x;
896
+ if (y < min[1]) min[1] = y;
897
+ if (y > max[1]) max[1] = y;
898
+ if (z < min[2]) min[2] = z;
899
+ if (z > max[2]) max[2] = z;
900
+ }
901
+
902
+ var epsilons = [
903
+ Math.max((max[0] - min[0]) * 0.15, 0.001),
904
+ Math.max((max[1] - min[1]) * 0.15, 0.001),
905
+ Math.max((max[2] - min[2]) * 0.15, 0.001)
906
+ ];
907
+
908
+ var counts = [
909
+ { min: 0, max: 0, sumMin: 0, sumMax: 0 },
910
+ { min: 0, max: 0, sumMin: 0, sumMax: 0 },
911
+ { min: 0, max: 0, sumMin: 0, sumMax: 0 }
912
+ ];
913
+
914
+ for (var idx = 0; idx < indices.length; idx += 3) {
915
+ var i0 = indices[idx];
916
+ var i1 = indices[idx + 1];
917
+ var i2 = indices[idx + 2];
918
+
919
+ for (var axis = 0; axis < 3; axis++) {
920
+ var epsilon = epsilons[axis];
921
+ if (epsilon <= 0) continue
922
+ var minVal = min[axis];
923
+ var maxVal = max[axis];
924
+
925
+ var c0 = positions[i0 * 3 + axis];
926
+ var c1 = positions[i1 * 3 + axis];
927
+ var c2 = positions[i2 * 3 + axis];
928
+
929
+ var nearMin = Math.abs(c0 - minVal) < epsilon &&
930
+ Math.abs(c1 - minVal) < epsilon &&
931
+ Math.abs(c2 - minVal) < epsilon;
932
+ var nearMax = Math.abs(c0 - maxVal) < epsilon &&
933
+ Math.abs(c1 - maxVal) < epsilon &&
934
+ Math.abs(c2 - maxVal) < epsilon;
935
+
936
+ if (nearMin) {
937
+ counts[axis].min++;
938
+ if (normals) {
939
+ counts[axis].sumMin += normals[i0 * 3 + axis] + normals[i1 * 3 + axis] + normals[i2 * 3 + axis];
940
+ }
941
+ } else if (nearMax) {
942
+ counts[axis].max++;
943
+ if (normals) {
944
+ counts[axis].sumMax += normals[i0 * 3 + axis] + normals[i1 * 3 + axis] + normals[i2 * 3 + axis];
945
+ }
946
+ }
947
+ }
948
+ }
949
+
950
+ var bestAxis = -1;
951
+ var bestCount = 0;
952
+ for (var axisIdx = 0; axisIdx < 3; axisIdx++) {
953
+ var total = counts[axisIdx].min + counts[axisIdx].max;
954
+ if (total > bestCount) {
955
+ bestCount = total;
956
+ bestAxis = axisIdx;
957
+ }
958
+ }
959
+
960
+ if (bestAxis === -1 || bestCount === 0) {
961
+ return null
962
+ }
963
+
964
+ // For 3D text, extrusion is typically along Y axis (axis=1).
965
+ // If Y axis has any face triangles and the detected axis is different,
966
+ // prefer Y unless the detected axis has significantly more triangles.
967
+ var yTotal = counts[1].min + counts[1].max;
968
+ if (bestAxis !== 1 && yTotal > 0) {
969
+ // Only switch away from Y if another axis has 2x more triangles
970
+ if (bestCount < yTotal * 2) {
971
+ bestAxis = 1;
972
+ bestCount = yTotal;
973
+ }
974
+ }
975
+
976
+ var chosen = counts[bestAxis];
977
+ var frontSide;
978
+ // For PolygonMeshBuilder extrusions: geometry goes from Y=0 (base) to Y=-depth (extruded end)
979
+ // After rotation -PI/2 around X: Y=0 → Z=0 (front, facing camera), Y=-depth → Z=+depth (back)
980
+ // So Y=max (Y=0) is the FRONT face, Y=min (Y=-depth) is the BACK face
981
+ var maxHasOutwardNormals = chosen.sumMax > 0;
982
+ var minHasOutwardNormals = chosen.sumMin < 0;
983
+
984
+ // Select MAX as front (the base at Y=0 which faces camera after rotation)
985
+ if (maxHasOutwardNormals) {
986
+ frontSide = 'max';
987
+ } else if (minHasOutwardNormals) {
988
+ frontSide = 'min';
989
+ } else {
990
+ // Fallback to max (the base)
991
+ frontSide = 'max';
992
+ }
993
+
994
+ return {
995
+ axis: bestAxis,
996
+ strategy: 'positions',
997
+ min: min[bestAxis],
998
+ max: max[bestAxis],
999
+ epsilon: epsilons[bestAxis],
1000
+ frontSide
1001
+ }
1002
+ }
1003
+
1004
+ function detectFaceAxis(normals) {
1005
+ if (!normals || normals.length === 0) {
1006
+ return null
1007
+ }
1008
+ var sumsAbs = [0, 0, 0];
1009
+ var sumsSigned = [0, 0, 0];
1010
+ for (var i = 0; i < normals.length; i += 3) {
1011
+ var nx = normals[i];
1012
+ var ny = normals[i + 1];
1013
+ var nz = normals[i + 2];
1014
+ sumsAbs[0] += Math.abs(nx);
1015
+ sumsAbs[1] += Math.abs(ny);
1016
+ sumsAbs[2] += Math.abs(nz);
1017
+ sumsSigned[0] += nx;
1018
+ sumsSigned[1] += ny;
1019
+ sumsSigned[2] += nz;
1020
+ }
1021
+ var axis = 0;
1022
+ var maxSum = sumsAbs[0];
1023
+ for (var j = 1; j < 3; j++) {
1024
+ if (sumsAbs[j] > maxSum) {
1025
+ maxSum = sumsAbs[j];
1026
+ axis = j;
1027
+ }
1028
+ }
1029
+ if (maxSum === 0) {
1030
+ return null
1031
+ }
1032
+ var frontSign = sumsSigned[axis] >= 0 ? 1 : -1;
1033
+ return {
1034
+ axis,
1035
+ frontSign,
1036
+ strategy: 'normals',
1037
+ min: 0,
1038
+ max: 0,
1039
+ epsilon: 0,
1040
+ frontSide: 'max'
1041
+ }
1042
+ }
1043
+
1044
+ function detectExtrudeAxisFromPositions(positions) {
1045
+ if (!positions || positions.length === 0) {
1046
+ return null
1047
+ }
1048
+ var min = [Infinity, Infinity, Infinity];
1049
+ var max = [-Infinity, -Infinity, -Infinity];
1050
+ for (var i = 0; i < positions.length; i += 3) {
1051
+ var x = positions[i];
1052
+ var y = positions[i + 1];
1053
+ var z = positions[i + 2];
1054
+ if (x < min[0]) min[0] = x;
1055
+ if (x > max[0]) max[0] = x;
1056
+ if (y < min[1]) min[1] = y;
1057
+ if (y > max[1]) max[1] = y;
1058
+ if (z < min[2]) min[2] = z;
1059
+ if (z > max[2]) max[2] = z;
1060
+ }
1061
+ var ranges = [
1062
+ max[0] - min[0],
1063
+ max[1] - min[1],
1064
+ max[2] - min[2]
1065
+ ];
1066
+ var axis = 0;
1067
+ var minRange = ranges[0];
1068
+ for (var j = 1; j < 3; j++) {
1069
+ if (ranges[j] < minRange) {
1070
+ minRange = ranges[j];
1071
+ axis = j;
1072
+ }
1073
+ }
1074
+ var epsilon = Math.max(minRange * 0.05, 0.0001);
1075
+ return {
1076
+ axis,
1077
+ strategy: 'positions',
1078
+ min: min[axis],
1079
+ max: max[axis],
1080
+ epsilon,
1081
+ frontSide: 'max'
1082
+ }
1083
+ }
1084
+
1085
+ function triangleIsFrontFace(i0, i1, i2, positions, normals, axisInfo) {
1086
+ if (!axisInfo) return false
1087
+ var axis = axisInfo.axis || 1;
1088
+
1089
+ if (axisInfo.strategy === 'normals' && normals) {
1090
+ var frontSign = axisInfo.frontSign || 1;
1091
+ var threshold = 0.5;
1092
+ var n0 = normals[i0 * 3 + axis] * frontSign;
1093
+ var n1 = normals[i1 * 3 + axis] * frontSign;
1094
+ var n2 = normals[i2 * 3 + axis] * frontSign;
1095
+ return (n0 > threshold && n1 > threshold && n2 > threshold)
1096
+ }
1097
+
1098
+ if (axisInfo.strategy === 'positions' && positions) {
1099
+ var epsilon = axisInfo.epsilon;
1100
+ var limitVal = axisInfo.frontSide === 'min' ? axisInfo.min : axisInfo.max;
1101
+ var c0 = positions[i0 * 3 + axis];
1102
+ var c1 = positions[i1 * 3 + axis];
1103
+ var c2 = positions[i2 * 3 + axis];
1104
+ return (
1105
+ Math.abs(c0 - limitVal) < epsilon &&
1106
+ Math.abs(c1 - limitVal) < epsilon &&
1107
+ Math.abs(c2 - limitVal) < epsilon
1108
+ )
1109
+ }
1110
+
1111
+ return false
1112
+ }
1113
+
1114
+ function createEmptyData() {
1115
+ return {
1116
+ positions: [],
1117
+ normals: [],
1118
+ uvs: [],
1119
+ indices: [],
1120
+ nextIndex: 0
1121
+ }
1122
+ }
1123
+
1124
+ function appendVertex(target, originalIndex, positions, normals, uvs) {
1125
+ var posOffset = originalIndex * 3;
1126
+ var uvOffset = originalIndex * 2;
1127
+
1128
+ target.positions.push(
1129
+ positions[posOffset],
1130
+ positions[posOffset + 1],
1131
+ positions[posOffset + 2]
1132
+ );
1133
+ target.normals.push(
1134
+ normals[posOffset],
1135
+ normals[posOffset + 1],
1136
+ normals[posOffset + 2]
1137
+ );
1138
+
1139
+ if (uvs && uvs.length) {
1140
+ target.uvs.push(uvs[uvOffset], uvs[uvOffset + 1]);
1141
+ } else {
1142
+ target.uvs.push(0, 0);
1143
+ }
1144
+
1145
+ var newIndex = target.nextIndex;
1146
+ target.nextIndex += 1;
1147
+ return newIndex
1148
+ }
1149
+
1150
+ function buildMesh(name, data, scene) {
1151
+ if (!data.positions.length) return null
1152
+ var newMesh = new mesh.Mesh(name, scene);
1153
+ var vertexData = new mesh_vertexData.VertexData();
1154
+ vertexData.positions = data.positions;
1155
+ vertexData.normals = data.normals;
1156
+ vertexData.indices = data.indices;
1157
+ vertexData.uvs = data.uvs;
1158
+ vertexData.applyToMesh(newMesh, true);
1159
+ newMesh.refreshBoundingInfo();
1160
+ return newMesh
1161
+ }
1162
+
1163
+ /**
1164
+ * MeshWriter Letter Mesh Construction
1165
+ * Builds 3D letter meshes from font specifications
1166
+ */
1167
+
1168
+
1169
+ /** @typedef {import('@babylonjs/core/scene').Scene} Scene */
1170
+ /** @typedef {import('@babylonjs/core/Materials/material').Material} Material */
1171
+
1172
+ // Handle both CJS default export and ESM module export
1173
+ const earcut = earcutModule.default || earcutModule;
1174
+
1175
+ // Constants
1176
+ const naturalLetterHeight = 1000;
1177
+
1178
+ /**
1179
+ * Get kerning value between two characters
1180
+ * @param {Object} fontSpec - Font specification object
1181
+ * @param {string} left - Left character
1182
+ * @param {string} right - Right character
1183
+ * @returns {number} - Kerning adjustment value (0 if none)
1184
+ */
1185
+ function getKerning(fontSpec, left, right) {
1186
+ if (!fontSpec.kern) return 0;
1187
+ const key = `${left},${right}`;
1188
+ return fontSpec.kern[key] || 0;
1189
+ }
1190
+
1191
+ /**
1192
+ * Decompress font letter on first use (lazy decompression)
1193
+ * @param {Object} fontSpec - Font specification object
1194
+ * @param {string} letter - Character to get spec for
1195
+ * @returns {Object|undefined} - Letter specification
1196
+ */
1197
+ function makeLetterSpec(fontSpec, letter) {
1198
+ const letterSpec = fontSpec[letter];
1199
+
1200
+ if (isObject(letterSpec)) {
1201
+ // Decompress shape commands if compressed
1202
+ if (!isArray(letterSpec.shapeCmds) && isArray(letterSpec.sC)) {
1203
+ letterSpec.shapeCmds = letterSpec.sC.map(cmds => decodeList(cmds));
1204
+ letterSpec.sC = null;
1205
+ }
1206
+ // Decompress hole commands if compressed
1207
+ if (!isArray(letterSpec.holeCmds) && isArray(letterSpec.hC)) {
1208
+ letterSpec.holeCmds = letterSpec.hC.map(cmdslists =>
1209
+ isArray(cmdslists) ? cmdslists.map(cmds => decodeList(cmds)) : cmdslists
1210
+ );
1211
+ letterSpec.hC = null;
1212
+ }
1213
+ }
1214
+ return letterSpec;
1215
+ }
1216
+
1217
+ /**
1218
+ * Convert point to Vector2
1219
+ */
1220
+ function point2Vector(point) {
1221
+ return new math_vector.Vector2(round(point.x), round(point.y));
1222
+ }
1223
+
1224
+ /**
1225
+ * Merge array of meshes
1226
+ */
1227
+ function merge(arrayOfMeshes) {
1228
+ return arrayOfMeshes.length === 1
1229
+ ? arrayOfMeshes[0]
1230
+ : mesh.Mesh.MergeMeshes(arrayOfMeshes, true);
1231
+ }
1232
+
1233
+ /**
1234
+ * @typedef {Object} LetterPolygonsResult
1235
+ * @property {number} xWidth - Total width of all letters
1236
+ * @property {number} count - Number of valid letter meshes
1237
+ */
1238
+ /**
1239
+ * @typedef {(any[] & LetterPolygonsResult) & { faceMeshes: Mesh[] }} LetterPolygonsCollection
1240
+ */
1241
+
1242
+ /**
1243
+ * Construct meshes for all letters in a string
1244
+ * @param {string} letters - Text string
1245
+ * @param {Object} fontSpec - Font specification
1246
+ * @param {number} xOffset - X offset
1247
+ * @param {number} yOffset - Y offset (unused, kept for API compatibility)
1248
+ * @param {number} zOffset - Z offset
1249
+ * @param {number} letterScale - Scale factor for letters
1250
+ * @param {number} thickness - Letter thickness (depth)
1251
+ * @param {Material} material - Material (unused in this function)
1252
+ * @param {string} meshOrigin - "letterCenter" or "fontOrigin"
1253
+ * @param {Scene} scene - Babylon scene
1254
+ * @param {Object} [spacingOpts] - Optional spacing options
1255
+ * @param {number} [spacingOpts.letterSpacing=0] - Extra spacing between letters (world units, added after kerning)
1256
+ * @param {number} [spacingOpts.wordSpacing=0] - Extra spacing for spaces (world units, added to space width)
1257
+ * @returns {any[] & LetterPolygonsResult} - [meshes, boxes, origins] with xWidth and count properties
1258
+ */
1259
+ function constructLetterPolygons(
1260
+ letters, fontSpec, xOffset, yOffset, zOffset,
1261
+ letterScale, thickness, material, meshOrigin, scene, spacingOpts = {}
1262
+ ) {
1263
+ // Extract spacing options (already in world units)
1264
+ const letterSpacing = isNumber(spacingOpts.letterSpacing) ? spacingOpts.letterSpacing : 0;
1265
+ const wordSpacing = isNumber(spacingOpts.wordSpacing) ? spacingOpts.wordSpacing : 0;
1266
+
1267
+ let letterOffsetX = 0;
1268
+ const lettersOrigins = new Array(letters.length);
1269
+ const lettersBoxes = new Array(letters.length);
1270
+ const lettersMeshes = new Array(letters.length);
1271
+ const faceMeshes = new Array(letters.length);
1272
+ let ix = 0;
1273
+
1274
+ for (let i = 0; i < letters.length; i++) {
1275
+ const letter = letters[i];
1276
+ const letterSpec = makeLetterSpec(fontSpec, letter);
1277
+
1278
+ if (isObject(letterSpec)) {
1279
+ const lists = buildLetterMeshes(
1280
+ letter, i, letterSpec, fontSpec.reverseShapes, fontSpec.reverseHoles,
1281
+ meshOrigin, letterScale, xOffset, zOffset, letterOffsetX, thickness, scene
1282
+ );
1283
+
1284
+ const shapesList = lists[0];
1285
+ const holesList = lists[1];
1286
+ const letterBox = lists[2];
1287
+ const letterOrigins = lists[3];
1288
+ let newOffsetX = lists[4];
1289
+
1290
+ // Apply kerning with next letter (if any)
1291
+ if (i < letters.length - 1) {
1292
+ const nextLetter = letters[i + 1];
1293
+ const kernValue = getKerning(fontSpec, letter, nextLetter);
1294
+ if (kernValue !== 0) {
1295
+ newOffsetX += kernValue * letterScale;
1296
+ }
1297
+ // Apply letter spacing after kerning (not after last letter)
1298
+ if (letterSpacing !== 0) {
1299
+ newOffsetX += letterSpacing;
1300
+ }
1301
+ }
1302
+
1303
+ // Apply extra word spacing for space characters
1304
+ if (letter === ' ' && wordSpacing !== 0) {
1305
+ newOffsetX += wordSpacing;
1306
+ }
1307
+
1308
+ letterOffsetX = newOffsetX;
1309
+
1310
+ const letterMeshes = punchHolesInShapes(shapesList, holesList, letter, i, scene);
1311
+
1312
+ if (letterMeshes.length) {
1313
+ const merged = merge(letterMeshes);
1314
+ const split = splitMeshByFaceNormals(merged, scene);
1315
+
1316
+ lettersMeshes[ix] = split.rimMesh;
1317
+ faceMeshes[ix] = split.faceMesh;
1318
+ lettersOrigins[ix] = letterOrigins;
1319
+ lettersBoxes[ix] = letterBox;
1320
+ ix++;
1321
+ }
1322
+ }
1323
+ }
1324
+
1325
+ /** @type {LetterPolygonsCollection} */
1326
+ const meshesAndBoxes = /** @type {any} */ ([lettersMeshes, lettersBoxes, lettersOrigins]);
1327
+ meshesAndBoxes.faceMeshes = faceMeshes;
1328
+ meshesAndBoxes.xWidth = round(letterOffsetX);
1329
+ meshesAndBoxes.count = ix;
1330
+ return meshesAndBoxes;
1331
+ }
1332
+
1333
+ /**
1334
+ * Build meshes for a single letter
1335
+ * @returns {Array} - [shapesList, holesList, letterBox, letterOrigins, newOffsetX]
1336
+ */
1337
+ function buildLetterMeshes(
1338
+ letter, index, spec, reverseShapes, reverseHoles,
1339
+ meshOrigin, letterScale, xOffset, zOffset, letterOffsetX, thickness, scene
1340
+ ) {
1341
+ // Offset calculations
1342
+ const balanced = meshOrigin === "letterCenter";
1343
+ const centerX = (spec.xMin + spec.xMax) / 2;
1344
+ const centerZ = (spec.yMin + spec.yMax) / 2;
1345
+ const xFactor = isNumber(spec.xFactor) ? spec.xFactor : 1;
1346
+ const zFactor = isNumber(spec.yFactor) ? spec.yFactor : 1;
1347
+ const xShift = isNumber(spec.xShift) ? spec.xShift : 0;
1348
+ const zShift = isNumber(spec.yShift) ? spec.yShift : 0;
1349
+ const reverseShape = isBoolean(spec.reverseShape) ? spec.reverseShape : reverseShapes;
1350
+ const reverseHole = isBoolean(spec.reverseHole) ? spec.reverseHole : reverseHoles;
1351
+ const offX = xOffset - (balanced ? centerX : 0);
1352
+ const offZ = zOffset - (balanced ? centerZ : 0);
1353
+ const shapeCmdsLists = isArray(spec.shapeCmds) ? spec.shapeCmds : [];
1354
+ const holeCmdsListsArray = isArray(spec.holeCmds) ? spec.holeCmds : [];
1355
+
1356
+ // Tracking for relative coordinates
1357
+ let thisX, lastX, thisZ, lastZ;
1358
+
1359
+ // Scaling functions
1360
+ const adjX = makeAdjust(letterScale, xFactor, offX, 0, false, true);
1361
+ const adjZ = makeAdjust(letterScale, zFactor, offZ, 0, false, false);
1362
+ const adjXfix = makeAdjust(letterScale, xFactor, offX, xShift, false, true);
1363
+ const adjZfix = makeAdjust(letterScale, zFactor, offZ, zShift, false, false);
1364
+ const adjXrel = makeAdjust(letterScale, xFactor, offX, xShift, true, true);
1365
+ const adjZrel = makeAdjust(letterScale, zFactor, offZ, zShift, true, false);
1366
+
1367
+ const letterBox = [adjX(spec.xMin), adjX(spec.xMax), adjZ(spec.yMin), adjZ(spec.yMax)];
1368
+ const letterOrigins = [round(letterOffsetX), -1 * adjX(0), -1 * adjZ(0)];
1369
+
1370
+ // Update letterOffsetX for next letter
1371
+ const newOffsetX = letterOffsetX + spec.wdth * letterScale;
1372
+
1373
+ const shapesList = shapeCmdsLists.map(makeCmdsToMesh(reverseShape));
1374
+ const holesList = holeCmdsListsArray.map(meshesFromCmdsListArray);
1375
+
1376
+ return [shapesList, holesList, letterBox, letterOrigins, newOffsetX];
1377
+
1378
+ function meshesFromCmdsListArray(cmdsListArray) {
1379
+ return cmdsListArray.map(makeCmdsToMesh(reverseHole));
1380
+ }
1381
+
1382
+ function makeCmdsToMesh(reverse) {
1383
+ return function cmdsToMesh(cmdsList) {
1384
+ let cmd = getCmd(cmdsList, 0);
1385
+ /** @type {any} */
1386
+ const path = new math_path.Path2(adjXfix(cmd[0]), adjZfix(cmd[1]));
1387
+
1388
+ // Process path commands
1389
+ for (let j = 1; j < cmdsList.length; j++) {
1390
+ cmd = getCmd(cmdsList, j);
1391
+
1392
+ // Line (2 coords = absolute, 3 = relative)
1393
+ if (cmd.length === 2) {
1394
+ path.addLineTo(adjXfix(cmd[0]), adjZfix(cmd[1]));
1395
+ }
1396
+ if (cmd.length === 3) {
1397
+ path.addLineTo(adjXrel(cmd[1]), adjZrel(cmd[2]));
1398
+ }
1399
+
1400
+ // Quadratic curve (4 = absolute, 5 = relative)
1401
+ if (cmd.length === 4) {
1402
+ path.addQuadraticCurveTo(
1403
+ adjXfix(cmd[0]), adjZfix(cmd[1]),
1404
+ adjXfix(cmd[2]), adjZfix(cmd[3])
1405
+ );
1406
+ }
1407
+ if (cmd.length === 5) {
1408
+ path.addQuadraticCurveTo(
1409
+ adjXrel(cmd[1]), adjZrel(cmd[2]),
1410
+ adjXrel(cmd[3]), adjZrel(cmd[4])
1411
+ );
1412
+ }
1413
+
1414
+ // Cubic curve (6 = absolute, 7 = relative)
1415
+ if (cmd.length === 6) {
1416
+ path.addCubicCurveTo(
1417
+ adjXfix(cmd[0]), adjZfix(cmd[1]),
1418
+ adjXfix(cmd[2]), adjZfix(cmd[3]),
1419
+ adjXfix(cmd[4]), adjZfix(cmd[5])
1420
+ );
1421
+ }
1422
+ if (cmd.length === 7) {
1423
+ path.addCubicCurveTo(
1424
+ adjXrel(cmd[1]), adjZrel(cmd[2]),
1425
+ adjXrel(cmd[3]), adjZrel(cmd[4]),
1426
+ adjXrel(cmd[5]), adjZrel(cmd[6])
1427
+ );
1428
+ }
1429
+ }
1430
+
1431
+ // Convert path to array and process
1432
+ let array = path.getPoints().map(point2Vector);
1433
+
1434
+ // Remove redundant start/end points
1435
+ const first = 0;
1436
+ const last = array.length - 1;
1437
+ if (array[first].x === array[last].x && array[first].y === array[last].y) {
1438
+ array = array.slice(1);
1439
+ }
1440
+ if (reverse) {
1441
+ array.reverse();
1442
+ }
1443
+
1444
+ const meshBuilder = new polygonMesh.PolygonMeshBuilder(
1445
+ "MeshWriter-" + letter + index + "-" + weeid(),
1446
+ array,
1447
+ scene,
1448
+ earcut
1449
+ );
1450
+ return meshBuilder.build(true, thickness);
1451
+ };
1452
+ }
1453
+
1454
+ function getCmd(list, ix) {
1455
+ lastX = thisX;
1456
+ lastZ = thisZ;
1457
+ const cmd = list[ix];
1458
+ const len = cmd.length;
1459
+ thisX = isRelativeLength(len)
1460
+ ? round((cmd[len - 2] * xFactor) + thisX)
1461
+ : round(cmd[len - 2] * xFactor);
1462
+ thisZ = isRelativeLength(len)
1463
+ ? round((cmd[len - 1] * zFactor) + thisZ)
1464
+ : round(cmd[len - 1] * zFactor);
1465
+ return cmd;
1466
+ }
1467
+
1468
+ function makeAdjust(letterScale, factor, off, shift, relative, xAxis) {
1469
+ if (relative) {
1470
+ if (xAxis) {
1471
+ return val => round(letterScale * ((val * factor) + shift + lastX + off));
1472
+ } else {
1473
+ return val => round(letterScale * ((val * factor) + shift + lastZ + off));
1474
+ }
1475
+ } else {
1476
+ return val => round(letterScale * ((val * factor) + shift + off));
1477
+ }
1478
+ }
1479
+ }
1480
+
1481
+ /**
1482
+ * Punch holes in letter shapes using CSG operations
1483
+ * @param {Array} shapesList - Array of shape meshes
1484
+ * @param {Array} holesList - Array of arrays of hole meshes
1485
+ * @param {string} letter - Letter character (for naming)
1486
+ * @param {number} letterIndex - Index of letter (for naming)
1487
+ * @param {Scene} scene - Babylon scene
1488
+ * @returns {Array} - Array of final letter meshes
1489
+ */
1490
+ function punchHolesInShapes(shapesList, holesList, letter, letterIndex, scene) {
1491
+ const csgVersion = getCSGVersion();
1492
+
1493
+ // Validate CSG is available and initialized
1494
+ if (csgVersion === 'CSG2' && !isCSGReady()) {
1495
+ throw new Error(
1496
+ "MeshWriter: CSG2 not initialized. " +
1497
+ "Use 'await MeshWriter.createAsync(scene, prefs)', call " +
1498
+ "'await BABYLON.InitializeCSG2Async()', or configure " +
1499
+ "MeshWriter.setCSGInitializer before creating MeshWriter."
1500
+ );
1501
+ }
1502
+ if (csgVersion === null) {
1503
+ throw new Error(
1504
+ "MeshWriter: No CSG implementation found. " +
1505
+ "Ensure BABYLON.CSG or BABYLON.CSG2 is available."
1506
+ );
1507
+ }
1508
+
1509
+ const letterMeshes = [];
1510
+ const csgLib = getCSGLib();
1511
+
1512
+ // Handle special case: single shape with multiple hole arrays
1513
+ // (e.g., "B" has 1 shape but 2 holes - top and bottom)
1514
+ // In this case, all holes should be punched into the single shape
1515
+ if (shapesList.length === 1 && holesList.length > 1) {
1516
+ const shape = shapesList[0];
1517
+ // Flatten all hole arrays into a single array
1518
+ const allHoles = holesList.flat();
1519
+
1520
+ if (allHoles.length > 0) {
1521
+ letterMeshes.push(punchHolesInShape(shape, allHoles, letter, letterIndex, csgLib, csgVersion, scene));
1522
+ } else {
1523
+ letterMeshes.push(shape);
1524
+ }
1525
+ return letterMeshes;
1526
+ }
1527
+
1528
+ // Standard case: 1:1 correspondence between shapes and hole arrays
1529
+ for (let j = 0; j < shapesList.length; j++) {
1530
+ const shape = shapesList[j];
1531
+ const holes = holesList[j];
1532
+
1533
+ if (isArray(holes) && holes.length) {
1534
+ letterMeshes.push(punchHolesInShape(shape, holes, letter, letterIndex, csgLib, csgVersion, scene));
1535
+ } else {
1536
+ // PolygonMeshBuilder creates meshes with correct normals by default
1537
+ // No flipFaces needed for shapes without holes
1538
+ letterMeshes.push(shape);
1539
+ }
1540
+ }
1541
+
1542
+ return letterMeshes;
1543
+ }
1544
+
1545
+ /**
1546
+ * Punch holes in a single shape
1547
+ */
1548
+ function punchHolesInShape(shape, holes, letter, letterIndex, csgLib, csgVersion, scene) {
1549
+ const meshName = "Net-" + letter + letterIndex + "-" + weeid();
1550
+
1551
+ let csgShape = csgLib.FromMesh(shape);
1552
+ for (let k = 0; k < holes.length; k++) {
1553
+ csgShape = csgShape.subtract(csgLib.FromMesh(holes[k]));
1554
+ }
1555
+
1556
+ const resultMesh = csgVersion === 'CSG2'
1557
+ ? csgShape.toMesh(meshName, scene, { centerMesh: false, rebuildNormals: true })
1558
+ : csgShape.toMesh(meshName, null, scene);
1559
+
1560
+ // CSG2 with rebuildNormals: true produces correct normals
1561
+ // No flipFaces needed
1562
+
1563
+ // Cleanup
1564
+ holes.forEach(h => h.dispose());
1565
+ shape.dispose();
1566
+
1567
+ return resultMesh;
1568
+ }
1569
+
1570
+ /**
1571
+ * MeshWriter Curve Extensions
1572
+ * Optimized Path2 curve methods for better performance
1573
+ */
1574
+
1575
+
1576
+ /**
1577
+ * Extended Path2 interface with MeshWriter curve methods
1578
+ * @typedef {Object} Path2Extensions
1579
+ * @property {(redX: number, redY: number, blueX: number, blueY: number) => Path2} addQuadraticCurveTo
1580
+ * @property {(redX: number, redY: number, greenX: number, greenY: number, blueX: number, blueY: number) => Path2} addCubicCurveTo
1581
+ */
1582
+
1583
+ // Optimized segment count for curves
1584
+ // Native Babylon 6+ uses 36 segments which causes slowdown
1585
+ // MeshWriter uses 6 for better performance
1586
+ const curveSampleSize = 6;
1587
+
1588
+ /**
1589
+ * Install optimized curve methods on Path2 prototype
1590
+ * Must be called after Babylon.js is loaded
1591
+ */
1592
+ function installCurveExtensions() {
1593
+ if (!math_path.Path2 || !math_path.Path2.prototype) {
1594
+ return;
1595
+ }
1596
+
1597
+ /** @type {any} */
1598
+ const proto = math_path.Path2.prototype;
1599
+
1600
+ // Quadratic Bezier with optimized segment count
1601
+ proto.addQuadraticCurveTo = function(redX, redY, blueX, blueY) {
1602
+ const points = this.getPoints();
1603
+ const lastPoint = points[points.length - 1];
1604
+ const origin = new math_vector.Vector3(lastPoint.x, lastPoint.y, 0);
1605
+ const control = new math_vector.Vector3(redX, redY, 0);
1606
+ const destination = new math_vector.Vector3(blueX, blueY, 0);
1607
+
1608
+ const curve = math_path.Curve3.CreateQuadraticBezier(origin, control, destination, curveSampleSize);
1609
+ const curvePoints = curve.getPoints();
1610
+
1611
+ for (let i = 1; i < curvePoints.length; i++) {
1612
+ this.addLineTo(curvePoints[i].x, curvePoints[i].y);
1613
+ }
1614
+ return this; // Return this for method chaining
1615
+ };
1616
+
1617
+ // Cubic Bezier with optimized segment count
1618
+ proto.addCubicCurveTo = function(redX, redY, greenX, greenY, blueX, blueY) {
1619
+ const points = this.getPoints();
1620
+ const lastPoint = points[points.length - 1];
1621
+ const origin = new math_vector.Vector3(lastPoint.x, lastPoint.y, 0);
1622
+ const control1 = new math_vector.Vector3(redX, redY, 0);
1623
+ const control2 = new math_vector.Vector3(greenX, greenY, 0);
1624
+ const destination = new math_vector.Vector3(blueX, blueY, 0);
1625
+
1626
+ const nbPoints = Math.floor(0.3 + curveSampleSize * 1.5);
1627
+ const curve = math_path.Curve3.CreateCubicBezier(origin, control1, control2, destination, nbPoints);
1628
+ const curvePoints = curve.getPoints();
1629
+
1630
+ for (let i = 1; i < curvePoints.length; i++) {
1631
+ this.addLineTo(curvePoints[i].x, curvePoints[i].y);
1632
+ }
1633
+ return this; // Return this for method chaining
1634
+ };
1635
+ }
1636
+
1637
+ /**
1638
+ * MeshWriter Core Class
1639
+ * Main MeshWriter implementation for 3D text rendering in Babylon.js
1640
+ */
1641
+
1642
+
1643
+ /** @typedef {import('@babylonjs/core/scene').Scene} Scene */
1644
+
1645
+ /**
1646
+ * @typedef {Object} MeshWriterPreferences
1647
+ * @property {string} [defaultFont] - Default font family
1648
+ * @property {number} [scale=1] - Scale factor
1649
+ * @property {string} [meshOrigin="letterCenter"] - "letterCenter" or "fontOrigin"
1650
+ * @property {boolean} [debug=false] - Enable debug logging
1651
+ * @property {Object} [babylon] - Babylon.js namespace object with CSG classes (for ES module builds)
1652
+ */
1653
+
1654
+ // Constants
1655
+ const defaultColor = "#808080";
1656
+ const defaultOpac = 1;
1657
+
1658
+ /**
1659
+ * Create a MeshWriter factory configured for a scene
1660
+ * @param {Scene} scene - Babylon.js scene
1661
+ * @param {MeshWriterPreferences} [preferences={}] - Configuration options
1662
+ * @returns {Function} - MeshWriter constructor
1663
+ */
1664
+ function createMeshWriter(scene, preferences = {}) {
1665
+ // Install curve extensions for Path2
1666
+ installCurveExtensions();
1667
+
1668
+ const defaultFont = isFontRegistered(preferences.defaultFont)
1669
+ ? preferences.defaultFont
1670
+ : (isFontRegistered("Helvetica") ? "Helvetica" : "HelveticaNeue-Medium");
1671
+ const meshOrigin = preferences.meshOrigin === "fontOrigin"
1672
+ ? "fontOrigin"
1673
+ : "letterCenter";
1674
+ const scale = isNumber(preferences.scale) ? preferences.scale : 1;
1675
+ isBoolean(preferences.debug) ? preferences.debug : false;
1676
+
1677
+ /**
1678
+ * MeshWriter constructor - creates 3D text
1679
+ * @param {string} lttrs - Text to render
1680
+ * @param {Object} opt - Options
1681
+ */
1682
+ function MeshWriter(lttrs, opt) {
1683
+ const options = isObject(opt) ? opt : {};
1684
+ const position = setOption(options, "position", isObject, {});
1685
+ const colors = setOption(options, "colors", isObject, {});
1686
+ const fontFamily = setOption(options, "font-family", isSupportedFont, defaultFont);
1687
+ const anchor = setOption(options, "anchor", isSupportedAnchor, "left");
1688
+ const rawheight = setOption(options, "letter-height", isPositiveNumber, 100);
1689
+ const rawThickness = setOption(options, "letter-thickness", isPositiveNumber, 1);
1690
+ const basicColor = setOption(options, "color", isString, defaultColor);
1691
+ const opac = setOption(options, "alpha", isAmplitude, defaultOpac);
1692
+ const y = setOption(position, "y", isNumber, 0);
1693
+ const x = setOption(position, "x", isNumber, 0);
1694
+ const z = setOption(position, "z", isNumber, 0);
1695
+ const diffuse = setOption(colors, "diffuse", isString, "#404040"); // Dark gray - lets emissive show
1696
+ const specular = setOption(colors, "specular", isString, "#000000");
1697
+ const ambient = setOption(colors, "ambient", isString, "#202020"); // Very dark - minimal ambient response
1698
+ const emissive = setOption(colors, "emissive", isString, basicColor);
1699
+ const emissiveOnly = setOption(options, "emissive-only", isBoolean, false);
1700
+ const fogEnabled = setOption(options, "fog-enabled", isBoolean, true);
1701
+ const letterSpacingRaw = setOption(options, "letter-spacing", isNumber, 0);
1702
+ const wordSpacingRaw = setOption(options, "word-spacing", isNumber, 0);
1703
+ const fontSpec = getFont(fontFamily);
1704
+ const letterScale = round(scale * rawheight / naturalLetterHeight);
1705
+ const thickness = round(scale * rawThickness);
1706
+ const letters = isString(lttrs) ? lttrs : "";
1707
+ // Scale spacing values to match letter scale
1708
+ const letterSpacing = letterSpacingRaw * scale;
1709
+ const wordSpacing = wordSpacingRaw * scale;
1710
+
1711
+ // Create material
1712
+ const material = makeMaterial(scene, letters, emissive, ambient, specular, diffuse, opac, emissiveOnly, fogEnabled);
1713
+
1714
+ // Create letter meshes
1715
+ const meshesAndBoxes = constructLetterPolygons(
1716
+ letters, fontSpec, 0, 0, 0, letterScale, thickness, material, meshOrigin, scene,
1717
+ { letterSpacing, wordSpacing }
1718
+ );
1719
+ meshesAndBoxes[0];
1720
+ const lettersBoxes = meshesAndBoxes[1];
1721
+ const lettersOrigins = meshesAndBoxes[2];
1722
+ const xWidth = meshesAndBoxes.xWidth;
1723
+
1724
+ // Convert to SPS
1725
+ const combo = makeSPS(scene, meshesAndBoxes, material);
1726
+ const sps = combo[0];
1727
+ const mesh = combo[1];
1728
+ const faceCombo = combo.face || [];
1729
+ const faceSps = faceCombo[0];
1730
+ const faceMesh = faceCombo[1];
1731
+ let faceMaterial;
1732
+
1733
+ if (faceMesh) {
1734
+ faceMaterial = makeFaceMaterial(scene, letters, emissive, opac, fogEnabled);
1735
+ faceMesh.material = faceMaterial;
1736
+ if (mesh) {
1737
+ faceMesh.parent = mesh;
1738
+ faceMesh.layerMask = mesh.layerMask;
1739
+ faceMesh.renderingGroupId = mesh.renderingGroupId;
1740
+ }
1741
+ // Tiny offset to prevent z-fighting without leaving a visible gap
1742
+ // rotation.x=-PI/2 maps: +Y → -Z (toward camera), -Y → +Z (away)
1743
+ // We want face IN FRONT of rim, so use POSITIVE Y
1744
+ faceMesh.position.y = 0.001;
1745
+ faceMesh.isPickable = false;
1746
+ }
1747
+
1748
+ // Position mesh based on anchor
1749
+ const offsetX = anchor === "right"
1750
+ ? (0 - xWidth)
1751
+ : (anchor === "center" ? (0 - xWidth / 2) : 0);
1752
+
1753
+ if (mesh) {
1754
+ mesh.position.x = scale * x + offsetX;
1755
+ mesh.position.y = scale * y;
1756
+ mesh.position.z = scale * z;
1757
+ }
1758
+
1759
+ // Instance methods
1760
+ let color = basicColor;
1761
+ this.getSPS = () => sps;
1762
+ this.getMesh = () => mesh;
1763
+ this.getMaterial = () => material;
1764
+ this.getFaceMesh = () => faceMesh;
1765
+ this.getFaceMaterial = () => faceMaterial;
1766
+ this.getFaceSPS = () => faceSps;
1767
+ this.getOffsetX = () => offsetX;
1768
+ this.getLettersBoxes = () => lettersBoxes;
1769
+ this.getLettersOrigins = () => lettersOrigins;
1770
+ this.color = c => isString(c) ? color = c : color;
1771
+ this.alpha = o => isAmplitude(o) ? opac : opac;
1772
+ // Track disposed state to prevent double-disposal
1773
+ let _disposed = false;
1774
+
1775
+ this.clearall = function() {
1776
+ // Mark as disposed - getters will return null after this
1777
+ _disposed = true;
1778
+ };
1779
+
1780
+ this.isDisposed = function() {
1781
+ return _disposed;
1782
+ };
1783
+ }
1784
+
1785
+ // Prototype methods
1786
+ MeshWriter.prototype.setColor = function(color) {
1787
+ const material = this.getMaterial();
1788
+ if (material && isString(color)) {
1789
+ const next = rgb2Color3(this.color(color));
1790
+ material.emissiveColor = next;
1791
+ const faceMaterial = this.getFaceMaterial && this.getFaceMaterial();
1792
+ if (faceMaterial) {
1793
+ faceMaterial.emissiveColor = next;
1794
+ }
1795
+ }
1796
+ };
1797
+
1798
+ MeshWriter.prototype.setAlpha = function(alpha) {
1799
+ const material = this.getMaterial();
1800
+ if (material && isAmplitude(alpha)) {
1801
+ const next = this.alpha(alpha);
1802
+ material.alpha = next;
1803
+ const faceMaterial = this.getFaceMaterial && this.getFaceMaterial();
1804
+ if (faceMaterial) {
1805
+ faceMaterial.alpha = next;
1806
+ }
1807
+ }
1808
+ };
1809
+
1810
+ MeshWriter.prototype.overrideAlpha = function(alpha) {
1811
+ const material = this.getMaterial();
1812
+ if (material && isAmplitude(alpha)) {
1813
+ material.alpha = alpha;
1814
+ const faceMaterial = this.getFaceMaterial && this.getFaceMaterial();
1815
+ if (faceMaterial) {
1816
+ faceMaterial.alpha = alpha;
1817
+ }
1818
+ }
1819
+ };
1820
+
1821
+ MeshWriter.prototype.resetAlpha = function() {
1822
+ const material = this.getMaterial();
1823
+ const alpha = this.alpha();
1824
+ if (material) {
1825
+ material.alpha = alpha;
1826
+ }
1827
+ const faceMaterial = this.getFaceMaterial && this.getFaceMaterial();
1828
+ if (faceMaterial) {
1829
+ faceMaterial.alpha = alpha;
1830
+ }
1831
+ };
1832
+
1833
+ MeshWriter.prototype.getLetterCenter = function(ix) {
1834
+ return new math_vector.Vector2(0, 0);
1835
+ };
1836
+
1837
+ MeshWriter.prototype.dispose = function() {
1838
+ // Prevent double-disposal
1839
+ if (this.isDisposed && this.isDisposed()) {
1840
+ return;
1841
+ }
1842
+
1843
+ // Dispose TextFogPlugin before materials to break circular references
1844
+ const material = this.getMaterial();
1845
+ if (material) {
1846
+ if (material._textFogPlugin && typeof material._textFogPlugin.dispose === 'function') {
1847
+ material._textFogPlugin.dispose();
1848
+ }
1849
+ material.dispose();
1850
+ }
1851
+
1852
+ const faceMaterial = this.getFaceMaterial && this.getFaceMaterial();
1853
+ if (faceMaterial) {
1854
+ if (faceMaterial._textFogPlugin && typeof faceMaterial._textFogPlugin.dispose === 'function') {
1855
+ faceMaterial._textFogPlugin.dispose();
1856
+ }
1857
+ faceMaterial.dispose();
1858
+ }
1859
+
1860
+ // Dispose SolidParticleSystem (which also disposes its mesh)
1861
+ const sps = this.getSPS();
1862
+ if (sps) {
1863
+ sps.dispose();
1864
+ }
1865
+
1866
+ // Dispose face mesh (merged mesh, not SPS-based)
1867
+ const faceMesh = this.getFaceMesh && this.getFaceMesh();
1868
+ if (faceMesh && typeof faceMesh.dispose === 'function') {
1869
+ faceMesh.dispose();
1870
+ }
1871
+
1872
+ // Mark as disposed
1873
+ this.clearall();
1874
+ };
1875
+
1876
+ return MeshWriter;
1877
+
1878
+ // Helper functions
1879
+ function isSupportedFont(ff) {
1880
+ return isFontRegistered(ff);
1881
+ }
1882
+
1883
+ function isSupportedAnchor(a) {
1884
+ return a === "left" || a === "right" || a === "center";
1885
+ }
1886
+ }
1887
+
1888
+ /**
1889
+ * Async factory for Babylon 8+ with CSG2
1890
+ * Handles CSG2 initialization automatically
1891
+ * @param {Scene} scene - Babylon.js scene
1892
+ * @param {MeshWriterPreferences} [preferences={}] - Configuration options
1893
+ * @returns {Promise<Function>} - MeshWriter constructor
1894
+ */
1895
+ async function createMeshWriterAsync(scene, preferences = {}) {
1896
+ // Initialize CSG module with Babylon methods
1897
+ if (preferences.babylon) {
1898
+ initCSGModule(preferences.babylon);
1899
+ } else {
1900
+ // Check for BABYLON global (UMD bundle usage)
1901
+ /** @type {any} */
1902
+ const globalBabylon = typeof globalThis !== 'undefined' ? globalThis.BABYLON : undefined;
1903
+ if (globalBabylon) {
1904
+ initCSGModule(globalBabylon);
1905
+ }
1906
+ }
1907
+
1908
+ // Initialize CSG2 if needed
1909
+ if (getCSGVersion() === 'CSG2' && !isCSGReady()) {
1910
+ await initializeCSG2();
1911
+ }
1912
+
1913
+ return createMeshWriter(scene, preferences);
1914
+ }
1915
+
1916
+ /**
1917
+ * MeshWriter Baked Font Loader
1918
+ * Loads pre-baked FontSpec JSON files at runtime.
1919
+ * Zero dependencies - just fetch and use.
1920
+ */
1921
+
1922
+ /**
1923
+ * Load a pre-baked font from a JSON file
1924
+ * @param {string} url - URL to the baked FontSpec JSON file
1925
+ * @returns {Promise<object>} - FontSpec object ready for use with MeshWriter
1926
+ *
1927
+ * @example
1928
+ * const fontSpec = await loadBakedFont('/fonts/baked/atkinson-hyperlegible-next-400.json');
1929
+ * registerFont('Atkinson400', fontSpec);
1930
+ */
1931
+ async function loadBakedFont(url) {
1932
+ const response = await fetch(url);
1933
+
1934
+ if (!response.ok) {
1935
+ throw new Error(`MeshWriter: Failed to load baked font from ${url} (HTTP ${response.status})`);
1936
+ }
1937
+
1938
+ const fontSpec = await response.json();
1939
+
1940
+ // Validate it looks like a FontSpec
1941
+ if (typeof fontSpec !== 'object' || fontSpec === null) {
1942
+ throw new Error(`MeshWriter: Invalid baked font data from ${url}`);
1943
+ }
1944
+
1945
+ return fontSpec;
1946
+ }
1947
+
1948
+ /**
1949
+ * Load multiple pre-baked weights from a manifest
1950
+ * @param {string} manifestUrl - URL to the manifest.json file
1951
+ * @param {number[]} [weights] - Specific weights to load (loads all if omitted)
1952
+ * @returns {Promise<Map<number, object>>} - Map of weight -> FontSpec
1953
+ *
1954
+ * @example
1955
+ * const fonts = await loadBakedFontsFromManifest('/fonts/baked/manifest.json', [400, 450]);
1956
+ * const fontSpec = fonts.get(400);
1957
+ */
1958
+ async function loadBakedFontsFromManifest(manifestUrl, weights) {
1959
+ const response = await fetch(manifestUrl);
1960
+
1961
+ if (!response.ok) {
1962
+ throw new Error(`MeshWriter: Failed to load manifest from ${manifestUrl} (HTTP ${response.status})`);
1963
+ }
1964
+
1965
+ const manifest = await response.json();
1966
+ const baseUrl = manifestUrl.substring(0, manifestUrl.lastIndexOf('/') + 1);
1967
+
1968
+ // Determine which weights to load
1969
+ const weightsToLoad = weights
1970
+ ? weights.filter(w => manifest.weights.includes(w))
1971
+ : manifest.weights;
1972
+
1973
+ if (weights && weightsToLoad.length !== weights.length) {
1974
+ const missing = weights.filter(w => !manifest.weights.includes(w));
1975
+ console.warn(`MeshWriter: Requested weights not available: ${missing.join(', ')}`);
1976
+ console.warn(`MeshWriter: Available weights: ${manifest.weights.join(', ')}`);
1977
+ }
1978
+
1979
+ // Load all requested weights in parallel
1980
+ const results = await Promise.all(
1981
+ weightsToLoad.map(async (weight) => {
1982
+ const idx = manifest.weights.indexOf(weight);
1983
+ const file = manifest.files[idx];
1984
+ const fontSpec = await loadBakedFont(baseUrl + file);
1985
+ return { weight, fontSpec };
1986
+ })
1987
+ );
1988
+
1989
+ // Build map
1990
+ const fontMap = new Map();
1991
+ for (const { weight, fontSpec } of results) {
1992
+ fontMap.set(weight, fontSpec);
1993
+ }
1994
+
1995
+ return fontMap;
1996
+ }
1997
+
1998
+ /**
1999
+ * Find the nearest available weight from a set of baked weights
2000
+ * @param {number} targetWeight - Desired weight
2001
+ * @param {number[]} availableWeights - Array of available weights
2002
+ * @returns {number} - Nearest available weight
2003
+ *
2004
+ * @example
2005
+ * const nearest = findNearestWeight(425, [400, 450, 500]);
2006
+ * // Returns 450 (closest to 425)
2007
+ */
2008
+ function findNearestWeight(targetWeight, availableWeights) {
2009
+ if (!availableWeights || availableWeights.length === 0) {
2010
+ throw new Error('MeshWriter: No available weights provided');
2011
+ }
2012
+
2013
+ let nearest = availableWeights[0];
2014
+ let minDiff = Math.abs(targetWeight - nearest);
2015
+
2016
+ for (const weight of availableWeights) {
2017
+ const diff = Math.abs(targetWeight - weight);
2018
+ if (diff < minDiff) {
2019
+ minDiff = diff;
2020
+ nearest = weight;
2021
+ }
2022
+ }
2023
+
2024
+ return nearest;
2025
+ }
2026
+
2027
+ /**
2028
+ * Get manifest info without loading fonts
2029
+ * @param {string} manifestUrl - URL to the manifest.json file
2030
+ * @returns {Promise<object>} - Manifest object with fontName, weights, etc.
2031
+ */
2032
+ async function getBakedFontManifest(manifestUrl) {
2033
+ const response = await fetch(manifestUrl);
2034
+
2035
+ if (!response.ok) {
2036
+ throw new Error(`MeshWriter: Failed to load manifest from ${manifestUrl} (HTTP ${response.status})`);
2037
+ }
2038
+
2039
+ return response.json();
2040
+ }
2041
+
2042
+ /**
2043
+ * Color Contrast Utilities for WCAG Compliance
2044
+ * Provides color manipulation for dyslexia accessibility
2045
+ */
2046
+
2047
+ // ============================================
2048
+ // Color Conversion Utilities
2049
+ // ============================================
2050
+
2051
+ /**
2052
+ * Convert hex color string to RGB object (0-1 range)
2053
+ * @param {string} hex - Hex color string (e.g., "#FF0000" or "FF0000")
2054
+ * @returns {{r: number, g: number, b: number}}
2055
+ */
2056
+ function hexToRgb(hex) {
2057
+ hex = hex.replace("#", "");
2058
+ return {
2059
+ r: parseInt(hex.substring(0, 2), 16) / 255,
2060
+ g: parseInt(hex.substring(2, 4), 16) / 255,
2061
+ b: parseInt(hex.substring(4, 6), 16) / 255
2062
+ };
2063
+ }
2064
+
2065
+ /**
2066
+ * Convert RGB object (0-1 range) to hex color string
2067
+ * @param {{r: number, g: number, b: number}} rgb
2068
+ * @returns {string}
2069
+ */
2070
+ function rgbToHex(rgb) {
2071
+ var r = Math.round(Math.max(0, Math.min(1, rgb.r)) * 255);
2072
+ var g = Math.round(Math.max(0, Math.min(1, rgb.g)) * 255);
2073
+ var b = Math.round(Math.max(0, Math.min(1, rgb.b)) * 255);
2074
+ return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase();
2075
+ }
2076
+
2077
+ /**
2078
+ * Convert RGB to HSL
2079
+ * @param {number} r - Red (0-1)
2080
+ * @param {number} g - Green (0-1)
2081
+ * @param {number} b - Blue (0-1)
2082
+ * @returns {{h: number, s: number, l: number}} - h in degrees (0-360), s and l in 0-1
2083
+ */
2084
+ function rgbToHsl(r, g, b) {
2085
+ var max = Math.max(r, g, b);
2086
+ var min = Math.min(r, g, b);
2087
+ var l = (max + min) / 2;
2088
+ var h, s;
2089
+
2090
+ if (max === min) {
2091
+ h = s = 0; // achromatic
2092
+ } else {
2093
+ var d = max - min;
2094
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
2095
+
2096
+ switch (max) {
2097
+ case r:
2098
+ h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
2099
+ break;
2100
+ case g:
2101
+ h = ((b - r) / d + 2) / 6;
2102
+ break;
2103
+ case b:
2104
+ h = ((r - g) / d + 4) / 6;
2105
+ break;
2106
+ }
2107
+ h *= 360;
2108
+ }
2109
+
2110
+ return { h: h, s: s, l: l };
2111
+ }
2112
+
2113
+ /**
2114
+ * Convert HSL to RGB
2115
+ * @param {number} h - Hue in degrees (0-360)
2116
+ * @param {number} s - Saturation (0-1)
2117
+ * @param {number} l - Lightness (0-1)
2118
+ * @returns {{r: number, g: number, b: number}}
2119
+ */
2120
+ function hslToRgb(h, s, l) {
2121
+ var r, g, b;
2122
+
2123
+ if (s === 0) {
2124
+ r = g = b = l; // achromatic
2125
+ } else {
2126
+ function hue2rgb(p, q, t) {
2127
+ if (t < 0) t += 1;
2128
+ if (t > 1) t -= 1;
2129
+ if (t < 1 / 6) return p + (q - p) * 6 * t;
2130
+ if (t < 1 / 2) return q;
2131
+ if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
2132
+ return p;
2133
+ }
2134
+
2135
+ var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
2136
+ var p = 2 * l - q;
2137
+ var hNorm = h / 360;
2138
+
2139
+ r = hue2rgb(p, q, hNorm + 1 / 3);
2140
+ g = hue2rgb(p, q, hNorm);
2141
+ b = hue2rgb(p, q, hNorm - 1 / 3);
2142
+ }
2143
+
2144
+ return { r: r, g: g, b: b };
2145
+ }
2146
+
2147
+ // ============================================
2148
+ // WCAG Luminance Calculations
2149
+ // ============================================
2150
+
2151
+ /**
2152
+ * Linearize an sRGB channel value
2153
+ * @param {number} c - Channel value (0-1)
2154
+ * @returns {number} - Linearized value
2155
+ */
2156
+ function linearize(c) {
2157
+ return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
2158
+ }
2159
+
2160
+ /**
2161
+ * Calculate relative luminance per WCAG 2.1
2162
+ * @param {number} r - Red (0-1)
2163
+ * @param {number} g - Green (0-1)
2164
+ * @param {number} b - Blue (0-1)
2165
+ * @returns {number} - Relative luminance (0-1)
2166
+ */
2167
+ function relativeLuminance(r, g, b) {
2168
+ var rLin = linearize(r);
2169
+ var gLin = linearize(g);
2170
+ var bLin = linearize(b);
2171
+ return 0.2126 * rLin + 0.7152 * gLin + 0.0722 * bLin;
2172
+ }
2173
+
2174
+ /**
2175
+ * Calculate WCAG contrast ratio between two luminance values
2176
+ * @param {number} L1 - Luminance of first color (0-1)
2177
+ * @param {number} L2 - Luminance of second color (0-1)
2178
+ * @returns {number} - Contrast ratio (1-21)
2179
+ */
2180
+ function contrastRatio(L1, L2) {
2181
+ var lighter = Math.max(L1, L2);
2182
+ var darker = Math.min(L1, L2);
2183
+ return (lighter + 0.05) / (darker + 0.05);
2184
+ }
2185
+
2186
+ // ============================================
2187
+ // Luminance Adjustment
2188
+ // ============================================
2189
+
2190
+ /**
2191
+ * Adjust color to target luminance while preserving hue
2192
+ * Uses binary search in HSL space
2193
+ * Desaturates significantly at low lightness for better visual contrast
2194
+ * @param {{r: number, g: number, b: number}} rgb
2195
+ * @param {number} targetLum - Target relative luminance (0-1)
2196
+ * @returns {{r: number, g: number, b: number}}
2197
+ */
2198
+ function adjustToLuminance(rgb, targetLum) {
2199
+ var hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
2200
+
2201
+ // Binary search to find lightness that achieves target luminance
2202
+ var minL = 0;
2203
+ var maxL = 1;
2204
+ var iterations = 20;
2205
+ var finalL;
2206
+
2207
+ for (var i = 0; i < iterations; i++) {
2208
+ var midL = (minL + maxL) / 2;
2209
+ var testRgb = hslToRgb(hsl.h, hsl.s, midL);
2210
+ var testLum = relativeLuminance(testRgb.r, testRgb.g, testRgb.b);
2211
+
2212
+ if (testLum < targetLum) {
2213
+ minL = midL;
2214
+ } else {
2215
+ maxL = midL;
2216
+ }
2217
+ }
2218
+
2219
+ finalL = (minL + maxL) / 2;
2220
+
2221
+ // Desaturate significantly at low lightness for better visual contrast
2222
+ // Dark saturated colors (e.g., dark yellow) don't look distinct enough
2223
+ // Scale saturation based on lightness: below 0.3 lightness, reduce saturation
2224
+ var finalS = hsl.s;
2225
+ if (finalL < 0.3) {
2226
+ // Linear ramp: at L=0.3, keep 100% saturation; at L=0, keep 20% saturation
2227
+ var saturationScale = 0.2 + (finalL / 0.3) * 0.8;
2228
+ finalS = hsl.s * saturationScale;
2229
+ }
2230
+
2231
+ return hslToRgb(hsl.h, finalS, finalL);
2232
+ }
2233
+
2234
+ // ============================================
2235
+ // Auto-Derive Edge Colors
2236
+ // ============================================
2237
+
2238
+ /**
2239
+ * Auto-derive edge colors (diffuse/ambient) from emissive color
2240
+ * Creates high-contrast edges for text legibility
2241
+ *
2242
+ * INVERTED APPROACH: Since emissive adds to all surfaces equally,
2243
+ * we flip the strategy - put bright color in diffuse (shows on lit surfaces)
2244
+ * and dark color in emissive (base for unlit surfaces).
2245
+ * Returns modified emissive along with diffuse/ambient.
2246
+ *
2247
+ * @param {string} emissiveHex - Hex color string for desired face color
2248
+ * @param {number} [targetContrast=4.5] - Target WCAG contrast ratio
2249
+ * @returns {{diffuse: string, ambient: string, emissive: string}}
2250
+ */
2251
+ function deriveEdgeColors(emissiveHex, targetContrast) {
2252
+ targetContrast = targetContrast || 4.5;
2253
+
2254
+ var rgb = hexToRgb(emissiveHex);
2255
+ var faceLum = relativeLuminance(rgb.r, rgb.g, rgb.b);
2256
+
2257
+ // Calculate target luminance for dark areas to achieve contrast
2258
+ var darkLum;
2259
+ if (faceLum > 0.5) {
2260
+ // Bright face needs dark edges
2261
+ darkLum = (faceLum + 0.05) / targetContrast - 0.05;
2262
+ darkLum = Math.max(darkLum, 0.0);
2263
+ } else {
2264
+ // Dark face needs light edges (invert the logic)
2265
+ darkLum = targetContrast * (faceLum + 0.05) - 0.05;
2266
+ darkLum = Math.min(darkLum, 1.0);
2267
+ }
2268
+
2269
+ // Handle edge cases
2270
+ if (faceLum > 0.95) {
2271
+ darkLum = Math.min(0.1, darkLum);
2272
+ } else if (faceLum < 0.05) {
2273
+ darkLum = Math.max(0.5, darkLum);
2274
+ }
2275
+
2276
+ // Generate dark color (desaturated at low lightness)
2277
+ var darkRgb = adjustToLuminance(rgb, darkLum);
2278
+
2279
+ // INVERTED APPROACH:
2280
+ // - diffuse = bright (the user's desired face color) - shows on lit surfaces
2281
+ // - emissive = dark - base color for all surfaces (unlit areas show this)
2282
+ // - ambient = very dark - shadowed areas
2283
+ var ambientLum = darkLum * 0.5;
2284
+ var ambientRgb = adjustToLuminance(rgb, Math.max(0, ambientLum));
2285
+
2286
+ return {
2287
+ diffuse: emissiveHex, // Bright color for lit surfaces
2288
+ ambient: rgbToHex(ambientRgb), // Very dark for shadows
2289
+ emissive: rgbToHex(darkRgb) // Dark base for unlit areas
2290
+ };
2291
+ }
2292
+
2293
+ // ============================================
2294
+ // High-Contrast Adjustment Algorithm
2295
+ // ============================================
2296
+
2297
+ /**
2298
+ * Adjust color by a factor (lightness change with optional hue shift)
2299
+ * @param {{r: number, g: number, b: number}} rgb
2300
+ * @param {number} factor - Adjustment factor (-1 to 1, negative = darken)
2301
+ * @param {boolean} allowHueShift
2302
+ * @returns {{r: number, g: number, b: number}}
2303
+ */
2304
+ function adjustColorByFactor(rgb, factor, allowHueShift) {
2305
+ var hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
2306
+
2307
+ // Adjust lightness
2308
+ var newL = hsl.l + factor;
2309
+ newL = Math.max(0, Math.min(1, newL));
2310
+
2311
+ // Optionally shift hue for extreme adjustments
2312
+ var newH = hsl.h;
2313
+ if (allowHueShift && Math.abs(factor) > 0.2) {
2314
+ // Slight hue shift toward yellow (high luminance) or blue (low luminance)
2315
+ var hueTarget = factor > 0 ? 60 : 240;
2316
+ newH = hsl.h + (hueTarget - hsl.h) * Math.abs(factor) * 0.1;
2317
+ newH = ((newH % 360) + 360) % 360;
2318
+ }
2319
+
2320
+ // Reduce saturation at extreme lightness for natural look
2321
+ var newS = hsl.s;
2322
+ if (newL > 0.9 || newL < 0.1) {
2323
+ newS *= 0.5;
2324
+ }
2325
+
2326
+ return hslToRgb(newH, newS, newL);
2327
+ }
2328
+
2329
+ /**
2330
+ * Oscillate edge colors to find best contrast
2331
+ * @param {{r: number, g: number, b: number}} emissive
2332
+ * @param {{r: number, g: number, b: number}} diffuse
2333
+ * @param {{r: number, g: number, b: number}} ambient
2334
+ * @param {object} options
2335
+ * @returns {{diffuse: object, ambient: object, achieved: number}}
2336
+ */
2337
+ function oscillateEdges(emissive, diffuse, ambient, options) {
2338
+ var emissiveLum = relativeLuminance(emissive.r, emissive.g, emissive.b);
2339
+ var diffuseLum = relativeLuminance(diffuse.r, diffuse.g, diffuse.b);
2340
+ var currentContrast = contrastRatio(emissiveLum, diffuseLum);
2341
+
2342
+ var bestResult = { diffuse: diffuse, ambient: ambient, achieved: currentContrast };
2343
+
2344
+ // Determine direction: edges should go opposite to emissive luminance
2345
+ var direction = emissiveLum > 0.5 ? -1 : 1;
2346
+
2347
+ var steps = 10;
2348
+ for (var i = 1; i <= steps; i++) {
2349
+ var factor = (i / steps) * options.range;
2350
+
2351
+ // Primary direction
2352
+ var testDiffuse = adjustColorByFactor(diffuse, direction * factor, options.allowHueShift);
2353
+ var testAmbient = adjustColorByFactor(ambient, direction * factor * 0.8, options.allowHueShift);
2354
+
2355
+ var testLum = relativeLuminance(testDiffuse.r, testDiffuse.g, testDiffuse.b);
2356
+ var contrast = contrastRatio(emissiveLum, testLum);
2357
+
2358
+ if (contrast > bestResult.achieved) {
2359
+ bestResult = { diffuse: testDiffuse, ambient: testAmbient, achieved: contrast };
2360
+ }
2361
+
2362
+ if (contrast >= options.targetContrast) break;
2363
+
2364
+ // Try opposite direction for edge cases
2365
+ testDiffuse = adjustColorByFactor(diffuse, -direction * factor, options.allowHueShift);
2366
+ testAmbient = adjustColorByFactor(ambient, -direction * factor * 0.8, options.allowHueShift);
2367
+
2368
+ testLum = relativeLuminance(testDiffuse.r, testDiffuse.g, testDiffuse.b);
2369
+ contrast = contrastRatio(emissiveLum, testLum);
2370
+
2371
+ if (contrast > bestResult.achieved) {
2372
+ bestResult = { diffuse: testDiffuse, ambient: testAmbient, achieved: contrast };
2373
+ }
2374
+ }
2375
+
2376
+ return bestResult;
2377
+ }
2378
+
2379
+ /**
2380
+ * Oscillate face color to find better contrast
2381
+ * @param {{r: number, g: number, b: number}} emissive
2382
+ * @param {{r: number, g: number, b: number}} diffuse
2383
+ * @param {object} options
2384
+ * @returns {{emissive: object, achieved: number}}
2385
+ */
2386
+ function oscillateFace(emissive, diffuse, options) {
2387
+ var diffuseLum = relativeLuminance(diffuse.r, diffuse.g, diffuse.b);
2388
+ var emissiveLum = relativeLuminance(emissive.r, emissive.g, emissive.b);
2389
+ var currentContrast = contrastRatio(emissiveLum, diffuseLum);
2390
+
2391
+ var bestResult = { emissive: emissive, achieved: currentContrast };
2392
+
2393
+ // Face should move opposite to edges
2394
+ var direction = diffuseLum > 0.5 ? -1 : 1;
2395
+
2396
+ var steps = 10;
2397
+ for (var i = 1; i <= steps; i++) {
2398
+ var factor = (i / steps) * options.range;
2399
+
2400
+ var testEmissive = adjustColorByFactor(emissive, direction * factor, options.allowHueShift);
2401
+ var testLum = relativeLuminance(testEmissive.r, testEmissive.g, testEmissive.b);
2402
+ var contrast = contrastRatio(testLum, diffuseLum);
2403
+
2404
+ if (contrast > bestResult.achieved) {
2405
+ bestResult = { emissive: testEmissive, achieved: contrast };
2406
+ }
2407
+
2408
+ if (contrast >= options.targetContrast) break;
2409
+ }
2410
+
2411
+ return bestResult;
2412
+ }
2413
+
2414
+ /**
2415
+ * Adjust colors to achieve WCAG contrast while preserving user intent
2416
+ * Priority: prefer edge modifications over face modifications
2417
+ *
2418
+ * @param {object} colors - User-provided colors
2419
+ * @param {string} colors.emissive - Face color (hex)
2420
+ * @param {string} colors.diffuse - Edge lit color (hex)
2421
+ * @param {string} [colors.ambient] - Edge shadow color (hex)
2422
+ * @param {object} [options]
2423
+ * @param {number} [options.targetContrast=4.5] - Target contrast ratio
2424
+ * @param {number} [options.edgeRange=0.4] - Max edge modification (0-1)
2425
+ * @param {number} [options.faceRange=0.1] - Max face modification (0-1)
2426
+ * @param {boolean} [options.allowHueShift=true] - Allow hue modifications
2427
+ * @returns {{emissive: string, diffuse: string, ambient: string, achieved: number}}
2428
+ */
2429
+ function adjustForContrast(colors, options) {
2430
+ options = options || {};
2431
+ var targetContrast = options.targetContrast || 4.5;
2432
+ var edgeRange = options.edgeRange || 0.4;
2433
+ var faceRange = options.faceRange || 0.1;
2434
+ var allowHueShift = options.allowHueShift !== false;
2435
+
2436
+ var emissive = hexToRgb(colors.emissive);
2437
+ var diffuse = hexToRgb(colors.diffuse);
2438
+ var ambient = colors.ambient ? hexToRgb(colors.ambient) : { r: diffuse.r * 0.5, g: diffuse.g * 0.5, b: diffuse.b * 0.5 };
2439
+
2440
+ var emissiveLum = relativeLuminance(emissive.r, emissive.g, emissive.b);
2441
+ var diffuseLum = relativeLuminance(diffuse.r, diffuse.g, diffuse.b);
2442
+ var currentContrast = contrastRatio(emissiveLum, diffuseLum);
2443
+
2444
+ // Already meets target?
2445
+ if (currentContrast >= targetContrast) {
2446
+ return {
2447
+ emissive: colors.emissive,
2448
+ diffuse: colors.diffuse,
2449
+ ambient: colors.ambient || rgbToHex(ambient),
2450
+ achieved: currentContrast
2451
+ };
2452
+ }
2453
+
2454
+ // Phase 1: Try edge modification only
2455
+ var edgeResult = oscillateEdges(emissive, diffuse, ambient, {
2456
+ targetContrast: targetContrast,
2457
+ range: edgeRange,
2458
+ allowHueShift: allowHueShift
2459
+ });
2460
+
2461
+ if (edgeResult.achieved >= targetContrast) {
2462
+ return {
2463
+ emissive: colors.emissive,
2464
+ diffuse: rgbToHex(edgeResult.diffuse),
2465
+ ambient: rgbToHex(edgeResult.ambient),
2466
+ achieved: edgeResult.achieved
2467
+ };
2468
+ }
2469
+
2470
+ // Phase 2: Try face modification
2471
+ var faceResult = oscillateFace(emissive, edgeResult.diffuse, {
2472
+ targetContrast: targetContrast,
2473
+ range: faceRange,
2474
+ allowHueShift: allowHueShift
2475
+ });
2476
+
2477
+ if (faceResult.achieved >= targetContrast) {
2478
+ return {
2479
+ emissive: rgbToHex(faceResult.emissive),
2480
+ diffuse: rgbToHex(edgeResult.diffuse),
2481
+ ambient: rgbToHex(edgeResult.ambient),
2482
+ achieved: faceResult.achieved
2483
+ };
2484
+ }
2485
+
2486
+ // Phase 3: Oscillate both until convergence
2487
+ var maxIterations = 5;
2488
+ var currentEmissive = faceResult.emissive;
2489
+ var currentDiffuse = edgeResult.diffuse;
2490
+ var currentAmbient = edgeResult.ambient;
2491
+ var bestAchieved = faceResult.achieved;
2492
+
2493
+ for (var iter = 0; iter < maxIterations; iter++) {
2494
+ // Try more edge adjustment
2495
+ var newEdgeResult = oscillateEdges(currentEmissive, currentDiffuse, currentAmbient, {
2496
+ targetContrast: targetContrast,
2497
+ range: edgeRange * 0.5,
2498
+ allowHueShift: allowHueShift
2499
+ });
2500
+
2501
+ if (newEdgeResult.achieved >= targetContrast) {
2502
+ return {
2503
+ emissive: rgbToHex(currentEmissive),
2504
+ diffuse: rgbToHex(newEdgeResult.diffuse),
2505
+ ambient: rgbToHex(newEdgeResult.ambient),
2506
+ achieved: newEdgeResult.achieved
2507
+ };
2508
+ }
2509
+
2510
+ // Try more face adjustment
2511
+ var newFaceResult = oscillateFace(currentEmissive, newEdgeResult.diffuse, {
2512
+ targetContrast: targetContrast,
2513
+ range: faceRange * 0.5,
2514
+ allowHueShift: allowHueShift
2515
+ });
2516
+
2517
+ if (newFaceResult.achieved >= targetContrast) {
2518
+ return {
2519
+ emissive: rgbToHex(newFaceResult.emissive),
2520
+ diffuse: rgbToHex(newEdgeResult.diffuse),
2521
+ ambient: rgbToHex(newEdgeResult.ambient),
2522
+ achieved: newFaceResult.achieved
2523
+ };
2524
+ }
2525
+
2526
+ // Update for next iteration
2527
+ if (newFaceResult.achieved > bestAchieved) {
2528
+ bestAchieved = newFaceResult.achieved;
2529
+ currentEmissive = newFaceResult.emissive;
2530
+ currentDiffuse = newEdgeResult.diffuse;
2531
+ currentAmbient = newEdgeResult.ambient;
2532
+ } else {
2533
+ // No improvement, stop
2534
+ break;
2535
+ }
2536
+ }
2537
+
2538
+ // Return best result even if target not achieved
2539
+ return {
2540
+ emissive: rgbToHex(currentEmissive),
2541
+ diffuse: rgbToHex(currentDiffuse),
2542
+ ambient: rgbToHex(currentAmbient),
2543
+ achieved: bestAchieved
2544
+ };
2545
+ }
2546
+
2547
+ // ============================================
2548
+ // Constants
2549
+ // ============================================
2550
+
2551
+ var CONTRAST_LEVELS = {
2552
+ AA_NORMAL: 4.5,
2553
+ AA_LARGE: 3.0,
2554
+ AAA_NORMAL: 7.0,
2555
+ AAA_LARGE: 4.5
2556
+ };
2557
+
2558
+ /**
2559
+ * MeshWriter - 3D Text Rendering for Babylon.js
2560
+ *
2561
+ * @example
2562
+ * // ES Module usage
2563
+ * import { MeshWriter, registerFont } from 'meshwriter';
2564
+ * import helvetica from 'meshwriter/fonts/helvetica';
2565
+ *
2566
+ * registerFont('Helvetica', helvetica);
2567
+ * const Writer = await MeshWriter.createAsync(scene);
2568
+ * const text = new Writer("Hello World", { "letter-height": 20 });
2569
+ */
2570
+
2571
+
2572
+ const MeshWriter = {
2573
+ /**
2574
+ * Create MeshWriter async (recommended for Babylon 8+)
2575
+ */
2576
+ createAsync: createMeshWriterAsync,
2577
+
2578
+ /**
2579
+ * Create MeshWriter sync (for Babylon < 8 or when CSG2 is pre-initialized)
2580
+ */
2581
+ create: createMeshWriter,
2582
+
2583
+ // Static CSG methods
2584
+ isReady: isCSGReady,
2585
+ getCSGVersion,
2586
+ setCSGInitializer,
2587
+ setCSGReadyCheck,
2588
+ onCSGReady,
2589
+ markCSGReady: markCSGInitialized,
2590
+ initCSGModule,
2591
+
2592
+ // Font methods
2593
+ registerFont,
2594
+ registerFontAliases,
2595
+ getFont,
2596
+ isFontRegistered,
2597
+
2598
+ // Encoding utilities
2599
+ codeList,
2600
+ decodeList,
2601
+
2602
+ // Baked font methods (lightweight alternative to variable fonts)
2603
+ loadBakedFont,
2604
+ loadBakedFontsFromManifest,
2605
+ findNearestWeight,
2606
+ getBakedFontManifest
2607
+ };
2608
+
2609
+ exports.CONTRAST_LEVELS = CONTRAST_LEVELS;
2610
+ exports.MeshWriter = MeshWriter;
2611
+ exports.TextFogPlugin = TextFogPlugin;
2612
+ exports.adjustForContrast = adjustForContrast;
2613
+ exports.clearFonts = clearFonts;
2614
+ exports.codeList = codeList;
2615
+ exports.contrastRatio = contrastRatio;
2616
+ exports.createMeshWriter = createMeshWriter;
2617
+ exports.createMeshWriterAsync = createMeshWriterAsync;
2618
+ exports.decodeFontData = decodeList;
2619
+ exports.decodeList = decodeList;
2620
+ exports.default = MeshWriter;
2621
+ exports.deriveEdgeColors = deriveEdgeColors;
2622
+ exports.encodeFontData = codeList;
2623
+ exports.findNearestWeight = findNearestWeight;
2624
+ exports.getBakedFontManifest = getBakedFontManifest;
2625
+ exports.getCSGVersion = getCSGVersion;
2626
+ exports.getFont = getFont;
2627
+ exports.getRegisteredFonts = getRegisteredFonts;
2628
+ exports.hexToRgb = hexToRgb;
2629
+ exports.hslToRgb = hslToRgb;
2630
+ exports.initCSGModule = initCSGModule;
2631
+ exports.isCSGReady = isCSGReady;
2632
+ exports.isFontRegistered = isFontRegistered;
2633
+ exports.loadBakedFont = loadBakedFont;
2634
+ exports.loadBakedFontsFromManifest = loadBakedFontsFromManifest;
2635
+ exports.markCSGReady = markCSGInitialized;
2636
+ exports.onCSGReady = onCSGReady;
2637
+ exports.registerFont = registerFont;
2638
+ exports.registerFontAliases = registerFontAliases;
2639
+ exports.relativeLuminance = relativeLuminance;
2640
+ exports.rgbToHex = rgbToHex;
2641
+ exports.rgbToHsl = rgbToHsl;
2642
+ exports.setCSGInitializer = setCSGInitializer;
2643
+ exports.setCSGReadyCheck = setCSGReadyCheck;
2644
+ exports.unregisterFont = unregisterFont;
2645
+ //# sourceMappingURL=meshwriter.cjs.js.map