@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
- export {};
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: prop.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.27",
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.12",
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",