@forgeax/engine-fbx 0.1.4 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -17
- package/dist/.tsbuildinfo +1 -1
- package/dist/fbx-importer.d.ts +1 -2
- package/dist/fbx-importer.d.ts.map +1 -1
- package/dist/index.mjs +129 -891
- package/dist/index.mjs.map +1 -1
- package/dist/parse-material.d.ts +1 -7
- package/dist/parse-material.d.ts.map +1 -1
- package/dist/parse-texture.d.ts +1 -5
- package/dist/parse-texture.d.ts.map +1 -1
- package/dist/to-asset-pack.d.ts +1 -8
- package/dist/to-asset-pack.d.ts.map +1 -1
- package/package.json +11 -12
- package/pkg/fbx-wasm.wasm +0 -0
- package/scripts/content-key.mjs +6 -5
- package/scripts/fetch-ufbx.mjs +15 -3
- package/scripts/ufbx-source.lock.json +15 -0
- package/src/__tests__/blendshape-import.integration.test.ts +12 -18
- package/src/__tests__/fbx-importer.test.ts +1 -165
- package/src/__tests__/index.test.ts +1 -0
- package/src/fbx-importer.ts +10 -236
- package/src/native/bridge.c +0 -118
- package/src/parse-material.ts +1 -49
- package/src/parse-texture.ts +3 -17
- package/src/to-asset-pack.ts +19 -90
- package/dist/__tests__/asset-runtime-fixture.d.ts +0 -9
- package/dist/__tests__/asset-runtime-fixture.d.ts.map +0 -1
- package/src/__tests__/asset-runtime-fixture.ts +0 -251
package/dist/parse-material.d.ts
CHANGED
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import type { MaterialPod
|
|
2
|
-
export interface FbxRawTextureBinding {
|
|
3
|
-
readonly slot: MaterialTextureBindingPod['slot'];
|
|
4
|
-
readonly textureIndex: number;
|
|
5
|
-
readonly texCoord?: number;
|
|
6
|
-
}
|
|
1
|
+
import type { MaterialPod } from '@forgeax/engine-types';
|
|
7
2
|
/** JSON shape emitted by binding.cc WriteMaterials for a single material. */
|
|
8
3
|
export interface FbxRawMaterial {
|
|
9
4
|
readonly name?: string;
|
|
@@ -12,7 +7,6 @@ export interface FbxRawMaterial {
|
|
|
12
7
|
readonly diffuse?: readonly [number, number, number];
|
|
13
8
|
readonly shininess?: number;
|
|
14
9
|
readonly specular?: readonly [number, number, number];
|
|
15
|
-
readonly textureBindings?: readonly FbxRawTextureBinding[];
|
|
16
10
|
readonly stingrayProps?: {
|
|
17
11
|
readonly baseColor?: readonly [number, number, number];
|
|
18
12
|
readonly metallic?: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parse-material.d.ts","sourceRoot":"","sources":["../src/parse-material.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,WAAW,EAAE,
|
|
1
|
+
{"version":3,"file":"parse-material.d.ts","sourceRoot":"","sources":["../src/parse-material.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAEzD,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,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,CAiDnF"}
|
package/dist/parse-texture.d.ts
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import type { TexturePod } from '@forgeax/engine-types';
|
|
2
2
|
export interface FbxRawTexture {
|
|
3
3
|
readonly name?: string;
|
|
4
|
-
readonly filePath
|
|
5
|
-
readonly relativeFilePath?: string;
|
|
6
|
-
readonly absoluteFilePath?: string;
|
|
7
|
-
readonly embeddedBytes?: readonly number[];
|
|
8
|
-
readonly type?: 'file' | 'layered' | 'procedural' | 'shader' | 'unknown';
|
|
4
|
+
readonly filePath: string;
|
|
9
5
|
readonly sourceIndex: number;
|
|
10
6
|
}
|
|
11
7
|
export interface FbxRawTextures {
|
|
@@ -1 +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,
|
|
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,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;CAC9C;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,cAAc,GAAG,UAAU,EAAE,CAO/D"}
|
package/dist/to-asset-pack.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AnimationClipPod,
|
|
1
|
+
import type { AnimationClipPod, ImportedAsset, MaterialPod, MeshMaterialSlotTopologyEntry, MeshPod, ScenePod, SkeletonPod, SkinPod, SourceOverrideMap, TexturePod } from '@forgeax/engine-types';
|
|
2
2
|
type SubAsset = {
|
|
3
3
|
readonly guid: string;
|
|
4
4
|
readonly sourceIndex: number;
|
|
@@ -16,12 +16,6 @@ export declare function buildMeshAsset(pod: MeshPod, guid: string, influences?:
|
|
|
16
16
|
readonly materialSlotDefaultOverrides?: Readonly<Record<string, string | null>>;
|
|
17
17
|
readonly meshSourceKey?: string;
|
|
18
18
|
}): ImportedAsset;
|
|
19
|
-
export interface FbxDecodedTexture {
|
|
20
|
-
readonly texture: TextureAsset;
|
|
21
|
-
readonly bytes: Uint8Array;
|
|
22
|
-
readonly mediaType?: string;
|
|
23
|
-
readonly assetCodec?: AssetCodec;
|
|
24
|
-
}
|
|
25
19
|
export declare function toAssetPack(params: {
|
|
26
20
|
readonly meshes: readonly MeshPod[];
|
|
27
21
|
readonly scene: ScenePod;
|
|
@@ -32,7 +26,6 @@ export declare function toAssetPack(params: {
|
|
|
32
26
|
readonly animationClips: readonly AnimationClipPod[];
|
|
33
27
|
readonly subAssets: readonly SubAsset[];
|
|
34
28
|
readonly sourceOverrides?: SourceOverrideMap;
|
|
35
|
-
readonly decodedTextures?: ReadonlyMap<number, FbxDecodedTexture>;
|
|
36
29
|
}): readonly ImportedAsset[];
|
|
37
30
|
export {};
|
|
38
31
|
//# sourceMappingURL=to-asset-pack.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"to-asset-pack.d.ts","sourceRoot":"","sources":["../src/to-asset-pack.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EACV,gBAAgB,
|
|
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgeax/engine-fbx",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -23,19 +23,18 @@
|
|
|
23
23
|
"README.md"
|
|
24
24
|
],
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@forgeax/engine-animation": "0.1.
|
|
27
|
-
"@forgeax/engine-math": "0.1.
|
|
28
|
-
"@forgeax/engine-
|
|
29
|
-
"@forgeax/engine-
|
|
30
|
-
"@forgeax/engine-
|
|
26
|
+
"@forgeax/engine-animation": "0.1.7",
|
|
27
|
+
"@forgeax/engine-math": "0.1.7",
|
|
28
|
+
"@forgeax/engine-pack": "0.1.7",
|
|
29
|
+
"@forgeax/engine-render": "0.1.7",
|
|
30
|
+
"@forgeax/engine-scene": "0.1.7",
|
|
31
|
+
"@forgeax/engine-types": "0.1.7"
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|
|
33
|
-
"@forgeax/engine-assets-runtime": "0.1.
|
|
34
|
-
"@forgeax/engine-
|
|
35
|
-
"@forgeax/engine-
|
|
36
|
-
"@forgeax/engine-
|
|
37
|
-
"@forgeax/engine-picking": "0.1.4",
|
|
38
|
-
"@forgeax/engine-runtime": "0.1.4",
|
|
34
|
+
"@forgeax/engine-assets-runtime": "0.1.7",
|
|
35
|
+
"@forgeax/engine-import": "0.1.7",
|
|
36
|
+
"@forgeax/engine-picking": "0.1.7",
|
|
37
|
+
"@forgeax/engine-runtime": "0.1.7",
|
|
39
38
|
"@types/node": "^20.14.0"
|
|
40
39
|
},
|
|
41
40
|
"forgeax": {
|
package/pkg/fbx-wasm.wasm
CHANGED
|
Binary file
|
package/scripts/content-key.mjs
CHANGED
|
@@ -15,10 +15,8 @@
|
|
|
15
15
|
//
|
|
16
16
|
// What determines pkg/ output, and therefore the content key:
|
|
17
17
|
// - src/native/bridge.c — the hand-written C bridge over ufbx.
|
|
18
|
-
// - scripts/fetch-ufbx.mjs
|
|
19
|
-
// upstream ufbx.h/.c
|
|
20
|
-
// uniquely fixes it, so hashing the pin script is equivalent to hashing the
|
|
21
|
-
// source.
|
|
18
|
+
// - scripts/fetch-ufbx.mjs + ufbx-source.lock.json — pin and verify the exact
|
|
19
|
+
// upstream ufbx.h/.c bytes, which are gitignored and downloaded on demand.
|
|
22
20
|
// - scripts/build-wasm.mjs — the emcc flag set that emits the .wasm/.mjs pair
|
|
23
21
|
// bit-for-bit.
|
|
24
22
|
//
|
|
@@ -35,7 +33,9 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
35
33
|
export const PKG_ROOT = join(__dirname, '..');
|
|
36
34
|
|
|
37
35
|
export const RELEASE_TAG = 'wasm-artifacts';
|
|
38
|
-
export const UFBX_VERSION =
|
|
36
|
+
export const UFBX_VERSION = JSON.parse(
|
|
37
|
+
await readFile(join(PKG_ROOT, 'scripts', 'ufbx-source.lock.json'), 'utf8'),
|
|
38
|
+
).version;
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
41
|
* Compute the full SHA-256 (hex) over every input that determines the pkg/
|
|
@@ -49,6 +49,7 @@ export async function computeContentSha256() {
|
|
|
49
49
|
const inputs = [
|
|
50
50
|
join(PKG_ROOT, 'src', 'native', 'bridge.c'),
|
|
51
51
|
join(PKG_ROOT, 'scripts', 'fetch-ufbx.mjs'),
|
|
52
|
+
join(PKG_ROOT, 'scripts', 'ufbx-source.lock.json'),
|
|
52
53
|
join(PKG_ROOT, 'scripts', 'build-wasm.mjs'),
|
|
53
54
|
];
|
|
54
55
|
const hash = createHash('sha256');
|
package/scripts/fetch-ufbx.mjs
CHANGED
|
@@ -3,18 +3,28 @@
|
|
|
3
3
|
// Usage: node scripts/fetch-ufbx.mjs [version]
|
|
4
4
|
// Default version: v0.23.0 (latest stable as of 2026-07)
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
import { writeFileSync, mkdirSync, existsSync, readFileSync } from 'node:fs';
|
|
7
8
|
import { join, dirname } from 'node:path';
|
|
8
9
|
import { fileURLToPath } from 'node:url';
|
|
9
10
|
|
|
10
11
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
12
|
const NATIVE_DIR = join(__dirname, '..', 'src', 'native');
|
|
12
13
|
|
|
13
|
-
const
|
|
14
|
+
const LOCK = JSON.parse(readFileSync(join(__dirname, 'ufbx-source.lock.json'), 'utf8'));
|
|
15
|
+
const VERSION = process.argv[2] || LOCK.version;
|
|
14
16
|
const BASE_URL = `https://raw.githubusercontent.com/ufbx/ufbx/${VERSION}`;
|
|
15
17
|
|
|
16
18
|
const FILES = ['ufbx.h', 'ufbx.c'];
|
|
17
19
|
|
|
20
|
+
function verify(name, content) {
|
|
21
|
+
const expected = LOCK.files[name]?.sha256;
|
|
22
|
+
const observed = createHash('sha256').update(content).digest('hex');
|
|
23
|
+
if (!expected || observed !== expected) {
|
|
24
|
+
throw new Error(`ufbx-source-digest-mismatch: ${name} expected ${expected ?? 'missing'} observed ${observed}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
18
28
|
async function fetchFile(name) {
|
|
19
29
|
const url = `${BASE_URL}/${name}`;
|
|
20
30
|
console.log(`Fetching ${url} ...`);
|
|
@@ -29,10 +39,12 @@ async function main() {
|
|
|
29
39
|
for (const file of FILES) {
|
|
30
40
|
const dest = join(NATIVE_DIR, file);
|
|
31
41
|
if (existsSync(dest)) {
|
|
32
|
-
|
|
42
|
+
verify(file, readFileSync(dest));
|
|
43
|
+
console.log(` ${file} already exists and matches the source lock`);
|
|
33
44
|
continue;
|
|
34
45
|
}
|
|
35
46
|
const content = await fetchFile(file);
|
|
47
|
+
verify(file, content);
|
|
36
48
|
writeFileSync(dest, content);
|
|
37
49
|
console.log(` → ${dest} (${(content.length / 1024).toFixed(0)} KB)`);
|
|
38
50
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "forgeax-ufbx-source-lock/v1",
|
|
3
|
+
"version": "v0.23.0",
|
|
4
|
+
"baseUrl": "https://raw.githubusercontent.com/ufbx/ufbx/v0.23.0",
|
|
5
|
+
"files": {
|
|
6
|
+
"ufbx.h": {
|
|
7
|
+
"sha256": "942481725372d2ac4da5e77a062b47c20054a3440e7ee09a6043f99fe1f130ed",
|
|
8
|
+
"bytes": 223898
|
|
9
|
+
},
|
|
10
|
+
"ufbx.c": {
|
|
11
|
+
"sha256": "7d8d6ae4373f71692f295ff49ee0826466306ebcaa80b0e587c13ed047b98cea",
|
|
12
|
+
"bytes": 1181162
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { meshAssetKind } from '@forgeax/engine-geometry';
|
|
1
|
+
import { AssetRegistry } from '@forgeax/engine-assets-runtime';
|
|
3
2
|
import { ImporterRegistry, type RunImportMeta, runImport } from '@forgeax/engine-import';
|
|
3
|
+
import type { MeshAsset } from '@forgeax/engine-types';
|
|
4
4
|
import { describe, expect, it } from 'vitest';
|
|
5
5
|
import { fbxImporter } from '../fbx-importer.js';
|
|
6
6
|
import { initFbxWasm, parseFbx } from '../index.js';
|
|
7
|
-
import { createAssetRuntimeFixture, installMeshDecoder } from './asset-runtime-fixture';
|
|
8
7
|
|
|
9
8
|
const MESH_GUID = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa';
|
|
10
9
|
const sourceUrl = new URL(
|
|
@@ -53,7 +52,6 @@ describe('real FBX BlendShape import through ufbx WASM', () => {
|
|
|
53
52
|
if (!result.ok || 'skipped' in result.value) throw new Error('expected FBX DDC pack');
|
|
54
53
|
const entry = result.value.pack.assets.find((asset) => asset.guid === MESH_GUID);
|
|
55
54
|
expect(entry?.kind).toBe('mesh');
|
|
56
|
-
if (entry === undefined) return;
|
|
57
55
|
const payload = entry?.payload as {
|
|
58
56
|
readonly vertices: readonly number[];
|
|
59
57
|
readonly morphTargets?: readonly { readonly position?: readonly number[] }[];
|
|
@@ -64,20 +62,16 @@ describe('real FBX BlendShape import through ufbx WASM', () => {
|
|
|
64
62
|
expect(payload.morphWeights).toEqual([0.25, 0.5]);
|
|
65
63
|
expect(payload.vertices.length).toBe(3 * 12);
|
|
66
64
|
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
expect(loaded.value.morphTargets).toHaveLength(2);
|
|
78
|
-
} finally {
|
|
79
|
-
assets.dispose();
|
|
80
|
-
}
|
|
65
|
+
const runtime = new AssetRegistry({} as never);
|
|
66
|
+
const parsedMesh = runtime.parseAssetPayload('mesh', entry?.payload ?? {});
|
|
67
|
+
expect(parsedMesh).toMatchObject({ kind: 'mesh' });
|
|
68
|
+
expect(runtime.catalog(MESH_GUID, parsedMesh as never).ok).toBe(true);
|
|
69
|
+
const loaded = await runtime.loadByGuid<MeshAsset>(runtime.parseGuid(MESH_GUID));
|
|
70
|
+
expect(loaded.ok).toBe(true);
|
|
71
|
+
if (!loaded.ok) return;
|
|
72
|
+
expect(loaded.value.kind).toBe('mesh');
|
|
73
|
+
expect(Array.from(loaded.value.morphWeights ?? [])).toEqual([0.25, 0.5]);
|
|
74
|
+
expect(loaded.value.morphTargets).toHaveLength(2);
|
|
81
75
|
});
|
|
82
76
|
|
|
83
77
|
it('records that this real fixture has no imported animation clip', async () => {
|
|
@@ -1,169 +1,5 @@
|
|
|
1
|
-
import { ImporterRegistry, type RunImportMeta, runImport } from '@forgeax/engine-import';
|
|
2
|
-
import type { ImportErrorDetail } from '@forgeax/engine-types';
|
|
3
1
|
import { describe, expect, it } from 'vitest';
|
|
4
|
-
import { deriveFbxSourceKeys,
|
|
5
|
-
|
|
6
|
-
const FBX_SOURCE = 'apps/hello/format-tier1/fixtures/multi-target-morph.fbx';
|
|
7
|
-
const FBX_GUID = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa';
|
|
8
|
-
const FBX_SOURCE_URL = new URL(`../../../../${FBX_SOURCE}`, import.meta.url);
|
|
9
|
-
|
|
10
|
-
async function readFbxFixture(): Promise<Uint8Array> {
|
|
11
|
-
const nodeFs = (await import('node:' + 'fs/promises')) as unknown as {
|
|
12
|
-
readonly readFile: (path: URL) => Promise<Uint8Array>;
|
|
13
|
-
};
|
|
14
|
-
return new Uint8Array(await nodeFs.readFile(FBX_SOURCE_URL));
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function fbxMeta(): RunImportMeta {
|
|
18
|
-
return {
|
|
19
|
-
importer: 'fbx',
|
|
20
|
-
source: FBX_SOURCE,
|
|
21
|
-
subAssets: [
|
|
22
|
-
{
|
|
23
|
-
guid: FBX_GUID,
|
|
24
|
-
sourceIndex: 0,
|
|
25
|
-
sourceKey: 'fbx:mesh:MorphTriangle',
|
|
26
|
-
kind: 'mesh',
|
|
27
|
-
},
|
|
28
|
-
],
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
describe('public FBX importer source-read recovery', () => {
|
|
33
|
-
it('preserves refusal and retries on the same registry and Meta/GUID', async () => {
|
|
34
|
-
const sourceBytes = await readFbxFixture();
|
|
35
|
-
const registry = new ImporterRegistry();
|
|
36
|
-
registry.register(fbxImporter);
|
|
37
|
-
const reads = { count: 0 };
|
|
38
|
-
let refuseImporterRead = true;
|
|
39
|
-
const fs = {
|
|
40
|
-
readSource: async () => {
|
|
41
|
-
reads.count += 1;
|
|
42
|
-
if (refuseImporterRead && reads.count === 2) {
|
|
43
|
-
return {
|
|
44
|
-
ok: false as const,
|
|
45
|
-
error: new Error('transient source read refusal: repairable'),
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
return { ok: true as const, value: sourceBytes };
|
|
49
|
-
},
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const refused = await runImport(fbxMeta(), registry, fs);
|
|
53
|
-
expect(refused).toMatchObject({
|
|
54
|
-
ok: false,
|
|
55
|
-
error: {
|
|
56
|
-
code: 'source-read-failed',
|
|
57
|
-
detail: {
|
|
58
|
-
source: FBX_SOURCE,
|
|
59
|
-
reason: 'transient source read refusal: repairable',
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
});
|
|
63
|
-
expect(refused).not.toHaveProperty('value.pack');
|
|
64
|
-
expect(reads.count).toBe(2);
|
|
65
|
-
|
|
66
|
-
refuseImporterRead = false;
|
|
67
|
-
const repaired = await runImport(fbxMeta(), registry, fs);
|
|
68
|
-
const repeated = await runImport(fbxMeta(), registry, fs);
|
|
69
|
-
expect(repaired.ok).toBe(true);
|
|
70
|
-
expect(repeated.ok).toBe(true);
|
|
71
|
-
if (
|
|
72
|
-
!repaired.ok ||
|
|
73
|
-
'skipped' in repaired.value ||
|
|
74
|
-
!repeated.ok ||
|
|
75
|
-
'skipped' in repeated.value
|
|
76
|
-
) {
|
|
77
|
-
throw new Error('repaired FBX import must publish a product');
|
|
78
|
-
}
|
|
79
|
-
expect(repaired.value.pack.assets).toHaveLength(1);
|
|
80
|
-
expect(repaired.value.pack.assets[0]).toMatchObject({
|
|
81
|
-
guid: FBX_GUID,
|
|
82
|
-
sourceKey: 'fbx:mesh:MorphTriangle',
|
|
83
|
-
kind: 'mesh',
|
|
84
|
-
});
|
|
85
|
-
expect(repeated.value.pack).toEqual(repaired.value.pack);
|
|
86
|
-
expect(reads.count).toBe(6);
|
|
87
|
-
expect(registry.registeredImporters()).toEqual(['fbx']);
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
describe('public FBX importer malformed-source recovery', () => {
|
|
92
|
-
it('returns one bounded source diagnostic, then retries deterministically on repaired bytes', async () => {
|
|
93
|
-
const sourceBytes = await readFbxFixture();
|
|
94
|
-
const meta = fbxMeta();
|
|
95
|
-
const registry = new ImporterRegistry();
|
|
96
|
-
registry.register(fbxImporter);
|
|
97
|
-
const reads = { count: 0 };
|
|
98
|
-
let bytes: Uint8Array<ArrayBufferLike> = new TextEncoder().encode(
|
|
99
|
-
'readable but malformed FBX source',
|
|
100
|
-
);
|
|
101
|
-
const fs = {
|
|
102
|
-
readSource: async (path: string) => {
|
|
103
|
-
reads.count += 1;
|
|
104
|
-
expect(path).toBe(FBX_SOURCE);
|
|
105
|
-
return { ok: true as const, value: bytes };
|
|
106
|
-
},
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
const refused = await runImport(meta, registry, fs);
|
|
110
|
-
expect(refused.ok).toBe(false);
|
|
111
|
-
expect(refused).not.toHaveProperty('value');
|
|
112
|
-
if (refused.ok) throw new Error('malformed FBX must be refused');
|
|
113
|
-
const diagnostics = (
|
|
114
|
-
refused.error.detail as Extract<
|
|
115
|
-
ImportErrorDetail,
|
|
116
|
-
{ readonly diagnostics: readonly unknown[] }
|
|
117
|
-
>
|
|
118
|
-
).diagnostics;
|
|
119
|
-
expect(refused.error.code).toBe('source-validation-failed');
|
|
120
|
-
expect(refused.error).not.toHaveProperty('value');
|
|
121
|
-
expect(refused.error).not.toHaveProperty('pack');
|
|
122
|
-
expect(refused.error).not.toHaveProperty('cookProducts');
|
|
123
|
-
expect(diagnostics).toHaveLength(1);
|
|
124
|
-
expect(refused.error).toMatchObject({
|
|
125
|
-
expected: 'a readable FBX source that ufbx can parse',
|
|
126
|
-
detail: {
|
|
127
|
-
diagnostics: [
|
|
128
|
-
{
|
|
129
|
-
code: 'fbx-source-parse-failed',
|
|
130
|
-
severity: 'error',
|
|
131
|
-
sourcePath: FBX_SOURCE,
|
|
132
|
-
sourceRange: { start: 0, end: 0, line: 1, column: 1 },
|
|
133
|
-
rule: 'fbx-source-parse',
|
|
134
|
-
expected: 'a readable FBX source that ufbx can parse',
|
|
135
|
-
actual: 'fbx-wasm: Unrecognized file format',
|
|
136
|
-
},
|
|
137
|
-
],
|
|
138
|
-
},
|
|
139
|
-
});
|
|
140
|
-
expect(diagnostics[0]?.actual.length).toBeLessThanOrEqual(256);
|
|
141
|
-
expect(reads.count).toBe(2);
|
|
142
|
-
|
|
143
|
-
bytes = sourceBytes;
|
|
144
|
-
const repaired = await runImport(meta, registry, fs);
|
|
145
|
-
const repeated = await runImport(meta, registry, fs);
|
|
146
|
-
expect(repaired.ok).toBe(true);
|
|
147
|
-
expect(repeated.ok).toBe(true);
|
|
148
|
-
if (
|
|
149
|
-
!repaired.ok ||
|
|
150
|
-
'skipped' in repaired.value ||
|
|
151
|
-
!repeated.ok ||
|
|
152
|
-
'skipped' in repeated.value
|
|
153
|
-
) {
|
|
154
|
-
throw new Error('repaired FBX import must publish a product');
|
|
155
|
-
}
|
|
156
|
-
expect(repaired.value.pack).toEqual(repeated.value.pack);
|
|
157
|
-
expect(repaired.value.pack.assets).toHaveLength(1);
|
|
158
|
-
expect(repaired.value.pack.assets[0]).toMatchObject({
|
|
159
|
-
guid: FBX_GUID,
|
|
160
|
-
sourceKey: 'fbx:mesh:MorphTriangle',
|
|
161
|
-
kind: 'mesh',
|
|
162
|
-
});
|
|
163
|
-
expect(reads.count).toBe(6);
|
|
164
|
-
expect(registry.registeredImporters()).toEqual(['fbx']);
|
|
165
|
-
});
|
|
166
|
-
});
|
|
2
|
+
import { deriveFbxSourceKeys, sourceKeyForFbxOutput } from '../fbx-importer.js';
|
|
167
3
|
|
|
168
4
|
describe('fbx importer contract migration fixture', () => {
|
|
169
5
|
it('requires the generic ImportProduct shape', () => {
|
|
@@ -52,6 +52,7 @@ describe('@forgeax/engine-fbx barrel', () => {
|
|
|
52
52
|
expect(typeof mod.parseSkin).toBe('function');
|
|
53
53
|
expect(typeof mod.parseAnimationClips).toBe('function');
|
|
54
54
|
expect(typeof mod.parseTextures).toBe('function');
|
|
55
|
+
expect(typeof mod.resolveFbxTexturePath).toBe('function');
|
|
55
56
|
expect(typeof mod.toAssetPack).toBe('function');
|
|
56
57
|
expect(typeof mod.fbxErr).toBe('function');
|
|
57
58
|
});
|