@series-inc/stowkit-cli 0.1.11 → 0.1.13

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.
@@ -0,0 +1,42 @@
1
+ import type { ImportedMesh, ImportedAnimation } from './interfaces.js';
2
+ import type { MaterialConfig } from '../app/state.js';
3
+ export interface PbrMaterialConfig {
4
+ baseColorFactor: [number, number, number, number];
5
+ metallicFactor: number;
6
+ roughnessFactor: number;
7
+ baseColorTextureName: string | null;
8
+ normalTextureName: string | null;
9
+ metallicRoughnessTextureName: string | null;
10
+ emissiveTextureName: string | null;
11
+ emissiveFactor: [number, number, number];
12
+ alphaMode: string;
13
+ alphaCutoff: number;
14
+ doubleSided: boolean;
15
+ }
16
+ export interface GlbExtractedTexture {
17
+ name: string;
18
+ data: Uint8Array;
19
+ mimeType: string;
20
+ }
21
+ export interface GlbExtractedMesh {
22
+ name: string;
23
+ imported: ImportedMesh;
24
+ hasSkeleton: boolean;
25
+ }
26
+ export interface GlbExtractedMaterial {
27
+ name: string;
28
+ pbrConfig: PbrMaterialConfig;
29
+ }
30
+ export interface GlbExtractResult {
31
+ meshes: GlbExtractedMesh[];
32
+ textures: GlbExtractedTexture[];
33
+ materials: GlbExtractedMaterial[];
34
+ animations: {
35
+ name: string;
36
+ clips: ImportedAnimation[];
37
+ }[];
38
+ }
39
+ export declare function parseGlb(data: Uint8Array, opts?: {
40
+ preserveHierarchy?: boolean;
41
+ }): Promise<GlbExtractResult>;
42
+ export declare function pbrToMaterialConfig(pbr: PbrMaterialConfig, containerPath: string): MaterialConfig;
@@ -0,0 +1,592 @@
1
+ import * as THREE from 'three';
2
+ import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
3
+ import { PropertyBinding } from 'three';
4
+ import { MaterialFieldType, PreviewPropertyFlag } from '../core/types.js';
5
+ // ─── DOM shim (shared pattern from fbx-loader) ─────────────────────────────
6
+ function ensureDomShim() {
7
+ const g = globalThis;
8
+ if (typeof g.document !== 'undefined')
9
+ return;
10
+ g.document = {
11
+ createElementNS(_ns, tag) {
12
+ if (tag === 'img') {
13
+ const listeners = {};
14
+ const img = {
15
+ width: 1,
16
+ height: 1,
17
+ data: new Uint8Array(4),
18
+ set src(_) {
19
+ // Fire load event async so GLTFLoader doesn't hang
20
+ queueMicrotask(() => {
21
+ for (const fn of (listeners['load'] ?? []))
22
+ fn({ target: img });
23
+ });
24
+ },
25
+ set onload(fn) { listeners['load'] = [fn]; },
26
+ set onerror(fn) { listeners['error'] = [fn]; },
27
+ addEventListener(event, fn) {
28
+ (listeners[event] ??= []).push(fn);
29
+ },
30
+ removeEventListener(event, fn) {
31
+ const arr = listeners[event];
32
+ if (arr)
33
+ listeners[event] = arr.filter(f => f !== fn);
34
+ },
35
+ };
36
+ return img;
37
+ }
38
+ return {};
39
+ },
40
+ };
41
+ if (typeof g.self === 'undefined')
42
+ g.self = g;
43
+ if (typeof g.window === 'undefined')
44
+ g.window = g;
45
+ if (typeof g.Blob === 'undefined') {
46
+ g.Blob = class Blob {
47
+ parts;
48
+ constructor(parts) { this.parts = parts; }
49
+ };
50
+ }
51
+ if (typeof g.URL === 'undefined' || !g.URL.createObjectURL) {
52
+ const UrlClass = g.URL ?? {};
53
+ UrlClass.createObjectURL = () => '';
54
+ UrlClass.revokeObjectURL = () => { };
55
+ }
56
+ }
57
+ // ─── Main entry point ───────────────────────────────────────────────────────
58
+ export async function parseGlb(data, opts) {
59
+ ensureDomShim();
60
+ const loader = new GLTFLoader();
61
+ const buffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
62
+ const gltf = await new Promise((resolve, reject) => {
63
+ loader.parse(buffer, '', resolve, reject);
64
+ });
65
+ const textures = extractTexturesFromGlb(data);
66
+ const materials = extractMaterials(gltf, textures);
67
+ const meshes = opts?.preserveHierarchy
68
+ ? extractMeshesHierarchical(gltf)
69
+ : extractMeshes(gltf);
70
+ const animations = extractAnimations(gltf);
71
+ return { meshes, textures, materials, animations };
72
+ }
73
+ // ─── Extract Textures from raw GLB binary ───────────────────────────────────
74
+ function extractTexturesFromGlb(glbData) {
75
+ const results = [];
76
+ if (glbData.length < 20)
77
+ return results;
78
+ const headerView = new DataView(glbData.buffer, glbData.byteOffset, glbData.byteLength);
79
+ const magic = headerView.getUint32(0, true);
80
+ if (magic !== 0x46546C67)
81
+ return results; // 'glTF'
82
+ const jsonChunkLength = headerView.getUint32(12, true);
83
+ const jsonStart = 20;
84
+ const jsonText = new TextDecoder().decode(glbData.subarray(jsonStart, jsonStart + jsonChunkLength));
85
+ const json = JSON.parse(jsonText);
86
+ if (!json.images || !json.bufferViews)
87
+ return results;
88
+ const binChunkStart = 12 + 8 + jsonChunkLength;
89
+ if (binChunkStart + 8 > glbData.length)
90
+ return results;
91
+ const binDataStart = binChunkStart + 8;
92
+ for (let i = 0; i < json.images.length; i++) {
93
+ const imageDef = json.images[i];
94
+ if (imageDef.bufferView === undefined)
95
+ continue;
96
+ const bufferView = json.bufferViews[imageDef.bufferView];
97
+ if (!bufferView)
98
+ continue;
99
+ const offset = bufferView.byteOffset ?? 0;
100
+ const length = bufferView.byteLength;
101
+ const mimeType = imageDef.mimeType || 'image/png';
102
+ const ext = mimeType === 'image/jpeg' ? 'jpg' : 'png';
103
+ const baseName = imageDef.name || `texture_${i}`;
104
+ const textureName = baseName.endsWith(`.${ext}`) ? baseName : `${baseName}.${ext}`;
105
+ const start = binDataStart + offset;
106
+ const imageData = glbData.slice(start, start + length);
107
+ results.push({ name: textureName, data: imageData, mimeType });
108
+ }
109
+ return results;
110
+ }
111
+ // ─── Extract Materials ──────────────────────────────────────────────────────
112
+ function extractMaterials(gltf, textures) {
113
+ const results = [];
114
+ const json = gltf.parser.json;
115
+ if (!json.materials)
116
+ return results;
117
+ function getTextureName(textureIndex) {
118
+ const texDef = json.textures?.[textureIndex];
119
+ if (!texDef || texDef.source === undefined)
120
+ return null;
121
+ return textures[texDef.source]?.name ?? null;
122
+ }
123
+ for (let i = 0; i < json.materials.length; i++) {
124
+ const mat = json.materials[i];
125
+ const pbr = mat.pbrMetallicRoughness;
126
+ const config = {
127
+ baseColorFactor: pbr?.baseColorFactor
128
+ ? [pbr.baseColorFactor[0], pbr.baseColorFactor[1], pbr.baseColorFactor[2], pbr.baseColorFactor[3] ?? 1]
129
+ : [1, 1, 1, 1],
130
+ metallicFactor: pbr?.metallicFactor ?? 1,
131
+ roughnessFactor: pbr?.roughnessFactor ?? 1,
132
+ baseColorTextureName: pbr?.baseColorTexture !== undefined ? getTextureName(pbr.baseColorTexture.index) : null,
133
+ normalTextureName: mat.normalTexture !== undefined ? getTextureName(mat.normalTexture.index) : null,
134
+ metallicRoughnessTextureName: pbr?.metallicRoughnessTexture !== undefined ? getTextureName(pbr.metallicRoughnessTexture.index) : null,
135
+ emissiveTextureName: mat.emissiveTexture !== undefined ? getTextureName(mat.emissiveTexture.index) : null,
136
+ emissiveFactor: mat.emissiveFactor
137
+ ? [mat.emissiveFactor[0], mat.emissiveFactor[1], mat.emissiveFactor[2]]
138
+ : [0, 0, 0],
139
+ alphaMode: mat.alphaMode ?? 'OPAQUE',
140
+ alphaCutoff: mat.alphaCutoff ?? 0.5,
141
+ doubleSided: mat.doubleSided ?? false,
142
+ };
143
+ results.push({ name: mat.name || `material_${i}`, pbrConfig: config });
144
+ }
145
+ return results;
146
+ }
147
+ // ─── Extract Meshes ─────────────────────────────────────────────────────────
148
+ function extractMeshes(gltf) {
149
+ const scene = gltf.scene;
150
+ if (!scene)
151
+ return [];
152
+ scene.updateMatrixWorld(true);
153
+ const meshObjects = [];
154
+ scene.traverse((child) => {
155
+ if (child.isMesh) {
156
+ meshObjects.push(child);
157
+ }
158
+ });
159
+ if (meshObjects.length === 0)
160
+ return [];
161
+ const sceneBones = [];
162
+ const seenBoneNames = new Set();
163
+ scene.traverse((child) => {
164
+ if (child.isBone) {
165
+ const name = child.name;
166
+ if (!seenBoneNames.has(name)) {
167
+ seenBoneNames.add(name);
168
+ sceneBones.push(child);
169
+ }
170
+ }
171
+ });
172
+ let hasSkeleton = false;
173
+ const perMeshSkeletons = new Map();
174
+ for (const mesh of meshObjects) {
175
+ if (mesh.isSkinnedMesh) {
176
+ const sm = mesh;
177
+ if (sm.skeleton && sm.skeleton.bones.length > 0) {
178
+ hasSkeleton = true;
179
+ perMeshSkeletons.set(sm, sm.skeleton);
180
+ }
181
+ }
182
+ }
183
+ const allBones = [];
184
+ if (sceneBones.length > 0 && hasSkeleton) {
185
+ const rootInverse = new THREE.Matrix4().copy(scene.matrixWorld).invert();
186
+ const boneInverses = new Map();
187
+ for (const skel of perMeshSkeletons.values()) {
188
+ for (let i = 0; i < skel.bones.length; i++) {
189
+ if (!boneInverses.has(skel.bones[i].name)) {
190
+ boneInverses.set(skel.bones[i].name, skel.boneInverses[i]);
191
+ }
192
+ }
193
+ }
194
+ const boneIndexMap = new Map();
195
+ for (let i = 0; i < sceneBones.length; i++) {
196
+ boneIndexMap.set(sceneBones[i], i);
197
+ }
198
+ function computeOffset(obj) {
199
+ const localWorld = new THREE.Matrix4().multiplyMatrices(rootInverse, obj.matrixWorld);
200
+ localWorld.invert();
201
+ return Array.from(localWorld.elements);
202
+ }
203
+ for (let i = 0; i < sceneBones.length; i++) {
204
+ const bone = sceneBones[i];
205
+ let parentIndex = -1;
206
+ let ancestor = bone.parent;
207
+ while (ancestor && ancestor !== scene) {
208
+ const boneIdx = boneIndexMap.get(ancestor);
209
+ if (boneIdx !== undefined) {
210
+ parentIndex = boneIdx;
211
+ break;
212
+ }
213
+ ancestor = ancestor.parent;
214
+ }
215
+ const inv = boneInverses.get(bone.name);
216
+ const offsetMatrix = inv ? Array.from(inv.elements) : computeOffset(bone);
217
+ allBones.push({ name: bone.name, parentIndex, offsetMatrix });
218
+ }
219
+ }
220
+ const subMeshes = [];
221
+ const materials = [];
222
+ const materialSet = new Map();
223
+ const nodes = [];
224
+ for (let mi = 0; mi < meshObjects.length; mi++) {
225
+ const mesh = meshObjects[mi];
226
+ const geometry = mesh.geometry;
227
+ const posAttr = geometry.getAttribute('position');
228
+ if (!posAttr)
229
+ continue;
230
+ const vertCount = posAttr.count;
231
+ const positions = new Float32Array(vertCount * 3);
232
+ {
233
+ const worldMatrix = mesh.matrixWorld;
234
+ const v = new THREE.Vector3();
235
+ for (let vi = 0; vi < vertCount; vi++) {
236
+ v.fromBufferAttribute(posAttr, vi);
237
+ v.applyMatrix4(worldMatrix);
238
+ positions[vi * 3] = v.x;
239
+ positions[vi * 3 + 1] = v.y;
240
+ positions[vi * 3 + 2] = v.z;
241
+ }
242
+ }
243
+ let normals = null;
244
+ const normAttr = geometry.getAttribute('normal');
245
+ if (normAttr && normAttr.count === vertCount) {
246
+ normals = new Float32Array(vertCount * 3);
247
+ const normalMatrix = new THREE.Matrix3().getNormalMatrix(mesh.matrixWorld);
248
+ const n = new THREE.Vector3();
249
+ for (let vi = 0; vi < vertCount; vi++) {
250
+ n.fromBufferAttribute(normAttr, vi);
251
+ n.applyMatrix3(normalMatrix).normalize();
252
+ normals[vi * 3] = n.x;
253
+ normals[vi * 3 + 1] = n.y;
254
+ normals[vi * 3 + 2] = n.z;
255
+ }
256
+ }
257
+ let uvs = null;
258
+ const uvAttr = geometry.getAttribute('uv');
259
+ if (uvAttr && uvAttr.count === vertCount) {
260
+ uvs = new Float32Array(vertCount * 2);
261
+ for (let vi = 0; vi < vertCount; vi++) {
262
+ uvs[vi * 2] = uvAttr.getX(vi);
263
+ // Flip V: glTF uses top-left origin (V=0 at top), but our KTX2 pipeline
264
+ // flips textures to bottom-left origin for Basis Universal encoding.
265
+ uvs[vi * 2 + 1] = 1 - uvAttr.getY(vi);
266
+ }
267
+ }
268
+ const indexAttr = geometry.getIndex();
269
+ let indices;
270
+ if (indexAttr) {
271
+ indices = new Uint32Array(indexAttr.count);
272
+ for (let ii = 0; ii < indexAttr.count; ii++)
273
+ indices[ii] = indexAttr.getX(ii);
274
+ }
275
+ else {
276
+ indices = new Uint32Array(vertCount);
277
+ for (let ii = 0; ii < vertCount; ii++)
278
+ indices[ii] = ii;
279
+ }
280
+ let skinData;
281
+ if (mesh.isSkinnedMesh) {
282
+ const skinIndexAttr = geometry.getAttribute('skinIndex');
283
+ const skinWeightAttr = geometry.getAttribute('skinWeight');
284
+ if (skinIndexAttr && skinWeightAttr) {
285
+ const meshSkeleton = perMeshSkeletons.get(mesh);
286
+ let remap = null;
287
+ if (meshSkeleton) {
288
+ remap = new Uint32Array(meshSkeleton.bones.length);
289
+ for (let si = 0; si < meshSkeleton.bones.length; si++) {
290
+ const idx = sceneBones.findIndex(b => b.name === meshSkeleton.bones[si].name);
291
+ remap[si] = idx >= 0 ? idx : 0;
292
+ }
293
+ }
294
+ const boneIndices = new Uint32Array(vertCount * 4);
295
+ const boneWeights = new Float32Array(vertCount * 4);
296
+ for (let vi = 0; vi < vertCount; vi++) {
297
+ const si0 = skinIndexAttr.getX(vi);
298
+ const si1 = skinIndexAttr.getY(vi);
299
+ const si2 = skinIndexAttr.getZ(vi);
300
+ const si3 = skinIndexAttr.getW(vi);
301
+ boneIndices[vi * 4] = remap && si0 < remap.length ? remap[si0] : si0;
302
+ boneIndices[vi * 4 + 1] = remap && si1 < remap.length ? remap[si1] : si1;
303
+ boneIndices[vi * 4 + 2] = remap && si2 < remap.length ? remap[si2] : si2;
304
+ boneIndices[vi * 4 + 3] = remap && si3 < remap.length ? remap[si3] : si3;
305
+ boneWeights[vi * 4] = skinWeightAttr.getX(vi);
306
+ boneWeights[vi * 4 + 1] = skinWeightAttr.getY(vi);
307
+ boneWeights[vi * 4 + 2] = skinWeightAttr.getZ(vi);
308
+ boneWeights[vi * 4 + 3] = skinWeightAttr.getW(vi);
309
+ }
310
+ skinData = { boneIndices, boneWeights };
311
+ }
312
+ }
313
+ const meshMaterial = Array.isArray(mesh.material) ? mesh.material[0] : mesh.material;
314
+ const matName = meshMaterial?.name || 'default';
315
+ let matIndex;
316
+ if (materialSet.has(matName)) {
317
+ matIndex = materialSet.get(matName);
318
+ }
319
+ else {
320
+ matIndex = materials.length;
321
+ materialSet.set(matName, matIndex);
322
+ materials.push({ name: matName });
323
+ }
324
+ subMeshes.push({ skinData, positions, normals, uvs, indices, materialIndex: matIndex });
325
+ nodes.push({
326
+ name: mesh.name || `mesh_${mi}`,
327
+ parentIndex: -1,
328
+ position: [0, 0, 0],
329
+ rotation: [0, 0, 0, 1],
330
+ scale: [1, 1, 1],
331
+ meshIndices: [mi],
332
+ });
333
+ }
334
+ if (materials.length === 0)
335
+ materials.push({ name: 'default' });
336
+ const imported = {
337
+ subMeshes,
338
+ materials,
339
+ nodes,
340
+ hasSkeleton,
341
+ bones: allBones,
342
+ animations: [],
343
+ };
344
+ const meshName = meshObjects[0]?.name || 'mesh';
345
+ return [{ name: meshName, imported, hasSkeleton }];
346
+ }
347
+ // ─── Extract Meshes (Hierarchical) ──────────────────────────────────────────
348
+ function extractMeshesHierarchical(gltf) {
349
+ const scene = gltf.scene;
350
+ if (!scene)
351
+ return [];
352
+ scene.updateMatrixWorld(true);
353
+ // Collect all relevant nodes (meshes and groups that form the hierarchy).
354
+ // Skip Bone, Light, Camera, and SkinnedMesh nodes.
355
+ const nodeList = [];
356
+ const nodeIndexMap = new Map();
357
+ function collectNodes(obj) {
358
+ // Skip skeleton bones, lights, cameras
359
+ if (obj.isBone)
360
+ return;
361
+ if (obj.isLight)
362
+ return;
363
+ if (obj.isCamera)
364
+ return;
365
+ // Skip skinned meshes — hierarchy preservation is static-mesh only
366
+ if (obj.isSkinnedMesh)
367
+ return;
368
+ nodeIndexMap.set(obj, nodeList.length);
369
+ nodeList.push(obj);
370
+ for (const child of obj.children) {
371
+ collectNodes(child);
372
+ }
373
+ }
374
+ // Start from scene children (skip the scene root itself)
375
+ for (const child of scene.children) {
376
+ collectNodes(child);
377
+ }
378
+ if (nodeList.length === 0)
379
+ return [];
380
+ const subMeshes = [];
381
+ const materials = [];
382
+ const materialSet = new Map();
383
+ const nodes = [];
384
+ for (let ni = 0; ni < nodeList.length; ni++) {
385
+ const obj = nodeList[ni];
386
+ const isMesh = obj.isMesh;
387
+ // Determine parent index
388
+ let parentIndex = -1;
389
+ let ancestor = obj.parent;
390
+ while (ancestor && ancestor !== scene) {
391
+ const idx = nodeIndexMap.get(ancestor);
392
+ if (idx !== undefined) {
393
+ parentIndex = idx;
394
+ break;
395
+ }
396
+ ancestor = ancestor.parent;
397
+ }
398
+ // Decompose local transform
399
+ const pos = [obj.position.x, obj.position.y, obj.position.z];
400
+ const rot = [obj.quaternion.x, obj.quaternion.y, obj.quaternion.z, obj.quaternion.w];
401
+ const scl = [obj.scale.x, obj.scale.y, obj.scale.z];
402
+ const meshIndices = [];
403
+ if (isMesh) {
404
+ const mesh = obj;
405
+ const geometry = mesh.geometry;
406
+ const posAttr = geometry.getAttribute('position');
407
+ if (!posAttr) {
408
+ nodes.push({ name: obj.name || `node_${ni}`, parentIndex, position: pos, rotation: rot, scale: scl, meshIndices: [] });
409
+ continue;
410
+ }
411
+ const vertCount = posAttr.count;
412
+ // Store positions in LOCAL space (no matrixWorld bake)
413
+ const positions = new Float32Array(vertCount * 3);
414
+ for (let vi = 0; vi < vertCount; vi++) {
415
+ positions[vi * 3] = posAttr.getX(vi);
416
+ positions[vi * 3 + 1] = posAttr.getY(vi);
417
+ positions[vi * 3 + 2] = posAttr.getZ(vi);
418
+ }
419
+ // Normals in local space (use local normal matrix = identity for unscaled,
420
+ // but compute from local matrix for correctness with non-uniform scale)
421
+ let normals = null;
422
+ const normAttr = geometry.getAttribute('normal');
423
+ if (normAttr && normAttr.count === vertCount) {
424
+ normals = new Float32Array(vertCount * 3);
425
+ const localNormalMatrix = new THREE.Matrix3().getNormalMatrix(mesh.matrix);
426
+ const n = new THREE.Vector3();
427
+ for (let vi = 0; vi < vertCount; vi++) {
428
+ n.fromBufferAttribute(normAttr, vi);
429
+ n.applyMatrix3(localNormalMatrix).normalize();
430
+ normals[vi * 3] = n.x;
431
+ normals[vi * 3 + 1] = n.y;
432
+ normals[vi * 3 + 2] = n.z;
433
+ }
434
+ }
435
+ let uvs = null;
436
+ const uvAttr = geometry.getAttribute('uv');
437
+ if (uvAttr && uvAttr.count === vertCount) {
438
+ uvs = new Float32Array(vertCount * 2);
439
+ for (let vi = 0; vi < vertCount; vi++) {
440
+ uvs[vi * 2] = uvAttr.getX(vi);
441
+ uvs[vi * 2 + 1] = 1 - uvAttr.getY(vi);
442
+ }
443
+ }
444
+ const indexAttr = geometry.getIndex();
445
+ let indices;
446
+ if (indexAttr) {
447
+ indices = new Uint32Array(indexAttr.count);
448
+ for (let ii = 0; ii < indexAttr.count; ii++)
449
+ indices[ii] = indexAttr.getX(ii);
450
+ }
451
+ else {
452
+ indices = new Uint32Array(vertCount);
453
+ for (let ii = 0; ii < vertCount; ii++)
454
+ indices[ii] = ii;
455
+ }
456
+ const meshMaterial = Array.isArray(mesh.material) ? mesh.material[0] : mesh.material;
457
+ const matName = meshMaterial?.name || 'default';
458
+ let matIndex;
459
+ if (materialSet.has(matName)) {
460
+ matIndex = materialSet.get(matName);
461
+ }
462
+ else {
463
+ matIndex = materials.length;
464
+ materialSet.set(matName, matIndex);
465
+ materials.push({ name: matName });
466
+ }
467
+ meshIndices.push(subMeshes.length);
468
+ subMeshes.push({ positions, normals, uvs, indices, materialIndex: matIndex });
469
+ }
470
+ nodes.push({
471
+ name: obj.name || `node_${ni}`,
472
+ parentIndex,
473
+ position: pos,
474
+ rotation: rot,
475
+ scale: scl,
476
+ meshIndices,
477
+ });
478
+ }
479
+ if (materials.length === 0)
480
+ materials.push({ name: 'default' });
481
+ const imported = {
482
+ subMeshes,
483
+ materials,
484
+ nodes,
485
+ hasSkeleton: false,
486
+ bones: [],
487
+ animations: [],
488
+ };
489
+ const meshName = nodeList[0]?.name || 'mesh';
490
+ return [{ name: meshName, imported, hasSkeleton: false }];
491
+ }
492
+ // ─── Extract Animations ─────────────────────────────────────────────────────
493
+ function extractAnimations(gltf) {
494
+ if (!gltf.animations || gltf.animations.length === 0)
495
+ return [];
496
+ const results = [];
497
+ for (const clip of gltf.animations) {
498
+ const tracks = [];
499
+ for (const track of clip.tracks) {
500
+ const sanitizedName = PropertyBinding.sanitizeNodeName(track.name);
501
+ const valuesPerKey = track.getValueSize();
502
+ tracks.push({
503
+ name: sanitizedName,
504
+ times: new Float32Array(track.times),
505
+ values: new Float32Array(track.values),
506
+ valuesPerKey,
507
+ });
508
+ }
509
+ if (tracks.length > 0) {
510
+ results.push({
511
+ name: clip.name || 'default',
512
+ clips: [{
513
+ name: clip.name || 'default',
514
+ duration: clip.duration,
515
+ tracks,
516
+ }],
517
+ });
518
+ }
519
+ }
520
+ return results;
521
+ }
522
+ // ─── PBR → MaterialConfig helper ────────────────────────────────────────────
523
+ export function pbrToMaterialConfig(pbr, containerPath) {
524
+ const properties = [];
525
+ if (pbr.baseColorTextureName) {
526
+ properties.push({
527
+ fieldName: 'BaseColor',
528
+ fieldType: MaterialFieldType.Texture,
529
+ previewFlag: PreviewPropertyFlag.MainTex,
530
+ value: [1, 1, 1, 1],
531
+ textureAssetId: `${containerPath}/${pbr.baseColorTextureName}`,
532
+ });
533
+ }
534
+ properties.push({
535
+ fieldName: 'Tint',
536
+ fieldType: MaterialFieldType.Color,
537
+ previewFlag: PreviewPropertyFlag.Tint,
538
+ value: pbr.baseColorFactor,
539
+ textureAssetId: null,
540
+ });
541
+ properties.push({
542
+ fieldName: 'Metallic',
543
+ fieldType: MaterialFieldType.Float,
544
+ previewFlag: PreviewPropertyFlag.None,
545
+ value: [pbr.metallicFactor, 0, 0, 0],
546
+ textureAssetId: null,
547
+ });
548
+ properties.push({
549
+ fieldName: 'Roughness',
550
+ fieldType: MaterialFieldType.Float,
551
+ previewFlag: PreviewPropertyFlag.None,
552
+ value: [pbr.roughnessFactor, 0, 0, 0],
553
+ textureAssetId: null,
554
+ });
555
+ if (pbr.normalTextureName) {
556
+ properties.push({
557
+ fieldName: 'NormalMap',
558
+ fieldType: MaterialFieldType.Texture,
559
+ previewFlag: PreviewPropertyFlag.None,
560
+ value: [1, 1, 1, 1],
561
+ textureAssetId: `${containerPath}/${pbr.normalTextureName}`,
562
+ });
563
+ }
564
+ if (pbr.emissiveTextureName) {
565
+ properties.push({
566
+ fieldName: 'Emissive',
567
+ fieldType: MaterialFieldType.Texture,
568
+ previewFlag: PreviewPropertyFlag.None,
569
+ value: [1, 1, 1, 1],
570
+ textureAssetId: `${containerPath}/${pbr.emissiveTextureName}`,
571
+ });
572
+ }
573
+ if (pbr.emissiveFactor[0] > 0 || pbr.emissiveFactor[1] > 0 || pbr.emissiveFactor[2] > 0) {
574
+ properties.push({
575
+ fieldName: 'EmissiveColor',
576
+ fieldType: MaterialFieldType.Color,
577
+ previewFlag: PreviewPropertyFlag.None,
578
+ value: [pbr.emissiveFactor[0], pbr.emissiveFactor[1], pbr.emissiveFactor[2], 1],
579
+ textureAssetId: null,
580
+ });
581
+ }
582
+ if (pbr.metallicRoughnessTextureName) {
583
+ properties.push({
584
+ fieldName: 'MetallicRoughness',
585
+ fieldType: MaterialFieldType.Texture,
586
+ previewFlag: PreviewPropertyFlag.None,
587
+ value: [1, 1, 1, 1],
588
+ textureAssetId: `${containerPath}/${pbr.metallicRoughnessTextureName}`,
589
+ });
590
+ }
591
+ return { schemaId: '', properties };
592
+ }
@@ -35,8 +35,11 @@ export interface ImportedMesh {
35
35
  bones: ImportedBone[];
36
36
  animations: ImportedAnimation[];
37
37
  }
38
+ export interface MeshImportOpts {
39
+ preserveHierarchy?: boolean;
40
+ }
38
41
  export interface IMeshImporter {
39
- import(data: Uint8Array, fileName: string): Promise<ImportedMesh>;
42
+ import(data: Uint8Array, fileName: string, opts?: MeshImportOpts): Promise<ImportedMesh>;
40
43
  }
41
44
  export interface MeshEncodeSettings {
42
45
  compressionLevel: number;
package/dist/node-fs.js CHANGED
@@ -106,6 +106,8 @@ async function walkDirectory(basePath, prefix, sourceFiles, metaFiles, matFiles,
106
106
  if (entry.isDirectory()) {
107
107
  if (entry.name.startsWith('.'))
108
108
  continue;
109
+ if (entry.name.endsWith('.children'))
110
+ continue;
109
111
  folders.push(relativePath);
110
112
  await walkDirectory(basePath, relativePath, sourceFiles, metaFiles, matFiles, folders);
111
113
  }