@series-inc/stowkit-cli 0.1.29 → 0.1.31
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/state.d.ts +2 -0
- package/dist/cli.js +12 -6
- package/dist/core/constants.d.ts +6 -1
- package/dist/core/constants.js +6 -1
- package/dist/core/types.d.ts +2 -1
- package/dist/encoders/fbx-loader.js +133 -41
- package/dist/encoders/skinned-mesh-builder.js +35 -15
- package/dist/format/metadata.js +2 -2
- package/dist/init.d.ts +2 -0
- package/dist/init.js +98 -0
- package/dist/orchestrator.js +2 -2
- package/dist/pipeline.d.ts +2 -0
- package/dist/pipeline.js +52 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.js +5 -3
- package/package.json +3 -3
- package/skill.md +2 -1
package/dist/app/state.d.ts
CHANGED
package/dist/cli.js
CHANGED
|
@@ -38,7 +38,9 @@ checkForUpdate();
|
|
|
38
38
|
function printUsage() {
|
|
39
39
|
console.log(`
|
|
40
40
|
Usage:
|
|
41
|
-
stowkit init [dir] Initialize a StowKit project
|
|
41
|
+
stowkit init [dir] Initialize a StowKit project (interactive menu)
|
|
42
|
+
stowkit init --with-engine Initialize with 3D engine included
|
|
43
|
+
stowkit init --no-engine Initialize without 3D engine (skip prompt)
|
|
42
44
|
stowkit init --update [dir] Update AI skill files to match installed CLI version
|
|
43
45
|
stowkit build [dir] Full build: scan + process + pack
|
|
44
46
|
stowkit scan [dir] Detect new assets, generate .stowmeta defaults
|
|
@@ -105,7 +107,11 @@ async function main() {
|
|
|
105
107
|
try {
|
|
106
108
|
switch (command) {
|
|
107
109
|
case 'init':
|
|
108
|
-
await initProject(projectDir, {
|
|
110
|
+
await initProject(projectDir, {
|
|
111
|
+
update: args.includes('--update'),
|
|
112
|
+
withEngine: args.includes('--with-engine'),
|
|
113
|
+
noEngine: args.includes('--no-engine'),
|
|
114
|
+
});
|
|
109
115
|
break;
|
|
110
116
|
case 'update': {
|
|
111
117
|
const currentVersion = getVersion();
|
|
@@ -226,7 +232,7 @@ async function main() {
|
|
|
226
232
|
process.exit(1);
|
|
227
233
|
}
|
|
228
234
|
const resolvedProject = path.resolve(projectDir);
|
|
229
|
-
await startServer({ port, projectDir: resolvedProject, staticApps: { '/': packerDir } });
|
|
235
|
+
await startServer({ port, projectDir: resolvedProject, force, staticApps: { '/': packerDir } });
|
|
230
236
|
console.log(`\n Packer: http://localhost:${port}\n`);
|
|
231
237
|
openBrowser(`http://localhost:${port}`);
|
|
232
238
|
break;
|
|
@@ -240,11 +246,11 @@ async function main() {
|
|
|
240
246
|
const resolvedProject = path.resolve(projectDir);
|
|
241
247
|
const packerPort = port + 1;
|
|
242
248
|
// Start editor server
|
|
243
|
-
await startServer({ port, projectDir: resolvedProject, staticApps: { '/': editorDir } });
|
|
249
|
+
await startServer({ port, projectDir: resolvedProject, force, staticApps: { '/': editorDir } });
|
|
244
250
|
// Start packer on next port if installed
|
|
245
251
|
const packerDir = resolveAppDir('@series-inc/stowkit-packer-gui', 'stowkit-packer-gui');
|
|
246
252
|
if (packerDir) {
|
|
247
|
-
await startServer({ port: packerPort, projectDir: resolvedProject, staticApps: { '/': packerDir } });
|
|
253
|
+
await startServer({ port: packerPort, projectDir: resolvedProject, force, staticApps: { '/': packerDir } });
|
|
248
254
|
console.log(`\n Editor: http://localhost:${port}`);
|
|
249
255
|
console.log(` Packer: http://localhost:${packerPort}\n`);
|
|
250
256
|
}
|
|
@@ -255,7 +261,7 @@ async function main() {
|
|
|
255
261
|
break;
|
|
256
262
|
}
|
|
257
263
|
case 'serve':
|
|
258
|
-
await startServer({ port, projectDir: projectDir !== '.' ? path.resolve(projectDir) : undefined });
|
|
264
|
+
await startServer({ port, force, projectDir: projectDir !== '.' ? path.resolve(projectDir) : undefined });
|
|
259
265
|
break;
|
|
260
266
|
default:
|
|
261
267
|
console.error(`Unknown command: ${command}`);
|
package/dist/core/constants.d.ts
CHANGED
|
@@ -50,10 +50,15 @@ export declare const SKINNED_MESH_GEOMETRY_INFO_SIZE = 72;
|
|
|
50
50
|
export declare const SKINNED_MESH_METADATA_FIXED_SIZE = 144;
|
|
51
51
|
/** Size of Bone on disk (bytes): name[64] + parent_index(4) + offset_matrix[64] */
|
|
52
52
|
export declare const BONE_SIZE = 132;
|
|
53
|
-
/** Size of VertexWeights on disk (bytes): bone_indices[
|
|
53
|
+
/** Size of VertexWeights on disk (bytes) — legacy: bone_indices[4*u32] + weights[4*f32] */
|
|
54
54
|
export declare const VERTEX_WEIGHTS_SIZE = 32;
|
|
55
|
+
/** Size of compact VertexWeights on disk: bone_indices[4*u16] + weights[4*u8] = 12 bytes */
|
|
56
|
+
export declare const COMPACT_VERTEX_WEIGHTS_SIZE = 12;
|
|
55
57
|
/** Interleaved vertex size for skinned meshes: pos[3] + normal[3] + uv[2] = 8 floats */
|
|
56
58
|
export declare const SKINNED_VERTEX_STRIDE = 32;
|
|
59
|
+
/** SkinnedMeshGeometryInfo flags */
|
|
60
|
+
export declare const SKINNED_INDEX16_FLAG = 1;
|
|
61
|
+
export declare const SKINNED_COMPACT_WEIGHTS_FLAG = 2;
|
|
57
62
|
/** Size of AnimationTrackDescriptor on disk (bytes) */
|
|
58
63
|
export declare const ANIMATION_TRACK_DESCRIPTOR_SIZE = 88;
|
|
59
64
|
/** Size of AnimationClipMetadata fixed portion on disk (bytes) */
|
package/dist/core/constants.js
CHANGED
|
@@ -50,10 +50,15 @@ export const SKINNED_MESH_GEOMETRY_INFO_SIZE = 72;
|
|
|
50
50
|
export const SKINNED_MESH_METADATA_FIXED_SIZE = 144;
|
|
51
51
|
/** Size of Bone on disk (bytes): name[64] + parent_index(4) + offset_matrix[64] */
|
|
52
52
|
export const BONE_SIZE = 132;
|
|
53
|
-
/** Size of VertexWeights on disk (bytes): bone_indices[
|
|
53
|
+
/** Size of VertexWeights on disk (bytes) — legacy: bone_indices[4*u32] + weights[4*f32] */
|
|
54
54
|
export const VERTEX_WEIGHTS_SIZE = 32;
|
|
55
|
+
/** Size of compact VertexWeights on disk: bone_indices[4*u16] + weights[4*u8] = 12 bytes */
|
|
56
|
+
export const COMPACT_VERTEX_WEIGHTS_SIZE = 12;
|
|
55
57
|
/** Interleaved vertex size for skinned meshes: pos[3] + normal[3] + uv[2] = 8 floats */
|
|
56
58
|
export const SKINNED_VERTEX_STRIDE = 32;
|
|
59
|
+
/** SkinnedMeshGeometryInfo flags */
|
|
60
|
+
export const SKINNED_INDEX16_FLAG = 0x1;
|
|
61
|
+
export const SKINNED_COMPACT_WEIGHTS_FLAG = 0x2;
|
|
57
62
|
// ─── Animation V2 Constants ─────────────────────────────────────────────────
|
|
58
63
|
/** Size of AnimationTrackDescriptor on disk (bytes) */
|
|
59
64
|
export const ANIMATION_TRACK_DESCRIPTOR_SIZE = 88;
|
package/dist/core/types.d.ts
CHANGED
|
@@ -159,7 +159,8 @@ export interface SkinnedMeshGeometryInfo {
|
|
|
159
159
|
weightsOffset: number;
|
|
160
160
|
weightsSize: number;
|
|
161
161
|
materialIndex: number;
|
|
162
|
-
|
|
162
|
+
/** Bit flags: 0x1 = index16, 0x2 = compact weights (u16 bone indices + u8 weights) */
|
|
163
|
+
flags: number;
|
|
163
164
|
}
|
|
164
165
|
export interface Bone {
|
|
165
166
|
name: string;
|
|
@@ -63,6 +63,14 @@ export class NodeFbxImporter {
|
|
|
63
63
|
return mesh;
|
|
64
64
|
}
|
|
65
65
|
catch (err) {
|
|
66
|
+
// Animation-only FBX files often crash Three.js's FBXLoader (missing skeleton connections).
|
|
67
|
+
// Fall back to extracting animations directly via fbx-parser.
|
|
68
|
+
const animations = parseFbxAnimations(data);
|
|
69
|
+
if (animations.length > 0) {
|
|
70
|
+
const mesh = emptyMesh();
|
|
71
|
+
mesh.animations = animations;
|
|
72
|
+
return mesh;
|
|
73
|
+
}
|
|
66
74
|
console.error('[NodeFbxImporter] Failed:', err);
|
|
67
75
|
return emptyMesh();
|
|
68
76
|
}
|
|
@@ -189,54 +197,57 @@ function extractMeshFromGroup(group) {
|
|
|
189
197
|
if (!posAttr)
|
|
190
198
|
continue;
|
|
191
199
|
const vertCount = posAttr.count;
|
|
192
|
-
|
|
200
|
+
// Pre-compute full-mesh vertex data (shared across groups)
|
|
201
|
+
const worldMatrix = mesh.matrixWorld;
|
|
202
|
+
const fullPositions = new Float32Array(vertCount * 3);
|
|
193
203
|
{
|
|
194
|
-
const worldMatrix = mesh.matrixWorld;
|
|
195
204
|
const v = new THREE.Vector3();
|
|
196
205
|
for (let vi = 0; vi < vertCount; vi++) {
|
|
197
206
|
v.fromBufferAttribute(posAttr, vi);
|
|
198
207
|
v.applyMatrix4(worldMatrix);
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
208
|
+
fullPositions[vi * 3] = v.x;
|
|
209
|
+
fullPositions[vi * 3 + 1] = v.y;
|
|
210
|
+
fullPositions[vi * 3 + 2] = v.z;
|
|
202
211
|
}
|
|
203
212
|
}
|
|
204
|
-
let
|
|
213
|
+
let fullNormals = null;
|
|
205
214
|
const normAttr = geometry.getAttribute('normal');
|
|
206
215
|
if (normAttr && normAttr.count === vertCount) {
|
|
207
|
-
|
|
216
|
+
fullNormals = new Float32Array(vertCount * 3);
|
|
208
217
|
const normalMatrix = new THREE.Matrix3().getNormalMatrix(mesh.matrixWorld);
|
|
209
218
|
const n = new THREE.Vector3();
|
|
210
219
|
for (let vi = 0; vi < vertCount; vi++) {
|
|
211
220
|
n.fromBufferAttribute(normAttr, vi);
|
|
212
221
|
n.applyMatrix3(normalMatrix).normalize();
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
222
|
+
fullNormals[vi * 3] = n.x;
|
|
223
|
+
fullNormals[vi * 3 + 1] = n.y;
|
|
224
|
+
fullNormals[vi * 3 + 2] = n.z;
|
|
216
225
|
}
|
|
217
226
|
}
|
|
218
|
-
let
|
|
227
|
+
let fullUvs = null;
|
|
219
228
|
const uvAttr = geometry.getAttribute('uv');
|
|
220
229
|
if (uvAttr && uvAttr.count === vertCount) {
|
|
221
|
-
|
|
230
|
+
fullUvs = new Float32Array(vertCount * 2);
|
|
222
231
|
for (let vi = 0; vi < vertCount; vi++) {
|
|
223
|
-
|
|
224
|
-
|
|
232
|
+
fullUvs[vi * 2] = uvAttr.getX(vi);
|
|
233
|
+
fullUvs[vi * 2 + 1] = uvAttr.getY(vi);
|
|
225
234
|
}
|
|
226
235
|
}
|
|
227
236
|
const indexAttr = geometry.getIndex();
|
|
228
|
-
let
|
|
237
|
+
let fullIndices;
|
|
229
238
|
if (indexAttr) {
|
|
230
|
-
|
|
239
|
+
fullIndices = new Uint32Array(indexAttr.count);
|
|
231
240
|
for (let ii = 0; ii < indexAttr.count; ii++)
|
|
232
|
-
|
|
241
|
+
fullIndices[ii] = indexAttr.getX(ii);
|
|
233
242
|
}
|
|
234
243
|
else {
|
|
235
|
-
|
|
244
|
+
fullIndices = new Uint32Array(vertCount);
|
|
236
245
|
for (let ii = 0; ii < vertCount; ii++)
|
|
237
|
-
|
|
246
|
+
fullIndices[ii] = ii;
|
|
238
247
|
}
|
|
239
|
-
|
|
248
|
+
// Pre-compute full skin data
|
|
249
|
+
let fullSkinBoneIndices;
|
|
250
|
+
let fullSkinBoneWeights;
|
|
240
251
|
if (mesh.isSkinnedMesh) {
|
|
241
252
|
const skinIndexAttr = geometry.getAttribute('skinIndex');
|
|
242
253
|
const skinWeightAttr = geometry.getAttribute('skinWeight');
|
|
@@ -250,44 +261,125 @@ function extractMeshFromGroup(group) {
|
|
|
250
261
|
remap[si] = idx >= 0 ? idx : 0;
|
|
251
262
|
}
|
|
252
263
|
}
|
|
253
|
-
|
|
254
|
-
|
|
264
|
+
fullSkinBoneIndices = new Uint32Array(vertCount * 4);
|
|
265
|
+
fullSkinBoneWeights = new Float32Array(vertCount * 4);
|
|
255
266
|
for (let vi = 0; vi < vertCount; vi++) {
|
|
256
267
|
const si0 = skinIndexAttr.getX(vi);
|
|
257
268
|
const si1 = skinIndexAttr.getY(vi);
|
|
258
269
|
const si2 = skinIndexAttr.getZ(vi);
|
|
259
270
|
const si3 = skinIndexAttr.getW(vi);
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
271
|
+
fullSkinBoneIndices[vi * 4] = remap && si0 < remap.length ? remap[si0] : si0;
|
|
272
|
+
fullSkinBoneIndices[vi * 4 + 1] = remap && si1 < remap.length ? remap[si1] : si1;
|
|
273
|
+
fullSkinBoneIndices[vi * 4 + 2] = remap && si2 < remap.length ? remap[si2] : si2;
|
|
274
|
+
fullSkinBoneIndices[vi * 4 + 3] = remap && si3 < remap.length ? remap[si3] : si3;
|
|
275
|
+
fullSkinBoneWeights[vi * 4] = skinWeightAttr.getX(vi);
|
|
276
|
+
fullSkinBoneWeights[vi * 4 + 1] = skinWeightAttr.getY(vi);
|
|
277
|
+
fullSkinBoneWeights[vi * 4 + 2] = skinWeightAttr.getZ(vi);
|
|
278
|
+
fullSkinBoneWeights[vi * 4 + 3] = skinWeightAttr.getW(vi);
|
|
268
279
|
}
|
|
269
|
-
skinData = { boneIndices, boneWeights };
|
|
270
280
|
}
|
|
271
281
|
}
|
|
272
|
-
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
282
|
+
// Determine groups: multi-material meshes have geometry.groups
|
|
283
|
+
const matArray = Array.isArray(mesh.material) ? mesh.material : null;
|
|
284
|
+
const groups = geometry.groups.length > 0 && matArray && matArray.length > 1
|
|
285
|
+
? geometry.groups
|
|
286
|
+
: null;
|
|
287
|
+
const meshSubMeshIndices = [];
|
|
288
|
+
if (groups) {
|
|
289
|
+
// Multi-material: split into one submesh per group
|
|
290
|
+
for (const group of groups) {
|
|
291
|
+
const groupMat = matArray[group.materialIndex ?? 0] ?? matArray[0];
|
|
292
|
+
const matName = groupMat?.name || 'default';
|
|
293
|
+
let matIndex;
|
|
294
|
+
if (materialSet.has(matName)) {
|
|
295
|
+
matIndex = materialSet.get(matName);
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
matIndex = materials.length;
|
|
299
|
+
materialSet.set(matName, matIndex);
|
|
300
|
+
materials.push({ name: matName });
|
|
301
|
+
}
|
|
302
|
+
// Extract the index slice for this group and remap vertices
|
|
303
|
+
const groupIndexStart = group.start;
|
|
304
|
+
const groupIndexCount = group.count;
|
|
305
|
+
const usedVerts = new Map(); // old vertex index → new vertex index
|
|
306
|
+
const groupIndicesRaw = [];
|
|
307
|
+
for (let ii = 0; ii < groupIndexCount; ii++) {
|
|
308
|
+
const oldVi = fullIndices[groupIndexStart + ii];
|
|
309
|
+
if (!usedVerts.has(oldVi)) {
|
|
310
|
+
usedVerts.set(oldVi, usedVerts.size);
|
|
311
|
+
}
|
|
312
|
+
groupIndicesRaw.push(usedVerts.get(oldVi));
|
|
313
|
+
}
|
|
314
|
+
const gVertCount = usedVerts.size;
|
|
315
|
+
const positions = new Float32Array(gVertCount * 3);
|
|
316
|
+
const normals = fullNormals ? new Float32Array(gVertCount * 3) : null;
|
|
317
|
+
const uvs = fullUvs ? new Float32Array(gVertCount * 2) : null;
|
|
318
|
+
let skinData;
|
|
319
|
+
let gBoneIndices;
|
|
320
|
+
let gBoneWeights;
|
|
321
|
+
if (fullSkinBoneIndices && fullSkinBoneWeights) {
|
|
322
|
+
gBoneIndices = new Uint32Array(gVertCount * 4);
|
|
323
|
+
gBoneWeights = new Float32Array(gVertCount * 4);
|
|
324
|
+
}
|
|
325
|
+
for (const [oldVi, newVi] of usedVerts) {
|
|
326
|
+
positions[newVi * 3] = fullPositions[oldVi * 3];
|
|
327
|
+
positions[newVi * 3 + 1] = fullPositions[oldVi * 3 + 1];
|
|
328
|
+
positions[newVi * 3 + 2] = fullPositions[oldVi * 3 + 2];
|
|
329
|
+
if (normals && fullNormals) {
|
|
330
|
+
normals[newVi * 3] = fullNormals[oldVi * 3];
|
|
331
|
+
normals[newVi * 3 + 1] = fullNormals[oldVi * 3 + 1];
|
|
332
|
+
normals[newVi * 3 + 2] = fullNormals[oldVi * 3 + 2];
|
|
333
|
+
}
|
|
334
|
+
if (uvs && fullUvs) {
|
|
335
|
+
uvs[newVi * 2] = fullUvs[oldVi * 2];
|
|
336
|
+
uvs[newVi * 2 + 1] = fullUvs[oldVi * 2 + 1];
|
|
337
|
+
}
|
|
338
|
+
if (gBoneIndices && gBoneWeights && fullSkinBoneIndices && fullSkinBoneWeights) {
|
|
339
|
+
gBoneIndices[newVi * 4] = fullSkinBoneIndices[oldVi * 4];
|
|
340
|
+
gBoneIndices[newVi * 4 + 1] = fullSkinBoneIndices[oldVi * 4 + 1];
|
|
341
|
+
gBoneIndices[newVi * 4 + 2] = fullSkinBoneIndices[oldVi * 4 + 2];
|
|
342
|
+
gBoneIndices[newVi * 4 + 3] = fullSkinBoneIndices[oldVi * 4 + 3];
|
|
343
|
+
gBoneWeights[newVi * 4] = fullSkinBoneWeights[oldVi * 4];
|
|
344
|
+
gBoneWeights[newVi * 4 + 1] = fullSkinBoneWeights[oldVi * 4 + 1];
|
|
345
|
+
gBoneWeights[newVi * 4 + 2] = fullSkinBoneWeights[oldVi * 4 + 2];
|
|
346
|
+
gBoneWeights[newVi * 4 + 3] = fullSkinBoneWeights[oldVi * 4 + 3];
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (gBoneIndices && gBoneWeights) {
|
|
350
|
+
skinData = { boneIndices: gBoneIndices, boneWeights: gBoneWeights };
|
|
351
|
+
}
|
|
352
|
+
meshSubMeshIndices.push(subMeshes.length);
|
|
353
|
+
subMeshes.push({ skinData, positions, normals, uvs, indices: new Uint32Array(groupIndicesRaw), materialIndex: matIndex });
|
|
354
|
+
}
|
|
277
355
|
}
|
|
278
356
|
else {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
357
|
+
// Single material
|
|
358
|
+
const meshMaterial = matArray ? matArray[0] : mesh.material;
|
|
359
|
+
const matName = meshMaterial?.name || 'default';
|
|
360
|
+
let matIndex;
|
|
361
|
+
if (materialSet.has(matName)) {
|
|
362
|
+
matIndex = materialSet.get(matName);
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
matIndex = materials.length;
|
|
366
|
+
materialSet.set(matName, matIndex);
|
|
367
|
+
materials.push({ name: matName });
|
|
368
|
+
}
|
|
369
|
+
let skinData;
|
|
370
|
+
if (fullSkinBoneIndices && fullSkinBoneWeights) {
|
|
371
|
+
skinData = { boneIndices: fullSkinBoneIndices, boneWeights: fullSkinBoneWeights };
|
|
372
|
+
}
|
|
373
|
+
meshSubMeshIndices.push(subMeshes.length);
|
|
374
|
+
subMeshes.push({ skinData, positions: fullPositions, normals: fullNormals, uvs: fullUvs, indices: fullIndices, materialIndex: matIndex });
|
|
282
375
|
}
|
|
283
|
-
subMeshes.push({ skinData, positions, normals, uvs, indices, materialIndex: matIndex });
|
|
284
376
|
nodes.push({
|
|
285
377
|
name: mesh.name || `mesh_${mi}`,
|
|
286
378
|
parentIndex: -1,
|
|
287
379
|
position: [0, 0, 0],
|
|
288
380
|
rotation: [0, 0, 0, 1],
|
|
289
381
|
scale: [1, 1, 1],
|
|
290
|
-
meshIndices:
|
|
382
|
+
meshIndices: meshSubMeshIndices,
|
|
291
383
|
});
|
|
292
384
|
}
|
|
293
385
|
if (materials.length === 0)
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import { SKINNED_VERTEX_STRIDE,
|
|
1
|
+
import { SKINNED_VERTEX_STRIDE, COMPACT_VERTEX_WEIGHTS_SIZE, SKINNED_INDEX16_FLAG, SKINNED_COMPACT_WEIGHTS_FLAG, } from '../core/constants.js';
|
|
2
2
|
export function buildSkinnedMeshData(imported, scaleFactor = 1.0) {
|
|
3
3
|
const geometries = [];
|
|
4
4
|
let totalDataSize = 0;
|
|
5
5
|
for (const sub of imported.subMeshes) {
|
|
6
6
|
const vertCount = sub.positions.length / 3;
|
|
7
|
+
const useIndex16 = vertCount < 65536;
|
|
7
8
|
const vertexBufSize = vertCount * SKINNED_VERTEX_STRIDE;
|
|
8
|
-
const indexBufSize = sub.indices.length * 4;
|
|
9
|
-
const weightsBufSize = vertCount *
|
|
9
|
+
const indexBufSize = sub.indices.length * (useIndex16 ? 2 : 4);
|
|
10
|
+
const weightsBufSize = vertCount * COMPACT_VERTEX_WEIGHTS_SIZE;
|
|
11
|
+
let flags = SKINNED_COMPACT_WEIGHTS_FLAG;
|
|
12
|
+
if (useIndex16)
|
|
13
|
+
flags |= SKINNED_INDEX16_FLAG;
|
|
10
14
|
geometries.push({
|
|
11
15
|
vertexCount: vertCount,
|
|
12
16
|
indexCount: sub.indices.length,
|
|
@@ -19,15 +23,19 @@ export function buildSkinnedMeshData(imported, scaleFactor = 1.0) {
|
|
|
19
23
|
weightsOffset: totalDataSize + vertexBufSize + indexBufSize,
|
|
20
24
|
weightsSize: weightsBufSize,
|
|
21
25
|
materialIndex: sub.materialIndex,
|
|
22
|
-
|
|
26
|
+
flags,
|
|
23
27
|
});
|
|
24
28
|
totalDataSize += vertexBufSize + indexBufSize + weightsBufSize;
|
|
25
29
|
}
|
|
26
30
|
const data = new Uint8Array(totalDataSize);
|
|
27
31
|
const view = new DataView(data.buffer);
|
|
28
32
|
let offset = 0;
|
|
29
|
-
for (
|
|
33
|
+
for (let si = 0; si < imported.subMeshes.length; si++) {
|
|
34
|
+
const sub = imported.subMeshes[si];
|
|
35
|
+
const geo = geometries[si];
|
|
30
36
|
const vertCount = sub.positions.length / 3;
|
|
37
|
+
const useIndex16 = (geo.flags & SKINNED_INDEX16_FLAG) !== 0;
|
|
38
|
+
// Vertex buffer (32 bytes per vert: pos[3] + normal[3] + uv[2])
|
|
31
39
|
for (let v = 0; v < vertCount; v++) {
|
|
32
40
|
view.setFloat32(offset, sub.positions[v * 3] * scaleFactor, true);
|
|
33
41
|
offset += 4;
|
|
@@ -64,25 +72,37 @@ export function buildSkinnedMeshData(imported, scaleFactor = 1.0) {
|
|
|
64
72
|
offset += 4;
|
|
65
73
|
}
|
|
66
74
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
75
|
+
// Index buffer — uint16 or uint32
|
|
76
|
+
if (useIndex16) {
|
|
77
|
+
for (let i = 0; i < sub.indices.length; i++) {
|
|
78
|
+
view.setUint16(offset, sub.indices[i], true);
|
|
79
|
+
offset += 2;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
for (let i = 0; i < sub.indices.length; i++) {
|
|
84
|
+
view.setUint32(offset, sub.indices[i], true);
|
|
85
|
+
offset += 4;
|
|
86
|
+
}
|
|
70
87
|
}
|
|
88
|
+
// Compact weights: bone_indices[4*u16] + weights[4*u8] = 12 bytes per vert
|
|
71
89
|
for (let v = 0; v < vertCount; v++) {
|
|
72
90
|
if (sub.skinData) {
|
|
73
91
|
for (let w = 0; w < 4; w++) {
|
|
74
|
-
view.
|
|
75
|
-
offset +=
|
|
92
|
+
view.setUint16(offset, sub.skinData.boneIndices[v * 4 + w], true);
|
|
93
|
+
offset += 2;
|
|
76
94
|
}
|
|
77
95
|
for (let w = 0; w < 4; w++) {
|
|
78
|
-
|
|
79
|
-
offset
|
|
96
|
+
// Quantize float weight [0,1] to uint8 [0,255]
|
|
97
|
+
view.setUint8(offset, Math.round(sub.skinData.boneWeights[v * 4 + w] * 255));
|
|
98
|
+
offset += 1;
|
|
80
99
|
}
|
|
81
100
|
}
|
|
82
101
|
else {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
offset
|
|
102
|
+
// 8 bytes zero indices + 4 bytes zero weights
|
|
103
|
+
for (let w = 0; w < 12; w++) {
|
|
104
|
+
view.setUint8(offset, 0);
|
|
105
|
+
offset += 1;
|
|
86
106
|
}
|
|
87
107
|
}
|
|
88
108
|
}
|
package/dist/format/metadata.js
CHANGED
|
@@ -262,7 +262,7 @@ function serializeSkinnedGeometryInfo(w, g) {
|
|
|
262
262
|
w.writeUint64FromNumber(g.weightsOffset);
|
|
263
263
|
w.writeUint64FromNumber(g.weightsSize);
|
|
264
264
|
w.writeUint32(g.materialIndex);
|
|
265
|
-
w.writeUint32(g.
|
|
265
|
+
w.writeUint32(g.flags);
|
|
266
266
|
}
|
|
267
267
|
function deserializeSkinnedGeometryInfo(r) {
|
|
268
268
|
return {
|
|
@@ -277,7 +277,7 @@ function deserializeSkinnedGeometryInfo(r) {
|
|
|
277
277
|
weightsOffset: r.readUint64AsNumber(),
|
|
278
278
|
weightsSize: r.readUint64AsNumber(),
|
|
279
279
|
materialIndex: r.readUint32(),
|
|
280
|
-
|
|
280
|
+
flags: r.readUint32(),
|
|
281
281
|
};
|
|
282
282
|
}
|
|
283
283
|
function serializeBone(w, bone) {
|
package/dist/init.d.ts
CHANGED
package/dist/init.js
CHANGED
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
import * as fs from 'node:fs/promises';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
+
import * as readline from 'node:readline';
|
|
3
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
+
async function promptMenu(question, choices) {
|
|
6
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
7
|
+
return new Promise((resolve) => {
|
|
8
|
+
console.log(`\n${question}\n`);
|
|
9
|
+
for (let i = 0; i < choices.length; i++) {
|
|
10
|
+
console.log(` ${i + 1}) ${choices[i]}`);
|
|
11
|
+
}
|
|
12
|
+
console.log('');
|
|
13
|
+
const ask = () => {
|
|
14
|
+
rl.question('Choose [1]: ', (answer) => {
|
|
15
|
+
const trimmed = answer.trim();
|
|
16
|
+
if (trimmed === '') {
|
|
17
|
+
rl.close();
|
|
18
|
+
resolve(0);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const num = parseInt(trimmed, 10);
|
|
22
|
+
if (num >= 1 && num <= choices.length) {
|
|
23
|
+
rl.close();
|
|
24
|
+
resolve(num - 1);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
console.log(` Please enter 1-${choices.length}`);
|
|
28
|
+
ask();
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
ask();
|
|
32
|
+
});
|
|
33
|
+
}
|
|
4
34
|
export async function initProject(projectDir, opts) {
|
|
5
35
|
const absDir = path.resolve(projectDir);
|
|
6
36
|
const configPath = path.join(absDir, '.felicityproject');
|
|
@@ -15,6 +45,7 @@ export async function initProject(projectDir, opts) {
|
|
|
15
45
|
process.exit(1);
|
|
16
46
|
}
|
|
17
47
|
await copySkillFiles(absDir);
|
|
48
|
+
await copyEngineSkillFiles(absDir);
|
|
18
49
|
console.log('Updated AI skill files:');
|
|
19
50
|
console.log(' .claude/skills/stowkit/SKILL.md');
|
|
20
51
|
console.log(' .cursor/rules/stowkit.mdc');
|
|
@@ -29,6 +60,16 @@ export async function initProject(projectDir, opts) {
|
|
|
29
60
|
catch {
|
|
30
61
|
// Does not exist — create it
|
|
31
62
|
}
|
|
63
|
+
// Prompt for engine setup unless explicitly set via flag
|
|
64
|
+
let withEngine = opts?.withEngine ?? false;
|
|
65
|
+
const noEngine = opts?.noEngine ?? false;
|
|
66
|
+
if (!withEngine && !noEngine && process.stdin.isTTY) {
|
|
67
|
+
const choice = await promptMenu('What would you like to set up?', [
|
|
68
|
+
'StowKit (asset pipeline only)',
|
|
69
|
+
'StowKit + 3D Engine (includes @series-inc/rundot-3d-engine)',
|
|
70
|
+
]);
|
|
71
|
+
withEngine = choice === 1;
|
|
72
|
+
}
|
|
32
73
|
// Create srcArtDir with .gitignore for cache files
|
|
33
74
|
const srcArtDir = 'assets';
|
|
34
75
|
await fs.mkdir(path.join(absDir, srcArtDir), { recursive: true });
|
|
@@ -72,10 +113,36 @@ export async function initProject(projectDir, opts) {
|
|
|
72
113
|
console.log(` Output dir: public/cdn-assets/`);
|
|
73
114
|
console.log(` Config: .felicityproject`);
|
|
74
115
|
console.log(` AI skills: .claude/skills/stowkit/SKILL.md, .cursor/rules/stowkit.mdc`);
|
|
116
|
+
// Install engine if selected
|
|
117
|
+
if (withEngine) {
|
|
118
|
+
await installEngine(absDir);
|
|
119
|
+
}
|
|
75
120
|
console.log('');
|
|
76
121
|
console.log('Drop your assets (PNG, JPG, FBX, WAV, etc.) into assets/');
|
|
77
122
|
console.log('Then run: stowkit build');
|
|
78
123
|
}
|
|
124
|
+
async function installEngine(absDir) {
|
|
125
|
+
console.log('');
|
|
126
|
+
console.log('Installing @series-inc/rundot-3d-engine and three...');
|
|
127
|
+
const { execSync } = await import('node:child_process');
|
|
128
|
+
try {
|
|
129
|
+
execSync('npm install @series-inc/rundot-3d-engine three', {
|
|
130
|
+
cwd: absDir,
|
|
131
|
+
stdio: 'inherit',
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
console.error('Failed to install engine packages. You can install manually:');
|
|
136
|
+
console.error(' npm install @series-inc/rundot-3d-engine three');
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
await copyEngineSkillFiles(absDir);
|
|
140
|
+
console.log('');
|
|
141
|
+
console.log(' 3D Engine installed:');
|
|
142
|
+
console.log(' @series-inc/rundot-3d-engine');
|
|
143
|
+
console.log(' three');
|
|
144
|
+
console.log(' AI skills: .claude/skills/stowkit-engine/SKILL.md, .cursor/rules/stowkit-engine.mdc');
|
|
145
|
+
}
|
|
79
146
|
async function copySkillFiles(absDir) {
|
|
80
147
|
const thisDir = path.dirname(fileURLToPath(import.meta.url));
|
|
81
148
|
const skillSrc = path.resolve(thisDir, '../skill.md');
|
|
@@ -92,3 +159,34 @@ async function copySkillFiles(absDir) {
|
|
|
92
159
|
// Skill file not found in package — skip silently
|
|
93
160
|
}
|
|
94
161
|
}
|
|
162
|
+
async function copyEngineSkillFiles(absDir) {
|
|
163
|
+
const thisDir = path.dirname(fileURLToPath(import.meta.url));
|
|
164
|
+
const candidates = [
|
|
165
|
+
// Monorepo dev: sibling folder
|
|
166
|
+
path.resolve(thisDir, '../../stowkit-engine/SKILL.md'),
|
|
167
|
+
// Installed in project's node_modules
|
|
168
|
+
path.join(absDir, 'node_modules/@series-inc/rundot-3d-engine/SKILL.md'),
|
|
169
|
+
// Installed as dep of CLI: nested node_modules
|
|
170
|
+
path.resolve(thisDir, '../node_modules/@series-inc/rundot-3d-engine/SKILL.md'),
|
|
171
|
+
// Hoisted in global node_modules
|
|
172
|
+
path.resolve(thisDir, '../../../@series-inc/rundot-3d-engine/SKILL.md'),
|
|
173
|
+
];
|
|
174
|
+
let skillContent = null;
|
|
175
|
+
for (const candidate of candidates) {
|
|
176
|
+
try {
|
|
177
|
+
skillContent = await fs.readFile(candidate, 'utf-8');
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
// not found, try next
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (!skillContent)
|
|
185
|
+
return;
|
|
186
|
+
const claudeDir = path.join(absDir, '.claude', 'skills', 'stowkit-engine');
|
|
187
|
+
await fs.mkdir(claudeDir, { recursive: true });
|
|
188
|
+
await fs.writeFile(path.join(claudeDir, 'SKILL.md'), skillContent);
|
|
189
|
+
const cursorDir = path.join(absDir, '.cursor', 'rules');
|
|
190
|
+
await fs.mkdir(cursorDir, { recursive: true });
|
|
191
|
+
await fs.writeFile(path.join(cursorDir, 'stowkit-engine.mdc'), `---\ndescription: Rundot 3D Engine — VenusGame, GameObject, Component, physics, animations, StowKit asset loading\nalwaysApply: true\n---\n\n${skillContent}`);
|
|
192
|
+
}
|
package/dist/orchestrator.js
CHANGED
|
@@ -263,10 +263,10 @@ export async function fullBuild(projectDir, opts) {
|
|
|
263
263
|
for (let si = 0; si < mesh.imported.subMeshes.length; si++) {
|
|
264
264
|
const matIdx = mesh.imported.subMeshes[si].materialIndex;
|
|
265
265
|
const matName = mesh.imported.materials[matIdx]?.name;
|
|
266
|
-
if (matName) {
|
|
266
|
+
if (matName && overrides[matIdx] === undefined) {
|
|
267
267
|
const matChildId = `${container.id}/${matName}.stowmat`;
|
|
268
268
|
if (assetsById.has(matChildId)) {
|
|
269
|
-
overrides[
|
|
269
|
+
overrides[matIdx] = matChildId;
|
|
270
270
|
}
|
|
271
271
|
}
|
|
272
272
|
}
|
package/dist/pipeline.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ export interface ProcessResult {
|
|
|
22
22
|
processedSize: number;
|
|
23
23
|
glbChildren?: GlbChildDescriptor[];
|
|
24
24
|
glbExtract?: GlbExtractResult;
|
|
25
|
+
warnings?: string[];
|
|
25
26
|
}
|
|
26
27
|
export declare function processAsset(id: string, sourceData: Uint8Array, type: AssetType, stringId: string, settings: AssetSettings, ctx: ProcessingContext): Promise<ProcessResult>;
|
|
27
28
|
export declare function processExtractedMesh(childId: string, imported: ImportedMesh, hasSkeleton: boolean, stringId: string, settings: AssetSettings, ctx: ProcessingContext): Promise<ProcessResult>;
|
|
@@ -40,3 +41,4 @@ export declare function buildAnimationDataBlobsV2(imported: ImportedMesh): {
|
|
|
40
41
|
data: Uint8Array;
|
|
41
42
|
metadata: AnimationClipMetadata;
|
|
42
43
|
}[];
|
|
44
|
+
export declare function generateAnimationWarnings(imported: ImportedMesh): string[];
|
package/dist/pipeline.js
CHANGED
|
@@ -99,6 +99,7 @@ export async function processAsset(id, sourceData, type, stringId, settings, ctx
|
|
|
99
99
|
log('importing FBX...');
|
|
100
100
|
const imported = await ctx.meshImporter.import(sourceData, id);
|
|
101
101
|
const animCount = imported.animations?.length ?? 0;
|
|
102
|
+
const animWarnings = generateAnimationWarnings(imported);
|
|
102
103
|
if (animCount === 0)
|
|
103
104
|
throw new Error('No animation clips found in FBX file');
|
|
104
105
|
const animBlobs = buildAnimationDataBlobsV2(imported);
|
|
@@ -117,7 +118,9 @@ export async function processAsset(id, sourceData, type, stringId, settings, ctx
|
|
|
117
118
|
BlobStore.setProcessed(`${id}:animCount`, new Uint8Array([animBlobs.length]));
|
|
118
119
|
const totalAnimBytes = animBlobs.reduce((s, a) => s + a.data.length, 0);
|
|
119
120
|
log(`animation serialized (${(totalAnimBytes / 1024).toFixed(0)} KB, ${animBlobs.length} clips)`);
|
|
120
|
-
|
|
121
|
+
if (animWarnings.length > 0)
|
|
122
|
+
log(`warnings: ${animWarnings.join('; ')}`);
|
|
123
|
+
return { metadata: primary.metadata, processedSize: totalAnimBytes, warnings: animWarnings.length > 0 ? animWarnings : undefined };
|
|
121
124
|
}
|
|
122
125
|
if (type === AssetType.Audio) {
|
|
123
126
|
log('decoding audio...');
|
|
@@ -494,3 +497,51 @@ export function buildAnimationDataBlobsV2(imported) {
|
|
|
494
497
|
}
|
|
495
498
|
return results;
|
|
496
499
|
}
|
|
500
|
+
// ─── Animation Diagnostics ──────────────────────────────────────────────────
|
|
501
|
+
export function generateAnimationWarnings(imported) {
|
|
502
|
+
const warnings = [];
|
|
503
|
+
if (!imported.animations || imported.animations.length === 0) {
|
|
504
|
+
warnings.push('No animation clips found in FBX');
|
|
505
|
+
return warnings;
|
|
506
|
+
}
|
|
507
|
+
const boneNames = new Set((imported.bones ?? []).map(b => b.name));
|
|
508
|
+
for (const clip of imported.animations) {
|
|
509
|
+
const prefix = imported.animations.length > 1 ? `Clip "${clip.name}": ` : '';
|
|
510
|
+
if (!clip.tracks || clip.tracks.length === 0) {
|
|
511
|
+
warnings.push(`${prefix}No animation tracks`);
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
if (clip.duration <= 0) {
|
|
515
|
+
warnings.push(`${prefix}Duration is 0`);
|
|
516
|
+
}
|
|
517
|
+
// Check for bone name mismatches
|
|
518
|
+
const trackBoneNames = new Set();
|
|
519
|
+
let singleKeyframeTracks = 0;
|
|
520
|
+
for (const track of clip.tracks) {
|
|
521
|
+
if (track.name.endsWith('.scale'))
|
|
522
|
+
continue;
|
|
523
|
+
const boneName = track.name.split('.')[0];
|
|
524
|
+
trackBoneNames.add(boneName);
|
|
525
|
+
if (track.times.length <= 1)
|
|
526
|
+
singleKeyframeTracks++;
|
|
527
|
+
}
|
|
528
|
+
if (boneNames.size > 0 && trackBoneNames.size > 0) {
|
|
529
|
+
const unmatched = [];
|
|
530
|
+
for (const tn of trackBoneNames) {
|
|
531
|
+
if (!boneNames.has(tn))
|
|
532
|
+
unmatched.push(tn);
|
|
533
|
+
}
|
|
534
|
+
if (unmatched.length > 0 && unmatched.length === trackBoneNames.size) {
|
|
535
|
+
warnings.push(`${prefix}No track bone names match skeleton (${unmatched.length} unmatched: ${unmatched.slice(0, 3).join(', ')}${unmatched.length > 3 ? '...' : ''})`);
|
|
536
|
+
}
|
|
537
|
+
else if (unmatched.length > 0) {
|
|
538
|
+
warnings.push(`${prefix}${unmatched.length}/${trackBoneNames.size} track bones not in skeleton: ${unmatched.slice(0, 3).join(', ')}${unmatched.length > 3 ? '...' : ''}`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
const nonScaleTracks = clip.tracks.filter(t => !t.name.endsWith('.scale'));
|
|
542
|
+
if (singleKeyframeTracks > 0 && singleKeyframeTracks === nonScaleTracks.length) {
|
|
543
|
+
warnings.push(`${prefix}All ${singleKeyframeTracks} tracks have only 1 keyframe (static pose)`);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return warnings;
|
|
547
|
+
}
|
package/dist/server.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export interface ServerOptions {
|
|
|
3
3
|
port?: number;
|
|
4
4
|
projectDir?: string;
|
|
5
5
|
wasmDir?: string;
|
|
6
|
+
/** Force reprocess all assets on startup, ignoring cache. */
|
|
7
|
+
force?: boolean;
|
|
6
8
|
/** Static apps to serve. Key = URL prefix (e.g. '/packer'), value = directory path. Use '/' for root. */
|
|
7
9
|
staticApps?: Record<string, string>;
|
|
8
10
|
}
|
package/dist/server.js
CHANGED
|
@@ -327,10 +327,10 @@ async function processGlbContainer(containerId) {
|
|
|
327
327
|
for (let si = 0; si < mesh.imported.subMeshes.length; si++) {
|
|
328
328
|
const matIdx = mesh.imported.subMeshes[si].materialIndex;
|
|
329
329
|
const matName = mesh.imported.materials[matIdx]?.name;
|
|
330
|
-
if (matName) {
|
|
330
|
+
if (matName && overrides[matIdx] === undefined) {
|
|
331
331
|
const matChildId = `${containerId}/${matName}.stowmat`;
|
|
332
332
|
if (childAssets.some(a => a.id === matChildId)) {
|
|
333
|
-
overrides[
|
|
333
|
+
overrides[matIdx] = matChildId;
|
|
334
334
|
}
|
|
335
335
|
}
|
|
336
336
|
}
|
|
@@ -735,6 +735,7 @@ async function processOneAsset(id) {
|
|
|
735
735
|
asset.metadata = result.metadata;
|
|
736
736
|
asset.processedSize = result.processedSize;
|
|
737
737
|
asset.sourceSize = sourceData.length;
|
|
738
|
+
asset.warnings = result.warnings;
|
|
738
739
|
broadcast({
|
|
739
740
|
type: 'asset-update', id,
|
|
740
741
|
updates: {
|
|
@@ -742,6 +743,7 @@ async function processOneAsset(id) {
|
|
|
742
743
|
metadata: result.metadata,
|
|
743
744
|
processedSize: result.processedSize,
|
|
744
745
|
sourceSize: sourceData.length,
|
|
746
|
+
warnings: result.warnings,
|
|
745
747
|
},
|
|
746
748
|
});
|
|
747
749
|
}
|
|
@@ -1576,7 +1578,7 @@ export async function startServer(opts = {}) {
|
|
|
1576
1578
|
// Open project if specified (skip if already loaded — e.g. stowkit editor starts two servers)
|
|
1577
1579
|
if (opts.projectDir && (!projectConfig || projectConfig.projectDir !== opts.projectDir)) {
|
|
1578
1580
|
await openProject(opts.projectDir);
|
|
1579
|
-
queueProcessing();
|
|
1581
|
+
queueProcessing({ force: opts.force });
|
|
1580
1582
|
}
|
|
1581
1583
|
return new Promise((resolve) => {
|
|
1582
1584
|
server.listen(port, () => {
|
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.31",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"stowkit": "./dist/cli.js"
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
"dev": "tsc --watch"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@series-inc/stowkit-packer-gui": "^0.1.
|
|
21
|
-
"@series-inc/stowkit-editor": "^0.1.
|
|
20
|
+
"@series-inc/stowkit-packer-gui": "^0.1.17",
|
|
21
|
+
"@series-inc/stowkit-editor": "^0.1.3",
|
|
22
22
|
"draco3d": "^1.5.7",
|
|
23
23
|
"fbx-parser": "^2.1.3",
|
|
24
24
|
"@strangeape/ffmpeg-audio-wasm": "^0.1.0",
|
package/skill.md
CHANGED
|
@@ -36,7 +36,8 @@ A StowKit project has a `.felicityproject` JSON file at its root:
|
|
|
36
36
|
## CLI Commands
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
|
-
stowkit init [dir] # Scaffold a new project (
|
|
39
|
+
stowkit init [dir] # Scaffold a new project (interactive menu: pipeline only or with 3D engine)
|
|
40
|
+
stowkit init --with-engine # Scaffold with 3D engine pre-installed
|
|
40
41
|
stowkit init --update [dir] # Update AI skill files to match installed CLI version
|
|
41
42
|
stowkit update # Update CLI to latest version and refresh skill files
|
|
42
43
|
stowkit version # Show installed version
|