@forgeax/engine-fbx 0.0.0-dev.8d955ade1c79

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 (79) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +188 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/__tests__/blendshape-import.integration.test.d.ts +2 -0
  5. package/dist/__tests__/blendshape-import.integration.test.d.ts.map +1 -0
  6. package/dist/__tests__/blendshape-import.unit.test.d.ts +2 -0
  7. package/dist/__tests__/blendshape-import.unit.test.d.ts.map +1 -0
  8. package/dist/__tests__/fbx-importer.test.d.ts +2 -0
  9. package/dist/__tests__/fbx-importer.test.d.ts.map +1 -0
  10. package/dist/__tests__/fbx-local-artifacts.test.d.ts +2 -0
  11. package/dist/__tests__/fbx-local-artifacts.test.d.ts.map +1 -0
  12. package/dist/__tests__/index.test.d.ts +2 -0
  13. package/dist/__tests__/index.test.d.ts.map +1 -0
  14. package/dist/__tests__/mesh-material-slots.unit.test.d.ts +2 -0
  15. package/dist/__tests__/mesh-material-slots.unit.test.d.ts.map +1 -0
  16. package/dist/__tests__/parse-mesh-multi-uv.test.d.ts +2 -0
  17. package/dist/__tests__/parse-mesh-multi-uv.test.d.ts.map +1 -0
  18. package/dist/__tests__/pick-e2e.integration.test.d.ts +2 -0
  19. package/dist/__tests__/pick-e2e.integration.test.d.ts.map +1 -0
  20. package/dist/__tests__/resolve-texture-path.unit.test.d.ts +2 -0
  21. package/dist/__tests__/resolve-texture-path.unit.test.d.ts.map +1 -0
  22. package/dist/errors.d.ts +50 -0
  23. package/dist/errors.d.ts.map +1 -0
  24. package/dist/fbx-importer.d.ts +17 -0
  25. package/dist/fbx-importer.d.ts.map +1 -0
  26. package/dist/index.d.ts +62 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.mjs +1499 -0
  29. package/dist/index.mjs.map +1 -0
  30. package/dist/parse-animation-clip.d.ts +35 -0
  31. package/dist/parse-animation-clip.d.ts.map +1 -0
  32. package/dist/parse-material.d.ts +24 -0
  33. package/dist/parse-material.d.ts.map +1 -0
  34. package/dist/parse-mesh.d.ts +21 -0
  35. package/dist/parse-mesh.d.ts.map +1 -0
  36. package/dist/parse-scene.d.ts +28 -0
  37. package/dist/parse-scene.d.ts.map +1 -0
  38. package/dist/parse-skeleton.d.ts +15 -0
  39. package/dist/parse-skeleton.d.ts.map +1 -0
  40. package/dist/parse-skin.d.ts +20 -0
  41. package/dist/parse-skin.d.ts.map +1 -0
  42. package/dist/parse-texture.d.ts +11 -0
  43. package/dist/parse-texture.d.ts.map +1 -0
  44. package/dist/resolve-texture-path.d.ts +25 -0
  45. package/dist/resolve-texture-path.d.ts.map +1 -0
  46. package/dist/to-asset-pack.d.ts +31 -0
  47. package/dist/to-asset-pack.d.ts.map +1 -0
  48. package/package.json +74 -0
  49. package/pkg/fbx-wasm.mjs +2 -0
  50. package/pkg/fbx-wasm.wasm +0 -0
  51. package/scripts/build-wasm.mjs +85 -0
  52. package/scripts/content-key.mjs +88 -0
  53. package/scripts/ensure-wasm.mjs +28 -0
  54. package/scripts/fetch-ufbx.mjs +42 -0
  55. package/scripts/fetch-wasm.mjs +91 -0
  56. package/scripts/parity-diff.mjs +127 -0
  57. package/scripts/test-wasm.mjs +91 -0
  58. package/src/__tests__/blendshape-import.integration.test.ts +90 -0
  59. package/src/__tests__/blendshape-import.unit.test.ts +83 -0
  60. package/src/__tests__/fbx-importer.test.ts +36 -0
  61. package/src/__tests__/fbx-local-artifacts.test.ts +39 -0
  62. package/src/__tests__/index.test.ts +168 -0
  63. package/src/__tests__/mesh-material-slots.unit.test.ts +125 -0
  64. package/src/__tests__/parse-mesh-multi-uv.test.ts +187 -0
  65. package/src/__tests__/pick-e2e.integration.test.ts +247 -0
  66. package/src/__tests__/resolve-texture-path.unit.test.ts +50 -0
  67. package/src/errors.ts +112 -0
  68. package/src/fbx-importer.ts +146 -0
  69. package/src/index.ts +215 -0
  70. package/src/native/bridge.c +942 -0
  71. package/src/parse-animation-clip.ts +271 -0
  72. package/src/parse-material.ts +92 -0
  73. package/src/parse-mesh.ts +223 -0
  74. package/src/parse-scene.ts +75 -0
  75. package/src/parse-skeleton.ts +46 -0
  76. package/src/parse-skin.ts +71 -0
  77. package/src/parse-texture.ts +22 -0
  78. package/src/resolve-texture-path.ts +152 -0
  79. package/src/to-asset-pack.ts +742 -0
