@series-inc/stowkit-cli 0.1.27 → 0.1.29
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.
|
@@ -2,6 +2,7 @@ import * as THREE from 'three';
|
|
|
2
2
|
import { FBXLoader } from 'three/addons/loaders/FBXLoader.js';
|
|
3
3
|
import { PropertyBinding } from 'three';
|
|
4
4
|
import { parseBinary as fbxParseBinary, parseText as fbxParseText, FBXReader } from 'fbx-parser';
|
|
5
|
+
import { mergeByMaterial, } from './interfaces.js';
|
|
5
6
|
// ─── Minimal DOM shim for Three.js FBXLoader in Node ────────────────────────
|
|
6
7
|
function ensureDomShim() {
|
|
7
8
|
const g = globalThis;
|
|
@@ -291,14 +292,14 @@ function extractMeshFromGroup(group) {
|
|
|
291
292
|
}
|
|
292
293
|
if (materials.length === 0)
|
|
293
294
|
materials.push({ name: 'default' });
|
|
294
|
-
return {
|
|
295
|
+
return mergeByMaterial({
|
|
295
296
|
subMeshes,
|
|
296
297
|
materials,
|
|
297
298
|
nodes,
|
|
298
299
|
hasSkeleton,
|
|
299
300
|
bones: allBones,
|
|
300
301
|
animations: [],
|
|
301
|
-
};
|
|
302
|
+
});
|
|
302
303
|
}
|
|
303
304
|
// ─── Hierarchical Group → ImportedMesh extraction ────────────────────────────
|
|
304
305
|
function extractMeshFromGroupHierarchical(group) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
2
|
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
|
3
|
+
import { mergeByMaterial } from './interfaces.js';
|
|
3
4
|
import { PropertyBinding } from 'three';
|
|
4
5
|
import { MaterialFieldType, PreviewPropertyFlag } from '../core/types.js';
|
|
5
6
|
// ─── DOM shim (shared pattern from fbx-loader) ─────────────────────────────
|
|
@@ -333,14 +334,14 @@ function extractMeshes(gltf) {
|
|
|
333
334
|
}
|
|
334
335
|
if (materials.length === 0)
|
|
335
336
|
materials.push({ name: 'default' });
|
|
336
|
-
const imported = {
|
|
337
|
+
const imported = mergeByMaterial({
|
|
337
338
|
subMeshes,
|
|
338
339
|
materials,
|
|
339
340
|
nodes,
|
|
340
341
|
hasSkeleton,
|
|
341
342
|
bones: allBones,
|
|
342
343
|
animations: [],
|
|
343
|
-
};
|
|
344
|
+
});
|
|
344
345
|
const meshName = meshObjects[0]?.name || 'mesh';
|
|
345
346
|
return [{ name: meshName, imported, hasSkeleton }];
|
|
346
347
|
}
|
|
@@ -83,6 +83,13 @@ export interface ImportedSkinData {
|
|
|
83
83
|
boneIndices: Uint32Array;
|
|
84
84
|
boneWeights: Float32Array;
|
|
85
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Merge submeshes that share the same material into a single submesh.
|
|
88
|
+
* Only used when preserveHierarchy is OFF — vertices are already in world space.
|
|
89
|
+
* Skinned submeshes are left unmerged.
|
|
90
|
+
* Returns a new ImportedMesh with merged submeshes and a single root node.
|
|
91
|
+
*/
|
|
92
|
+
export declare function mergeByMaterial(mesh: ImportedMesh): ImportedMesh;
|
|
86
93
|
export interface DecodedPcm {
|
|
87
94
|
channels: Float32Array[];
|
|
88
95
|
sampleRate: number;
|
|
@@ -1 +1,107 @@
|
|
|
1
|
-
|
|
1
|
+
// ─── Mesh Merging (flat mode) ────────────────────────────────────────────────
|
|
2
|
+
/**
|
|
3
|
+
* Merge submeshes that share the same material into a single submesh.
|
|
4
|
+
* Only used when preserveHierarchy is OFF — vertices are already in world space.
|
|
5
|
+
* Skinned submeshes are left unmerged.
|
|
6
|
+
* Returns a new ImportedMesh with merged submeshes and a single root node.
|
|
7
|
+
*/
|
|
8
|
+
export function mergeByMaterial(mesh) {
|
|
9
|
+
// Group non-skinned submeshes by materialIndex
|
|
10
|
+
const groups = new Map(); // materialIndex → submesh indices
|
|
11
|
+
const skinnedIndices = [];
|
|
12
|
+
for (let i = 0; i < mesh.subMeshes.length; i++) {
|
|
13
|
+
const sm = mesh.subMeshes[i];
|
|
14
|
+
if (sm.skinData) {
|
|
15
|
+
skinnedIndices.push(i);
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
let list = groups.get(sm.materialIndex);
|
|
19
|
+
if (!list) {
|
|
20
|
+
list = [];
|
|
21
|
+
groups.set(sm.materialIndex, list);
|
|
22
|
+
}
|
|
23
|
+
list.push(i);
|
|
24
|
+
}
|
|
25
|
+
// Nothing to merge
|
|
26
|
+
if (groups.size === mesh.subMeshes.length - skinnedIndices.length && skinnedIndices.length === 0) {
|
|
27
|
+
// Already one submesh per material, just collapse to single node
|
|
28
|
+
return collapseToSingleNode(mesh);
|
|
29
|
+
}
|
|
30
|
+
const mergedSubMeshes = [];
|
|
31
|
+
// Merge each material group
|
|
32
|
+
for (const [matIdx, smIndices] of groups) {
|
|
33
|
+
if (smIndices.length === 1) {
|
|
34
|
+
mergedSubMeshes.push(mesh.subMeshes[smIndices[0]]);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
// Calculate total sizes
|
|
38
|
+
let totalVerts = 0;
|
|
39
|
+
let totalIndices = 0;
|
|
40
|
+
let hasNormals = true;
|
|
41
|
+
let hasUvs = true;
|
|
42
|
+
for (const si of smIndices) {
|
|
43
|
+
const sm = mesh.subMeshes[si];
|
|
44
|
+
totalVerts += sm.positions.length / 3;
|
|
45
|
+
totalIndices += sm.indices.length;
|
|
46
|
+
if (!sm.normals)
|
|
47
|
+
hasNormals = false;
|
|
48
|
+
if (!sm.uvs)
|
|
49
|
+
hasUvs = false;
|
|
50
|
+
}
|
|
51
|
+
const positions = new Float32Array(totalVerts * 3);
|
|
52
|
+
const normals = hasNormals ? new Float32Array(totalVerts * 3) : null;
|
|
53
|
+
const uvs = hasUvs ? new Float32Array(totalVerts * 2) : null;
|
|
54
|
+
const indices = new Uint32Array(totalIndices);
|
|
55
|
+
let vertOffset = 0;
|
|
56
|
+
let idxOffset = 0;
|
|
57
|
+
for (const si of smIndices) {
|
|
58
|
+
const sm = mesh.subMeshes[si];
|
|
59
|
+
const vc = sm.positions.length / 3;
|
|
60
|
+
positions.set(sm.positions, vertOffset * 3);
|
|
61
|
+
if (normals && sm.normals)
|
|
62
|
+
normals.set(sm.normals, vertOffset * 3);
|
|
63
|
+
if (uvs && sm.uvs)
|
|
64
|
+
uvs.set(sm.uvs, vertOffset * 2);
|
|
65
|
+
// Offset indices to account for merged vertex buffer
|
|
66
|
+
for (let j = 0; j < sm.indices.length; j++) {
|
|
67
|
+
indices[idxOffset + j] = sm.indices[j] + vertOffset;
|
|
68
|
+
}
|
|
69
|
+
vertOffset += vc;
|
|
70
|
+
idxOffset += sm.indices.length;
|
|
71
|
+
}
|
|
72
|
+
mergedSubMeshes.push({ positions, normals, uvs, indices, materialIndex: matIdx });
|
|
73
|
+
}
|
|
74
|
+
// Append skinned submeshes unchanged
|
|
75
|
+
for (const si of skinnedIndices) {
|
|
76
|
+
mergedSubMeshes.push(mesh.subMeshes[si]);
|
|
77
|
+
}
|
|
78
|
+
// Single node referencing all submeshes — the user sees one mesh
|
|
79
|
+
const node = {
|
|
80
|
+
name: mesh.nodes[0]?.name || 'mesh',
|
|
81
|
+
parentIndex: -1,
|
|
82
|
+
position: [0, 0, 0],
|
|
83
|
+
rotation: [0, 0, 0, 1],
|
|
84
|
+
scale: [1, 1, 1],
|
|
85
|
+
meshIndices: mergedSubMeshes.map((_, i) => i),
|
|
86
|
+
};
|
|
87
|
+
return {
|
|
88
|
+
subMeshes: mergedSubMeshes,
|
|
89
|
+
materials: mesh.materials,
|
|
90
|
+
nodes: [node],
|
|
91
|
+
hasSkeleton: mesh.hasSkeleton,
|
|
92
|
+
bones: mesh.bones,
|
|
93
|
+
animations: mesh.animations,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/** Collapse N flat nodes into a single node referencing all submeshes */
|
|
97
|
+
function collapseToSingleNode(mesh) {
|
|
98
|
+
const node = {
|
|
99
|
+
name: mesh.nodes[0]?.name || 'mesh',
|
|
100
|
+
parentIndex: -1,
|
|
101
|
+
position: [0, 0, 0],
|
|
102
|
+
rotation: [0, 0, 0, 1],
|
|
103
|
+
scale: [1, 1, 1],
|
|
104
|
+
meshIndices: mesh.subMeshes.map((_, i) => i),
|
|
105
|
+
};
|
|
106
|
+
return { ...mesh, nodes: [node] };
|
|
107
|
+
}
|
package/dist/pipeline.js
CHANGED
|
@@ -367,8 +367,14 @@ function resolveAssignedMaterial(meshAsset, subMeshIndex, sourceMaterialName, as
|
|
|
367
367
|
}
|
|
368
368
|
function toMaterialPropertyValue(prop, assetsById) {
|
|
369
369
|
const textureAsset = prop.textureAssetId ? assetsById.get(prop.textureAssetId) : undefined;
|
|
370
|
+
// Use previewFlag name as fallback when fieldName is empty (e.g. alphaTest)
|
|
371
|
+
const flagNames = {
|
|
372
|
+
1: 'mainTex', 2: 'tint', 3: 'alphaTest',
|
|
373
|
+
4: 'metalness', 5: 'roughness', 6: 'normalMap', 7: 'emissiveMap', 8: 'emissive',
|
|
374
|
+
};
|
|
375
|
+
const fieldName = prop.fieldName.trim() || flagNames[prop.previewFlag] || '';
|
|
370
376
|
return {
|
|
371
|
-
fieldName
|
|
377
|
+
fieldName,
|
|
372
378
|
value: [prop.value[0], prop.value[1], prop.value[2], prop.value[3]],
|
|
373
379
|
textureId: textureAsset?.stringId ?? '',
|
|
374
380
|
};
|
|
@@ -390,7 +396,7 @@ function applyMaterialAssignments(meshAsset, metadata, assetsById, materialsBySt
|
|
|
390
396
|
continue;
|
|
391
397
|
const config = assigned.settings.materialConfig;
|
|
392
398
|
const propertyValues = config.properties
|
|
393
|
-
.filter((prop) => prop.fieldName.trim().length > 0)
|
|
399
|
+
.filter((prop) => prop.fieldName.trim().length > 0 || prop.previewFlag > 0)
|
|
394
400
|
.map((prop) => toMaterialPropertyValue(prop, assetsById));
|
|
395
401
|
materials[i].schemaId = config.schemaId || assigned.stringId;
|
|
396
402
|
materials[i].properties = propertyValues;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@series-inc/stowkit-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.29",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"stowkit": "./dist/cli.js"
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"dev": "tsc --watch"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@series-inc/stowkit-packer-gui": "^0.1.
|
|
20
|
+
"@series-inc/stowkit-packer-gui": "^0.1.16",
|
|
21
21
|
"@series-inc/stowkit-editor": "^0.1.2",
|
|
22
22
|
"draco3d": "^1.5.7",
|
|
23
23
|
"fbx-parser": "^2.1.3",
|