@forgeax/engine-fbx 0.1.2

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 (82) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +196 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/__tests__/asset-runtime-fixture.d.ts +9 -0
  5. package/dist/__tests__/asset-runtime-fixture.d.ts.map +1 -0
  6. package/dist/__tests__/blendshape-import.integration.test.d.ts +2 -0
  7. package/dist/__tests__/blendshape-import.integration.test.d.ts.map +1 -0
  8. package/dist/__tests__/blendshape-import.unit.test.d.ts +2 -0
  9. package/dist/__tests__/blendshape-import.unit.test.d.ts.map +1 -0
  10. package/dist/__tests__/fbx-importer.test.d.ts +2 -0
  11. package/dist/__tests__/fbx-importer.test.d.ts.map +1 -0
  12. package/dist/__tests__/fbx-local-artifacts.test.d.ts +2 -0
  13. package/dist/__tests__/fbx-local-artifacts.test.d.ts.map +1 -0
  14. package/dist/__tests__/index.test.d.ts +2 -0
  15. package/dist/__tests__/index.test.d.ts.map +1 -0
  16. package/dist/__tests__/mesh-material-slots.unit.test.d.ts +2 -0
  17. package/dist/__tests__/mesh-material-slots.unit.test.d.ts.map +1 -0
  18. package/dist/__tests__/parse-mesh-multi-uv.test.d.ts +2 -0
  19. package/dist/__tests__/parse-mesh-multi-uv.test.d.ts.map +1 -0
  20. package/dist/__tests__/pick-e2e.integration.test.d.ts +2 -0
  21. package/dist/__tests__/pick-e2e.integration.test.d.ts.map +1 -0
  22. package/dist/__tests__/resolve-texture-path.unit.test.d.ts +2 -0
  23. package/dist/__tests__/resolve-texture-path.unit.test.d.ts.map +1 -0
  24. package/dist/errors.d.ts +50 -0
  25. package/dist/errors.d.ts.map +1 -0
  26. package/dist/fbx-importer.d.ts +18 -0
  27. package/dist/fbx-importer.d.ts.map +1 -0
  28. package/dist/index.d.ts +62 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.mjs +9546 -0
  31. package/dist/index.mjs.map +1 -0
  32. package/dist/parse-animation-clip.d.ts +35 -0
  33. package/dist/parse-animation-clip.d.ts.map +1 -0
  34. package/dist/parse-material.d.ts +30 -0
  35. package/dist/parse-material.d.ts.map +1 -0
  36. package/dist/parse-mesh.d.ts +21 -0
  37. package/dist/parse-mesh.d.ts.map +1 -0
  38. package/dist/parse-scene.d.ts +28 -0
  39. package/dist/parse-scene.d.ts.map +1 -0
  40. package/dist/parse-skeleton.d.ts +15 -0
  41. package/dist/parse-skeleton.d.ts.map +1 -0
  42. package/dist/parse-skin.d.ts +20 -0
  43. package/dist/parse-skin.d.ts.map +1 -0
  44. package/dist/parse-texture.d.ts +15 -0
  45. package/dist/parse-texture.d.ts.map +1 -0
  46. package/dist/resolve-texture-path.d.ts +25 -0
  47. package/dist/resolve-texture-path.d.ts.map +1 -0
  48. package/dist/to-asset-pack.d.ts +38 -0
  49. package/dist/to-asset-pack.d.ts.map +1 -0
  50. package/package.json +75 -0
  51. package/pkg/fbx-wasm.mjs +2 -0
  52. package/pkg/fbx-wasm.wasm +0 -0
  53. package/scripts/build-wasm.mjs +85 -0
  54. package/scripts/content-key.mjs +88 -0
  55. package/scripts/ensure-wasm.mjs +28 -0
  56. package/scripts/fetch-ufbx.mjs +42 -0
  57. package/scripts/fetch-wasm.mjs +91 -0
  58. package/scripts/parity-diff.mjs +127 -0
  59. package/scripts/test-wasm.mjs +91 -0
  60. package/src/__tests__/asset-runtime-fixture.ts +251 -0
  61. package/src/__tests__/blendshape-import.integration.test.ts +96 -0
  62. package/src/__tests__/blendshape-import.unit.test.ts +83 -0
  63. package/src/__tests__/fbx-importer.test.ts +200 -0
  64. package/src/__tests__/fbx-local-artifacts.test.ts +39 -0
  65. package/src/__tests__/index.test.ts +167 -0
  66. package/src/__tests__/mesh-material-slots.unit.test.ts +125 -0
  67. package/src/__tests__/parse-mesh-multi-uv.test.ts +187 -0
  68. package/src/__tests__/pick-e2e.integration.test.ts +247 -0
  69. package/src/__tests__/resolve-texture-path.unit.test.ts +50 -0
  70. package/src/errors.ts +112 -0
  71. package/src/fbx-importer.ts +372 -0
  72. package/src/index.ts +215 -0
  73. package/src/native/bridge.c +1060 -0
  74. package/src/parse-animation-clip.ts +271 -0
  75. package/src/parse-material.ts +140 -0
  76. package/src/parse-mesh.ts +223 -0
  77. package/src/parse-scene.ts +75 -0
  78. package/src/parse-skeleton.ts +46 -0
  79. package/src/parse-skin.ts +71 -0
  80. package/src/parse-texture.ts +36 -0
  81. package/src/resolve-texture-path.ts +152 -0
  82. package/src/to-asset-pack.ts +813 -0