package/dist/index.mjs ADDED
@@ -0,0 +1,1499 @@
1
+ import { reconcileMeshMaterialSlotTopology, ImportError, IMPORT_ERROR_HINTS, resolveMeshMaterialSlotDefaultGuid, err, ok } from '@forgeax/engine-types';
2
+ import { deriveAnimationTargetId } from '@forgeax/engine-animation/target-id';
3
+ import '@forgeax/engine-pack';
4
+ import { box3 } from '@forgeax/engine-math';
5
+ import { AssetGuid } from '@forgeax/engine-pack/guid';
6
+ import { MESH_BIN_HEADER_V4_BYTES, writeMeshBinHeader } from '@forgeax/engine-pack/mesh-bin-contract';
7
+
8
+ // src/errors.ts
9
+ var fbxErrorPolicy = {
10
+ "fbx-mesh-type-unsupported": {
11
+ expected: "all meshes in the file are polygon (triangles/quads), not NURBS or patch surfaces",
12
+ hint: "NURBS and patch surfaces are not supported; convert to polygon mesh in a DCC tool before import"
13
+ },
14
+ "fbx-animation-target-invalid": {
15
+ expected: "an acyclic hierarchy where every animation channel uniquely matches one named Scene node and stable target ID",
16
+ hint: "name every node, keep the hierarchy acyclic, and export unique full animation target paths"
17
+ }
18
+ };
19
+ var FBX_ERROR_HINTS = Object.fromEntries(
20
+ Object.entries(fbxErrorPolicy).map(([code, policy]) => [code, policy.hint])
21
+ );
22
+ function fbxErr(code, detail) {
23
+ return {
24
+ code,
25
+ expected: fbxErrorPolicy[code].expected,
26
+ hint: fbxErrorPolicy[code].hint,
27
+ detail
28
+ };
29
+ }
30
+
31
+ // src/parse-scene.ts
32
+ function buildFbxNodePaths(nodes) {
33
+ const parents = /* @__PURE__ */ new Map();
34
+ for (let index = 0; index < nodes.length; index++) {
35
+ for (const child of nodes[index]?.children ?? []) parents.set(child, index);
36
+ }
37
+ return nodes.map((_, index) => {
38
+ const reversed = [];
39
+ const visited = /* @__PURE__ */ new Set();
40
+ let current = index;
41
+ while (current !== void 0) {
42
+ if (visited.has(current)) {
43
+ return { ok: false, reason: "hierarchy-cycle", nodeIndex: current };
44
+ }
45
+ visited.add(current);
46
+ const name = nodes[current]?.name;
47
+ if (name === void 0 || name.length === 0) {
48
+ return { ok: false, reason: "name-missing", nodeIndex: current };
49
+ }
50
+ if (name.includes("/")) {
51
+ return { ok: false, reason: "path-invalid", nodeIndex: current };
52
+ }
53
+ reversed.push(name);
54
+ current = parents.get(current);
55
+ }
56
+ return { ok: true, value: reversed.reverse() };
57
+ });
58
+ }
59
+ function parseScene(rawNodes) {
60
+ const nodes = rawNodes.nodes ?? [];
61
+ const entities = nodes.map((n) => ({
62
+ name: n.name,
63
+ transform: {
64
+ translation: n.transform.translation,
65
+ rotation: n.transform.rotation,
66
+ scale: n.transform.scale
67
+ },
68
+ meshIndex: n.meshIndex >= 0 ? n.meshIndex : null,
69
+ children: n.children
70
+ }));
71
+ return {
72
+ entities,
73
+ rootEntityIndex: 0
74
+ };
75
+ }
76
+
77
+ // src/parse-animation-clip.ts
78
+ var DEFAULT_FPS = 30;
79
+ function sampleStrided(keyTimes, keyValues, stride, t, out, rotation) {
80
+ const n = keyTimes.length;
81
+ if (n === 0) {
82
+ for (let c = 0; c < stride; c++) out[c] = rotation && c === 3 ? 1 : 0;
83
+ return;
84
+ }
85
+ const first = keyTimes[0];
86
+ const lastIdx = n - 1;
87
+ const last = keyTimes[lastIdx];
88
+ if (t <= first) {
89
+ for (let c = 0; c < stride; c++) out[c] = keyValues[c] ?? 0;
90
+ return;
91
+ }
92
+ if (t >= last) {
93
+ for (let c = 0; c < stride; c++) out[c] = keyValues[lastIdx * stride + c] ?? 0;
94
+ return;
95
+ }
96
+ let i = 1;
97
+ while (i < n && keyTimes[i] < t) i++;
98
+ const t0 = keyTimes[i - 1];
99
+ const t1 = keyTimes[i];
100
+ const frac = t1 > t0 ? (t - t0) / (t1 - t0) : 0;
101
+ const b0 = (i - 1) * stride;
102
+ const b1 = i * stride;
103
+ for (let c = 0; c < stride; c++) {
104
+ const v0 = keyValues[b0 + c] ?? 0;
105
+ const v1 = keyValues[b1 + c] ?? 0;
106
+ out[c] = v0 + (v1 - v0) * frac;
107
+ }
108
+ }
109
+ function buildSampler(ch, duration, fps) {
110
+ const frameInterval = 1 / fps;
111
+ const frameCount = Math.floor(duration * fps) + 1;
112
+ const input = new Float32Array(frameCount);
113
+ for (let f = 0; f < frameCount; f++) {
114
+ input[f] = f * frameInterval;
115
+ }
116
+ const stride = ch.property === "rotation" ? 4 : ch.property === "weights" ? ch.weightCount ?? 0 : 3;
117
+ if (ch.property === "weights" && (!Number.isInteger(stride) || stride < 1 || stride > 8)) {
118
+ throw new Error("fbx-morph-invalid: weight channel count must be an integer in [1, 8]");
119
+ }
120
+ const output = new Float32Array(frameCount * stride);
121
+ const keyTimes = ch.keyTimes ?? [];
122
+ const keyValues = ch.keyValues ?? [];
123
+ if (keyTimes.length > 0 && keyValues.length !== keyTimes.length * stride) {
124
+ throw new Error("fbx-morph-invalid: animation key value width does not match key times");
125
+ }
126
+ const tmp = new Array(stride);
127
+ for (let f = 0; f < frameCount; f++) {
128
+ const t = input[f];
129
+ sampleStrided(keyTimes, keyValues, stride, t, tmp, ch.property === "rotation");
130
+ const base = f * stride;
131
+ if (ch.property === "rotation") {
132
+ const len = Math.hypot(tmp[0] ?? 0, tmp[1] ?? 0, tmp[2] ?? 0, tmp[3] ?? 0);
133
+ const inv = len > 0 ? 1 / len : 0;
134
+ output[base + 0] = (tmp[0] ?? 0) * inv;
135
+ output[base + 1] = (tmp[1] ?? 0) * inv;
136
+ output[base + 2] = (tmp[2] ?? 0) * inv;
137
+ output[base + 3] = len > 0 ? (tmp[3] ?? 0) * inv : 1;
138
+ } else {
139
+ for (let c = 0; c < stride; c++) output[base + c] = tmp[c] ?? 0;
140
+ }
141
+ }
142
+ return { input, output, interpolation: "LINEAR" };
143
+ }
144
+ function buildChannel(ch, duration, fps) {
145
+ return {
146
+ targetId: deriveAnimationTargetId(ch.targetNode.split("/")),
147
+ property: ch.property,
148
+ sampler: buildSampler(ch, duration, fps)
149
+ };
150
+ }
151
+ function resolveAnimationTargetIds(nodes, clips) {
152
+ const nodesByPath = /* @__PURE__ */ new Map();
153
+ const nodePaths = buildFbxNodePaths(nodes);
154
+ const cycle = nodePaths.find((path) => !path.ok && path.reason === "hierarchy-cycle");
155
+ if (cycle && !cycle.ok) {
156
+ return err(
157
+ fbxErr("fbx-animation-target-invalid", {
158
+ reason: "hierarchy-cycle",
159
+ nodeIndex: cycle.nodeIndex
160
+ })
161
+ );
162
+ }
163
+ const hasAmbiguousName = nodePaths.some((path) => !path.ok);
164
+ for (let index = 0; index < nodePaths.length; index++) {
165
+ const result = nodePaths[index];
166
+ if (result === void 0 || !result.ok) continue;
167
+ const path = result.value.join("/");
168
+ const matches = nodesByPath.get(path) ?? [];
169
+ matches.push(index);
170
+ nodesByPath.set(path, matches);
171
+ }
172
+ const pathById = /* @__PURE__ */ new Map();
173
+ for (let clipIndex = 0; clipIndex < clips.length; clipIndex++) {
174
+ const clip = clips[clipIndex];
175
+ if (clip === void 0) continue;
176
+ for (let channelIndex = 0; channelIndex < clip.channels.length; channelIndex++) {
177
+ const channel = clip.channels[channelIndex];
178
+ if (channel === void 0) continue;
179
+ const segments = channel.targetNode.split("/");
180
+ if (segments.some((segment) => segment.length === 0)) {
181
+ return err(
182
+ fbxErr("fbx-animation-target-invalid", {
183
+ reason: "path-invalid",
184
+ clipIndex,
185
+ channelIndex,
186
+ targetNode: channel.targetNode
187
+ })
188
+ );
189
+ }
190
+ const matches = nodesByPath.get(channel.targetNode);
191
+ if (matches === void 0) {
192
+ return err(
193
+ fbxErr("fbx-animation-target-invalid", {
194
+ reason: hasAmbiguousName ? "name-missing" : "path-not-found",
195
+ clipIndex,
196
+ channelIndex,
197
+ targetNode: channel.targetNode
198
+ })
199
+ );
200
+ }
201
+ if (matches.length !== 1) {
202
+ return err(
203
+ fbxErr("fbx-animation-target-invalid", {
204
+ reason: "path-duplicate",
205
+ clipIndex,
206
+ channelIndex,
207
+ targetNode: channel.targetNode
208
+ })
209
+ );
210
+ }
211
+ const id = deriveAnimationTargetId(segments);
212
+ const previous = pathById.get(id);
213
+ if (previous !== void 0 && previous !== channel.targetNode) {
214
+ return err(
215
+ fbxErr("fbx-animation-target-invalid", {
216
+ reason: "id-collision",
217
+ clipIndex,
218
+ channelIndex,
219
+ targetNode: channel.targetNode
220
+ })
221
+ );
222
+ }
223
+ pathById.set(id, channel.targetNode);
224
+ }
225
+ }
226
+ return ok(void 0);
227
+ }
228
+ function parseAnimationClips(doc, fps = DEFAULT_FPS) {
229
+ const clips = doc.clips;
230
+ if (!clips || clips.length === 0) return [];
231
+ return clips.map((clip) => {
232
+ return {
233
+ ...clip.name !== void 0 && { name: clip.name },
234
+ duration: clip.duration,
235
+ channels: clip.channels.map((ch) => buildChannel(ch, clip.duration, fps))
236
+ };
237
+ });
238
+ }
239
+
240
+ // src/parse-material.ts
241
+ function clamp(v, lo, hi) {
242
+ return v < lo ? lo : v > hi ? hi : v;
243
+ }
244
+ function phongRoughness(shininess, maxGloss = 100) {
245
+ return clamp(1 - Math.sqrt(shininess / maxGloss), 0, 1);
246
+ }
247
+ function parseMaterial(raw, sourceIndex) {
248
+ switch (raw.kind) {
249
+ case "stingray-pbs": {
250
+ const sp = raw.stingrayProps ?? {};
251
+ return {
252
+ ...raw.name !== void 0 && { name: raw.name },
253
+ baseColorFactor: [
254
+ sp.baseColor?.[0] ?? 0.5,
255
+ sp.baseColor?.[1] ?? 0.5,
256
+ sp.baseColor?.[2] ?? 0.5,
257
+ 1
258
+ ],
259
+ metallicFactor: sp.metallic ?? 0,
260
+ roughnessFactor: sp.roughness ?? 0.5
261
+ };
262
+ }
263
+ case "phong": {
264
+ const d = raw.diffuse ?? raw.baseColor ?? [0.5, 0.5, 0.5];
265
+ const gloss = raw.shininess ?? 100;
266
+ return {
267
+ ...raw.name !== void 0 && { name: raw.name },
268
+ baseColorFactor: [d[0] ?? 0.5, d[1] ?? 0.5, d[2] ?? 0.5, 1],
269
+ metallicFactor: 0,
270
+ roughnessFactor: phongRoughness(gloss)
271
+ };
272
+ }
273
+ case "lambert": {
274
+ const d = raw.diffuse ?? raw.baseColor ?? [0.5, 0.5, 0.5];
275
+ return {
276
+ ...raw.name !== void 0 && { name: raw.name },
277
+ baseColorFactor: [d[0] ?? 0.5, d[1] ?? 0.5, d[2] ?? 0.5, 1],
278
+ metallicFactor: 0,
279
+ roughnessFactor: 0.5
280
+ // lambert has no specular → default roughness
281
+ };
282
+ }
283
+ default: {
284
+ return {
285
+ baseColorFactor: [0.5, 0.5, 0.5, 1],
286
+ metallicFactor: 0,
287
+ roughnessFactor: 0.5
288
+ };
289
+ }
290
+ }
291
+ }
292
+
293
+ // src/parse-mesh.ts
294
+ var MORPH_MAX_TARGETS = 8;
295
+ var MORPH_MAX_ATTRIBUTES = 8;
296
+ var TEXCOORD_PREFIX = "TEXCOORD_";
297
+ function isCornerMapped(attrLen, components, posCount, idxCount) {
298
+ const elems = attrLen / components;
299
+ return idxCount > 0 && elems === idxCount && elems !== posCount;
300
+ }
301
+ function parseMesh(raw, sourceIndex) {
302
+ const rawIndices = raw.indices && raw.indices.length > 0 ? raw.indices : void 0;
303
+ const posCount = raw.vertices.length / 3;
304
+ const idxCount = rawIndices?.length ?? 0;
305
+ const normal = raw.attributes.NORMAL;
306
+ const rawMorphTargets = raw.morphTargets ?? [];
307
+ const morphAttributeCount = rawMorphTargets.reduce(
308
+ (count, target) => count + (target.position ? 1 : 0) + (target.normal ? 1 : 0) + (target.tangent ? 1 : 0),
309
+ 0
310
+ );
311
+ if (rawMorphTargets.length > MORPH_MAX_TARGETS || morphAttributeCount > MORPH_MAX_ATTRIBUTES) {
312
+ throw new Error("fbx-morph-invalid: morph target or attribute limit exceeded");
313
+ }
314
+ if (rawMorphTargets.some(
315
+ (target) => target.position === void 0 && target.normal === void 0 && target.tangent === void 0
316
+ )) {
317
+ throw new Error("fbx-morph-invalid: every target must contain POSITION, NORMAL, or TANGENT");
318
+ }
319
+ const morphTargets = rawMorphTargets.map((target) => {
320
+ const out = {};
321
+ for (const [key, values, components] of [
322
+ ["position", target.position, 3],
323
+ ["normal", target.normal, 3],
324
+ ["tangent", target.tangent, 4]
325
+ ]) {
326
+ if (values === void 0) continue;
327
+ if (values.length !== posCount * components) {
328
+ throw new Error(`fbx-morph-invalid: ${key} length does not match vertex count`);
329
+ }
330
+ out[key] = new Float32Array(values);
331
+ }
332
+ return out;
333
+ });
334
+ if (raw.morphWeights !== void 0 && raw.morphWeights.length !== morphTargets.length) {
335
+ throw new Error("fbx-morph-invalid: default weight count does not match target count");
336
+ }
337
+ const normalCorner = normal !== void 0 && isCornerMapped(normal.length, 3, posCount, idxCount);
338
+ const uvCornerMap = /* @__PURE__ */ new Map();
339
+ for (const key of Object.keys(raw.attributes)) {
340
+ if (key.startsWith(TEXCOORD_PREFIX)) {
341
+ const arr = raw.attributes[key];
342
+ if (arr !== void 0 && isCornerMapped(arr.length, 2, posCount, idxCount)) {
343
+ uvCornerMap.set(key, true);
344
+ }
345
+ }
346
+ }
347
+ const hasAnyCorner = normalCorner || uvCornerMap.size > 0;
348
+ if (rawIndices !== void 0 && hasAnyCorner) {
349
+ const expandedPos = new Float32Array(idxCount * 3);
350
+ const expandedNormal = normal !== void 0 ? new Float32Array(idxCount * 3) : void 0;
351
+ const expandedUvs = /* @__PURE__ */ new Map();
352
+ for (const key of Object.keys(raw.attributes)) {
353
+ if (key.startsWith(TEXCOORD_PREFIX)) {
354
+ expandedUvs.set(key, new Float32Array(idxCount * 2));
355
+ }
356
+ }
357
+ const expandedMorphTargets = morphTargets.map((target) => {
358
+ const expanded = {};
359
+ for (const [key, values, components] of [
360
+ ["position", target.position, 3],
361
+ ["normal", target.normal, 3],
362
+ ["tangent", target.tangent, 4]
363
+ ]) {
364
+ if (values === void 0) continue;
365
+ const output = new Float32Array(idxCount * components);
366
+ for (let corner = 0; corner < idxCount; corner++) {
367
+ const vi = rawIndices[corner] ?? 0;
368
+ for (let component = 0; component < components; component++) {
369
+ output[corner * components + component] = values[vi * components + component] ?? 0;
370
+ }
371
+ }
372
+ expanded[key] = output;
373
+ }
374
+ return expanded;
375
+ });
376
+ for (let corner = 0; corner < idxCount; corner++) {
377
+ const vi = rawIndices[corner] ?? 0;
378
+ expandedPos[corner * 3 + 0] = raw.vertices[vi * 3 + 0] ?? 0;
379
+ expandedPos[corner * 3 + 1] = raw.vertices[vi * 3 + 1] ?? 0;
380
+ expandedPos[corner * 3 + 2] = raw.vertices[vi * 3 + 2] ?? 0;
381
+ if (normal !== void 0 && expandedNormal !== void 0) {
382
+ const src = normalCorner ? corner : vi;
383
+ expandedNormal[corner * 3 + 0] = normal[src * 3 + 0] ?? 0;
384
+ expandedNormal[corner * 3 + 1] = normal[src * 3 + 1] ?? 0;
385
+ expandedNormal[corner * 3 + 2] = normal[src * 3 + 2] ?? 0;
386
+ }
387
+ for (const [key, expanded] of expandedUvs) {
388
+ const srcArr = raw.attributes[key];
389
+ if (srcArr !== void 0) {
390
+ const src = uvCornerMap.get(key) ? corner : vi;
391
+ expanded[corner * 2 + 0] = srcArr[src * 2 + 0] ?? 0;
392
+ expanded[corner * 2 + 1] = srcArr[src * 2 + 1] ?? 0;
393
+ }
394
+ }
395
+ }
396
+ const identity = new Uint32Array(idxCount);
397
+ for (let i = 0; i < idxCount; i++) identity[i] = i;
398
+ const expandedAttrs = {};
399
+ if (expandedNormal !== void 0) expandedAttrs.NORMAL = expandedNormal;
400
+ for (const [key, val] of expandedUvs) {
401
+ expandedAttrs[key] = val;
402
+ }
403
+ return {
404
+ ...raw.name !== void 0 ? { name: raw.name } : {},
405
+ vertices: expandedPos,
406
+ indices: identity,
407
+ attributes: expandedAttrs,
408
+ submeshes: [
409
+ {
410
+ topology: "triangle-list",
411
+ indexOffset: 0,
412
+ indexCount: idxCount,
413
+ vertexCount: idxCount,
414
+ materialIndex: raw.materialIndex >= 0 ? raw.materialIndex : null
415
+ }
416
+ ],
417
+ sourceIndex,
418
+ ...expandedMorphTargets.length === 0 ? {} : { morphTargets: expandedMorphTargets },
419
+ ...raw.morphWeights === void 0 ? {} : { morphWeights: new Float32Array(raw.morphWeights) }
420
+ };
421
+ }
422
+ const vertices = new Float32Array(raw.vertices);
423
+ const indices = rawIndices ? new Uint16Array(rawIndices) : void 0;
424
+ const attributes = {};
425
+ for (const [key, arr] of Object.entries(raw.attributes)) {
426
+ attributes[key] = new Float32Array(arr);
427
+ }
428
+ const indexCount = indices?.length ?? 0;
429
+ const submeshes = [
430
+ {
431
+ topology: "triangle-list",
432
+ indexOffset: 0,
433
+ indexCount,
434
+ vertexCount: vertices.length / 3,
435
+ materialIndex: raw.materialIndex >= 0 ? raw.materialIndex : null
436
+ }
437
+ ];
438
+ return {
439
+ ...raw.name !== void 0 ? { name: raw.name } : {},
440
+ vertices,
441
+ ...indices ? { indices } : {},
442
+ attributes,
443
+ submeshes,
444
+ sourceIndex,
445
+ ...morphTargets.length === 0 ? {} : { morphTargets },
446
+ ...raw.morphWeights === void 0 ? {} : { morphWeights: new Float32Array(raw.morphWeights) }
447
+ };
448
+ }
449
+
450
+ // src/parse-skeleton.ts
451
+ function parseSkeleton(doc) {
452
+ const skeletons = doc.skeletons;
453
+ if (!skeletons || skeletons.length === 0) {
454
+ return { jointCount: 0, inverseBindMatrices: new Float32Array(0), jointPaths: [] };
455
+ }
456
+ const skel = skeletons[0];
457
+ if (!skel) {
458
+ return { jointCount: 0, inverseBindMatrices: new Float32Array(0), jointPaths: [] };
459
+ }
460
+ return {
461
+ jointCount: skel.jointCount,
462
+ inverseBindMatrices: new Float32Array(skel.inverseBindMatrices),
463
+ jointPaths: [...skel.jointPaths]
464
+ };
465
+ }
466
+
467
+ // src/parse-skin.ts
468
+ function bridgeSkeletonGuid(_jointPaths) {
469
+ return "fbx-skeleton-000000000000";
470
+ }
471
+ function toInfluence(raw) {
472
+ const ji = new Uint16Array(4);
473
+ const jw = new Float32Array(4);
474
+ for (let i = 0; i < 4; i++) {
475
+ ji[i] = raw.jointIndices[i] ?? 0;
476
+ jw[i] = raw.jointWeights[i] ?? 0;
477
+ }
478
+ return { jointIndices: ji, jointWeights: jw };
479
+ }
480
+ function parseSkin(doc) {
481
+ const skins = doc.skins;
482
+ if (!skins || skins.length === 0) {
483
+ return { skeletonGuid: "", jointPaths: [], vertexCount: 0, influences: [] };
484
+ }
485
+ const skin = skins[0];
486
+ if (!skin) {
487
+ return { skeletonGuid: "", jointPaths: [], vertexCount: 0, influences: [] };
488
+ }
489
+ return {
490
+ skeletonGuid: bridgeSkeletonGuid(skin.jointPaths),
491
+ jointPaths: [...skin.jointPaths],
492
+ vertexCount: skin.vertexCount,
493
+ influences: skin.influences.map(toInfluence)
494
+ };
495
+ }
496
+
497
+ // src/parse-texture.ts
498
+ function parseTextures(raw) {
499
+ const textures = raw.textures ?? [];
500
+ return textures.map((t) => ({
501
+ name: t.name ?? t.filePath,
502
+ filePath: t.filePath,
503
+ sourceIndex: t.sourceIndex
504
+ }));
505
+ }
506
+ var ATTRIBUTE_FORMAT_MAP = {
507
+ position: "float32x3",
508
+ normal: "float32x3",
509
+ uv: "float32x2",
510
+ tangent: "float32x4",
511
+ skinIndex: "uint16x4",
512
+ skinWeight: "float32x4",
513
+ uv1: "float32x2",
514
+ uv2: "float32x2",
515
+ uv3: "float32x2",
516
+ uv4: "float32x2",
517
+ uv5: "float32x2",
518
+ uv6: "float32x2",
519
+ uv7: "float32x2",
520
+ color: "float32x4"
521
+ };
522
+ var isAttributeKey = (key) => key in ATTRIBUTE_FORMAT_MAP;
523
+ var CANONICAL_KEYS = Object.keys(ATTRIBUTE_FORMAT_MAP).filter(isAttributeKey);
524
+ CANONICAL_KEYS.filter(
525
+ (key) => key === "uv" || key.startsWith("uv")
526
+ );
527
+ function bytesForFormat(format) {
528
+ if (format === "uint16x4" || format === "float32x2") return 8;
529
+ if (format === "float32x3") return 12;
530
+ return 16;
531
+ }
532
+ function fnv1a(input) {
533
+ let hash = 2166136261;
534
+ for (let i = 0; i < input.length; i++) {
535
+ hash ^= input.charCodeAt(i);
536
+ hash = Math.imul(hash, 16777619);
537
+ }
538
+ return `vlp-v1-${(hash >>> 0).toString(16).padStart(8, "0")}`;
539
+ }
540
+ function deriveVertexLayoutProjection(map) {
541
+ const attributes = [];
542
+ let offset = 0;
543
+ let mask = 0;
544
+ for (const key of CANONICAL_KEYS) {
545
+ const value = map[key];
546
+ if (value === void 0) continue;
547
+ const format = ATTRIBUTE_FORMAT_MAP[key];
548
+ const byteLength = bytesForFormat(format);
549
+ attributes.push({
550
+ key,
551
+ shaderLocation: CANONICAL_KEYS.indexOf(key),
552
+ offset,
553
+ format,
554
+ byteLength
555
+ });
556
+ mask |= 1 << CANONICAL_KEYS.indexOf(key);
557
+ offset += byteLength;
558
+ }
559
+ const digestInput = [
560
+ "1",
561
+ String(mask),
562
+ String(offset),
563
+ ...attributes.map(
564
+ (entry) => `${entry.key},${entry.shaderLocation},${entry.offset},${entry.format}`
565
+ )
566
+ ].join("|");
567
+ return Object.freeze({
568
+ schemaVersion: 1,
569
+ attributes: Object.freeze(attributes),
570
+ mask,
571
+ arrayStride: offset,
572
+ digest: fnv1a(digestInput)
573
+ });
574
+ }
575
+ function failure(sourceKey, expected, actual) {
576
+ return {
577
+ code: "mesh-bin-payload-invalid",
578
+ subject: "mesh-bin",
579
+ sourceKey,
580
+ expected,
581
+ actual,
582
+ recovery: "re-cook the source with its Meta sidecar through the build-time importer"
583
+ };
584
+ }
585
+ function asAttributeMap(value) {
586
+ return value ?? {};
587
+ }
588
+ function jsonValue(value) {
589
+ if (value instanceof Float32Array || value instanceof Uint16Array) return Array.from(value);
590
+ if (Array.isArray(value)) return value.map(jsonValue);
591
+ if (value !== null && typeof value === "object") {
592
+ return Object.fromEntries(
593
+ Object.entries(value).map(([key, nested]) => [key, jsonValue(nested)])
594
+ );
595
+ }
596
+ return value;
597
+ }
598
+ function refsMeta(payload, refs) {
599
+ const materialSlots = (payload.materialSlots ?? [{ slotName: "Default" }]).map(
600
+ (slot, slotIndex) => {
601
+ const defaultMaterial = slot.defaultMaterial;
602
+ let defaultMaterialRef;
603
+ if (defaultMaterial !== void 0) {
604
+ const guid = AssetGuid.format(defaultMaterial);
605
+ defaultMaterialRef = refs.findIndex((candidate) => candidate.toLowerCase() === guid);
606
+ if (defaultMaterialRef < 0) {
607
+ throw new Error(
608
+ `material slot ${slotIndex} default material ${guid} is absent from refs`
609
+ );
610
+ }
611
+ }
612
+ return {
613
+ slotName: slot.slotName,
614
+ ...slot.sourceKey === void 0 ? {} : { sourceKey: slot.sourceKey },
615
+ ...defaultMaterialRef === void 0 ? {} : { defaultMaterialRef }
616
+ };
617
+ }
618
+ );
619
+ return {
620
+ submeshes: payload.submeshes === void 0 || payload.submeshes.length === 0 ? [{ indexOffset: 0, indexCount: payload.indices?.length ?? 0, materialSlot: 0 }] : payload.submeshes,
621
+ materialSlots,
622
+ ...payload.aabb === void 0 ? {} : { aabb: jsonValue(payload.aabb) },
623
+ ...payload.morphTargets === void 0 ? {} : { morphTargets: jsonValue(payload.morphTargets) },
624
+ ...payload.morphWeights === void 0 ? {} : { morphWeights: jsonValue(payload.morphWeights) }
625
+ };
626
+ }
627
+ function packMeshBinV4(payload, sourceKey, refs = []) {
628
+ try {
629
+ const vertices = payload.vertices;
630
+ const indices = payload.indices;
631
+ if (!(vertices instanceof Float32Array)) {
632
+ return err(
633
+ failure(sourceKey, "Float32Array interleaved vertices", "vertices is not Float32Array")
634
+ );
635
+ }
636
+ if (indices !== void 0 && !(indices instanceof Uint16Array || indices instanceof Uint32Array)) {
637
+ return err(
638
+ failure(sourceKey, "Uint16Array or Uint32Array indices", "indices has an unsupported type")
639
+ );
640
+ }
641
+ const attributes = asAttributeMap(payload.attributes);
642
+ const projection = deriveVertexLayoutProjection(attributes);
643
+ if (projection.attributes.length === 0 || projection.arrayStride === 0) {
644
+ return err(
645
+ failure(
646
+ sourceKey,
647
+ "a non-empty canonical geometry projection",
648
+ "projection has no attributes"
649
+ )
650
+ );
651
+ }
652
+ const vertexCount = payload.vertexCount ?? vertices.byteLength / projection.arrayStride;
653
+ if (!Number.isSafeInteger(vertexCount) || vertexCount < 0) {
654
+ return err(
655
+ failure(sourceKey, "a non-negative safe vertex cardinality", `vertexCount=${vertexCount}`)
656
+ );
657
+ }
658
+ if (vertices.byteLength !== vertexCount * projection.arrayStride) {
659
+ return err(
660
+ failure(
661
+ sourceKey,
662
+ `vertices.byteLength=${vertexCount * projection.arrayStride}`,
663
+ `vertices.byteLength=${vertices.byteLength}; stride=${projection.arrayStride}`
664
+ )
665
+ );
666
+ }
667
+ for (const attribute of projection.attributes) {
668
+ const value = attributes[attribute.key];
669
+ const components = attribute.byteLength / (attribute.format === "uint16x4" ? 2 : 4);
670
+ if (value === void 0 || !(value instanceof Float32Array) && !(value instanceof Uint16Array) || value.length !== vertexCount * components) {
671
+ return err(
672
+ failure(
673
+ sourceKey,
674
+ `${attribute.key} cardinality=${vertexCount * components}`,
675
+ `${attribute.key} cardinality=${value?.byteLength ?? "missing"}`
676
+ )
677
+ );
678
+ }
679
+ }
680
+ const interleaved = new Uint8Array(vertexCount * projection.arrayStride);
681
+ const interleavedView = new DataView(interleaved.buffer);
682
+ for (const attribute of projection.attributes) {
683
+ const value = attributes[attribute.key];
684
+ if (value === void 0) continue;
685
+ const components = attribute.byteLength / (attribute.format === "uint16x4" ? 2 : 4);
686
+ for (let vertex = 0; vertex < vertexCount; vertex++) {
687
+ for (let component = 0; component < components; component++) {
688
+ const sourceIndex = vertex * components + component;
689
+ const targetOffset = vertex * projection.arrayStride + attribute.offset + component * (attribute.format === "uint16x4" ? 2 : 4);
690
+ if (attribute.format === "uint16x4") {
691
+ interleavedView.setUint16(targetOffset, value[sourceIndex] ?? 0, true);
692
+ } else {
693
+ interleavedView.setFloat32(
694
+ targetOffset,
695
+ value[sourceIndex] ?? 0,
696
+ true
697
+ );
698
+ }
699
+ }
700
+ }
701
+ }
702
+ const indexCount = indices?.length ?? 0;
703
+ const indexWidth = indices === void 0 || indexCount === 0 ? 0 : indices.BYTES_PER_ELEMENT;
704
+ const indexBytes = indexCount * indexWidth;
705
+ if (!Number.isSafeInteger(indexBytes) || indexBytes > 4294967295) {
706
+ return err(failure(sourceKey, "safe index payload byte length", `indexBytes=${indexBytes}`));
707
+ }
708
+ const meta = new TextEncoder().encode(JSON.stringify(refsMeta(payload, refs)));
709
+ const header = {
710
+ version: 4,
711
+ projectionVersion: projection.schemaVersion,
712
+ mask: projection.mask,
713
+ digest: projection.digest,
714
+ stride: projection.arrayStride,
715
+ vertexCount,
716
+ vertexBytes: interleaved.byteLength,
717
+ indexCount,
718
+ indexWidth,
719
+ indexBytes,
720
+ jsonBytes: meta.byteLength
721
+ };
722
+ const total = MESH_BIN_HEADER_V4_BYTES + interleaved.byteLength + indexBytes + meta.byteLength;
723
+ if (!Number.isSafeInteger(total) || total > 4294967295) {
724
+ return err(failure(sourceKey, "safe mesh binary byte length", `total=${total}`));
725
+ }
726
+ const out = new Uint8Array(total);
727
+ writeMeshBinHeader(header, out);
728
+ let offset = MESH_BIN_HEADER_V4_BYTES;
729
+ out.set(interleaved, offset);
730
+ offset += interleaved.byteLength;
731
+ if (indices !== void 0 && indexBytes > 0) {
732
+ out.set(new Uint8Array(indices.buffer, indices.byteOffset, indices.byteLength), offset);
733
+ offset += indexBytes;
734
+ }
735
+ out.set(meta, offset);
736
+ return ok(out);
737
+ } catch (error) {
738
+ return err(
739
+ failure(
740
+ sourceKey,
741
+ "valid canonical mesh payload",
742
+ error instanceof Error ? error.message : String(error)
743
+ )
744
+ );
745
+ }
746
+ }
747
+ function makeGuidResolver(subAssets) {
748
+ const byKey = /* @__PURE__ */ new Map();
749
+ for (const sub of subAssets) byKey.set(`${sub.kind}:${sub.sourceIndex}`, sub.guid);
750
+ return (kind, sourceIndex) => byKey.get(`${kind}:${sourceIndex}`);
751
+ }
752
+ function buildMeshAsset(pod, guid, influences, materialContext = {}) {
753
+ const vc = pod.vertices.length / 3;
754
+ const n = pod.attributes.NORMAL;
755
+ let uvSetCount = 1;
756
+ for (const key of Object.keys(pod.attributes)) {
757
+ if (key.startsWith("TEXCOORD_")) {
758
+ const n2 = Number(key.slice("TEXCOORD_".length));
759
+ if (Number.isFinite(n2) && n2 >= 0 && n2 <= 7) {
760
+ uvSetCount = Math.max(uvSetCount, n2 + 1);
761
+ }
762
+ }
763
+ }
764
+ const u = pod.attributes.TEXCOORD_0;
765
+ const skinned = influences !== void 0 && influences.length === vc && vc > 0;
766
+ const BASE_FLOATS = skinned ? 18 : 12;
767
+ const UV1_OFFSET = skinned ? 18 : 12;
768
+ const FLOATS_PER_VERT = BASE_FLOATS + (uvSetCount - 1) * 2;
769
+ const ib = new Float32Array(vc * FLOATS_PER_VERT);
770
+ const ibU16 = skinned ? new Uint16Array(ib.buffer) : void 0;
771
+ const skinIndexAttr = skinned ? new Uint16Array(vc * 4) : void 0;
772
+ const skinWeightAttr = skinned ? new Float32Array(vc * 4) : void 0;
773
+ for (let i = 0; i < vc; i++) {
774
+ const d = i * FLOATS_PER_VERT;
775
+ const p = i * 3;
776
+ const t = i * 2;
777
+ ib[d + 0] = pod.vertices[p + 0] ?? 0;
778
+ ib[d + 1] = pod.vertices[p + 1] ?? 0;
779
+ ib[d + 2] = pod.vertices[p + 2] ?? 0;
780
+ ib[d + 3] = n?.[p + 0] ?? 0;
781
+ ib[d + 4] = n?.[p + 1] ?? 0;
782
+ ib[d + 5] = n?.[p + 2] ?? 0;
783
+ ib[d + 6] = u?.[t + 0] ?? 0;
784
+ ib[d + 7] = u?.[t + 1] ?? 0;
785
+ ib[d + 8] = 1;
786
+ ib[d + 9] = 0;
787
+ ib[d + 10] = 0;
788
+ ib[d + 11] = 1;
789
+ if (skinned && ibU16 && skinIndexAttr && skinWeightAttr) {
790
+ const inf = influences[i];
791
+ const u16Base = (d + 12) * 2;
792
+ const sd = i * 4;
793
+ for (let k = 0; k < 4; k++) {
794
+ const ji = inf?.jointIndices[k] ?? 0;
795
+ const jw = inf?.jointWeights[k] ?? 0;
796
+ ibU16[u16Base + k] = ji;
797
+ ib[d + 14 + k] = jw;
798
+ skinIndexAttr[sd + k] = ji;
799
+ skinWeightAttr[sd + k] = jw;
800
+ }
801
+ }
802
+ for (let k = 1; k < uvSetCount; k++) {
803
+ const srcKey = `TEXCOORD_${k}`;
804
+ const srcArr = pod.attributes[srcKey];
805
+ const interleavedOffset = UV1_OFFSET + (k - 1) * 2;
806
+ if (srcArr !== void 0) {
807
+ ib[d + interleavedOffset + 0] = srcArr[t + 0] ?? 0;
808
+ ib[d + interleavedOffset + 1] = srcArr[t + 1] ?? 0;
809
+ }
810
+ }
811
+ }
812
+ const extraUvAttrs = {};
813
+ for (let k = 1; k < uvSetCount; k++) {
814
+ const srcKey = `TEXCOORD_${k}`;
815
+ const srcArr = pod.attributes[srcKey];
816
+ if (srcArr !== void 0) {
817
+ const cat = new Float32Array(vc * 2);
818
+ for (let i = 0; i < vc; i++) {
819
+ const t2 = i * 2;
820
+ cat[t2 + 0] = srcArr[t2 + 0] ?? 0;
821
+ cat[t2 + 1] = srcArr[t2 + 1] ?? 0;
822
+ }
823
+ extraUvAttrs[`uv${k}`] = cat;
824
+ }
825
+ }
826
+ const attributes = {
827
+ position: pod.vertices,
828
+ normal: n ?? new Float32Array(vc * 3).fill(0),
829
+ uv: u ?? new Float32Array(vc * 2).fill(0),
830
+ tangent: new Float32Array(vc * 4).fill(0).map((_, i) => i % 4 === 0 || i % 4 === 3 ? 1 : 0),
831
+ ...skinIndexAttr ? { skinIndex: skinIndexAttr } : {},
832
+ ...skinWeightAttr ? { skinWeight: skinWeightAttr } : {},
833
+ ...extraUvAttrs
834
+ };
835
+ const materialSlots = [];
836
+ const slotByMaterial = /* @__PURE__ */ new Map();
837
+ const usedNames = /* @__PURE__ */ new Set();
838
+ const uniqueName = (raw) => {
839
+ const base = raw.trim() || "Material";
840
+ let candidate = base;
841
+ let suffix = 2;
842
+ while (usedNames.has(candidate)) candidate = `${base}_${suffix++}`;
843
+ usedNames.add(candidate);
844
+ return candidate;
845
+ };
846
+ const materialSlotFor = (materialIndex) => {
847
+ const existing = slotByMaterial.get(materialIndex);
848
+ if (existing !== void 0) return existing;
849
+ const guidString = materialIndex === null ? void 0 : materialContext.guidByIndex?.get(materialIndex);
850
+ const parsed = guidString === void 0 ? void 0 : AssetGuid.parse(guidString);
851
+ const slotIndex = materialSlots.length;
852
+ materialSlots.push({
853
+ slotName: uniqueName(
854
+ materialIndex === null ? "Default" : materialContext.nameByIndex?.get(materialIndex) ?? `Material_${materialIndex}`
855
+ ),
856
+ sourceKey: materialIndex === null ? "fbx:default" : materialContext.sourceKeyByIndex?.get(materialIndex) ?? `fbx:material:${materialIndex}`,
857
+ ...parsed?.ok ? { defaultMaterial: parsed.value } : {}
858
+ });
859
+ slotByMaterial.set(materialIndex, slotIndex);
860
+ return slotIndex;
861
+ };
862
+ const currentMesh = {
863
+ kind: "mesh",
864
+ vertices: ib,
865
+ ...pod.indices ? { indices: pod.indices } : {},
866
+ aabb: box3.fromPositions(box3.create(), pod.vertices),
867
+ attributes,
868
+ ...pod.morphTargets === void 0 ? {} : {
869
+ morphTargets: pod.morphTargets.map((target) => ({
870
+ ...target.position === void 0 ? {} : { position: new Float32Array(target.position) },
871
+ ...target.normal === void 0 ? {} : { normal: new Float32Array(target.normal) },
872
+ ...target.tangent === void 0 ? {} : { tangent: new Float32Array(target.tangent) }
873
+ }))
874
+ },
875
+ ...pod.morphWeights === void 0 ? {} : { morphWeights: new Float32Array(pod.morphWeights) },
876
+ submeshes: pod.submeshes.map((sm) => ({
877
+ indexOffset: sm.indexOffset,
878
+ indexCount: sm.indexCount,
879
+ vertexCount: vc,
880
+ topology: sm.topology,
881
+ materialSlot: materialSlotFor(sm.materialIndex)
882
+ })),
883
+ materialSlots
884
+ };
885
+ const reconciled = reconcileMeshMaterialSlotTopology(
886
+ currentMesh.materialSlots.map((slot) => ({
887
+ slotName: slot.slotName,
888
+ ...slot.sourceKey === void 0 ? {} : { sourceKey: slot.sourceKey },
889
+ ...slot.defaultMaterial === void 0 ? {} : { defaultMaterialGuid: AssetGuid.format(slot.defaultMaterial) }
890
+ })),
891
+ materialContext.previousMaterialSlots
892
+ );
893
+ if (!reconciled.ok) {
894
+ throw new ImportError({
895
+ code: "mesh-material-slot-topology-change",
896
+ expected: `unambiguous material slot identity for mesh ${guid}`,
897
+ hint: IMPORT_ERROR_HINTS["mesh-material-slot-topology-change"],
898
+ detail: {
899
+ meshGuid: guid,
900
+ ...materialContext.meshSourceKey === void 0 ? {} : { meshSourceKey: materialContext.meshSourceKey },
901
+ previousIndices: reconciled.error.previousIndices,
902
+ nextIndices: reconciled.error.nextIndices
903
+ }
904
+ });
905
+ }
906
+ const mesh = {
907
+ ...currentMesh,
908
+ submeshes: currentMesh.submeshes.map((submesh) => ({
909
+ ...submesh,
910
+ materialSlot: reconciled.currentToStableSlot[submesh.materialSlot]
911
+ })),
912
+ materialSlots: reconciled.slots.map((slot, stableIndex) => {
913
+ const active = reconciled.currentToStableSlot.includes(stableIndex);
914
+ const effectiveDefault = resolveMeshMaterialSlotDefaultGuid(
915
+ slot,
916
+ active ? materialContext.materialSlotDefaultOverrides?.[slot.sourceKey ?? slot.slotName] : void 0
917
+ );
918
+ const parsed = effectiveDefault === void 0 ? void 0 : AssetGuid.parse(effectiveDefault);
919
+ return {
920
+ slotName: slot.slotName,
921
+ ...slot.sourceKey === void 0 ? {} : { sourceKey: slot.sourceKey },
922
+ ...active && parsed?.ok ? { defaultMaterial: parsed.value } : {}
923
+ };
924
+ })
925
+ };
926
+ const wireAttributes = { ...mesh.attributes };
927
+ for (let k = 1; k < uvSetCount; k++) {
928
+ if (wireAttributes[`uv${k}`] === void 0) {
929
+ wireAttributes[`uv${k}`] = new Float32Array(vc * 2);
930
+ }
931
+ }
932
+ const wireMesh = { ...mesh, attributes: wireAttributes };
933
+ const refs = [];
934
+ const seenRefs = /* @__PURE__ */ new Set();
935
+ for (let slotIndex = 0; slotIndex < mesh.materialSlots.length; slotIndex++) {
936
+ const defaultMaterial = mesh.materialSlots[slotIndex]?.defaultMaterial;
937
+ const materialGuid = defaultMaterial === void 0 ? void 0 : AssetGuid.format(defaultMaterial);
938
+ if (materialGuid !== void 0 && !seenRefs.has(materialGuid.toLowerCase())) {
939
+ seenRefs.add(materialGuid.toLowerCase());
940
+ refs.push({
941
+ guid: materialGuid,
942
+ sourceField: { fieldName: "materialSlots", arrayIndex: slotIndex }
943
+ });
944
+ }
945
+ }
946
+ return {
947
+ guid,
948
+ kind: "mesh",
949
+ ...pod.name !== void 0 ? { name: pod.name } : {},
950
+ payload: mesh,
951
+ refs,
952
+ artifacts: {
953
+ body: {
954
+ mediaType: "application/x-forgeax-mesh",
955
+ assetCodec: { name: "mesh-binary", version: "4" },
956
+ bytes: (() => {
957
+ const packed = packMeshBinV4(
958
+ wireMesh,
959
+ materialContext.meshSourceKey ?? "fbx://mesh",
960
+ refs.map((ref) => ref.guid)
961
+ );
962
+ if (!packed.ok) {
963
+ throw new ImportError({
964
+ code: "import-internal-error",
965
+ expected: "mesh-bin v4 producer to accept the canonical FBX mesh projection",
966
+ hint: "re-cook the FBX source with its Meta sidecar after fixing the mesh payload",
967
+ detail: { reason: `${packed.error.code}: ${packed.error.actual}` }
968
+ });
969
+ }
970
+ return packed.value;
971
+ })()
972
+ }
973
+ }
974
+ };
975
+ }
976
+ function buildMaterialAsset(pod, guid, skinned = false) {
977
+ const mat = {
978
+ kind: "material",
979
+ colorSpace: "linear",
980
+ passes: [
981
+ {
982
+ name: "Forward",
983
+ program: { module: skinned ? "forgeax::pbr-skin" : "forgeax::default-standard-pbr" },
984
+ renderState: { tags: { LightMode: "Forward" }, queue: 2e3 }
985
+ }
986
+ ],
987
+ values: {
988
+ baseColor: pod.baseColorFactor,
989
+ metallic: pod.metallicFactor,
990
+ roughness: pod.roughnessFactor
991
+ }
992
+ };
993
+ return {
994
+ guid,
995
+ kind: "material",
996
+ ...pod.name !== void 0 ? { name: pod.name } : {},
997
+ payload: mat,
998
+ refs: [],
999
+ artifacts: {}
1000
+ };
1001
+ }
1002
+ function buildSceneAsset(pod, guid, ctx) {
1003
+ const parentOf = /* @__PURE__ */ new Map();
1004
+ for (let i = 0; i < pod.entities.length; i++) {
1005
+ const e = pod.entities[i];
1006
+ if (!e) continue;
1007
+ for (const childIdx of e.children ?? []) parentOf.set(childIdx, i);
1008
+ }
1009
+ const targetIdByEntity = /* @__PURE__ */ new Map();
1010
+ const nodePaths = buildFbxNodePaths(pod.entities);
1011
+ for (let index = 0; index < nodePaths.length; index++) {
1012
+ const path = nodePaths[index];
1013
+ if (path === void 0 || !path.ok) continue;
1014
+ const targetId = deriveAnimationTargetId(path.value);
1015
+ if (ctx.animationTargetIds.has(targetId)) targetIdByEntity.set(index, targetId);
1016
+ }
1017
+ const entities = pod.entities.map((e, idx) => {
1018
+ const components = {
1019
+ Transform: {
1020
+ pos: [e.transform.translation[0], e.transform.translation[1], e.transform.translation[2]],
1021
+ // Quaternion component order [x, y, z, w] (E6).
1022
+ quat: [
1023
+ e.transform.rotation[0],
1024
+ e.transform.rotation[1],
1025
+ e.transform.rotation[2],
1026
+ e.transform.rotation[3]
1027
+ ],
1028
+ scale: [e.transform.scale[0], e.transform.scale[1], e.transform.scale[2]]
1029
+ }
1030
+ };
1031
+ if (e.name) components.Name = { value: e.name };
1032
+ const targetId = targetIdByEntity.get(idx);
1033
+ if (targetId !== void 0) components.AnimationTargetId = { value: targetId };
1034
+ const parent = parentOf.get(idx);
1035
+ if (parent !== void 0) components.ChildOf = { parent };
1036
+ if (e.meshIndex !== null) {
1037
+ const meshHandle = ctx.meshHandleByIndex.get(e.meshIndex);
1038
+ if (meshHandle !== void 0) components.MeshFilter = { assetHandle: meshHandle };
1039
+ components.MeshRenderer = { materials: [] };
1040
+ const morph = ctx.morphWeightsByMeshIndex.get(e.meshIndex);
1041
+ if (morph !== void 0 && morph.targetCount > 0) {
1042
+ const weights = morph.weights ?? new Float32Array(morph.targetCount);
1043
+ if (weights.length !== morph.targetCount) {
1044
+ throw new Error("fbxScene: MorphWeights length does not match morph targets");
1045
+ }
1046
+ components.MorphWeights = { weights: Array.from(weights) };
1047
+ }
1048
+ if (ctx.skinnedMeshIndex === e.meshIndex && ctx.skeletonHandle !== void 0) {
1049
+ components.Skin = { skeleton: ctx.skeletonHandle };
1050
+ }
1051
+ }
1052
+ return { localId: idx, components };
1053
+ });
1054
+ const scene = {
1055
+ kind: "scene",
1056
+ entities,
1057
+ ...ctx.skinGuids.length > 0 ? { skinGuids: ctx.skinGuids } : {}
1058
+ };
1059
+ return {
1060
+ guid,
1061
+ kind: "scene",
1062
+ ...pod.name !== void 0 ? { name: pod.name } : {},
1063
+ payload: scene,
1064
+ refs: ctx.refs.map((guid2) => ({ guid: guid2 })),
1065
+ artifacts: {}
1066
+ };
1067
+ }
1068
+ function buildTextureNote(_pod, _guid) {
1069
+ return {
1070
+ guid: _guid,
1071
+ kind: "texture",
1072
+ ..._pod.name !== void 0 ? { name: _pod.name } : {},
1073
+ payload: {},
1074
+ refs: [],
1075
+ artifacts: {}
1076
+ };
1077
+ }
1078
+ function toAssetPack(params) {
1079
+ const assets = [];
1080
+ const guidOf = makeGuidResolver(params.subAssets);
1081
+ const materialGuidByIndex = /* @__PURE__ */ new Map();
1082
+ const materialNameByIndex = /* @__PURE__ */ new Map();
1083
+ const materialSourceKeyByIndex = /* @__PURE__ */ new Map();
1084
+ for (let materialIndex = 0; materialIndex < params.materials.length; materialIndex++) {
1085
+ const materialGuid = guidOf("material", materialIndex);
1086
+ if (materialGuid !== void 0) materialGuidByIndex.set(materialIndex, materialGuid);
1087
+ const materialSourceKey = params.subAssets.find(
1088
+ (entry) => entry.kind === "material" && entry.sourceIndex === materialIndex
1089
+ )?.sourceKey;
1090
+ if (materialSourceKey !== void 0) {
1091
+ materialSourceKeyByIndex.set(materialIndex, materialSourceKey);
1092
+ }
1093
+ const materialName = params.materials[materialIndex]?.name;
1094
+ if (materialName !== void 0) materialNameByIndex.set(materialIndex, materialName);
1095
+ }
1096
+ const hasSkin = params.skin.vertexCount > 0;
1097
+ const skinnedMeshSourceIndex = hasSkin ? params.meshes[0]?.sourceIndex ?? null : null;
1098
+ for (const mesh of params.meshes) {
1099
+ const meshDeclaration = params.subAssets.find(
1100
+ (entry) => entry.kind === "mesh" && entry.sourceIndex === mesh.sourceIndex
1101
+ );
1102
+ const guid = meshDeclaration?.guid;
1103
+ if (guid === void 0) continue;
1104
+ const previousRaw = meshDeclaration?.sourceKey === void 0 ? void 0 : params.sourceOverrides?.[meshDeclaration.sourceKey]?.materialSlots;
1105
+ const previousMaterialSlots = Array.isArray(previousRaw) ? previousRaw.filter(
1106
+ (slot) => slot !== null && typeof slot === "object" && !Array.isArray(slot) && typeof slot.slotName === "string"
1107
+ ) : void 0;
1108
+ const authoredRaw = meshDeclaration?.sourceKey === void 0 ? void 0 : params.sourceOverrides?.[meshDeclaration.sourceKey]?.materialSlotDefaultOverrides;
1109
+ const materialSlotDefaultOverrides = authoredRaw !== null && typeof authoredRaw === "object" && !Array.isArray(authoredRaw) ? Object.fromEntries(
1110
+ Object.entries(authoredRaw).filter(
1111
+ (entry) => typeof entry[1] === "string" || entry[1] === null
1112
+ )
1113
+ ) : void 0;
1114
+ const inf = mesh.sourceIndex === skinnedMeshSourceIndex ? params.skin.influences : void 0;
1115
+ assets.push(
1116
+ buildMeshAsset(mesh, guid, inf, {
1117
+ guidByIndex: materialGuidByIndex,
1118
+ nameByIndex: materialNameByIndex,
1119
+ sourceKeyByIndex: materialSourceKeyByIndex,
1120
+ ...previousMaterialSlots === void 0 ? {} : { previousMaterialSlots },
1121
+ ...materialSlotDefaultOverrides === void 0 ? {} : { materialSlotDefaultOverrides },
1122
+ ...meshDeclaration?.sourceKey === void 0 ? {} : { meshSourceKey: meshDeclaration.sourceKey }
1123
+ })
1124
+ );
1125
+ }
1126
+ for (let i = 0; i < params.materials.length; i++) {
1127
+ const mat = params.materials[i];
1128
+ if (!mat) continue;
1129
+ const guid = guidOf("material", i);
1130
+ if (guid !== void 0) assets.push(buildMaterialAsset(mat, guid, hasSkin));
1131
+ }
1132
+ for (const tex of params.textures) {
1133
+ const guid = guidOf("texture", tex.sourceIndex);
1134
+ if (guid !== void 0) assets.push(buildTextureNote(tex, guid));
1135
+ }
1136
+ const declaredByKind = (kind) => params.subAssets.filter((s) => s.kind === kind).slice().sort((a, b) => a.sourceIndex - b.sourceIndex).map((s) => s.guid);
1137
+ const meshGuids = declaredByKind("mesh");
1138
+ const skeletonGuids = declaredByKind("skeleton");
1139
+ const skinGuids = declaredByKind("skin");
1140
+ const sceneRefs = [...meshGuids, ...skeletonGuids, ...skinGuids];
1141
+ const meshHandleByIndex = /* @__PURE__ */ new Map();
1142
+ params.subAssets.filter((s) => s.kind === "mesh").forEach((s) => {
1143
+ const idx = meshGuids.indexOf(s.guid);
1144
+ if (idx >= 0) meshHandleByIndex.set(s.sourceIndex, idx);
1145
+ });
1146
+ const skeletonRefBase = meshGuids.length;
1147
+ const skeletonHandle = skeletonGuids.length > 0 ? skeletonRefBase : void 0;
1148
+ const skinnedMeshIndex = params.skin.vertexCount > 0 ? params.meshes[0]?.sourceIndex ?? null : null;
1149
+ const skeletonGuid = guidOf("skeleton", 0);
1150
+ if (params.skeleton.jointCount > 0 && skeletonGuid !== void 0) {
1151
+ assets.push({
1152
+ guid: skeletonGuid,
1153
+ kind: "skeleton",
1154
+ payload: {
1155
+ kind: "skeleton",
1156
+ inverseBindMatrices: params.skeleton.inverseBindMatrices,
1157
+ jointCount: params.skeleton.jointCount
1158
+ },
1159
+ refs: [],
1160
+ artifacts: {}
1161
+ });
1162
+ }
1163
+ const skinGuid = guidOf("skin", 0);
1164
+ if (params.skin.vertexCount > 0 && skinGuid !== void 0) {
1165
+ assets.push({
1166
+ guid: skinGuid,
1167
+ kind: "skin",
1168
+ payload: {
1169
+ kind: "skin",
1170
+ skeletonGuid: skeletonGuid ?? "",
1171
+ jointPaths: params.skin.jointPaths
1172
+ },
1173
+ refs: skeletonGuid !== void 0 ? [{ guid: skeletonGuid, sourceField: { fieldName: "skeleton" } }] : [],
1174
+ artifacts: {}
1175
+ });
1176
+ }
1177
+ for (let i = 0; i < params.animationClips.length; i++) {
1178
+ const clip = params.animationClips[i];
1179
+ if (!clip) continue;
1180
+ const guid = guidOf("animation-clip", i);
1181
+ if (guid === void 0) continue;
1182
+ assets.push({
1183
+ guid,
1184
+ kind: "animation-clip",
1185
+ payload: {
1186
+ kind: "animation-clip",
1187
+ name: clip.name ?? `Clip${i}`,
1188
+ duration: clip.duration,
1189
+ channels: clip.channels.map((ch) => ({
1190
+ targetId: ch.targetId,
1191
+ property: ch.property,
1192
+ sampler: {
1193
+ input: Array.from(ch.sampler.input),
1194
+ output: Array.from(ch.sampler.output),
1195
+ interpolation: ch.sampler.interpolation
1196
+ }
1197
+ }))
1198
+ },
1199
+ refs: [],
1200
+ artifacts: {}
1201
+ });
1202
+ }
1203
+ const sceneGuid = guidOf("scene", 0);
1204
+ const animationTargetIds = new Set(
1205
+ params.animationClips.flatMap((clip) => clip.channels.map((channel) => channel.targetId))
1206
+ );
1207
+ const morphWeightsByMeshIndex = /* @__PURE__ */ new Map();
1208
+ for (const mesh of params.meshes) {
1209
+ const targetCount = mesh.morphTargets?.length ?? 0;
1210
+ if (targetCount > 0) {
1211
+ morphWeightsByMeshIndex.set(mesh.sourceIndex, {
1212
+ targetCount,
1213
+ ...mesh.morphWeights === void 0 ? {} : { weights: new Float32Array(mesh.morphWeights) }
1214
+ });
1215
+ }
1216
+ }
1217
+ if (sceneGuid !== void 0) {
1218
+ assets.push(
1219
+ buildSceneAsset(params.scene, sceneGuid, {
1220
+ meshHandleByIndex,
1221
+ skinnedMeshIndex,
1222
+ skeletonHandle,
1223
+ refs: sceneRefs,
1224
+ skinGuids,
1225
+ animationTargetIds,
1226
+ morphWeightsByMeshIndex
1227
+ })
1228
+ );
1229
+ }
1230
+ if (assets.length === 1) {
1231
+ const only = assets[0];
1232
+ if (only && "name" in only) {
1233
+ const { name: _dropped, ...rest } = only;
1234
+ assets[0] = rest;
1235
+ }
1236
+ }
1237
+ return assets;
1238
+ }
1239
+
1240
+ // src/fbx-importer.ts
1241
+ function sourceKeyForFbxOutput(output) {
1242
+ const kind = output.kind.trim();
1243
+ if (kind.length === 0) return void 0;
1244
+ const name = output.name?.trim();
1245
+ return name === void 0 || name.length === 0 ? `fbx:${kind}` : `fbx:${kind}:${name}`;
1246
+ }
1247
+ function deriveFbxSourceKeys(outputs) {
1248
+ const keys = outputs.map(sourceKeyForFbxOutput);
1249
+ if (keys.some((key) => key === void 0)) return { ok: false, code: "missing-source-key" };
1250
+ const seen = /* @__PURE__ */ new Set();
1251
+ for (const [index, key] of keys.entries()) {
1252
+ if (key === void 0) continue;
1253
+ if (seen.has(key)) {
1254
+ return {
1255
+ ok: false,
1256
+ code: outputs[index]?.name === void 0 ? "ambiguous-source-key" : "duplicate-source-key"
1257
+ };
1258
+ }
1259
+ seen.add(key);
1260
+ }
1261
+ return { ok: true, keys };
1262
+ }
1263
+ var fbxImporter = {
1264
+ key: "fbx",
1265
+ async import(ctx) {
1266
+ const read = await ctx.readSource();
1267
+ if (!read.ok) {
1268
+ const wrapper = new Error(`fbx-source-unreadable: ${ctx.source}`);
1269
+ wrapper.cause = read.error;
1270
+ throw wrapper;
1271
+ }
1272
+ await initFbxWasm();
1273
+ const jsonStr = parseFbx(read.value);
1274
+ const doc = JSON.parse(jsonStr);
1275
+ const maybeError = doc;
1276
+ if (maybeError.error?.code === "fbx-mesh-type-unsupported") {
1277
+ const e = fbxErr("fbx-mesh-type-unsupported", {
1278
+ meshType: maybeError.error.meshType,
1279
+ meshName: maybeError.error.meshName
1280
+ });
1281
+ const wrapper = new Error(`${e.code}: ${e.expected}`);
1282
+ wrapper.cause = e;
1283
+ throw wrapper;
1284
+ }
1285
+ const rawMeshes = doc.meshes ?? [];
1286
+ const meshes = rawMeshes.map((raw, i) => parseMesh(raw, i));
1287
+ const scene = parseScene(doc);
1288
+ const texturesModule = doc;
1289
+ const textures = parseTextures({ textures: texturesModule.textures });
1290
+ const materialDocs = doc.materials ?? [];
1291
+ const materials = materialDocs.length > 0 ? materialDocs.map((raw, i) => parseMaterial(raw)) : [parseMaterial({ kind: "fallback" })];
1292
+ const skeleton = parseSkeleton(doc);
1293
+ const skin = parseSkin(doc);
1294
+ const animationTargets = resolveAnimationTargetIds(
1295
+ doc.nodes ?? [],
1296
+ doc.clips ?? []
1297
+ );
1298
+ if (!animationTargets.ok) {
1299
+ const wrapper = new Error(
1300
+ `${animationTargets.error.code}: ${animationTargets.error.expected}`
1301
+ );
1302
+ wrapper.cause = animationTargets.error;
1303
+ throw wrapper;
1304
+ }
1305
+ const animationClips = parseAnimationClips(doc);
1306
+ return {
1307
+ ok: true,
1308
+ value: {
1309
+ assets: toAssetPack({
1310
+ meshes,
1311
+ scene,
1312
+ materials,
1313
+ textures,
1314
+ skeleton,
1315
+ skin,
1316
+ animationClips,
1317
+ subAssets: ctx.subAssets,
1318
+ ...ctx.sourceOverrides === void 0 ? {} : { sourceOverrides: ctx.sourceOverrides }
1319
+ }),
1320
+ sourceDependencies: []
1321
+ }
1322
+ };
1323
+ }
1324
+ };
1325
+
1326
+ // src/resolve-texture-path.ts
1327
+ function normalizeSourceRelativePath(raw) {
1328
+ const value = raw.replaceAll("\\", "/");
1329
+ if (value.startsWith("/") || /^[A-Za-z]:\//.test(value) || value.startsWith("//"))
1330
+ return void 0;
1331
+ const parts = [];
1332
+ for (const part of value.split("/")) {
1333
+ if (part === "" || part === ".") continue;
1334
+ if (part === "..") {
1335
+ if (parts.at(-1) !== void 0 && parts.at(-1) !== "..") parts.pop();
1336
+ else parts.push("..");
1337
+ } else {
1338
+ parts.push(part);
1339
+ }
1340
+ }
1341
+ return parts.join("/");
1342
+ }
1343
+ function requestSegments(raw) {
1344
+ const value = raw.replaceAll("\\", "/").replace(/^[A-Za-z]:\//, "").replace(/^\/+/, "");
1345
+ return value.split("/").filter((part) => part !== "" && part !== ".");
1346
+ }
1347
+ function uniqueMatches(paths, predicate) {
1348
+ return [...new Set(paths.filter(predicate))];
1349
+ }
1350
+ function longestSuffixLength(request, candidate) {
1351
+ let count = 0;
1352
+ while (count < request.length && count < candidate.length && request[request.length - 1 - count]?.toLowerCase() === candidate[candidate.length - 1 - count]?.toLowerCase()) {
1353
+ count++;
1354
+ }
1355
+ return count;
1356
+ }
1357
+ function resolveFbxTexturePath(sourcePath, request, candidates) {
1358
+ const candidatePaths = candidates.flatMap((candidate) => {
1359
+ const normalized = normalizeSourceRelativePath(candidate.relativePath);
1360
+ return normalized === void 0 || normalized.length === 0 ? [] : [normalized];
1361
+ });
1362
+ const requestedPath = request.declaredRelativePath ?? request.declaredFilename ?? request.declaredAbsolutePath ?? "";
1363
+ const choose = (matches, strategy) => {
1364
+ const unique = [...new Set(matches)];
1365
+ if (unique.length === 1) {
1366
+ const relativePath = unique[0];
1367
+ if (relativePath === void 0) return void 0;
1368
+ return {
1369
+ ok: true,
1370
+ relativePath,
1371
+ readUri: relativePath,
1372
+ strategy
1373
+ };
1374
+ }
1375
+ if (unique.length > 1) {
1376
+ return {
1377
+ ok: false,
1378
+ code: "fbx-external-texture-ambiguous",
1379
+ requestedPath,
1380
+ candidates: unique
1381
+ };
1382
+ }
1383
+ return void 0;
1384
+ };
1385
+ const relativeRequest = request.declaredRelativePath ?? request.declaredFilename;
1386
+ if (relativeRequest !== void 0 && !/^(?:[A-Za-z]:[\\/]|[\\/])/.test(relativeRequest)) {
1387
+ const normalizedRequest = normalizeSourceRelativePath(relativeRequest);
1388
+ if (normalizedRequest !== void 0) {
1389
+ const scopeExact = choose(
1390
+ uniqueMatches(candidatePaths, (path) => path === normalizedRequest),
1391
+ "exact"
1392
+ );
1393
+ if (scopeExact !== void 0) return scopeExact;
1394
+ const scopeFolded = choose(
1395
+ uniqueMatches(
1396
+ candidatePaths,
1397
+ (path) => path.toLowerCase() === normalizedRequest.toLowerCase()
1398
+ ),
1399
+ "case-folded"
1400
+ );
1401
+ if (scopeFolded !== void 0) return scopeFolded;
1402
+ }
1403
+ }
1404
+ const requestedSegments = requestSegments(requestedPath);
1405
+ if (requestedSegments.length > 0) {
1406
+ const scored = candidatePaths.map((path) => ({
1407
+ path,
1408
+ score: longestSuffixLength(requestedSegments, path.split("/"))
1409
+ }));
1410
+ const bestScore = Math.max(0, ...scored.map((entry) => entry.score));
1411
+ if (bestScore > 0) {
1412
+ const best = scored.filter((entry) => entry.score === bestScore).map((entry) => entry.path);
1413
+ const strategy = bestScore === 1 ? "basename" : "suffix";
1414
+ const suffix = choose(best, strategy);
1415
+ if (suffix !== void 0) return suffix;
1416
+ }
1417
+ }
1418
+ return {
1419
+ ok: false,
1420
+ code: "fbx-external-texture-missing",
1421
+ requestedPath,
1422
+ candidates: candidatePaths
1423
+ };
1424
+ }
1425
+
1426
+ // src/index.ts
1427
+ var wasmModule = null;
1428
+ var initPromise = null;
1429
+ async function _loadWasm(overrideUrl) {
1430
+ const glueId = "../pkg/fbx-wasm.mjs";
1431
+ const createModule = (await import(
1432
+ /* @vite-ignore */
1433
+ glueId
1434
+ )).default;
1435
+ const wasmAssetUrl = overrideUrl ? new URL(overrideUrl, import.meta.url) : new URL("../pkg/fbx-wasm.wasm", import.meta.url);
1436
+ const opts = {
1437
+ locateFile: () => wasmAssetUrl.href
1438
+ };
1439
+ try {
1440
+ return await createModule(opts);
1441
+ } catch (cause) {
1442
+ throw new Error(
1443
+ `@forgeax/engine-fbx: failed to load WASM from ${wasmAssetUrl.href}. pkg/fbx-wasm.wasm may be missing. Self-help: (1) fetch a prebuilt artifact via \`pnpm -F @forgeax/engine-fbx fetch-wasm\`, or (2) compile locally with emcc via \`pnpm -F @forgeax/engine-fbx build:wasm\`.`,
1444
+ { cause }
1445
+ );
1446
+ }
1447
+ }
1448
+ async function initFbxWasm(wasmUrl) {
1449
+ if (wasmModule) return;
1450
+ if (!initPromise) {
1451
+ initPromise = _loadWasm(wasmUrl).catch((e) => {
1452
+ initPromise = null;
1453
+ throw e;
1454
+ });
1455
+ }
1456
+ wasmModule = await initPromise;
1457
+ }
1458
+ function parseFbx(fbxBytes) {
1459
+ if (!wasmModule) {
1460
+ throw new Error("@forgeax/engine-fbx: WASM not initialized. Call initFbxWasm() first.");
1461
+ }
1462
+ const mod = wasmModule;
1463
+ const size = fbxBytes.byteLength;
1464
+ const ptr = mod._malloc(size);
1465
+ if (!ptr) throw new Error("fbx-wasm: malloc failed for input buffer");
1466
+ try {
1467
+ mod.HEAPU8.set(fbxBytes, ptr);
1468
+ mod._parseFbxWasm(ptr, size);
1469
+ } finally {
1470
+ mod._free(ptr);
1471
+ }
1472
+ const resultPtr = mod._getResultPtr();
1473
+ const resultLen = mod._getResultLen();
1474
+ if (!resultPtr || !resultLen) {
1475
+ mod._freeResult();
1476
+ throw new Error("fbx-wasm: parseFbxWasm returned empty result");
1477
+ }
1478
+ const bytes = mod.HEAPU8.slice(resultPtr, resultPtr + resultLen);
1479
+ mod._freeResult();
1480
+ const json = new TextDecoder().decode(bytes);
1481
+ const firstChars = json.substring(0, 30);
1482
+ if (firstChars.includes('"error"')) {
1483
+ const parsed = JSON.parse(json);
1484
+ if (parsed.error) {
1485
+ throw new Error(`fbx-wasm: ${parsed.error.message || "parse failed"}`);
1486
+ }
1487
+ }
1488
+ return json;
1489
+ }
1490
+ function parseFbxToObject(fbxBytes) {
1491
+ return JSON.parse(parseFbx(fbxBytes));
1492
+ }
1493
+ function isFbxWasmReady() {
1494
+ return wasmModule !== null;
1495
+ }
1496
+
1497
+ export { FBX_ERROR_HINTS, deriveFbxSourceKeys, fbxErr, fbxImporter, initFbxWasm, isFbxWasmReady, parseAnimationClips, parseFbx, parseFbxToObject, parseMaterial, parseMesh, parseScene, parseSkeleton, parseSkin, parseTextures, resolveFbxTexturePath, sourceKeyForFbxOutput, toAssetPack };
1498
+ //# sourceMappingURL=index.mjs.map
1499
+ //# sourceMappingURL=index.mjs.map