@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.
- package/LICENSE +202 -0
- package/README.md +188 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/blendshape-import.integration.test.d.ts +2 -0
- package/dist/__tests__/blendshape-import.integration.test.d.ts.map +1 -0
- package/dist/__tests__/blendshape-import.unit.test.d.ts +2 -0
- package/dist/__tests__/blendshape-import.unit.test.d.ts.map +1 -0
- package/dist/__tests__/fbx-importer.test.d.ts +2 -0
- package/dist/__tests__/fbx-importer.test.d.ts.map +1 -0
- package/dist/__tests__/fbx-local-artifacts.test.d.ts +2 -0
- package/dist/__tests__/fbx-local-artifacts.test.d.ts.map +1 -0
- package/dist/__tests__/index.test.d.ts +2 -0
- package/dist/__tests__/index.test.d.ts.map +1 -0
- package/dist/__tests__/mesh-material-slots.unit.test.d.ts +2 -0
- package/dist/__tests__/mesh-material-slots.unit.test.d.ts.map +1 -0
- package/dist/__tests__/parse-mesh-multi-uv.test.d.ts +2 -0
- package/dist/__tests__/parse-mesh-multi-uv.test.d.ts.map +1 -0
- package/dist/__tests__/pick-e2e.integration.test.d.ts +2 -0
- package/dist/__tests__/pick-e2e.integration.test.d.ts.map +1 -0
- package/dist/__tests__/resolve-texture-path.unit.test.d.ts +2 -0
- package/dist/__tests__/resolve-texture-path.unit.test.d.ts.map +1 -0
- package/dist/errors.d.ts +50 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/fbx-importer.d.ts +17 -0
- package/dist/fbx-importer.d.ts.map +1 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +1499 -0
- package/dist/index.mjs.map +1 -0
- package/dist/parse-animation-clip.d.ts +35 -0
- package/dist/parse-animation-clip.d.ts.map +1 -0
- package/dist/parse-material.d.ts +24 -0
- package/dist/parse-material.d.ts.map +1 -0
- package/dist/parse-mesh.d.ts +21 -0
- package/dist/parse-mesh.d.ts.map +1 -0
- package/dist/parse-scene.d.ts +28 -0
- package/dist/parse-scene.d.ts.map +1 -0
- package/dist/parse-skeleton.d.ts +15 -0
- package/dist/parse-skeleton.d.ts.map +1 -0
- package/dist/parse-skin.d.ts +20 -0
- package/dist/parse-skin.d.ts.map +1 -0
- package/dist/parse-texture.d.ts +11 -0
- package/dist/parse-texture.d.ts.map +1 -0
- package/dist/resolve-texture-path.d.ts +25 -0
- package/dist/resolve-texture-path.d.ts.map +1 -0
- package/dist/to-asset-pack.d.ts +31 -0
- package/dist/to-asset-pack.d.ts.map +1 -0
- package/package.json +74 -0
- package/pkg/fbx-wasm.mjs +2 -0
- package/pkg/fbx-wasm.wasm +0 -0
- package/scripts/build-wasm.mjs +85 -0
- package/scripts/content-key.mjs +88 -0
- package/scripts/ensure-wasm.mjs +28 -0
- package/scripts/fetch-ufbx.mjs +42 -0
- package/scripts/fetch-wasm.mjs +91 -0
- package/scripts/parity-diff.mjs +127 -0
- package/scripts/test-wasm.mjs +91 -0
- package/src/__tests__/blendshape-import.integration.test.ts +90 -0
- package/src/__tests__/blendshape-import.unit.test.ts +83 -0
- package/src/__tests__/fbx-importer.test.ts +36 -0
- package/src/__tests__/fbx-local-artifacts.test.ts +39 -0
- package/src/__tests__/index.test.ts +168 -0
- package/src/__tests__/mesh-material-slots.unit.test.ts +125 -0
- package/src/__tests__/parse-mesh-multi-uv.test.ts +187 -0
- package/src/__tests__/pick-e2e.integration.test.ts +247 -0
- package/src/__tests__/resolve-texture-path.unit.test.ts +50 -0
- package/src/errors.ts +112 -0
- package/src/fbx-importer.ts +146 -0
- package/src/index.ts +215 -0
- package/src/native/bridge.c +942 -0
- package/src/parse-animation-clip.ts +271 -0
- package/src/parse-material.ts +92 -0
- package/src/parse-mesh.ts +223 -0
- package/src/parse-scene.ts +75 -0
- package/src/parse-skeleton.ts +46 -0
- package/src/parse-skin.ts +71 -0
- package/src/parse-texture.ts +22 -0
- package/src/resolve-texture-path.ts +152 -0
- package/src/to-asset-pack.ts +742 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// parse-skeleton.ts — M5 t48: TS bridge for skeleton POD data.
|
|
2
|
+
//
|
|
3
|
+
// Consumes the C++ JSON POD skeleton section emitted by t45 binding.cc
|
|
4
|
+
// (WriteSkeletonData), produces SkeletonPod (types SSOT per requirements AC-05).
|
|
5
|
+
//
|
|
6
|
+
// Input schema (from JSON.parse of binding output):
|
|
7
|
+
// skeletons?: [{
|
|
8
|
+
// jointCount: number,
|
|
9
|
+
// inverseBindMatrices: number[], // jointCount * 16 packed doubles
|
|
10
|
+
// jointPaths: string[],
|
|
11
|
+
// }]
|
|
12
|
+
//
|
|
13
|
+
// Output: SkeletonPod { jointCount, inverseBindMatrices: Float32Array, jointPaths }
|
|
14
|
+
|
|
15
|
+
import type { SkeletonPod } from '@forgeax/engine-types';
|
|
16
|
+
|
|
17
|
+
export interface FbxRawSkeleton {
|
|
18
|
+
readonly jointCount: number;
|
|
19
|
+
readonly inverseBindMatrices: number[];
|
|
20
|
+
readonly jointPaths: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface FbxRawSkeletonDoc {
|
|
24
|
+
readonly skeletons?: readonly FbxRawSkeleton[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Parse the first skeleton from a C++ JSON POD document.
|
|
29
|
+
* Returns an empty skeleton when the document has no skeleton data.
|
|
30
|
+
*/
|
|
31
|
+
export function parseSkeleton(doc: FbxRawSkeletonDoc): SkeletonPod {
|
|
32
|
+
const skeletons = doc.skeletons;
|
|
33
|
+
if (!skeletons || skeletons.length === 0) {
|
|
34
|
+
return { jointCount: 0, inverseBindMatrices: new Float32Array(0), jointPaths: [] };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const skel = skeletons[0];
|
|
38
|
+
if (!skel) {
|
|
39
|
+
return { jointCount: 0, inverseBindMatrices: new Float32Array(0), jointPaths: [] };
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
jointCount: skel.jointCount,
|
|
43
|
+
inverseBindMatrices: new Float32Array(skel.inverseBindMatrices),
|
|
44
|
+
jointPaths: [...skel.jointPaths],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// parse-skin.ts — M5 t49: TS bridge for skin POD data.
|
|
2
|
+
//
|
|
3
|
+
// Consumes the C++ JSON POD skin section emitted by t45 binding.cc
|
|
4
|
+
// (WriteSkinData), produces SkinPod (types SSOT per requirements AC-06).
|
|
5
|
+
//
|
|
6
|
+
// Input schema (from JSON.parse of binding output):
|
|
7
|
+
// skins?: [{
|
|
8
|
+
// meshSourceIndex: number,
|
|
9
|
+
// jointPaths: string[],
|
|
10
|
+
// vertexCount: number,
|
|
11
|
+
// influences: [{ jointIndices: number[], jointWeights: number[] }],
|
|
12
|
+
// }]
|
|
13
|
+
//
|
|
14
|
+
// Output: SkinPod { skeletonGuid, jointPaths, vertexCount, influences }
|
|
15
|
+
|
|
16
|
+
import type { SkinPod, SkinVertexInfluencePod } from '@forgeax/engine-types';
|
|
17
|
+
|
|
18
|
+
export interface FbxRawSkinInfluence {
|
|
19
|
+
readonly jointIndices: number[];
|
|
20
|
+
readonly jointWeights: number[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface FbxRawSkin {
|
|
24
|
+
readonly meshSourceIndex: number;
|
|
25
|
+
readonly jointPaths: string[];
|
|
26
|
+
readonly vertexCount: number;
|
|
27
|
+
readonly influences: readonly FbxRawSkinInfluence[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface FbxRawSkinDoc {
|
|
31
|
+
readonly skins?: readonly FbxRawSkin[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function bridgeSkeletonGuid(_jointPaths: readonly string[]): string {
|
|
35
|
+
// Deterministic GUID from joint paths. Real GUID assignment happens
|
|
36
|
+
// at import-runner time; this is a placeholder bridge identifier.
|
|
37
|
+
return 'fbx-skeleton-000000000000';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function toInfluence(raw: FbxRawSkinInfluence): SkinVertexInfluencePod {
|
|
41
|
+
// Pad to exactly 4 entries
|
|
42
|
+
const ji = new Uint16Array(4);
|
|
43
|
+
const jw = new Float32Array(4);
|
|
44
|
+
for (let i = 0; i < 4; i++) {
|
|
45
|
+
ji[i] = raw.jointIndices[i] ?? 0;
|
|
46
|
+
jw[i] = raw.jointWeights[i] ?? 0;
|
|
47
|
+
}
|
|
48
|
+
return { jointIndices: ji, jointWeights: jw };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Parse the first skin from a C++ JSON POD document.
|
|
53
|
+
* Returns an empty skin when the document has no skin data.
|
|
54
|
+
*/
|
|
55
|
+
export function parseSkin(doc: FbxRawSkinDoc): SkinPod {
|
|
56
|
+
const skins = doc.skins;
|
|
57
|
+
if (!skins || skins.length === 0) {
|
|
58
|
+
return { skeletonGuid: '', jointPaths: [], vertexCount: 0, influences: [] };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const skin = skins[0];
|
|
62
|
+
if (!skin) {
|
|
63
|
+
return { skeletonGuid: '', jointPaths: [], vertexCount: 0, influences: [] };
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
skeletonGuid: bridgeSkeletonGuid(skin.jointPaths),
|
|
67
|
+
jointPaths: [...skin.jointPaths],
|
|
68
|
+
vertexCount: skin.vertexCount,
|
|
69
|
+
influences: skin.influences.map(toInfluence),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// parse-texture.ts — FBX JSON POD to TexturePod bridge (t30).
|
|
2
|
+
|
|
3
|
+
import type { TexturePod } from '@forgeax/engine-types';
|
|
4
|
+
|
|
5
|
+
export interface FbxRawTexture {
|
|
6
|
+
readonly name?: string;
|
|
7
|
+
readonly filePath: string;
|
|
8
|
+
readonly sourceIndex: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface FbxRawTextures {
|
|
12
|
+
readonly textures?: readonly FbxRawTexture[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function parseTextures(raw: FbxRawTextures): TexturePod[] {
|
|
16
|
+
const textures = raw.textures ?? [];
|
|
17
|
+
return textures.map((t) => ({
|
|
18
|
+
name: t.name ?? t.filePath,
|
|
19
|
+
filePath: t.filePath,
|
|
20
|
+
sourceIndex: t.sourceIndex,
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
export type FbxTextureResolutionStrategy = 'exact' | 'case-folded' | 'suffix' | 'basename';
|
|
2
|
+
|
|
3
|
+
export interface FbxTexturePathRequest {
|
|
4
|
+
readonly declaredRelativePath?: string;
|
|
5
|
+
readonly declaredFilename?: string;
|
|
6
|
+
readonly declaredAbsolutePath?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface FbxTextureCandidate {
|
|
10
|
+
/** Candidate path relative to the FBX source file's directory. */
|
|
11
|
+
readonly relativePath: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type FbxTextureResolution =
|
|
15
|
+
| {
|
|
16
|
+
readonly ok: true;
|
|
17
|
+
readonly relativePath: string;
|
|
18
|
+
/** URI passed to ImportContext.readSibling from the FBX source. */
|
|
19
|
+
readonly readUri: string;
|
|
20
|
+
readonly strategy: FbxTextureResolutionStrategy;
|
|
21
|
+
}
|
|
22
|
+
| {
|
|
23
|
+
readonly ok: false;
|
|
24
|
+
readonly code: 'fbx-external-texture-missing' | 'fbx-external-texture-ambiguous';
|
|
25
|
+
readonly requestedPath: string;
|
|
26
|
+
readonly candidates: readonly string[];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function normalizeSourceRelativePath(raw: string): string | undefined {
|
|
30
|
+
const value = raw.replaceAll('\\', '/');
|
|
31
|
+
if (value.startsWith('/') || /^[A-Za-z]:\//.test(value) || value.startsWith('//'))
|
|
32
|
+
return undefined;
|
|
33
|
+
const parts: string[] = [];
|
|
34
|
+
for (const part of value.split('/')) {
|
|
35
|
+
if (part === '' || part === '.') continue;
|
|
36
|
+
if (part === '..') {
|
|
37
|
+
if (parts.at(-1) !== undefined && parts.at(-1) !== '..') parts.pop();
|
|
38
|
+
else parts.push('..');
|
|
39
|
+
} else {
|
|
40
|
+
parts.push(part);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return parts.join('/');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function requestSegments(raw: string): string[] {
|
|
47
|
+
const value = raw
|
|
48
|
+
.replaceAll('\\', '/')
|
|
49
|
+
.replace(/^[A-Za-z]:\//, '')
|
|
50
|
+
.replace(/^\/+/, '');
|
|
51
|
+
return value.split('/').filter((part) => part !== '' && part !== '.');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function uniqueMatches(paths: readonly string[], predicate: (path: string) => boolean): string[] {
|
|
55
|
+
return [...new Set(paths.filter(predicate))];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function longestSuffixLength(request: readonly string[], candidate: readonly string[]): number {
|
|
59
|
+
let count = 0;
|
|
60
|
+
while (
|
|
61
|
+
count < request.length &&
|
|
62
|
+
count < candidate.length &&
|
|
63
|
+
request[request.length - 1 - count]?.toLowerCase() ===
|
|
64
|
+
candidate[candidate.length - 1 - count]?.toLowerCase()
|
|
65
|
+
) {
|
|
66
|
+
count++;
|
|
67
|
+
}
|
|
68
|
+
return count;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Resolve an FBX-declared texture against a caller-authorized candidate tree. */
|
|
72
|
+
export function resolveFbxTexturePath(
|
|
73
|
+
sourcePath: string,
|
|
74
|
+
request: FbxTexturePathRequest,
|
|
75
|
+
candidates: readonly FbxTextureCandidate[],
|
|
76
|
+
): FbxTextureResolution {
|
|
77
|
+
const candidatePaths = candidates.flatMap((candidate) => {
|
|
78
|
+
const normalized = normalizeSourceRelativePath(candidate.relativePath);
|
|
79
|
+
return normalized === undefined || normalized.length === 0 ? [] : [normalized];
|
|
80
|
+
});
|
|
81
|
+
const requestedPath =
|
|
82
|
+
request.declaredRelativePath ?? request.declaredFilename ?? request.declaredAbsolutePath ?? '';
|
|
83
|
+
void sourcePath;
|
|
84
|
+
|
|
85
|
+
const choose = (
|
|
86
|
+
matches: readonly string[],
|
|
87
|
+
strategy: FbxTextureResolutionStrategy,
|
|
88
|
+
): FbxTextureResolution | undefined => {
|
|
89
|
+
const unique = [...new Set(matches)];
|
|
90
|
+
if (unique.length === 1) {
|
|
91
|
+
const relativePath = unique[0];
|
|
92
|
+
if (relativePath === undefined) return undefined;
|
|
93
|
+
return {
|
|
94
|
+
ok: true,
|
|
95
|
+
relativePath,
|
|
96
|
+
readUri: relativePath,
|
|
97
|
+
strategy,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (unique.length > 1) {
|
|
101
|
+
return {
|
|
102
|
+
ok: false,
|
|
103
|
+
code: 'fbx-external-texture-ambiguous',
|
|
104
|
+
requestedPath,
|
|
105
|
+
candidates: unique,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return undefined;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const relativeRequest = request.declaredRelativePath ?? request.declaredFilename;
|
|
112
|
+
if (relativeRequest !== undefined && !/^(?:[A-Za-z]:[\\/]|[\\/])/.test(relativeRequest)) {
|
|
113
|
+
const normalizedRequest = normalizeSourceRelativePath(relativeRequest);
|
|
114
|
+
if (normalizedRequest !== undefined) {
|
|
115
|
+
const scopeExact = choose(
|
|
116
|
+
uniqueMatches(candidatePaths, (path) => path === normalizedRequest),
|
|
117
|
+
'exact',
|
|
118
|
+
);
|
|
119
|
+
if (scopeExact !== undefined) return scopeExact;
|
|
120
|
+
const scopeFolded = choose(
|
|
121
|
+
uniqueMatches(
|
|
122
|
+
candidatePaths,
|
|
123
|
+
(path) => path.toLowerCase() === normalizedRequest.toLowerCase(),
|
|
124
|
+
),
|
|
125
|
+
'case-folded',
|
|
126
|
+
);
|
|
127
|
+
if (scopeFolded !== undefined) return scopeFolded;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const requestedSegments = requestSegments(requestedPath);
|
|
132
|
+
if (requestedSegments.length > 0) {
|
|
133
|
+
const scored = candidatePaths.map((path) => ({
|
|
134
|
+
path,
|
|
135
|
+
score: longestSuffixLength(requestedSegments, path.split('/')),
|
|
136
|
+
}));
|
|
137
|
+
const bestScore = Math.max(0, ...scored.map((entry) => entry.score));
|
|
138
|
+
if (bestScore > 0) {
|
|
139
|
+
const best = scored.filter((entry) => entry.score === bestScore).map((entry) => entry.path);
|
|
140
|
+
const strategy: FbxTextureResolutionStrategy = bestScore === 1 ? 'basename' : 'suffix';
|
|
141
|
+
const suffix = choose(best, strategy);
|
|
142
|
+
if (suffix !== undefined) return suffix;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
ok: false,
|
|
148
|
+
code: 'fbx-external-texture-missing',
|
|
149
|
+
requestedPath,
|
|
150
|
+
candidates: candidatePaths,
|
|
151
|
+
};
|
|
152
|
+
}
|