@@ -0,0 +1,35 @@
1
+ import type { AnimationClipPod } from '@forgeax/engine-types';
2
+ import { type FbxError, type Result } from './errors.js';
3
+ export interface FbxRawAnimChannel {
4
+ readonly targetNode: string;
5
+ readonly property: 'translation' | 'rotation' | 'scale' | 'weights';
6
+ /** Ascending key timestamps in seconds. */
7
+ readonly keyTimes?: number[];
8
+ /** Interleaved key values, stride 3/4 or weightCount for morph weights. */
9
+ readonly keyValues?: number[];
10
+ /** Number of morph weight elements in each sampled key. */
11
+ readonly weightCount?: number;
12
+ }
13
+ export interface FbxRawClip {
14
+ readonly name?: string;
15
+ readonly duration: number;
16
+ readonly channels: readonly FbxRawAnimChannel[];
17
+ }
18
+ export interface FbxRawAnimDoc {
19
+ readonly clips?: readonly FbxRawClip[];
20
+ }
21
+ interface FbxAnimationNode {
22
+ readonly name: string;
23
+ readonly children: readonly number[];
24
+ }
25
+ export declare function resolveAnimationTargetIds(nodes: readonly FbxAnimationNode[], clips: readonly FbxRawClip[]): Result<void, FbxError>;
26
+ /**
27
+ * Parse animation clips from a C++ JSON POD document.
28
+ * Performs merge-keys + linear resample at the requested fps
29
+ * (default 30). OOS-9: Hermite tangent data is discarded.
30
+ *
31
+ * Returns an empty array when the document has no clip data.
32
+ */
33
+ export declare function parseAnimationClips(doc: FbxRawAnimDoc, fps?: number): AnimationClipPod[];
34
+ export {};
35
+ //# sourceMappingURL=parse-animation-clip.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse-animation-clip.d.ts","sourceRoot":"","sources":["../src/parse-animation-clip.ts"],"names":[],"mappings":"AA6BA,OAAO,KAAK,EAEV,gBAAgB,EAEjB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAO,KAAK,QAAQ,EAAc,KAAK,MAAM,EAAE,MAAM,aAAa,CAAC;AAG1E,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,aAAa,GAAG,UAAU,GAAG,OAAO,GAAG,SAAS,CAAC;IACpE,2CAA2C;IAC3C,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,2EAA2E;IAC3E,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IAC9B,2DAA2D;IAC3D,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,SAAS,iBAAiB,EAAE,CAAC;CACjD;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,UAAU,EAAE,CAAC;CACxC;AA0GD,UAAU,gBAAgB;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;CACtC;AAED,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,SAAS,gBAAgB,EAAE,EAClC,KAAK,EAAE,SAAS,UAAU,EAAE,GAC3B,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,CA6ExB;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,aAAa,EAClB,GAAG,GAAE,MAAoB,GACxB,gBAAgB,EAAE,CAWpB"}
@@ -0,0 +1,30 @@
1
+ import type { MaterialPod, MaterialTextureBindingPod } from '@forgeax/engine-types';
2
+ export interface FbxRawTextureBinding {
3
+ readonly slot: MaterialTextureBindingPod['slot'];
4
+ readonly textureIndex: number;
5
+ readonly texCoord?: number;
6
+ }
7
+ /** JSON shape emitted by binding.cc WriteMaterials for a single material. */
8
+ export interface FbxRawMaterial {
9
+ readonly name?: string;
10
+ readonly kind: 'stingray-pbs' | 'phong' | 'lambert' | 'fallback';
11
+ readonly baseColor?: readonly [number, number, number];
12
+ readonly diffuse?: readonly [number, number, number];
13
+ readonly shininess?: number;
14
+ readonly specular?: readonly [number, number, number];
15
+ readonly textureBindings?: readonly FbxRawTextureBinding[];
16
+ readonly stingrayProps?: {
17
+ readonly baseColor?: readonly [number, number, number];
18
+ readonly metallic?: number;
19
+ readonly roughness?: number;
20
+ readonly emissive?: readonly [number, number, number];
21
+ };
22
+ }
23
+ /**
24
+ * Parse a single FBX raw material JSON node into a MaterialPod.
25
+ *
26
+ * The caller (fbx-importer.ts) iterates the top-level `materials` array
27
+ * from the C++ binding JSON output.
28
+ */
29
+ export declare function parseMaterial(raw: FbxRawMaterial, sourceIndex: number): MaterialPod;
30
+ //# sourceMappingURL=parse-material.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse-material.d.ts","sourceRoot":"","sources":["../src/parse-material.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,WAAW,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAC;AAEpF,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,IAAI,EAAE,yBAAyB,CAAC,MAAM,CAAC,CAAC;IACjD,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,6EAA6E;AAC7E,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,GAAG,SAAS,GAAG,UAAU,CAAC;IACjE,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IACvD,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IACrD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IACtD,QAAQ,CAAC,eAAe,CAAC,EAAE,SAAS,oBAAoB,EAAE,CAAC;IAC3D,QAAQ,CAAC,aAAa,CAAC,EAAE;QACvB,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;QACvD,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAC5B,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;KACvD,CAAC;CACH;AAWD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,GAAG,WAAW,CA0FnF"}
@@ -0,0 +1,21 @@
1
+ import type { MeshPod } from '@forgeax/engine-types';
2
+ export interface FbxRawMesh {
3
+ readonly name?: string;
4
+ readonly vertices: number[];
5
+ readonly indices?: number[];
6
+ readonly attributes: Record<string, number[]>;
7
+ readonly polygonCount: number;
8
+ readonly sourceIndex: number;
9
+ readonly materialIndex: number;
10
+ readonly morphTargets?: readonly {
11
+ readonly position?: number[];
12
+ readonly normal?: number[];
13
+ readonly tangent?: number[];
14
+ }[];
15
+ readonly morphWeights?: number[];
16
+ }
17
+ export interface FbxRawDocument {
18
+ readonly meshes?: readonly FbxRawMesh[];
19
+ }
20
+ export declare function parseMesh(raw: FbxRawMesh, sourceIndex: number): MeshPod;
21
+ //# sourceMappingURL=parse-mesh.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse-mesh.d.ts","sourceRoot":"","sources":["../src/parse-mesh.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAe,MAAM,uBAAuB,CAAC;AAKlE,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,CAAC;IAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAC9C,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS;QAC/B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;QAC7B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;QAC3B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;KAC7B,EAAE,CAAC;IACJ,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CAClC;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,UAAU,EAAE,CAAC;CACzC;AAsBD,wBAAgB,SAAS,CAAC,GAAG,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CA+KvE"}
@@ -0,0 +1,28 @@
1
+ import type { ScenePod } from '@forgeax/engine-types';
2
+ export interface FbxRawNode {
3
+ readonly name: string;
4
+ readonly transform: {
5
+ readonly translation: [number, number, number];
6
+ readonly rotation: [number, number, number, number];
7
+ readonly scale: [number, number, number];
8
+ };
9
+ readonly meshIndex: number;
10
+ readonly children: readonly number[];
11
+ }
12
+ export interface FbxRawNodes {
13
+ readonly nodes?: readonly FbxRawNode[];
14
+ }
15
+ export type FbxNodePathResult = {
16
+ readonly ok: true;
17
+ readonly value: readonly string[];
18
+ } | {
19
+ readonly ok: false;
20
+ readonly reason: 'name-missing' | 'path-invalid' | 'hierarchy-cycle';
21
+ readonly nodeIndex: number;
22
+ };
23
+ export declare function buildFbxNodePaths(nodes: readonly {
24
+ readonly name?: string;
25
+ readonly children?: readonly number[];
26
+ }[]): readonly FbxNodePathResult[];
27
+ export declare function parseScene(rawNodes: FbxRawNodes): ScenePod;
28
+ //# sourceMappingURL=parse-scene.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse-scene.d.ts","sourceRoot":"","sources":["../src/parse-scene.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAkB,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAEtE,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE;QAClB,QAAQ,CAAC,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;QAC/C,QAAQ,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;QACpD,QAAQ,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;KAC1C,CAAC;IACF,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;CACtC;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,UAAU,EAAE,CAAC;CACxC;AAED,MAAM,MAAM,iBAAiB,GACzB;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,GACxD;IACE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,cAAc,GAAG,iBAAiB,CAAC;IACrE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEN,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,SAAS;IAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,EAAE,GAClF,SAAS,iBAAiB,EAAE,CA0B9B;AAED,wBAAgB,UAAU,CAAC,QAAQ,EAAE,WAAW,GAAG,QAAQ,CAiB1D"}
@@ -0,0 +1,15 @@
1
+ import type { SkeletonPod } from '@forgeax/engine-types';
2
+ export interface FbxRawSkeleton {
3
+ readonly jointCount: number;
4
+ readonly inverseBindMatrices: number[];
5
+ readonly jointPaths: string[];
6
+ }
7
+ export interface FbxRawSkeletonDoc {
8
+ readonly skeletons?: readonly FbxRawSkeleton[];
9
+ }
10
+ /**
11
+ * Parse the first skeleton from a C++ JSON POD document.
12
+ * Returns an empty skeleton when the document has no skeleton data.
13
+ */
14
+ export declare function parseSkeleton(doc: FbxRawSkeletonDoc): SkeletonPod;
15
+ //# sourceMappingURL=parse-skeleton.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse-skeleton.d.ts","sourceRoot":"","sources":["../src/parse-skeleton.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAEzD,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,mBAAmB,EAAE,MAAM,EAAE,CAAC;IACvC,QAAQ,CAAC,UAAU,EAAE,MAAM,EAAE,CAAC;CAC/B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;CAChD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,iBAAiB,GAAG,WAAW,CAejE"}
@@ -0,0 +1,20 @@
1
+ import type { SkinPod } from '@forgeax/engine-types';
2
+ export interface FbxRawSkinInfluence {
3
+ readonly jointIndices: number[];
4
+ readonly jointWeights: number[];
5
+ }
6
+ export interface FbxRawSkin {
7
+ readonly meshSourceIndex: number;
8
+ readonly jointPaths: string[];
9
+ readonly vertexCount: number;
10
+ readonly influences: readonly FbxRawSkinInfluence[];
11
+ }
12
+ export interface FbxRawSkinDoc {
13
+ readonly skins?: readonly FbxRawSkin[];
14
+ }
15
+ /**
16
+ * Parse the first skin from a C++ JSON POD document.
17
+ * Returns an empty skin when the document has no skin data.
18
+ */
19
+ export declare function parseSkin(doc: FbxRawSkinDoc): SkinPod;
20
+ //# sourceMappingURL=parse-skin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse-skin.d.ts","sourceRoot":"","sources":["../src/parse-skin.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,OAAO,EAA0B,MAAM,uBAAuB,CAAC;AAE7E,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,YAAY,EAAE,MAAM,EAAE,CAAC;IAChC,QAAQ,CAAC,YAAY,EAAE,MAAM,EAAE,CAAC;CACjC;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,UAAU,EAAE,MAAM,EAAE,CAAC;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,UAAU,EAAE,SAAS,mBAAmB,EAAE,CAAC;CACrD;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,UAAU,EAAE,CAAC;CACxC;AAmBD;;;GAGG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,aAAa,GAAG,OAAO,CAgBrD"}
@@ -0,0 +1,15 @@
1
+ import type { TexturePod } from '@forgeax/engine-types';
2
+ export interface FbxRawTexture {
3
+ readonly name?: string;
4
+ readonly filePath?: string;
5
+ readonly relativeFilePath?: string;
6
+ readonly absoluteFilePath?: string;
7
+ readonly embeddedBytes?: readonly number[];
8
+ readonly type?: 'file' | 'layered' | 'procedural' | 'shader' | 'unknown';
9
+ readonly sourceIndex: number;
10
+ }
11
+ export interface FbxRawTextures {
12
+ readonly textures?: readonly FbxRawTexture[];
13
+ }
14
+ export declare function parseTextures(raw: FbxRawTextures): TexturePod[];
15
+ //# sourceMappingURL=parse-texture.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse-texture.d.ts","sourceRoot":"","sources":["../src/parse-texture.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAExD,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,YAAY,GAAG,QAAQ,GAAG,SAAS,CAAC;IACzE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;CAC9C;AAMD,wBAAgB,aAAa,CAAC,GAAG,EAAE,cAAc,GAAG,UAAU,EAAE,CAa/D"}
@@ -0,0 +1,25 @@
1
+ export type FbxTextureResolutionStrategy = 'exact' | 'case-folded' | 'suffix' | 'basename';
2
+ export interface FbxTexturePathRequest {
3
+ readonly declaredRelativePath?: string;
4
+ readonly declaredFilename?: string;
5
+ readonly declaredAbsolutePath?: string;
6
+ }
7
+ export interface FbxTextureCandidate {
8
+ /** Candidate path relative to the FBX source file's directory. */
9
+ readonly relativePath: string;
10
+ }
11
+ export type FbxTextureResolution = {
12
+ readonly ok: true;
13
+ readonly relativePath: string;
14
+ /** URI passed to ImportContext.readSibling from the FBX source. */
15
+ readonly readUri: string;
16
+ readonly strategy: FbxTextureResolutionStrategy;
17
+ } | {
18
+ readonly ok: false;
19
+ readonly code: 'fbx-external-texture-missing' | 'fbx-external-texture-ambiguous';
20
+ readonly requestedPath: string;
21
+ readonly candidates: readonly string[];
22
+ };
23
+ /** Resolve an FBX-declared texture against a caller-authorized candidate tree. */
24
+ export declare function resolveFbxTexturePath(sourcePath: string, request: FbxTexturePathRequest, candidates: readonly FbxTextureCandidate[]): FbxTextureResolution;
25
+ //# sourceMappingURL=resolve-texture-path.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-texture-path.d.ts","sourceRoot":"","sources":["../src/resolve-texture-path.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,4BAA4B,GAAG,OAAO,GAAG,aAAa,GAAG,QAAQ,GAAG,UAAU,CAAC;AAE3F,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,CAAC;IACvC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,CAAC;CACxC;AAED,MAAM,WAAW,mBAAmB;IAClC,kEAAkE;IAClE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,MAAM,oBAAoB,GAC5B;IACE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,mEAAmE;IACnE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,4BAA4B,CAAC;CACjD,GACD;IACE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,IAAI,EAAE,8BAA8B,GAAG,gCAAgC,CAAC;IACjF,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;CACxC,CAAC;AA4CN,kFAAkF;AAClF,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,qBAAqB,EAC9B,UAAU,EAAE,SAAS,mBAAmB,EAAE,GACzC,oBAAoB,CA4EtB"}
@@ -0,0 +1,38 @@
1
+ import type { AnimationClipPod, AssetCodec, ImportedAsset, MaterialPod, MeshMaterialSlotTopologyEntry, MeshPod, ScenePod, SkeletonPod, SkinPod, SourceOverrideMap, TextureAsset, TexturePod } from '@forgeax/engine-types';
2
+ type SubAsset = {
3
+ readonly guid: string;
4
+ readonly sourceIndex: number;
5
+ readonly kind: string;
6
+ readonly sourceKey?: string;
7
+ };
8
+ export declare function buildMeshAsset(pod: MeshPod, guid: string, influences?: readonly {
9
+ jointIndices: Uint16Array;
10
+ jointWeights: Float32Array;
11
+ }[], materialContext?: {
12
+ readonly guidByIndex?: ReadonlyMap<number, string>;
13
+ readonly nameByIndex?: ReadonlyMap<number, string>;
14
+ readonly sourceKeyByIndex?: ReadonlyMap<number, string>;
15
+ readonly previousMaterialSlots?: readonly MeshMaterialSlotTopologyEntry[];
16
+ readonly materialSlotDefaultOverrides?: Readonly<Record<string, string | null>>;
17
+ readonly meshSourceKey?: string;
18
+ }): ImportedAsset;
19
+ export interface FbxDecodedTexture {
20
+ readonly texture: TextureAsset;
21
+ readonly bytes: Uint8Array;
22
+ readonly mediaType?: string;
23
+ readonly assetCodec?: AssetCodec;
24
+ }
25
+ export declare function toAssetPack(params: {
26
+ readonly meshes: readonly MeshPod[];
27
+ readonly scene: ScenePod;
28
+ readonly materials: readonly MaterialPod[];
29
+ readonly textures: readonly TexturePod[];
30
+ readonly skeleton: SkeletonPod;
31
+ readonly skin: SkinPod;
32
+ readonly animationClips: readonly AnimationClipPod[];
33
+ readonly subAssets: readonly SubAsset[];
34
+ readonly sourceOverrides?: SourceOverrideMap;
35
+ readonly decodedTextures?: ReadonlyMap<number, FbxDecodedTexture>;
36
+ }): readonly ImportedAsset[];
37
+ export {};
38
+ //# sourceMappingURL=to-asset-pack.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"to-asset-pack.d.ts","sourceRoot":"","sources":["../src/to-asset-pack.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EACV,gBAAgB,EAChB,UAAU,EAEV,aAAa,EAEb,WAAW,EAKX,6BAA6B,EAC7B,OAAO,EAEP,QAAQ,EACR,WAAW,EACX,OAAO,EACP,iBAAiB,EACjB,YAAY,EACZ,UAAU,EACX,MAAM,uBAAuB,CAAC;AAS/B,KAAK,QAAQ,GAAG;IACd,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B,CAAC;AAWF,wBAAgB,cAAc,CAC5B,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,MAAM,EACZ,UAAU,CAAC,EAAE,SAAS;IAAE,YAAY,EAAE,WAAW,CAAC;IAAC,YAAY,EAAE,YAAY,CAAA;CAAE,EAAE,EACjF,eAAe,GAAE;IACf,QAAQ,CAAC,WAAW,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnD,QAAQ,CAAC,WAAW,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnD,QAAQ,CAAC,gBAAgB,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxD,QAAQ,CAAC,qBAAqB,CAAC,EAAE,SAAS,6BAA6B,EAAE,CAAC;IAC1E,QAAQ,CAAC,4BAA4B,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC;IAChF,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;CAC5B,GACL,aAAa,CAqRf;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAC/B,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAC;CAClC;AAwMD,wBAAgB,WAAW,CAAC,MAAM,EAAE;IAClC,QAAQ,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE,CAAC;IACpC,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,SAAS,WAAW,EAAE,CAAC;IAC3C,QAAQ,CAAC,QAAQ,EAAE,SAAS,UAAU,EAAE,CAAC;IACzC,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,cAAc,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACrD,QAAQ,CAAC,SAAS,EAAE,SAAS,QAAQ,EAAE,CAAC;IACxC,QAAQ,CAAC,eAAe,CAAC,EAAE,iBAAiB,CAAC;IAC7C,QAAQ,CAAC,eAAe,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;CACnE,GAAG,SAAS,aAAa,EAAE,CAwP3B"}
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "@forgeax/engine-fbx",
3
+ "version": "0.1.2",
4
+ "private": false,
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "sideEffects": false,
8
+ "description": "FBX importer for the forgeax engine via ufbx compiled to WebAssembly. Single parser for both browser and Node; no Autodesk FBX SDK or native addon. Emits the engine FBX POD schema (meshes, nodes, materials, skeletons, skins, clips).",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.mjs"
13
+ },
14
+ "./package.json": "./package.json"
15
+ },
16
+ "main": "./dist/index.mjs",
17
+ "types": "./dist/index.d.ts",
18
+ "files": [
19
+ "dist",
20
+ "pkg",
21
+ "src",
22
+ "scripts",
23
+ "README.md"
24
+ ],
25
+ "dependencies": {
26
+ "@forgeax/engine-animation": "0.1.2",
27
+ "@forgeax/engine-math": "0.1.2",
28
+ "@forgeax/engine-render": "0.1.2",
29
+ "@forgeax/engine-scene": "0.1.2",
30
+ "@forgeax/engine-types": "0.1.2"
31
+ },
32
+ "devDependencies": {
33
+ "@forgeax/engine-assets-runtime": "0.1.2",
34
+ "@forgeax/engine-ecs": "0.1.2",
35
+ "@forgeax/engine-geometry": "0.1.2",
36
+ "@forgeax/engine-import": "0.1.2",
37
+ "@forgeax/engine-picking": "0.1.2",
38
+ "@forgeax/engine-runtime": "0.1.2",
39
+ "@types/node": "^20.14.0"
40
+ },
41
+ "forgeax": {
42
+ "metrics": {
43
+ "bundle-size": {
44
+ "enabled": true,
45
+ "path": "pkg/fbx-wasm.wasm",
46
+ "compression": "gzip",
47
+ "expected": 147043
48
+ },
49
+ "fps": {
50
+ "enabled": false,
51
+ "reason": "parser package: no render loop; FPS is reported by the apps that consume imported FBX assets"
52
+ },
53
+ "bench": {
54
+ "enabled": false,
55
+ "reason": "no hot-path bench at the FBX parse boundary yet; import cost is one-shot cook, not a per-frame path"
56
+ },
57
+ "gate": {
58
+ "enabled": true,
59
+ "command": "pnpm --filter @forgeax/engine-fbx test"
60
+ },
61
+ "spike-report": {
62
+ "enabled": false,
63
+ "reason": "not a spike package; productionised browser FBX parser sized by the bundle-size MetricKind above"
64
+ }
65
+ }
66
+ },
67
+ "scripts": {
68
+ "build:wasm": "node scripts/fetch-ufbx.mjs && node scripts/build-wasm.mjs",
69
+ "fetch-ufbx": "node scripts/fetch-ufbx.mjs",
70
+ "fetch-wasm": "node scripts/fetch-wasm.mjs",
71
+ "postinstall": "node scripts/ensure-wasm.mjs",
72
+ "build": "tsup",
73
+ "test": "vitest run"
74
+ }
75
+ }
@@ -0,0 +1,2 @@
1
+ async function Module(moduleArg={}){var Module=moduleArg;var ENVIRONMENT_IS_WEB=!!globalThis.window;var ENVIRONMENT_IS_WORKER=!!globalThis.WorkerGlobalScope;var ENVIRONMENT_IS_NODE=globalThis.process?.versions?.node&&globalThis.process?.type!="renderer";if(ENVIRONMENT_IS_NODE){const{createRequire}=await import("node:module");var require=createRequire(import.meta.url)}var programArgs=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var _scriptName=import.meta.url;var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("node:fs");if(_scriptName.startsWith("file:")){scriptDirectory=require("node:path").dirname(require("node:url").fileURLToPath(_scriptName))+"/"}readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}programArgs=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{readAsync=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;var isFileURI=filename=>filename.startsWith("file://");class EmscriptenEH{}class EmscriptenSjLj extends EmscriptenEH{}var runtimeInitialized=false;function getMemoryBuffer(){try{var b=wasmMemory.toResizableBuffer();return b}catch{}return wasmMemory.buffer}function updateMemoryViews(){if(HEAP8?.buffer?.resizable)return;var b=getMemoryBuffer();HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);HEAPU16=new Uint16Array(b);HEAP32=new Int32Array(b);HEAPU32=new Uint32Array(b);HEAPF32=new Float32Array(b);HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){var preRun=Module["preRun"];if(preRun){if(typeof preRun=="function")preRun=[preRun];onPreRuns.push(...preRun)}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;wasmExports["k"]()}function postRun(){var postRun=Module["postRun"];if(postRun){if(typeof postRun=="function")postRun=[postRun];onPostRuns.push(...postRun)}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what=`Aborted(${what})`;err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);throw e}var wasmBinaryFile;function findWasmBinary(){if(Module["locateFile"]){return locateFile("fbx-wasm.wasm")}return new URL("fbx-wasm.wasm",import.meta.url).href}function getBinarySync(file){if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={a:wasmImports};return imports}async function createWasm(){function receiveInstance(instance){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();var instantiateWasm=Module["instantiateWasm"];if(instantiateWasm){return new Promise(resolve=>{instantiateWasm(info,inst=>resolve(receiveInstance(inst)))})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var HEAP16;var HEAP32;var HEAP64;var HEAP8;var HEAPF32;var HEAPF64;var HEAPU16;var HEAPU32;var HEAPU64;var HEAPU8;var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var onPreRuns=[];var noExitRuntime=true;var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();var UTF8Decoder=globalThis.TextDecoder&&new TextDecoder;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx<endPtr){var u0=heapOrArray[idx++];if(!(u0&128)){str+=String.fromCharCode(u0);continue}var u1=heapOrArray[idx++]&63;if((u0&224)==192){str+=String.fromCharCode((u0&31)<<6|u1);continue}var u2=heapOrArray[idx++]&63;if((u0&240)==224){u0=(u0&15)<<12|u1<<6|u2}else{u0=(u0&7)<<18|u1<<12|u2<<6|heapOrArray[idx++]&63}if(u0<65536){str+=String.fromCharCode(u0)}else{var ch=u0-65536;str+=String.fromCharCode(55296|ch>>10,56320|ch&1023)}}return str};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var ___assert_fail=(condition,filename,line,func)=>abort(`Assertion failed: ${UTF8ToString(condition)}, at: `+[filename?UTF8ToString(filename):"unknown filename",line,func?UTF8ToString(func):"unknown function"]);var SYSCALLS={varargs:undefined,getStr(ptr){var ret=UTF8ToString(ptr);return ret}};function ___syscall_fcntl64(fd,cmd,varargs){SYSCALLS.varargs=varargs;return 0}function ___syscall_ioctl(fd,op,varargs){SYSCALLS.varargs=varargs;return 0}function ___syscall_openat(dirfd,path,flags,varargs){SYSCALLS.varargs=varargs}var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var _fd_close=fd=>52;var _fd_read=(fd,iov,iovcnt,pnum)=>52;var INT53_MAX=9007199254740992;var INT53_MIN=-9007199254740992;var bigintToI53Checked=num=>num<INT53_MIN||num>INT53_MAX?NaN:Number(num);function _fd_seek(fd,offset,whence,newOffset){offset=bigintToI53Checked(offset);return 70}var printCharBuffers=[null,[],[]];var printChar=(stream,curr)=>{var buffer=printCharBuffers[stream];if(curr===0||curr===10){(stream===1?out:err)(UTF8ArrayToString(buffer));buffer.length=0}else{buffer.push(curr)}};var _fd_write=(fd,iov,iovcnt,pnum)=>{var num=0;for(var i=0;i<iovcnt;i++){var ptr=HEAPU32[iov>>2];var len=HEAPU32[iov+4>>2];iov+=8;for(var j=0;j<len;j++){printChar(fd,HEAPU8[ptr+j])}num+=len}HEAPU32[pnum>>2]=num;return 0};var getCFunc=ident=>{var func=Module["_"+ident];return func};var writeArrayToMemory=(array,buffer)=>{HEAP8.set(array,buffer)};var lengthBytesUTF8=str=>{var len=0;for(var i=0;i<str.length;++i){var c=str.charCodeAt(i);if(c<=127){len++}else if(c<=2047){len+=2}else if(c>=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i<str.length;++i){var u=str.codePointAt(i);if(u<=127){if(outIdx>=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};var ccall=(ident,returnType,argTypes,args,opts)=>{var toC={string:str=>{var ret=0;if(str!==null&&str!==undefined&&str!==0){ret=stringToUTF8OnStack(str)}return ret},array:arr=>{var ret=stackAlloc(arr.length);writeArrayToMemory(arr,ret);return ret}};function convertReturnValue(ret){if(returnType==="string"){return UTF8ToString(ret)}if(returnType==="boolean")return Boolean(ret);return ret}var func=getCFunc(ident);var cArgs=[];var stack=0;if(args){for(var i=0;i<args.length;i++){var converter=toC[argTypes[i]];if(converter){if(stack===0)stack=stackSave();cArgs[i]=converter(args[i])}else{cArgs[i]=args[i]}}}var ret=func(...cArgs);function onDone(ret){if(stack!==0)stackRestore(stack);return convertReturnValue(ret)}ret=onDone(ret);return ret};var cwrap=(ident,returnType,argTypes,opts)=>{var numericArgs=!argTypes||argTypes.every(type=>type==="number"||type==="boolean");var numericRet=returnType!=="string";if(numericRet&&numericArgs&&!opts){return getCFunc(ident)}return(...args)=>ccall(ident,returnType,argTypes,args,opts)};{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["arguments"])programArgs=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];var preInit=Module["preInit"];if(preInit){if(typeof preInit=="function")Module["preInit"]=preInit=[preInit];while(preInit.length>0){preInit.shift()()}}}Module["ccall"]=ccall;Module["cwrap"]=cwrap;Module["UTF8ToString"]=UTF8ToString;var _malloc,_free,_parseFbxWasm,_getResultPtr,_getResultLen,_freeResult,__emscripten_stack_restore,__emscripten_stack_alloc,_emscripten_stack_get_current,memory,__indirect_function_table,wasmMemory;function assignWasmExports(wasmExports){_malloc=Module["_malloc"]=wasmExports["l"];_free=Module["_free"]=wasmExports["m"];_parseFbxWasm=Module["_parseFbxWasm"]=wasmExports["n"];_getResultPtr=Module["_getResultPtr"]=wasmExports["o"];_getResultLen=Module["_getResultLen"]=wasmExports["p"];_freeResult=Module["_freeResult"]=wasmExports["q"];__emscripten_stack_restore=wasmExports["r"];__emscripten_stack_alloc=wasmExports["s"];_emscripten_stack_get_current=wasmExports["t"];memory=wasmMemory=wasmExports["j"];__indirect_function_table=wasmExports["__indirect_function_table"]}var wasmImports={a:___assert_fail,d:___syscall_fcntl64,h:___syscall_ioctl,i:___syscall_openat,e:_emscripten_resize_heap,b:_fd_close,g:_fd_read,f:_fd_seek,c:_fd_write};async function run(){preRun();var setStatus=Module["setStatus"];if(setStatus){setStatus("Running...");await new Promise(resolve=>setTimeout(resolve,1));setTimeout(setStatus,1,"")}if(ABORT)return;initRuntime();Module["onRuntimeInitialized"]?.();postRun()}var wasmExports;wasmExports=await createWasm();await run();
2
+ ;return Module}export default Module;
Binary file
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env node
2
+ // build-wasm.mjs — Compile ufbx + bridge to WebAssembly via Emscripten.
3
+ //
4
+ // Prerequisites:
5
+ // 1. Install Emscripten SDK: https://emscripten.org/docs/getting_started/downloads.html
6
+ // 2. Run `emsdk activate latest` so `emcc` is on PATH
7
+ // 3. Run `pnpm fetch-ufbx` to download ufbx.h + ufbx.c
8
+ //
9
+ // Output: pkg/fbx-wasm.mjs + pkg/fbx-wasm.wasm
10
+ //
11
+ // The WASM module exports a single function `parseFbx(ptr, size) → ptr`
12
+ // that takes FBX file bytes and returns a JSON string pointer (same schema
13
+ // as @forgeax/engine-fbx binding.cc output).
14
+
15
+ import { execSync } from 'node:child_process';
16
+ import { existsSync, mkdirSync } from 'node:fs';
17
+ import { join, dirname } from 'node:path';
18
+ import { fileURLToPath } from 'node:url';
19
+
20
+ const __dirname = dirname(fileURLToPath(import.meta.url));
21
+ const ROOT = join(__dirname, '..');
22
+ const NATIVE = join(ROOT, 'src', 'native');
23
+ const PKG = join(ROOT, 'pkg');
24
+ const REPRODUCIBLE_PATH = `-ffile-prefix-map=${ROOT}=/forgeax/fbx`;
25
+
26
+ if (!existsSync(join(NATIVE, 'ufbx.c'))) {
27
+ console.error('ufbx.c not found. Run `pnpm fetch-ufbx` first.');
28
+ process.exit(1);
29
+ }
30
+
31
+ if (!existsSync(join(NATIVE, 'bridge.c'))) {
32
+ console.error('bridge.c not found in src/native/.');
33
+ process.exit(1);
34
+ }
35
+
36
+ // pkg/ is gitignored (zero-binary invariant) so a bare checkout has no such
37
+ // dir; emcc will not create the -o parent itself. Ensure it exists.
38
+ mkdirSync(PKG, { recursive: true });
39
+
40
+ // Emscripten compilation flags:
41
+ // -O3 : optimize for size+speed
42
+ // -s WASM=1 : emit .wasm (not asm.js)
43
+ // -s EXPORTED_FUNCTIONS: export malloc/free + our bridge function
44
+ // -s EXPORTED_RUNTIME_METHODS: expose ccall/cwrap/UTF8ToString/stringToUTF8
45
+ // -s ALLOW_MEMORY_GROWTH=1: FBX files can be large
46
+ // -s MODULARIZE=1 : wrap in a factory function (ESM-friendly)
47
+ // -s EXPORT_ES6=1 : emit ES6 module
48
+ // -s ENVIRONMENT=web,node : browser + Node; the Node glue self-loads the
49
+ // .wasm via fs (no manual wasmBinary hand-off needed)
50
+ // -s FILESYSTEM=0 : we use ufbx_load_memory, no FS needed
51
+ // -s SINGLE_FILE=0 : keep .wasm separate for streaming compilation
52
+ // -lm : link math library
53
+ const cmd = [
54
+ 'emcc',
55
+ '-O3',
56
+ REPRODUCIBLE_PATH,
57
+ '-s WASM=1',
58
+ '-s "EXPORTED_FUNCTIONS=[\'_parseFbxWasm\',\'_getResultPtr\',\'_getResultLen\',\'_freeResult\',\'_malloc\',\'_free\']"',
59
+ '-s "EXPORTED_RUNTIME_METHODS=[\'ccall\',\'cwrap\',\'HEAPU8\',\'UTF8ToString\']"',
60
+ '-s ALLOW_MEMORY_GROWTH=1',
61
+ '-s MODULARIZE=1',
62
+ '-s EXPORT_ES6=1',
63
+ '-s ENVIRONMENT=web,node',
64
+ '-s FILESYSTEM=0',
65
+ '-s STACK_SIZE=1048576',
66
+ '-lm',
67
+ '-I', NATIVE,
68
+ join(NATIVE, 'ufbx.c'),
69
+ join(NATIVE, 'bridge.c'),
70
+ '-o', join(PKG, 'fbx-wasm.mjs'),
71
+ ].join(' ');
72
+
73
+ console.log('Building WASM...');
74
+ console.log(` ${cmd}\n`);
75
+
76
+ try {
77
+ execSync(cmd, { stdio: 'inherit', cwd: ROOT });
78
+ console.log('\nBuild complete:');
79
+ console.log(` ${join(PKG, 'fbx-wasm.mjs')}`);
80
+ console.log(` ${join(PKG, 'fbx-wasm.wasm')}`);
81
+ } catch (e) {
82
+ console.error('\nEmscripten build failed. Is emcc on PATH?');
83
+ console.error('Install: https://emscripten.org/docs/getting_started/downloads.html');
84
+ process.exit(1);
85
+ }
@@ -0,0 +1,88 @@
1
+ // content-key.mjs — Single source of truth for the fbx-wasm release asset key.
2
+ //
3
+ // The pkg/ bundle (fbx-wasm.wasm + fbx-wasm.mjs emcc glue) is an emcc build
4
+ // artifact — NOT committed to git (zero-binary invariant, see
5
+ // packages/fbx/.gitignore). It is published to the `wasm-artifacts` GitHub
6
+ // Release, content-keyed so a bundle is uploaded once per unique source state
7
+ // and fetched deterministically.
8
+ //
9
+ // Like @forgeax/engine-wgpu-wasm and @forgeax/engine-codec (and unlike the old
10
+ // lone-.wasm scheme), the fbx pkg/ carries TWO files that emcc emits together —
11
+ // the .wasm binary plus its self-loading .mjs glue (MODULARIZE + EXPORT_ES6).
12
+ // src/index.ts imports the .mjs glue at runtime, so a fetch that grabs only the
13
+ // .wasm leaves consumers without a loader. The release asset is therefore the
14
+ // WHOLE pkg/ packed as a .tar.gz.
15
+ //
16
+ // What determines pkg/ output, and therefore the content key:
17
+ // - src/native/bridge.c — the hand-written C bridge over ufbx.
18
+ // - scripts/fetch-ufbx.mjs — pins the exact ufbx version (UFBX_VERSION); the
19
+ // upstream ufbx.h/.c is gitignored (downloaded on demand) but this pin
20
+ // uniquely fixes it, so hashing the pin script is equivalent to hashing the
21
+ // source.
22
+ // - scripts/build-wasm.mjs — the emcc flag set that emits the .wasm/.mjs pair
23
+ // bit-for-bit.
24
+ //
25
+ // Both scripts/fetch-wasm.mjs (consumer) and the CI publish job import this
26
+ // module so producer and consumer never disagree on the asset name (Pipeline
27
+ // Isolation + SSOT: architecture-principles #1, #4).
28
+
29
+ import { createHash } from 'node:crypto';
30
+ import { readFile } from 'node:fs/promises';
31
+ import { join, dirname, relative } from 'node:path';
32
+ import { fileURLToPath } from 'node:url';
33
+
34
+ const __dirname = dirname(fileURLToPath(import.meta.url));
35
+ export const PKG_ROOT = join(__dirname, '..');
36
+
37
+ export const RELEASE_TAG = 'wasm-artifacts';
38
+ export const UFBX_VERSION = 'v0.23.0';
39
+
40
+ /**
41
+ * Compute the full SHA-256 (hex) over every input that determines the pkg/
42
+ * output. Path is included in the hash so a rename alone shifts the key.
43
+ *
44
+ * Inputs: the C bridge source + the two build scripts that fully determine the
45
+ * emitted pair (fetch-ufbx.mjs pins the ufbx version, build-wasm.mjs the emcc
46
+ * flags).
47
+ */
48
+ export async function computeContentSha256() {
49
+ const inputs = [
50
+ join(PKG_ROOT, 'src', 'native', 'bridge.c'),
51
+ join(PKG_ROOT, 'scripts', 'fetch-ufbx.mjs'),
52
+ join(PKG_ROOT, 'scripts', 'build-wasm.mjs'),
53
+ ];
54
+ const hash = createHash('sha256');
55
+ for (const path of inputs) {
56
+ const rel = relative(PKG_ROOT, path).split('\\').join('/');
57
+ const content = await readFile(path);
58
+ hash.update(rel);
59
+ hash.update('\0');
60
+ hash.update(content);
61
+ hash.update('\0');
62
+ }
63
+ return hash.digest('hex');
64
+ }
65
+
66
+ export function buildAssetName(sha8) {
67
+ return `fbx-wasm-${UFBX_VERSION}-${sha8}.tar.gz`;
68
+ }
69
+
70
+ /** Convenience: returns { sha256, sha8, assetName }. */
71
+ export async function resolveAsset() {
72
+ const sha256 = await computeContentSha256();
73
+ const sha8 = sha256.slice(0, 8);
74
+ return { sha256, sha8, assetName: buildAssetName(sha8) };
75
+ }
76
+
77
+ // CLI: `node scripts/content-key.mjs` prints the asset name (used by CI shell).
78
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
79
+ resolveAsset()
80
+ .then(({ sha256, assetName }) => {
81
+ // Two lines: full sha then asset name, so shell can pick either.
82
+ process.stdout.write(`${sha256}\n${assetName}\n`);
83
+ })
84
+ .catch((e) => {
85
+ console.error('content-key: failed to compute:', e.message || e);
86
+ process.exit(1);
87
+ });
88
+ }
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ // ensure-wasm.mjs — postinstall best-effort hydration of the fbx pkg/ WASM.
3
+ // Thin config over scripts/lib/ensure-wasm-lib.mjs (shared with -codec, -wgpu-wasm).
4
+
5
+ import { existsSync } from 'node:fs';
6
+ import { dirname, join } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
10
+ const PKG = join(SCRIPT_DIR, '..', 'pkg');
11
+ const presenceMarkers = [join(PKG, 'fbx-wasm.mjs'), join(PKG, 'fbx-wasm.wasm')];
12
+ if (presenceMarkers.every(existsSync)) {
13
+ process.stdout.write('[fbx] pkg/ WASM already present -- skipping fetch.\n');
14
+ process.exit(0);
15
+ }
16
+ const { ensureWasm } = await import('../../../scripts/lib/ensure-wasm-lib.mjs');
17
+
18
+ process.exit(
19
+ ensureWasm({
20
+ pkgLabel: 'fbx',
21
+ presenceMarkers,
22
+ fetchScript: join(SCRIPT_DIR, 'fetch-wasm.mjs'),
23
+ skipEnv: 'FORGEAX_SKIP_FBX_WASM_FETCH',
24
+ buildHint:
25
+ 'pnpm -F @forgeax/engine-fbx fetch-wasm (needs network + published release)\n' +
26
+ ' or pnpm -F @forgeax/engine-fbx build:wasm (needs Emscripten emcc)',
27
+ }),
28
+ );
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+ // fetch-ufbx.mjs — Download ufbx.h + ufbx.c from the official GitHub repo.
3
+ // Usage: node scripts/fetch-ufbx.mjs [version]
4
+ // Default version: v0.23.0 (latest stable as of 2026-07)
5
+
6
+ import { writeFileSync, mkdirSync, existsSync } from 'node:fs';
7
+ import { join, dirname } from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ const __dirname = dirname(fileURLToPath(import.meta.url));
11
+ const NATIVE_DIR = join(__dirname, '..', 'src', 'native');
12
+
13
+ const VERSION = process.argv[2] || 'v0.23.0';
14
+ const BASE_URL = `https://raw.githubusercontent.com/ufbx/ufbx/${VERSION}`;
15
+
16
+ const FILES = ['ufbx.h', 'ufbx.c'];
17
+
18
+ async function fetchFile(name) {
19
+ const url = `${BASE_URL}/${name}`;
20
+ console.log(`Fetching ${url} ...`);
21
+ const resp = await fetch(url);
22
+ if (!resp.ok) throw new Error(`Failed to fetch ${url}: ${resp.status}`);
23
+ return resp.text();
24
+ }
25
+
26
+ async function main() {
27
+ if (!existsSync(NATIVE_DIR)) mkdirSync(NATIVE_DIR, { recursive: true });
28
+
29
+ for (const file of FILES) {
30
+ const dest = join(NATIVE_DIR, file);
31
+ if (existsSync(dest)) {
32
+ console.log(` ${file} already exists, skipping (delete to re-fetch)`);
33
+ continue;
34
+ }
35
+ const content = await fetchFile(file);
36
+ writeFileSync(dest, content);
37
+ console.log(` → ${dest} (${(content.length / 1024).toFixed(0)} KB)`);
38
+ }
39
+ console.log('Done.');
40
+ }
41
+
42
+ main().catch((e) => { console.error(e); process.exit(1); });