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