@series-inc/stowkit-cli 0.1.0
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/dist/app/blob-store.d.ts +9 -0
- package/dist/app/blob-store.js +42 -0
- package/dist/app/disk-project.d.ts +84 -0
- package/dist/app/disk-project.js +70 -0
- package/dist/app/process-cache.d.ts +10 -0
- package/dist/app/process-cache.js +126 -0
- package/dist/app/state.d.ts +38 -0
- package/dist/app/state.js +16 -0
- package/dist/app/stowmat-io.d.ts +6 -0
- package/dist/app/stowmat-io.js +48 -0
- package/dist/app/stowmeta-io.d.ts +14 -0
- package/dist/app/stowmeta-io.js +207 -0
- package/dist/cleanup.d.ts +3 -0
- package/dist/cleanup.js +72 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +148 -0
- package/dist/core/binary.d.ts +41 -0
- package/dist/core/binary.js +118 -0
- package/dist/core/constants.d.ts +64 -0
- package/dist/core/constants.js +65 -0
- package/dist/core/path.d.ts +3 -0
- package/dist/core/path.js +27 -0
- package/dist/core/types.d.ts +204 -0
- package/dist/core/types.js +76 -0
- package/dist/encoders/aac-encoder.d.ts +12 -0
- package/dist/encoders/aac-encoder.js +179 -0
- package/dist/encoders/basis-encoder.d.ts +15 -0
- package/dist/encoders/basis-encoder.js +116 -0
- package/dist/encoders/draco-encoder.d.ts +11 -0
- package/dist/encoders/draco-encoder.js +155 -0
- package/dist/encoders/fbx-loader.d.ts +4 -0
- package/dist/encoders/fbx-loader.js +540 -0
- package/dist/encoders/image-decoder.d.ts +13 -0
- package/dist/encoders/image-decoder.js +33 -0
- package/dist/encoders/interfaces.d.ts +105 -0
- package/dist/encoders/interfaces.js +1 -0
- package/dist/encoders/skinned-mesh-builder.d.ts +7 -0
- package/dist/encoders/skinned-mesh-builder.js +135 -0
- package/dist/format/metadata.d.ts +18 -0
- package/dist/format/metadata.js +381 -0
- package/dist/format/packer.d.ts +8 -0
- package/dist/format/packer.js +87 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +35 -0
- package/dist/init.d.ts +1 -0
- package/dist/init.js +73 -0
- package/dist/node-fs.d.ts +22 -0
- package/dist/node-fs.js +148 -0
- package/dist/orchestrator.d.ts +20 -0
- package/dist/orchestrator.js +301 -0
- package/dist/pipeline.d.ts +23 -0
- package/dist/pipeline.js +354 -0
- package/dist/server.d.ts +9 -0
- package/dist/server.js +859 -0
- package/package.json +35 -0
- package/skill.md +211 -0
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { FBXLoader } from 'three/addons/loaders/FBXLoader.js';
|
|
3
|
+
import { PropertyBinding } from 'three';
|
|
4
|
+
import { parseBinary as fbxParseBinary, parseText as fbxParseText, FBXReader } from 'fbx-parser';
|
|
5
|
+
// ─── Minimal DOM shim for Three.js FBXLoader in Node ────────────────────────
|
|
6
|
+
function ensureDomShim() {
|
|
7
|
+
const g = globalThis;
|
|
8
|
+
if (typeof g.document !== 'undefined')
|
|
9
|
+
return;
|
|
10
|
+
// Minimal shim — FBXLoader only needs createElementNS for canvas (which we don't use)
|
|
11
|
+
g.document = {
|
|
12
|
+
createElementNS(_ns, tag) {
|
|
13
|
+
if (tag === 'img') {
|
|
14
|
+
return { set src(_) { }, set onload(_) { }, set onerror(_) { } };
|
|
15
|
+
}
|
|
16
|
+
return {};
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
if (typeof g.self === 'undefined') {
|
|
20
|
+
g.self = g;
|
|
21
|
+
}
|
|
22
|
+
if (typeof g.window === 'undefined') {
|
|
23
|
+
g.window = g;
|
|
24
|
+
}
|
|
25
|
+
if (typeof g.Blob === 'undefined') {
|
|
26
|
+
g.Blob = class Blob {
|
|
27
|
+
parts;
|
|
28
|
+
constructor(parts) { this.parts = parts; }
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (typeof g.URL === 'undefined' || !g.URL.createObjectURL) {
|
|
32
|
+
const UrlClass = g.URL ?? {};
|
|
33
|
+
UrlClass.createObjectURL = () => '';
|
|
34
|
+
UrlClass.revokeObjectURL = () => { };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// ─── FBX Importer ────────────────────────────────────────────────────────────
|
|
38
|
+
export class NodeFbxImporter {
|
|
39
|
+
async import(data, _fileName) {
|
|
40
|
+
ensureDomShim();
|
|
41
|
+
try {
|
|
42
|
+
const loader = new FBXLoader();
|
|
43
|
+
const buffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
44
|
+
const group = loader.parse(buffer, '');
|
|
45
|
+
const mesh = extractMeshFromGroup(group);
|
|
46
|
+
mesh.animations = parseFbxAnimations(data);
|
|
47
|
+
return mesh;
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
console.error('[NodeFbxImporter] Failed:', err);
|
|
51
|
+
return emptyMesh();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// ─── Group → ImportedMesh extraction ────────────────────────────────────────
|
|
56
|
+
function extractMeshFromGroup(group) {
|
|
57
|
+
const subMeshes = [];
|
|
58
|
+
const materials = [];
|
|
59
|
+
const materialSet = new Map();
|
|
60
|
+
const nodes = [];
|
|
61
|
+
let hasSkeleton = false;
|
|
62
|
+
const allBones = [];
|
|
63
|
+
const meshObjects = [];
|
|
64
|
+
group.traverse((child) => {
|
|
65
|
+
if (child.isMesh) {
|
|
66
|
+
meshObjects.push(child);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
const sceneBones = [];
|
|
70
|
+
const seenBoneNames = new Set();
|
|
71
|
+
group.traverse((child) => {
|
|
72
|
+
if (child.isBone) {
|
|
73
|
+
const name = child.name;
|
|
74
|
+
if (!seenBoneNames.has(name)) {
|
|
75
|
+
seenBoneNames.add(name);
|
|
76
|
+
sceneBones.push(child);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
const perMeshSkeletons = new Map();
|
|
81
|
+
for (const mesh of meshObjects) {
|
|
82
|
+
if (mesh.isSkinnedMesh) {
|
|
83
|
+
const sm = mesh;
|
|
84
|
+
if (sm.skeleton && sm.skeleton.bones.length > 0) {
|
|
85
|
+
hasSkeleton = true;
|
|
86
|
+
perMeshSkeletons.set(sm, sm.skeleton);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (sceneBones.length > 0 && hasSkeleton) {
|
|
91
|
+
group.updateMatrixWorld(true);
|
|
92
|
+
const rootInverse = new THREE.Matrix4().copy(group.matrixWorld).invert();
|
|
93
|
+
const fbxBoneInverses = new Map();
|
|
94
|
+
for (const skel of perMeshSkeletons.values()) {
|
|
95
|
+
for (let i = 0; i < skel.bones.length; i++) {
|
|
96
|
+
if (!fbxBoneInverses.has(skel.bones[i].name)) {
|
|
97
|
+
fbxBoneInverses.set(skel.bones[i].name, skel.boneInverses[i]);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const boneIndexMap = new Map();
|
|
102
|
+
for (let i = 0; i < sceneBones.length; i++) {
|
|
103
|
+
boneIndexMap.set(sceneBones[i], i);
|
|
104
|
+
}
|
|
105
|
+
const intermediateMap = new Map();
|
|
106
|
+
for (const bone of sceneBones) {
|
|
107
|
+
let hasBoneAncestor = false;
|
|
108
|
+
let check = bone.parent;
|
|
109
|
+
while (check && check !== group) {
|
|
110
|
+
if (boneIndexMap.has(check)) {
|
|
111
|
+
hasBoneAncestor = true;
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
check = check.parent;
|
|
115
|
+
}
|
|
116
|
+
if (!hasBoneAncestor && bone.parent && bone.parent !== group) {
|
|
117
|
+
const parent = bone.parent;
|
|
118
|
+
if (!parent.isBone && !intermediateMap.has(parent)) {
|
|
119
|
+
const idx = sceneBones.length + intermediateMap.size;
|
|
120
|
+
intermediateMap.set(parent, idx);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function computeOffset(obj) {
|
|
125
|
+
const offsetMatrix = new Array(16);
|
|
126
|
+
const localWorld = new THREE.Matrix4().multiplyMatrices(rootInverse, obj.matrixWorld);
|
|
127
|
+
localWorld.invert().toArray(offsetMatrix);
|
|
128
|
+
return offsetMatrix;
|
|
129
|
+
}
|
|
130
|
+
for (let i = 0; i < sceneBones.length; i++) {
|
|
131
|
+
const bone = sceneBones[i];
|
|
132
|
+
let parentIndex = -1;
|
|
133
|
+
let ancestor = bone.parent;
|
|
134
|
+
while (ancestor && ancestor !== group) {
|
|
135
|
+
const boneIdx = boneIndexMap.get(ancestor);
|
|
136
|
+
if (boneIdx !== undefined) {
|
|
137
|
+
parentIndex = boneIdx;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
const interIdx = intermediateMap.get(ancestor);
|
|
141
|
+
if (interIdx !== undefined) {
|
|
142
|
+
parentIndex = interIdx;
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
ancestor = ancestor.parent;
|
|
146
|
+
}
|
|
147
|
+
const fbxInv = fbxBoneInverses.get(bone.name);
|
|
148
|
+
const offsetMatrix = fbxInv ? Array.from(fbxInv.elements) : computeOffset(bone);
|
|
149
|
+
allBones.push({ name: bone.name, parentIndex, offsetMatrix });
|
|
150
|
+
}
|
|
151
|
+
for (const [obj, _idx] of intermediateMap) {
|
|
152
|
+
let parentIndex = -1;
|
|
153
|
+
let ancestor = obj.parent;
|
|
154
|
+
while (ancestor && ancestor !== group) {
|
|
155
|
+
const interIdx = intermediateMap.get(ancestor);
|
|
156
|
+
if (interIdx !== undefined) {
|
|
157
|
+
parentIndex = interIdx;
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
ancestor = ancestor.parent;
|
|
161
|
+
}
|
|
162
|
+
allBones.push({
|
|
163
|
+
name: obj.name || '__armature',
|
|
164
|
+
parentIndex,
|
|
165
|
+
offsetMatrix: computeOffset(obj),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
for (let mi = 0; mi < meshObjects.length; mi++) {
|
|
170
|
+
const mesh = meshObjects[mi];
|
|
171
|
+
const geometry = mesh.geometry;
|
|
172
|
+
const posAttr = geometry.getAttribute('position');
|
|
173
|
+
if (!posAttr)
|
|
174
|
+
continue;
|
|
175
|
+
const vertCount = posAttr.count;
|
|
176
|
+
const positions = new Float32Array(vertCount * 3);
|
|
177
|
+
{
|
|
178
|
+
const worldMatrix = mesh.matrixWorld;
|
|
179
|
+
const v = new THREE.Vector3();
|
|
180
|
+
for (let vi = 0; vi < vertCount; vi++) {
|
|
181
|
+
v.fromBufferAttribute(posAttr, vi);
|
|
182
|
+
v.applyMatrix4(worldMatrix);
|
|
183
|
+
positions[vi * 3] = v.x;
|
|
184
|
+
positions[vi * 3 + 1] = v.y;
|
|
185
|
+
positions[vi * 3 + 2] = v.z;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
let normals = null;
|
|
189
|
+
const normAttr = geometry.getAttribute('normal');
|
|
190
|
+
if (normAttr && normAttr.count === vertCount) {
|
|
191
|
+
normals = new Float32Array(vertCount * 3);
|
|
192
|
+
const normalMatrix = new THREE.Matrix3().getNormalMatrix(mesh.matrixWorld);
|
|
193
|
+
const n = new THREE.Vector3();
|
|
194
|
+
for (let vi = 0; vi < vertCount; vi++) {
|
|
195
|
+
n.fromBufferAttribute(normAttr, vi);
|
|
196
|
+
n.applyMatrix3(normalMatrix).normalize();
|
|
197
|
+
normals[vi * 3] = n.x;
|
|
198
|
+
normals[vi * 3 + 1] = n.y;
|
|
199
|
+
normals[vi * 3 + 2] = n.z;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
let uvs = null;
|
|
203
|
+
const uvAttr = geometry.getAttribute('uv');
|
|
204
|
+
if (uvAttr && uvAttr.count === vertCount) {
|
|
205
|
+
uvs = new Float32Array(vertCount * 2);
|
|
206
|
+
for (let vi = 0; vi < vertCount; vi++) {
|
|
207
|
+
uvs[vi * 2] = uvAttr.getX(vi);
|
|
208
|
+
uvs[vi * 2 + 1] = uvAttr.getY(vi);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const indexAttr = geometry.getIndex();
|
|
212
|
+
let indices;
|
|
213
|
+
if (indexAttr) {
|
|
214
|
+
indices = new Uint32Array(indexAttr.count);
|
|
215
|
+
for (let ii = 0; ii < indexAttr.count; ii++)
|
|
216
|
+
indices[ii] = indexAttr.getX(ii);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
indices = new Uint32Array(vertCount);
|
|
220
|
+
for (let ii = 0; ii < vertCount; ii++)
|
|
221
|
+
indices[ii] = ii;
|
|
222
|
+
}
|
|
223
|
+
let skinData;
|
|
224
|
+
if (mesh.isSkinnedMesh) {
|
|
225
|
+
const skinIndexAttr = geometry.getAttribute('skinIndex');
|
|
226
|
+
const skinWeightAttr = geometry.getAttribute('skinWeight');
|
|
227
|
+
if (skinIndexAttr && skinWeightAttr) {
|
|
228
|
+
const meshSkeleton = perMeshSkeletons.get(mesh);
|
|
229
|
+
let remap = null;
|
|
230
|
+
if (meshSkeleton) {
|
|
231
|
+
remap = new Uint32Array(meshSkeleton.bones.length);
|
|
232
|
+
for (let si = 0; si < meshSkeleton.bones.length; si++) {
|
|
233
|
+
const idx = sceneBones.findIndex(b => b.name === meshSkeleton.bones[si].name);
|
|
234
|
+
remap[si] = idx >= 0 ? idx : 0;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const boneIndices = new Uint32Array(vertCount * 4);
|
|
238
|
+
const boneWeights = new Float32Array(vertCount * 4);
|
|
239
|
+
for (let vi = 0; vi < vertCount; vi++) {
|
|
240
|
+
const si0 = skinIndexAttr.getX(vi);
|
|
241
|
+
const si1 = skinIndexAttr.getY(vi);
|
|
242
|
+
const si2 = skinIndexAttr.getZ(vi);
|
|
243
|
+
const si3 = skinIndexAttr.getW(vi);
|
|
244
|
+
boneIndices[vi * 4] = remap && si0 < remap.length ? remap[si0] : si0;
|
|
245
|
+
boneIndices[vi * 4 + 1] = remap && si1 < remap.length ? remap[si1] : si1;
|
|
246
|
+
boneIndices[vi * 4 + 2] = remap && si2 < remap.length ? remap[si2] : si2;
|
|
247
|
+
boneIndices[vi * 4 + 3] = remap && si3 < remap.length ? remap[si3] : si3;
|
|
248
|
+
boneWeights[vi * 4] = skinWeightAttr.getX(vi);
|
|
249
|
+
boneWeights[vi * 4 + 1] = skinWeightAttr.getY(vi);
|
|
250
|
+
boneWeights[vi * 4 + 2] = skinWeightAttr.getZ(vi);
|
|
251
|
+
boneWeights[vi * 4 + 3] = skinWeightAttr.getW(vi);
|
|
252
|
+
}
|
|
253
|
+
skinData = { boneIndices, boneWeights };
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const meshMaterial = Array.isArray(mesh.material) ? mesh.material[0] : mesh.material;
|
|
257
|
+
const matName = meshMaterial?.name || 'default';
|
|
258
|
+
let matIndex;
|
|
259
|
+
if (materialSet.has(matName)) {
|
|
260
|
+
matIndex = materialSet.get(matName);
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
matIndex = materials.length;
|
|
264
|
+
materialSet.set(matName, matIndex);
|
|
265
|
+
materials.push({ name: matName });
|
|
266
|
+
}
|
|
267
|
+
subMeshes.push({ skinData, positions, normals, uvs, indices, materialIndex: matIndex });
|
|
268
|
+
nodes.push({
|
|
269
|
+
name: mesh.name || `mesh_${mi}`,
|
|
270
|
+
parentIndex: -1,
|
|
271
|
+
position: [0, 0, 0],
|
|
272
|
+
rotation: [0, 0, 0, 1],
|
|
273
|
+
scale: [1, 1, 1],
|
|
274
|
+
meshIndices: [mi],
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
if (materials.length === 0)
|
|
278
|
+
materials.push({ name: 'default' });
|
|
279
|
+
return {
|
|
280
|
+
subMeshes,
|
|
281
|
+
materials,
|
|
282
|
+
nodes,
|
|
283
|
+
hasSkeleton,
|
|
284
|
+
bones: allBones,
|
|
285
|
+
animations: [],
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
function emptyMesh() {
|
|
289
|
+
return { subMeshes: [], materials: [{ name: 'default' }], nodes: [], hasSkeleton: false, bones: [], animations: [] };
|
|
290
|
+
}
|
|
291
|
+
// ─── fbx-parser animation extraction ─────────────────────────────────────────
|
|
292
|
+
const FBX_TICKS_PER_SECOND = 46186158000;
|
|
293
|
+
const DEG2RAD = Math.PI / 180;
|
|
294
|
+
function isArrayLike(v) {
|
|
295
|
+
return Array.isArray(v) || ArrayBuffer.isView(v);
|
|
296
|
+
}
|
|
297
|
+
function parseFbxAnimations(data) {
|
|
298
|
+
let fbx;
|
|
299
|
+
try {
|
|
300
|
+
fbx = fbxParseBinary(data);
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
try {
|
|
304
|
+
fbx = fbxParseText(new TextDecoder().decode(data));
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
return [];
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const reader = new FBXReader(fbx);
|
|
311
|
+
const objects = reader.node('Objects');
|
|
312
|
+
const connectionsNode = reader.node('Connections');
|
|
313
|
+
if (!objects || !connectionsNode)
|
|
314
|
+
return [];
|
|
315
|
+
let unitScale = 0.01;
|
|
316
|
+
const globalSettings = reader.node('GlobalSettings');
|
|
317
|
+
if (globalSettings) {
|
|
318
|
+
const props70 = globalSettings.fbxNode.nodes.find(n => n.name === 'Properties70');
|
|
319
|
+
if (props70) {
|
|
320
|
+
const unitProp = props70.nodes.find(n => n.name === 'P' && String(n.props[0]) === 'UnitScaleFactor');
|
|
321
|
+
if (unitProp) {
|
|
322
|
+
const raw = Number(unitProp.props[4]);
|
|
323
|
+
if (raw > 0)
|
|
324
|
+
unitScale = raw / 100;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
const connMap = new Map();
|
|
329
|
+
function getConn(id) {
|
|
330
|
+
let e = connMap.get(id);
|
|
331
|
+
if (!e) {
|
|
332
|
+
e = { parents: [], children: [] };
|
|
333
|
+
connMap.set(id, e);
|
|
334
|
+
}
|
|
335
|
+
return e;
|
|
336
|
+
}
|
|
337
|
+
for (const c of connectionsNode.fbxNode.nodes) {
|
|
338
|
+
if (c.name !== 'C')
|
|
339
|
+
continue;
|
|
340
|
+
const childID = c.props[1];
|
|
341
|
+
const parentID = c.props[2];
|
|
342
|
+
const rel = c.props.length > 3 ? String(c.props[3]) : undefined;
|
|
343
|
+
getConn(childID).parents.push({ id: parentID, rel });
|
|
344
|
+
getConn(parentID).children.push({ id: childID, rel });
|
|
345
|
+
}
|
|
346
|
+
const FBX_EULER_ORDER = ['ZYX', 'YZX', 'XZY', 'ZXY', 'YXZ', 'XYZ'];
|
|
347
|
+
const modelNames = new Map();
|
|
348
|
+
const modelInfo = new Map();
|
|
349
|
+
for (const node of objects.fbxNode.nodes) {
|
|
350
|
+
if (node.name !== 'Model')
|
|
351
|
+
continue;
|
|
352
|
+
const id = node.props[0];
|
|
353
|
+
const rawName = String(node.props[1] ?? '');
|
|
354
|
+
const name = rawName.includes('\x00\x01') ? rawName.split('\x00\x01')[0] : rawName.replace(/^Model::/, '');
|
|
355
|
+
if (!name)
|
|
356
|
+
continue;
|
|
357
|
+
modelNames.set(id, name);
|
|
358
|
+
let preRotation;
|
|
359
|
+
let postRotation;
|
|
360
|
+
let eulerOrder = 'ZYX';
|
|
361
|
+
const props70 = node.nodes.find(n => n.name === 'Properties70');
|
|
362
|
+
if (props70) {
|
|
363
|
+
for (const p of props70.nodes) {
|
|
364
|
+
if (p.name !== 'P')
|
|
365
|
+
continue;
|
|
366
|
+
const pn = String(p.props[0]);
|
|
367
|
+
if (pn === 'PreRotation')
|
|
368
|
+
preRotation = [Number(p.props[4]), Number(p.props[5]), Number(p.props[6])];
|
|
369
|
+
else if (pn === 'PostRotation')
|
|
370
|
+
postRotation = [Number(p.props[4]), Number(p.props[5]), Number(p.props[6])];
|
|
371
|
+
else if (pn === 'RotationOrder') {
|
|
372
|
+
const idx = Number(p.props[4]);
|
|
373
|
+
if (idx >= 0 && idx < FBX_EULER_ORDER.length)
|
|
374
|
+
eulerOrder = FBX_EULER_ORDER[idx];
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
modelInfo.set(id, { preRotation, postRotation, eulerOrder });
|
|
379
|
+
}
|
|
380
|
+
const curveNodeMap = new Map();
|
|
381
|
+
for (const node of objects.fbxNode.nodes) {
|
|
382
|
+
if (node.name !== 'AnimationCurveNode')
|
|
383
|
+
continue;
|
|
384
|
+
const id = node.props[0];
|
|
385
|
+
const rawName = String(node.props[1] ?? '');
|
|
386
|
+
const cleaned = rawName.includes('\x00\x01') ? rawName.split('\x00\x01')[0] : rawName;
|
|
387
|
+
let attr = null;
|
|
388
|
+
if (cleaned === 'T' || cleaned === 'Lcl Translation' || cleaned.endsWith('::T'))
|
|
389
|
+
attr = 'T';
|
|
390
|
+
else if (cleaned === 'R' || cleaned === 'Lcl Rotation' || cleaned.endsWith('::R'))
|
|
391
|
+
attr = 'R';
|
|
392
|
+
else if (cleaned === 'S' || cleaned === 'Lcl Scaling' || cleaned.endsWith('::S'))
|
|
393
|
+
attr = 'S';
|
|
394
|
+
if (attr)
|
|
395
|
+
curveNodeMap.set(id, { id, attr, curves: {} });
|
|
396
|
+
}
|
|
397
|
+
if (curveNodeMap.size === 0)
|
|
398
|
+
return [];
|
|
399
|
+
for (const node of objects.fbxNode.nodes) {
|
|
400
|
+
if (node.name !== 'AnimationCurve')
|
|
401
|
+
continue;
|
|
402
|
+
const curveId = node.props[0];
|
|
403
|
+
const keyTimeNode = node.nodes.find(n => n.name === 'KeyTime');
|
|
404
|
+
const keyValueNode = node.nodes.find(n => n.name === 'KeyValueFloat');
|
|
405
|
+
if (!keyTimeNode || !keyValueNode)
|
|
406
|
+
continue;
|
|
407
|
+
const rawTimes = keyTimeNode.props[0];
|
|
408
|
+
const rawValues = keyValueNode.props[0];
|
|
409
|
+
if (!isArrayLike(rawTimes) || !isArrayLike(rawValues))
|
|
410
|
+
continue;
|
|
411
|
+
const timesArr = Array.from(rawTimes);
|
|
412
|
+
const times = timesArr.map(t => Number(t) / FBX_TICKS_PER_SECOND);
|
|
413
|
+
const values = Array.isArray(rawValues) ? rawValues : Array.from(rawValues);
|
|
414
|
+
const conn = connMap.get(curveId);
|
|
415
|
+
if (!conn)
|
|
416
|
+
continue;
|
|
417
|
+
for (const parent of conn.parents) {
|
|
418
|
+
const curveNode = curveNodeMap.get(parent.id);
|
|
419
|
+
if (!curveNode)
|
|
420
|
+
continue;
|
|
421
|
+
const rel = parent.rel ?? '';
|
|
422
|
+
const curve = { times, values };
|
|
423
|
+
if (rel.includes('d|X'))
|
|
424
|
+
curveNode.curves.x = curve;
|
|
425
|
+
else if (rel.includes('d|Y'))
|
|
426
|
+
curveNode.curves.y = curve;
|
|
427
|
+
else if (rel.includes('d|Z'))
|
|
428
|
+
curveNode.curves.z = curve;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
const targetedNodes = [];
|
|
432
|
+
for (const [, cn] of curveNodeMap) {
|
|
433
|
+
if (!cn.curves.x && !cn.curves.y && !cn.curves.z)
|
|
434
|
+
continue;
|
|
435
|
+
const conn = connMap.get(cn.id);
|
|
436
|
+
if (!conn)
|
|
437
|
+
continue;
|
|
438
|
+
for (const parent of conn.parents) {
|
|
439
|
+
const name = modelNames.get(parent.id);
|
|
440
|
+
if (name) {
|
|
441
|
+
targetedNodes.push({ modelName: name, modelId: parent.id, attr: cn.attr, curves: cn.curves });
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (targetedNodes.length === 0)
|
|
447
|
+
return [];
|
|
448
|
+
let clipName = 'default';
|
|
449
|
+
let clipDuration = 0;
|
|
450
|
+
for (const node of objects.fbxNode.nodes) {
|
|
451
|
+
if (node.name !== 'AnimationStack')
|
|
452
|
+
continue;
|
|
453
|
+
const rawName = String(node.props[1] ?? 'default');
|
|
454
|
+
clipName = rawName.includes('\x00\x01') ? rawName.split('\x00\x01')[0] : rawName;
|
|
455
|
+
const props70 = node.nodes.find(n => n.name === 'Properties70');
|
|
456
|
+
if (props70) {
|
|
457
|
+
const localStop = props70.nodes.find(n => n.name === 'P' && String(n.props[0]) === 'LocalStop');
|
|
458
|
+
if (localStop) {
|
|
459
|
+
const val = localStop.props[4];
|
|
460
|
+
const num = typeof val === 'bigint' ? Number(val) : Number(val);
|
|
461
|
+
if (num > 0)
|
|
462
|
+
clipDuration = num / FBX_TICKS_PER_SECOND;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
467
|
+
const defaultEulerOrder = 'ZYX';
|
|
468
|
+
const tracks = [];
|
|
469
|
+
for (const tn of targetedNodes) {
|
|
470
|
+
const refCurve = tn.curves.x || tn.curves.y || tn.curves.z;
|
|
471
|
+
if (!refCurve)
|
|
472
|
+
continue;
|
|
473
|
+
const keyCount = refCurve.times.length;
|
|
474
|
+
const times = new Float32Array(refCurve.times);
|
|
475
|
+
const info = modelInfo.get(tn.modelId);
|
|
476
|
+
const sanitizedName = PropertyBinding.sanitizeNodeName(tn.modelName);
|
|
477
|
+
if (tn.attr === 'T') {
|
|
478
|
+
const values = new Float32Array(keyCount * 3);
|
|
479
|
+
for (let k = 0; k < keyCount; k++) {
|
|
480
|
+
values[k * 3] = (tn.curves.x?.values[k] ?? 0) * unitScale;
|
|
481
|
+
values[k * 3 + 1] = (tn.curves.y?.values[k] ?? 0) * unitScale;
|
|
482
|
+
values[k * 3 + 2] = (tn.curves.z?.values[k] ?? 0) * unitScale;
|
|
483
|
+
}
|
|
484
|
+
tracks.push({ name: `${sanitizedName}.position`, times, values, valuesPerKey: 3 });
|
|
485
|
+
if (clipDuration === 0 && keyCount > 0)
|
|
486
|
+
clipDuration = Math.max(clipDuration, times[keyCount - 1]);
|
|
487
|
+
}
|
|
488
|
+
else if (tn.attr === 'R') {
|
|
489
|
+
const eulerOrder = info?.eulerOrder ?? defaultEulerOrder;
|
|
490
|
+
let preRotQuat = null;
|
|
491
|
+
if (info?.preRotation) {
|
|
492
|
+
const pr = info.preRotation;
|
|
493
|
+
preRotQuat = new THREE.Quaternion().setFromEuler(new THREE.Euler(pr[0] * DEG2RAD, pr[1] * DEG2RAD, pr[2] * DEG2RAD, defaultEulerOrder));
|
|
494
|
+
}
|
|
495
|
+
let postRotQuat = null;
|
|
496
|
+
if (info?.postRotation) {
|
|
497
|
+
const po = info.postRotation;
|
|
498
|
+
postRotQuat = new THREE.Quaternion().setFromEuler(new THREE.Euler(po[0] * DEG2RAD, po[1] * DEG2RAD, po[2] * DEG2RAD, defaultEulerOrder)).invert();
|
|
499
|
+
}
|
|
500
|
+
const values = new Float32Array(keyCount * 4);
|
|
501
|
+
const euler = new THREE.Euler();
|
|
502
|
+
const quat = new THREE.Quaternion();
|
|
503
|
+
const prevQuat = new THREE.Quaternion();
|
|
504
|
+
for (let k = 0; k < keyCount; k++) {
|
|
505
|
+
euler.set((tn.curves.x?.values[k] ?? 0) * DEG2RAD, (tn.curves.y?.values[k] ?? 0) * DEG2RAD, (tn.curves.z?.values[k] ?? 0) * DEG2RAD, eulerOrder);
|
|
506
|
+
quat.setFromEuler(euler);
|
|
507
|
+
if (preRotQuat)
|
|
508
|
+
quat.premultiply(preRotQuat);
|
|
509
|
+
if (postRotQuat)
|
|
510
|
+
quat.multiply(postRotQuat);
|
|
511
|
+
if (k > 0 && quat.dot(prevQuat) < 0) {
|
|
512
|
+
quat.x = -quat.x;
|
|
513
|
+
quat.y = -quat.y;
|
|
514
|
+
quat.z = -quat.z;
|
|
515
|
+
quat.w = -quat.w;
|
|
516
|
+
}
|
|
517
|
+
values[k * 4] = quat.x;
|
|
518
|
+
values[k * 4 + 1] = quat.y;
|
|
519
|
+
values[k * 4 + 2] = quat.z;
|
|
520
|
+
values[k * 4 + 3] = quat.w;
|
|
521
|
+
prevQuat.copy(quat);
|
|
522
|
+
}
|
|
523
|
+
tracks.push({ name: `${sanitizedName}.quaternion`, times, values, valuesPerKey: 4 });
|
|
524
|
+
if (clipDuration === 0 && keyCount > 0)
|
|
525
|
+
clipDuration = Math.max(clipDuration, times[keyCount - 1]);
|
|
526
|
+
}
|
|
527
|
+
else if (tn.attr === 'S') {
|
|
528
|
+
const values = new Float32Array(keyCount * 3);
|
|
529
|
+
for (let k = 0; k < keyCount; k++) {
|
|
530
|
+
values[k * 3] = tn.curves.x?.values[k] ?? 1;
|
|
531
|
+
values[k * 3 + 1] = tn.curves.y?.values[k] ?? 1;
|
|
532
|
+
values[k * 3 + 2] = tn.curves.z?.values[k] ?? 1;
|
|
533
|
+
}
|
|
534
|
+
tracks.push({ name: `${sanitizedName}.scale`, times, values, valuesPerKey: 3 });
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
if (tracks.length === 0)
|
|
538
|
+
return [];
|
|
539
|
+
return [{ name: clipName, duration: clipDuration, tracks }];
|
|
540
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { IImageDecoder } from './interfaces.js';
|
|
2
|
+
export declare class SharpImageDecoder implements IImageDecoder {
|
|
3
|
+
loadPixels(data: Uint8Array): Promise<{
|
|
4
|
+
pixels: Uint8Array;
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
}>;
|
|
8
|
+
resizePixels(pixels: Uint8Array, width: number, height: number, divisor: number): Promise<{
|
|
9
|
+
pixels: Uint8Array;
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
}>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
export class SharpImageDecoder {
|
|
3
|
+
async loadPixels(data) {
|
|
4
|
+
const image = sharp(Buffer.from(data));
|
|
5
|
+
const metadata = await image.metadata();
|
|
6
|
+
const { data: rawPixels, info } = await image
|
|
7
|
+
.ensureAlpha()
|
|
8
|
+
.raw()
|
|
9
|
+
.toBuffer({ resolveWithObject: true });
|
|
10
|
+
return {
|
|
11
|
+
pixels: new Uint8Array(rawPixels.buffer, rawPixels.byteOffset, rawPixels.byteLength),
|
|
12
|
+
width: info.width,
|
|
13
|
+
height: info.height,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
async resizePixels(pixels, width, height, divisor) {
|
|
17
|
+
if (divisor <= 1)
|
|
18
|
+
return { pixels, width, height };
|
|
19
|
+
const newW = Math.max(1, Math.floor(width / divisor));
|
|
20
|
+
const newH = Math.max(1, Math.floor(height / divisor));
|
|
21
|
+
const { data: resizedPixels, info } = await sharp(Buffer.from(pixels), {
|
|
22
|
+
raw: { width, height, channels: 4 },
|
|
23
|
+
})
|
|
24
|
+
.resize(newW, newH)
|
|
25
|
+
.raw()
|
|
26
|
+
.toBuffer({ resolveWithObject: true });
|
|
27
|
+
return {
|
|
28
|
+
pixels: new Uint8Array(resizedPixels.buffer, resizedPixels.byteOffset, resizedPixels.byteLength),
|
|
29
|
+
width: info.width,
|
|
30
|
+
height: info.height,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { KTX2Quality, TextureChannelFormat, TextureMetadata, MeshMetadata } from '../core/types.js';
|
|
2
|
+
export interface TextureEncodeResult {
|
|
3
|
+
data: Uint8Array;
|
|
4
|
+
metadata: TextureMetadata;
|
|
5
|
+
}
|
|
6
|
+
export interface ITextureEncoder {
|
|
7
|
+
encode(pixels: Uint8Array, width: number, height: number, channels: number, quality: KTX2Quality, channelFormat: TextureChannelFormat, generateMipmaps?: boolean): Promise<TextureEncodeResult>;
|
|
8
|
+
isReady(): boolean;
|
|
9
|
+
initialize(): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
export interface ImportedSubMesh {
|
|
12
|
+
skinData?: ImportedSkinData;
|
|
13
|
+
positions: Float32Array;
|
|
14
|
+
normals: Float32Array | null;
|
|
15
|
+
uvs: Float32Array | null;
|
|
16
|
+
indices: Uint32Array;
|
|
17
|
+
materialIndex: number;
|
|
18
|
+
}
|
|
19
|
+
export interface ImportedMaterial {
|
|
20
|
+
name: string;
|
|
21
|
+
}
|
|
22
|
+
export interface ImportedNode {
|
|
23
|
+
name: string;
|
|
24
|
+
parentIndex: number;
|
|
25
|
+
position: [number, number, number];
|
|
26
|
+
rotation: [number, number, number, number];
|
|
27
|
+
scale: [number, number, number];
|
|
28
|
+
meshIndices: number[];
|
|
29
|
+
}
|
|
30
|
+
export interface ImportedMesh {
|
|
31
|
+
subMeshes: ImportedSubMesh[];
|
|
32
|
+
materials: ImportedMaterial[];
|
|
33
|
+
nodes: ImportedNode[];
|
|
34
|
+
hasSkeleton: boolean;
|
|
35
|
+
bones: ImportedBone[];
|
|
36
|
+
animations: ImportedAnimation[];
|
|
37
|
+
}
|
|
38
|
+
export interface IMeshImporter {
|
|
39
|
+
import(data: Uint8Array, fileName: string): Promise<ImportedMesh>;
|
|
40
|
+
}
|
|
41
|
+
export interface MeshEncodeSettings {
|
|
42
|
+
compressionLevel: number;
|
|
43
|
+
positionQuantization: number;
|
|
44
|
+
normalQuantization: number;
|
|
45
|
+
uvQuantization: number;
|
|
46
|
+
scaleFactor: number;
|
|
47
|
+
}
|
|
48
|
+
export interface MeshEncodeResult {
|
|
49
|
+
data: Uint8Array;
|
|
50
|
+
metadata: MeshMetadata;
|
|
51
|
+
}
|
|
52
|
+
export interface IMeshEncoder {
|
|
53
|
+
encode(mesh: ImportedMesh, settings: MeshEncodeSettings): Promise<MeshEncodeResult>;
|
|
54
|
+
isReady(): boolean;
|
|
55
|
+
initialize(): Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
export interface ImportedAnimationChannel {
|
|
58
|
+
boneName: string;
|
|
59
|
+
property: 'position' | 'quaternion' | 'scale';
|
|
60
|
+
times: Float32Array;
|
|
61
|
+
values: Float32Array;
|
|
62
|
+
}
|
|
63
|
+
export interface ImportedAnimationTrack {
|
|
64
|
+
name: string;
|
|
65
|
+
times: Float32Array;
|
|
66
|
+
values: Float32Array;
|
|
67
|
+
valuesPerKey: number;
|
|
68
|
+
}
|
|
69
|
+
export interface ImportedAnimation {
|
|
70
|
+
name: string;
|
|
71
|
+
duration: number;
|
|
72
|
+
tracks: ImportedAnimationTrack[];
|
|
73
|
+
}
|
|
74
|
+
export interface ImportedBone {
|
|
75
|
+
name: string;
|
|
76
|
+
parentIndex: number;
|
|
77
|
+
offsetMatrix: number[];
|
|
78
|
+
}
|
|
79
|
+
export interface ImportedSkinData {
|
|
80
|
+
boneIndices: Uint32Array;
|
|
81
|
+
boneWeights: Float32Array;
|
|
82
|
+
}
|
|
83
|
+
export interface DecodedPcm {
|
|
84
|
+
channels: Float32Array[];
|
|
85
|
+
sampleRate: number;
|
|
86
|
+
durationMs: number;
|
|
87
|
+
}
|
|
88
|
+
export interface IAudioDecoder {
|
|
89
|
+
decodeToPcm(audioData: Uint8Array, fileName: string): Promise<DecodedPcm>;
|
|
90
|
+
}
|
|
91
|
+
export interface IAacEncoder {
|
|
92
|
+
encode(channels: Float32Array[], sampleRate: number, quality: import('../core/types.js').AacQuality): Promise<Uint8Array>;
|
|
93
|
+
}
|
|
94
|
+
export interface IImageDecoder {
|
|
95
|
+
loadPixels(data: Uint8Array): Promise<{
|
|
96
|
+
pixels: Uint8Array;
|
|
97
|
+
width: number;
|
|
98
|
+
height: number;
|
|
99
|
+
}>;
|
|
100
|
+
resizePixels(pixels: Uint8Array, width: number, height: number, divisor: number): Promise<{
|
|
101
|
+
pixels: Uint8Array;
|
|
102
|
+
width: number;
|
|
103
|
+
height: number;
|
|
104
|
+
}>;
|
|
105
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|