@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
@@ -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,31 @@
1
+ import type { AnimationClipPod, ImportedAsset, MaterialPod, MeshMaterialSlotTopologyEntry, MeshPod, ScenePod, SkeletonPod, SkinPod, SourceOverrideMap, 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 declare function toAssetPack(params: {
20
+ readonly meshes: readonly MeshPod[];
21
+ readonly scene: ScenePod;
22
+ readonly materials: readonly MaterialPod[];
23
+ readonly textures: readonly TexturePod[];
24
+ readonly skeleton: SkeletonPod;
25
+ readonly skin: SkinPod;
26
+ readonly animationClips: readonly AnimationClipPod[];
27
+ readonly subAssets: readonly SubAsset[];
28
+ readonly sourceOverrides?: SourceOverrideMap;
29
+ }): readonly ImportedAsset[];
30
+ export {};
31
+ //# 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,EAEhB,aAAa,EAEb,WAAW,EAGX,6BAA6B,EAC7B,OAAO,EAEP,QAAQ,EACR,WAAW,EACX,OAAO,EACP,iBAAiB,EACjB,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;AAyJD,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;CAC9C,GAAG,SAAS,aAAa,EAAE,CA4O3B"}
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "@forgeax/engine-fbx",
3
+ "version": "0.0.0-dev.8d955ade1c79",
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.0.0-dev.8d955ade1c79",
27
+ "@forgeax/engine-math": "0.0.0-dev.8d955ade1c79",
28
+ "@forgeax/engine-pack": "0.0.0-dev.8d955ade1c79",
29
+ "@forgeax/engine-render": "0.0.0-dev.8d955ade1c79",
30
+ "@forgeax/engine-scene": "0.0.0-dev.8d955ade1c79",
31
+ "@forgeax/engine-types": "0.0.0-dev.8d955ade1c79"
32
+ },
33
+ "devDependencies": {
34
+ "@forgeax/engine-assets-runtime": "0.0.0-dev.8d955ade1c79",
35
+ "@forgeax/engine-import": "0.0.0-dev.8d955ade1c79",
36
+ "@forgeax/engine-picking": "0.0.0-dev.8d955ade1c79",
37
+ "@forgeax/engine-runtime": "0.0.0-dev.8d955ade1c79",
38
+ "@types/node": "^20.14.0"
39
+ },
40
+ "forgeax": {
41
+ "metrics": {
42
+ "bundle-size": {
43
+ "enabled": true,
44
+ "path": "pkg/fbx-wasm.wasm",
45
+ "compression": "gzip",
46
+ "expected": 147043
47
+ },
48
+ "fps": {
49
+ "enabled": false,
50
+ "reason": "parser package: no render loop; FPS is reported by the apps that consume imported FBX assets"
51
+ },
52
+ "bench": {
53
+ "enabled": false,
54
+ "reason": "no hot-path bench at the FBX parse boundary yet; import cost is one-shot cook, not a per-frame path"
55
+ },
56
+ "gate": {
57
+ "enabled": true,
58
+ "command": "pnpm --filter @forgeax/engine-fbx test"
59
+ },
60
+ "spike-report": {
61
+ "enabled": false,
62
+ "reason": "not a spike package; productionised browser FBX parser sized by the bundle-size MetricKind above"
63
+ }
64
+ }
65
+ },
66
+ "scripts": {
67
+ "build:wasm": "node scripts/fetch-ufbx.mjs && node scripts/build-wasm.mjs",
68
+ "fetch-ufbx": "node scripts/fetch-ufbx.mjs",
69
+ "fetch-wasm": "node scripts/fetch-wasm.mjs",
70
+ "postinstall": "node scripts/ensure-wasm.mjs",
71
+ "build": "tsup",
72
+ "test": "vitest run"
73
+ }
74
+ }
@@ -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); });
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+ // fetch-wasm.mjs — Download the pre-built fbx-wasm pkg/ bundle from GitHub
3
+ // Releases so no-Emscripten consumers (Studio setup, editor standalone, CI
4
+ // typecheck, fresh clones) can resolve @forgeax/engine-fbx without an emcc
5
+ // toolchain.
6
+ //
7
+ // Like @forgeax/engine-wgpu-wasm and @forgeax/engine-codec, the fbx pkg/ carries
8
+ // TWO files emcc emits together — fbx-wasm.wasm + its self-loading fbx-wasm.mjs
9
+ // glue (MODULARIZE + EXPORT_ES6). src/index.ts imports the .mjs glue at runtime,
10
+ // so fetching only the .wasm would leave consumers without a loader. The release
11
+ // asset is therefore the WHOLE pkg/ packed as a content-keyed .tar.gz; this
12
+ // script downloads and extracts it into packages/fbx/pkg/.
13
+ //
14
+ // The content key (see content-key.mjs) is the SSOT shared with the CI publish
15
+ // job, so producer and consumer always agree on the asset name.
16
+ //
17
+ // Usage:
18
+ // pnpm -F @forgeax/engine-fbx fetch-wasm
19
+ // node scripts/fetch-wasm.mjs
20
+ //
21
+ // Environment:
22
+ // GITHUB_TOKEN — optional Bearer token for private-repo auth. Falls back to
23
+ // `gh auth token`. Anonymous requests work for public repos.
24
+ //
25
+ // Exit codes: 0 on success; 1 on any FetchError. Callers that want a build
26
+ // fallback treat a non-zero exit as "release unavailable, compile via
27
+ // build:wasm" (graceful degradation: architecture-principles #9).
28
+
29
+ import { rm, readdir } from 'node:fs/promises';
30
+ import { join } from 'node:path';
31
+
32
+ import { FetchError, getGitOrigin, getReleaseAsset, downloadAsset, extractTarball } from '../../../scripts/lib/fetch-wasm-lib.mjs';
33
+ import { PKG_ROOT, RELEASE_TAG, resolveAsset } from './content-key.mjs';
34
+
35
+ const PKG_DIR = join(PKG_ROOT, 'pkg');
36
+ const TMP_TARBALL = join(PKG_ROOT, '.pkg-fetch.tar.gz');
37
+
38
+ const PKG_LABEL = 'fbx-wasm';
39
+ const BUILD_HINT =
40
+ 'compile locally with `pnpm -F @forgeax/engine-fbx build:wasm` (needs the Emscripten emcc toolchain)';
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Main
44
+ // ---------------------------------------------------------------------------
45
+
46
+ async function main() {
47
+ console.log('Resolving git origin...');
48
+ const { owner, repo } = getGitOrigin();
49
+ console.log(` origin: ${owner}/${repo}`);
50
+
51
+ console.log('Computing content key (bridge.c + fetch-ufbx.mjs + build-wasm.mjs)...');
52
+ const { sha256, assetName } = await resolveAsset();
53
+ console.log(` content SHA256: ${sha256}`);
54
+ console.log(` asset name: ${assetName}`);
55
+
56
+ console.log(`Fetching release tag "${RELEASE_TAG}" from ${owner}/${repo}...`);
57
+ const asset = await getReleaseAsset(owner, repo, RELEASE_TAG, assetName, {
58
+ pkgLabel: PKG_LABEL,
59
+ buildHint: BUILD_HINT,
60
+ missingAssetHint:
61
+ 'bridge.c or the fbx build scripts have uncommitted changes, or the release for this content is not yet published.',
62
+ });
63
+
64
+ console.log(`Downloading ${assetName} (${(asset.size / 1024).toFixed(0)} KB)...`);
65
+ await downloadAsset(asset, TMP_TARBALL, {
66
+ owner,
67
+ repo,
68
+ tag: RELEASE_TAG,
69
+ pkgLabel: PKG_LABEL,
70
+ buildHint: BUILD_HINT,
71
+ });
72
+
73
+ console.log(`Extracting into ${PKG_DIR} ...`);
74
+ await extractTarball(TMP_TARBALL, PKG_DIR);
75
+ await rm(TMP_TARBALL, { force: true });
76
+
77
+ const members = await readdir(PKG_DIR);
78
+ console.log(` pkg/ now holds: ${members.join(', ')}`);
79
+ console.log('Done.');
80
+ }
81
+
82
+ main().catch(async (e) => {
83
+ await rm(TMP_TARBALL, { force: true }).catch(() => {});
84
+ if (e instanceof FetchError) {
85
+ console.error(`\n[${e.code}] ${e.message}`);
86
+ console.error(`Hint: ${e.hint}`);
87
+ } else {
88
+ console.error('\nUnexpected error:', e.message || e);
89
+ }
90
+ process.exit(1);
91
+ });
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env node
2
+ // freeze-snapshot.mjs (historically parity-diff.mjs) — re-freeze the parity
3
+ // regression snapshots from live ufbx output.
4
+ //
5
+ // History: during M1 this script double-ran ufbx (WASM) vs the in-repo FBX SDK
6
+ // binding to prove field-for-field parity before the SDK was deleted (M2). Once
7
+ // the SDK left the repo that diff half became unrunnable, so it was removed; the
8
+ // human-signed parity baseline it produced now lives frozen under
9
+ // __tests__/__snapshots__/ and parity-snapshot.test.ts asserts live ufbx output
10
+ // against it (see that file's header). What remains here is the re-freeze tool:
11
+ // it recomputes the compact structural digest from live ufbx and overwrites the
12
+ // snapshots. Re-freezing changes the human-signed baseline, so run it only with
13
+ // a deliberate parity re-sign (charter: Human as Final Authority).
14
+ //
15
+ // Usage:
16
+ // pnpm -F @forgeax/engine-fbx build # dist/ must be current
17
+ // node scripts/parity-diff.mjs # print each sample's digest
18
+ // node scripts/parity-diff.mjs --freeze # overwrite __tests__/__snapshots__/*.json
19
+ //
20
+ // The digest() below MUST stay identical to parity-snapshot.test.ts's copy so
21
+ // the freeze and the assertion agree (the .mjs script and the .ts test cannot
22
+ // cleanly import each other).
23
+
24
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
25
+ import { dirname, join } from 'node:path';
26
+ import { fileURLToPath } from 'node:url';
27
+ import { initFbxWasm, parseFbxToObject } from '../dist/index.mjs';
28
+
29
+ const HERE = dirname(fileURLToPath(import.meta.url));
30
+ const PKG = join(HERE, '..');
31
+ const VENDOR = join(PKG, '../../forgeax-engine-assets/vendor/fbx-test');
32
+ const SNAP_DIR = join(PKG, '__tests__/__snapshots__');
33
+
34
+ const SAMPLES = ['cube', 'humanoid'];
35
+
36
+ const ufbxRaw = (name) =>
37
+ parseFbxToObject(new Uint8Array(readFileSync(join(VENDOR, `${name}.fbx`))));
38
+
39
+ // A compact, git-friendly structural digest of the raw ufbx output. The full
40
+ // raw dump is ~13 MB (dense 9s animation keyframes) — too large to commit — so
41
+ // the frozen snapshot captures the parity-critical structure plus representative
42
+ // value spot-checks. It stays falsifiable: any bridge regression on counts,
43
+ // material, geometry bounds, node roster, or sampled key values trips it.
44
+ function bboxOf(vertices) {
45
+ const min = [Infinity, Infinity, Infinity];
46
+ const max = [-Infinity, -Infinity, -Infinity];
47
+ for (let i = 0; i < vertices.length; i += 3) {
48
+ for (let k = 0; k < 3; k++) {
49
+ const v = vertices[i + k];
50
+ if (v < min[k]) min[k] = v;
51
+ if (v > max[k]) max[k] = v;
52
+ }
53
+ }
54
+ return { min: min.map((x) => +x.toFixed(4)), max: max.map((x) => +x.toFixed(4)) };
55
+ }
56
+ function digest(raw) {
57
+ const meshes = (raw.meshes ?? []).map((m) => ({
58
+ name: m.name,
59
+ vertexFloats: m.vertices.length,
60
+ indexCount: (m.indices ?? []).length,
61
+ bbox: bboxOf(m.vertices),
62
+ attributes: Object.keys(m.attributes ?? {}).sort(),
63
+ }));
64
+ const nodes = (raw.nodes ?? []).map((n) => n.name);
65
+ const materials = (raw.materials ?? []).map((m) => ({
66
+ name: m.name,
67
+ kind: m.kind,
68
+ ...(m.diffuse && { diffuse: m.diffuse.map((x) => +x.toFixed(6)) }),
69
+ ...(m.shininess !== undefined && { shininess: +m.shininess.toFixed(4) }),
70
+ ...(m.stingrayProps && { stingrayProps: m.stingrayProps }),
71
+ }));
72
+ const clips = (raw.clips ?? []).map((c) => ({
73
+ name: c.name,
74
+ duration: +c.duration.toFixed(6),
75
+ channelCount: c.channels.length,
76
+ channelsByProperty: {
77
+ translation: c.channels.filter((ch) => ch.property === 'translation').length,
78
+ rotation: c.channels.filter((ch) => ch.property === 'rotation').length,
79
+ scale: c.channels.filter((ch) => ch.property === 'scale').length,
80
+ },
81
+ // Spot-check the first channel's first/last key so value regressions trip.
82
+ firstChannel: c.channels[0] && {
83
+ targetNode: c.channels[0].targetNode,
84
+ property: c.channels[0].property,
85
+ keyCount: c.channels[0].keyTimes.length,
86
+ firstKeyValues: c.channels[0].keyValues.slice(0, 4).map((x) => +x.toFixed(4)),
87
+ },
88
+ }));
89
+ const skeletons = (raw.skeletons ?? []).map((s) => ({
90
+ jointCount: s.jointCount,
91
+ jointPaths: s.jointPaths,
92
+ }));
93
+ const skins = (raw.skins ?? []).map((s) => ({
94
+ meshSourceIndex: s.meshSourceIndex,
95
+ vertexCount: s.vertexCount,
96
+ jointPathCount: s.jointPaths.length,
97
+ }));
98
+ return { meshes, nodes, materials, clips, skeletons, skins };
99
+ }
100
+
101
+ async function main() {
102
+ const freeze = process.argv.includes('--freeze');
103
+ await initFbxWasm();
104
+
105
+ const frozen = {};
106
+ for (const name of SAMPLES) {
107
+ const raw = ufbxRaw(name);
108
+ frozen[name] = digest(raw);
109
+ console.log(`[${name}] ${JSON.stringify(frozen[name]).length} bytes digest`);
110
+ }
111
+
112
+ if (freeze) {
113
+ mkdirSync(SNAP_DIR, { recursive: true });
114
+ for (const name of SAMPLES) {
115
+ const dst = join(SNAP_DIR, `${name}-snapshot.json`);
116
+ writeFileSync(dst, `${JSON.stringify(frozen[name], null, 2)}\n`);
117
+ console.log(`froze ${dst}`);
118
+ }
119
+ }
120
+
121
+ process.exit(0);
122
+ }
123
+
124
+ main().catch((e) => {
125
+ console.error(e);
126
+ process.exit(1);
127
+ });