@playcraft/cli 0.0.40 → 0.0.41
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/README.md +66 -3
- package/dist/atom-plan/validate-atom-plan.js +298 -0
- package/dist/cli-root-help.js +1 -1
- package/dist/commands/3d.js +363 -0
- package/dist/commands/create.js +337 -0
- package/dist/commands/image.js +1337 -43
- package/dist/commands/recommend.js +1 -1
- package/dist/commands/remix.js +213 -0
- package/dist/commands/skills.js +1379 -0
- package/dist/commands/tools-3d.js +473 -0
- package/dist/commands/tools-generation.js +454 -0
- package/dist/commands/tools-project.js +400 -0
- package/dist/commands/tools-research.js +37 -0
- package/dist/commands/tools-research.test.js +216 -0
- package/dist/commands/tools-utils.js +164 -0
- package/dist/commands/tools.js +7 -616
- package/dist/config.js +2 -0
- package/dist/index.js +19 -1
- package/package.json +9 -3
- package/project-template/.claude/agents/designer.md +116 -0
- package/project-template/.claude/agents/developer.md +133 -0
- package/project-template/.claude/agents/pm.md +164 -0
- package/project-template/.claude/agents/refs/README.md +67 -0
- package/project-template/.claude/agents/refs/designer-art-style-catalog.md +533 -0
- package/project-template/.claude/agents/refs/designer-color-audio-recipes.md +153 -0
- package/project-template/.claude/agents/refs/designer-deliverable-spec.md +167 -0
- package/project-template/.claude/agents/refs/designer-dimension-axis.md +27 -0
- package/project-template/.claude/agents/refs/designer-handoff-v2-checklist.md +68 -0
- package/project-template/.claude/agents/refs/designer-master-composite-recipes.md +216 -0
- package/project-template/.claude/agents/refs/designer-style-exploration-flow.md +37 -0
- package/project-template/.claude/agents/refs/developer-dev-handoff.md +109 -0
- package/project-template/.claude/agents/refs/developer-impl-cookbook.md +134 -0
- package/project-template/.claude/agents/refs/developer-phase1-flow.md +211 -0
- package/project-template/.claude/agents/refs/pm-workflow-detail.md +545 -0
- package/project-template/.claude/agents/refs/reviewer-six-dimension-eval.md +286 -0
- package/project-template/.claude/agents/refs/ta-3d-flip-recipe.md +85 -0
- package/project-template/.claude/agents/refs/ta-atlas-deliverable-standard.md +46 -0
- package/project-template/.claude/agents/refs/ta-batch-pipeline-recipes.md +120 -0
- package/project-template/.claude/agents/refs/ta-image-generation-detail.md +356 -0
- package/project-template/.claude/agents/refs/ta-image-ops-reference.md +495 -0
- package/project-template/.claude/agents/refs/ta-pipeline-cookbook.md +699 -0
- package/project-template/.claude/agents/refs/ta-tools-reference.md +111 -0
- package/project-template/.claude/agents/refs/ta-vfx-preset-catalog.md +365 -0
- package/project-template/.claude/agents/reviewer.md +103 -0
- package/project-template/.claude/agents/technical-artist.md +111 -0
- package/project-template/.claude/hooks/README.md +36 -0
- package/project-template/.claude/hooks/validate-atom-plan.mjs +224 -0
- package/project-template/.claude/hooks/validate-workflow-stop.mjs +258 -0
- package/project-template/.claude/settings.json +32 -0
- package/project-template/.claude/settings.local.json +4 -0
- package/project-template/.claude/skills/playcraft-ad-psychology/SKILL.md +182 -0
- package/project-template/.claude/skills/playcraft-art-style-guide/SKILL.md +123 -0
- package/project-template/.claude/skills/playcraft-asset-state-sheet/SKILL.md +141 -0
- package/project-template/.claude/skills/playcraft-audio-generation/SKILL.md +280 -0
- package/project-template/.claude/skills/playcraft-batch-pipeline/SKILL.md +184 -0
- package/project-template/.claude/skills/playcraft-build-optimizer/SKILL.md +306 -0
- package/project-template/.claude/skills/playcraft-image-generation/SKILL.md +229 -0
- package/project-template/.claude/skills/playcraft-image-generation/reference/build-sprite-sheet.template.mjs +123 -0
- package/project-template/.claude/skills/playcraft-image-generation/reference/compare-style.template.mjs +254 -0
- package/project-template/.claude/skills/playcraft-image-generation/reference/gen-batch-sprite.template.mjs +235 -0
- package/project-template/.claude/skills/playcraft-image-generation/reference/gen-batch.template.mjs +97 -0
- package/project-template/.claude/skills/playcraft-image-generation/reference/gen-edit-variants.template.mjs +118 -0
- package/project-template/.claude/skills/playcraft-image-generation/reference/process-batch.template.mjs +137 -0
- package/project-template/.claude/skills/playcraft-image-generation/reference/prompt-cookbook.md +397 -0
- package/project-template/.claude/skills/playcraft-image-generation/reference/validate-sprite-sheet.template.mjs +296 -0
- package/project-template/.claude/skills/playcraft-image-ops/SKILL.md +122 -0
- package/project-template/.claude/skills/playcraft-masking/SKILL.md +373 -0
- package/project-template/.claude/skills/playcraft-research/SKILL.md +212 -0
- package/project-template/.claude/skills/playcraft-sprite-generation/SKILL.md +423 -0
- package/project-template/.claude/skills/playcraft-storyboard/SKILL.md +148 -0
- package/project-template/.claude/skills/playcraft-style-qa/SKILL.md +270 -0
- package/project-template/.claude/skills/playcraft-text-rendering/SKILL.md +236 -0
- package/project-template/.claude/skills/playcraft-vfx-animation/SKILL.md +130 -0
- package/project-template/.claude/skills/playcraft-workflow/SKILL.md +396 -0
- package/project-template/.cursor/hooks.json +17 -0
- package/project-template/.cursor/rules/playcraft-orchestrator.mdc +87 -0
- package/project-template/.cursor/rules/playcraft-subagent-boundary.mdc +18 -0
- package/project-template/CLAUDE.md +240 -0
- package/project-template/assets/audio/bgm/.gitkeep +0 -0
- package/project-template/assets/audio/sfx/.gitkeep +0 -0
- package/project-template/assets/bundles/.gitkeep +0 -0
- package/project-template/assets/images/bg/.gitkeep +0 -0
- package/project-template/assets/images/reference/.gitkeep +0 -0
- package/project-template/assets/images/storyboard/.gitkeep +0 -0
- package/project-template/assets/images/tiles/.gitkeep +0 -0
- package/project-template/assets/images/ui/.gitkeep +0 -0
- package/project-template/assets/images/vfx/.gitkeep +0 -0
- package/project-template/assets/models/.gitkeep +0 -0
- package/project-template/docs/team/agent-conduct.md +105 -0
- package/project-template/docs/team/agent-runtime-matrix.md +62 -0
- package/project-template/docs/team/atom-plan-format.md +74 -0
- package/project-template/docs/team/collaboration.md +288 -0
- package/project-template/docs/team/core-model.md +50 -0
- package/project-template/docs/team/platform-capabilities.md +15 -0
- package/project-template/docs/team/workflow-changelog.md +51 -0
- package/project-template/docs/team/workflow-consistency-checklist.md +128 -0
- package/project-template/game/config/.gitkeep +0 -0
- package/project-template/game/gameplay/.gitkeep +0 -0
- package/project-template/game/scenes/.gitkeep +0 -0
- package/project-template/logs/.gitkeep +0 -0
- package/project-template/ta-workspace/logs/.gitkeep +0 -0
- package/project-template/ta-workspace/scripts/.gitkeep +0 -0
- package/project-template/ta-workspace/tmp/.gitkeep +0 -0
- package/project-template/templates/atom-plan.template.json +26 -0
- package/project-template/templates/atom-plan.template.md +76 -0
- package/project-template/templates/design-brief.template.md +195 -0
- package/project-template/templates/design-lens-checklist.reference.md +117 -0
- package/project-template/templates/design-methodology.md +99 -0
- package/project-template/templates/designer-log.template.md +98 -0
- package/project-template/templates/developer-log.template.md +140 -0
- package/project-template/templates/five-axis-framework.md +186 -0
- package/project-template/templates/intent-clarifications.template.md +58 -0
- package/project-template/templates/layout-spec.template.md +132 -0
- package/project-template/templates/project-state.template.md +219 -0
- package/project-template/templates/review-report.template.md +166 -0
- package/project-template/templates/style-exploration.template.md +93 -0
- package/project-template/templates/ta-log.template.md +205 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, statSync, } from 'fs';
|
|
2
|
+
import { basename, dirname, extname, join, resolve } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
const PRIMITIVES_DIR = join(__dirname, '../../assets/models/primitives');
|
|
6
|
+
const PRIMITIVES_METADATA = join(PRIMITIVES_DIR, 'metadata.json');
|
|
7
|
+
// ─────────────────────────────────────────────────────────
|
|
8
|
+
// Helpers
|
|
9
|
+
// ─────────────────────────────────────────────────────────
|
|
10
|
+
function sizeLabel(bytes) {
|
|
11
|
+
if (bytes < 1024)
|
|
12
|
+
return `${bytes}B`;
|
|
13
|
+
if (bytes < 1024 * 1024)
|
|
14
|
+
return `${(bytes / 1024).toFixed(1)}KB`;
|
|
15
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)}MB`;
|
|
16
|
+
}
|
|
17
|
+
function handleError(e) {
|
|
18
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
19
|
+
console.error(`Error: ${msg}`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
function ensureOutputDir(outputPath) {
|
|
23
|
+
const dir = dirname(resolve(outputPath));
|
|
24
|
+
if (!existsSync(dir)) {
|
|
25
|
+
mkdirSync(dir, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function loadPrimitivesMetadata() {
|
|
29
|
+
if (!existsSync(PRIMITIVES_METADATA)) {
|
|
30
|
+
console.error(`Error: primitives metadata not found at ${PRIMITIVES_METADATA}`);
|
|
31
|
+
console.error('Run: npx tsx scripts/generate-primitives.ts (from packages/cli)');
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
const raw = JSON.parse(readFileSync(PRIMITIVES_METADATA, 'utf-8'));
|
|
35
|
+
return raw.primitives;
|
|
36
|
+
}
|
|
37
|
+
// ─────────────────────────────────────────────────────────
|
|
38
|
+
// list-models
|
|
39
|
+
// ─────────────────────────────────────────────────────────
|
|
40
|
+
function registerListModelsCmd(parent) {
|
|
41
|
+
parent
|
|
42
|
+
.command('list-models')
|
|
43
|
+
.description('List all bundled white model primitives')
|
|
44
|
+
.option('--json', 'Output as JSON')
|
|
45
|
+
.action((opts) => {
|
|
46
|
+
try {
|
|
47
|
+
const primitives = loadPrimitivesMetadata();
|
|
48
|
+
if (opts.json) {
|
|
49
|
+
console.log(JSON.stringify({ primitives }, null, 2));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
console.log('\nBundled White Model Primitives:\n');
|
|
53
|
+
for (const p of primitives) {
|
|
54
|
+
const filePath = join(PRIMITIVES_DIR, p.file);
|
|
55
|
+
const size = existsSync(filePath) ? sizeLabel(statSync(filePath).size) : 'missing';
|
|
56
|
+
const dim = `${p.dimensions.width}×${p.dimensions.height}×${p.dimensions.depth}`;
|
|
57
|
+
console.log(` ${p.name.padEnd(12)} ${size.padEnd(8)} UV:${p.uvType.padEnd(14)} ${p.description}`);
|
|
58
|
+
console.log(` Best for: ${p.bestFor.join(', ')}`);
|
|
59
|
+
}
|
|
60
|
+
console.log(`\nUsage: playcraft 3d apply-texture --model <name> --texture <img> --output <out.glb>`);
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
handleError(e);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
// ─────────────────────────────────────────────────────────
|
|
68
|
+
// info
|
|
69
|
+
// ─────────────────────────────────────────────────────────
|
|
70
|
+
function registerInfoCmd(parent) {
|
|
71
|
+
parent
|
|
72
|
+
.command('info <model>')
|
|
73
|
+
.description('Show info about a GLB file or bundled white model')
|
|
74
|
+
.option('--json', 'Output as JSON')
|
|
75
|
+
.action(async (model, opts) => {
|
|
76
|
+
try {
|
|
77
|
+
const { NodeIO } = await import('@gltf-transform/core');
|
|
78
|
+
let filePath = model;
|
|
79
|
+
// Check if it's a bundled primitive name (no extension)
|
|
80
|
+
if (!extname(model) && !existsSync(model)) {
|
|
81
|
+
const primitives = loadPrimitivesMetadata();
|
|
82
|
+
const found = primitives.find((p) => p.name === model);
|
|
83
|
+
if (found) {
|
|
84
|
+
filePath = join(PRIMITIVES_DIR, found.file);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
console.error(`Error: model '${model}' not found. Use 'playcraft 3d list-models' to see available models.`);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (!existsSync(filePath)) {
|
|
92
|
+
console.error(`Error: file not found: ${filePath}`);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
const io = new NodeIO();
|
|
96
|
+
const doc = await io.read(filePath);
|
|
97
|
+
const root = doc.getRoot();
|
|
98
|
+
const meshes = root.listMeshes();
|
|
99
|
+
const meshInfo = meshes.map((mesh) => {
|
|
100
|
+
const primitives = mesh.listPrimitives();
|
|
101
|
+
return {
|
|
102
|
+
name: mesh.getName(),
|
|
103
|
+
primitives: primitives.map((prim) => {
|
|
104
|
+
const posAttr = prim.getAttribute('POSITION');
|
|
105
|
+
const normAttr = prim.getAttribute('NORMAL');
|
|
106
|
+
const uvAttr = prim.getAttribute('TEXCOORD_0');
|
|
107
|
+
const indices = prim.getIndices();
|
|
108
|
+
const vertCount = posAttr?.getCount() ?? 0;
|
|
109
|
+
const triCount = indices ? indices.getCount() / 3 : 0;
|
|
110
|
+
return {
|
|
111
|
+
vertices: vertCount,
|
|
112
|
+
triangles: Math.round(triCount),
|
|
113
|
+
hasNormals: !!normAttr,
|
|
114
|
+
hasUVs: !!uvAttr,
|
|
115
|
+
};
|
|
116
|
+
}),
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
const fileSize = statSync(filePath).size;
|
|
120
|
+
const info = {
|
|
121
|
+
file: filePath,
|
|
122
|
+
size: sizeLabel(fileSize),
|
|
123
|
+
sizeBytes: fileSize,
|
|
124
|
+
meshes: meshInfo,
|
|
125
|
+
totalVertices: meshInfo.reduce((s, m) => s + m.primitives.reduce((ps, p) => ps + p.vertices, 0), 0),
|
|
126
|
+
totalTriangles: meshInfo.reduce((s, m) => s + m.primitives.reduce((ps, p) => ps + p.triangles, 0), 0),
|
|
127
|
+
};
|
|
128
|
+
if (opts.json) {
|
|
129
|
+
console.log(JSON.stringify(info, null, 2));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
console.log(`\nGLB Info: ${info.file}`);
|
|
133
|
+
console.log(` Size: ${info.size}`);
|
|
134
|
+
console.log(` Meshes: ${meshInfo.length}`);
|
|
135
|
+
console.log(` Total vertices: ${info.totalVertices}`);
|
|
136
|
+
console.log(` Total triangles: ${info.totalTriangles}`);
|
|
137
|
+
for (const mesh of meshInfo) {
|
|
138
|
+
for (const prim of mesh.primitives) {
|
|
139
|
+
console.log(` Normals: ${prim.hasNormals ? '✓' : '✗'} UVs: ${prim.hasUVs ? '✓' : '✗'}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch (e) {
|
|
144
|
+
handleError(e);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
// ─────────────────────────────────────────────────────────
|
|
149
|
+
// apply-texture
|
|
150
|
+
// ─────────────────────────────────────────────────────────
|
|
151
|
+
function registerApplyTextureCmd(parent) {
|
|
152
|
+
parent
|
|
153
|
+
.command('apply-texture')
|
|
154
|
+
.description('Apply an image texture to a white model GLB')
|
|
155
|
+
.requiredOption('--model <name>', 'Bundled primitive name (sphere, cube, etc.) or path to a GLB file')
|
|
156
|
+
.requiredOption('--texture <path>', 'Image file path (PNG/JPG/WebP) to use as base color texture')
|
|
157
|
+
.requiredOption('--output <path>', 'Output GLB file path')
|
|
158
|
+
.option('--scale <n>', 'Uniform scale factor (default: 1.0)', '1.0')
|
|
159
|
+
.option('--color <hex>', 'Solid base color instead of texture (e.g. "#FF4400")')
|
|
160
|
+
.option('--roughness <n>', 'Material roughness 0.0–1.0 (default: 0.8)', '0.8')
|
|
161
|
+
.option('--metallic <n>', 'Material metalness 0.0–1.0 (default: 0.0)', '0.0')
|
|
162
|
+
.action(async (opts) => {
|
|
163
|
+
try {
|
|
164
|
+
const { NodeIO, TextureChannel } = await import('@gltf-transform/core');
|
|
165
|
+
const sharp = (await import('sharp')).default;
|
|
166
|
+
// Resolve source GLB
|
|
167
|
+
let sourceGlb = opts.model;
|
|
168
|
+
if (!extname(opts.model) && !existsSync(opts.model)) {
|
|
169
|
+
const primitives = loadPrimitivesMetadata();
|
|
170
|
+
const found = primitives.find((p) => p.name === opts.model);
|
|
171
|
+
if (!found) {
|
|
172
|
+
console.error(`Error: model '${opts.model}' not found. Use 'playcraft 3d list-models' to see available models.`);
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
sourceGlb = join(PRIMITIVES_DIR, found.file);
|
|
176
|
+
}
|
|
177
|
+
if (!existsSync(sourceGlb)) {
|
|
178
|
+
console.error(`Error: GLB file not found: ${sourceGlb}`);
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
const io = new NodeIO();
|
|
182
|
+
const doc = await io.read(sourceGlb);
|
|
183
|
+
// Apply scale
|
|
184
|
+
const scale = parseFloat(opts.scale);
|
|
185
|
+
if (scale !== 1.0) {
|
|
186
|
+
for (const node of doc.getRoot().listNodes()) {
|
|
187
|
+
const cur = node.getScale();
|
|
188
|
+
node.setScale([cur[0] * scale, cur[1] * scale, cur[2] * scale]);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// Create/update material
|
|
192
|
+
for (const mesh of doc.getRoot().listMeshes()) {
|
|
193
|
+
for (const prim of mesh.listPrimitives()) {
|
|
194
|
+
let mat = prim.getMaterial();
|
|
195
|
+
if (!mat) {
|
|
196
|
+
mat = doc.createMaterial('material');
|
|
197
|
+
prim.setMaterial(mat);
|
|
198
|
+
}
|
|
199
|
+
const roughness = parseFloat(opts.roughness);
|
|
200
|
+
const metallic = parseFloat(opts.metallic);
|
|
201
|
+
mat.setRoughnessFactor(roughness);
|
|
202
|
+
mat.setMetallicFactor(metallic);
|
|
203
|
+
if (opts.color) {
|
|
204
|
+
// Parse hex color
|
|
205
|
+
const hex = opts.color.replace('#', '');
|
|
206
|
+
const r = parseInt(hex.substring(0, 2), 16) / 255;
|
|
207
|
+
const g = parseInt(hex.substring(2, 4), 16) / 255;
|
|
208
|
+
const b = parseInt(hex.substring(4, 6), 16) / 255;
|
|
209
|
+
mat.setBaseColorFactor([r, g, b, 1]);
|
|
210
|
+
mat.setBaseColorTexture(null);
|
|
211
|
+
}
|
|
212
|
+
else if (opts.texture) {
|
|
213
|
+
// Load texture image
|
|
214
|
+
if (!existsSync(opts.texture)) {
|
|
215
|
+
console.error(`Error: texture file not found: ${opts.texture}`);
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
// Resize to power-of-2 for WebGL compatibility
|
|
219
|
+
const imgInfo = await sharp(opts.texture).metadata();
|
|
220
|
+
const w = imgInfo.width ?? 512;
|
|
221
|
+
const h = imgInfo.height ?? 512;
|
|
222
|
+
const pow2w = Math.pow(2, Math.ceil(Math.log2(Math.min(w, 1024))));
|
|
223
|
+
const pow2h = Math.pow(2, Math.ceil(Math.log2(Math.min(h, 1024))));
|
|
224
|
+
const imgBuffer = await sharp(opts.texture)
|
|
225
|
+
.resize(pow2w, pow2h, { fit: 'fill' })
|
|
226
|
+
.png()
|
|
227
|
+
.toBuffer();
|
|
228
|
+
const texture = doc
|
|
229
|
+
.createTexture(basename(opts.texture))
|
|
230
|
+
.setImage(imgBuffer)
|
|
231
|
+
.setMimeType('image/png');
|
|
232
|
+
mat.setBaseColorTexture(texture);
|
|
233
|
+
mat.setBaseColorFactor([1, 1, 1, 1]);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
ensureOutputDir(opts.output);
|
|
238
|
+
await io.write(opts.output, doc);
|
|
239
|
+
const outSize = statSync(opts.output).size;
|
|
240
|
+
console.log(`Texture applied: ${opts.output} (${sizeLabel(outSize)})`);
|
|
241
|
+
if (opts.texture) {
|
|
242
|
+
console.log(` Source model: ${sourceGlb}`);
|
|
243
|
+
console.log(` Texture: ${opts.texture}`);
|
|
244
|
+
}
|
|
245
|
+
if (opts.color) {
|
|
246
|
+
console.log(` Base color: ${opts.color}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
catch (e) {
|
|
250
|
+
handleError(e);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
// ─────────────────────────────────────────────────────────
|
|
255
|
+
// convert
|
|
256
|
+
// ─────────────────────────────────────────────────────────
|
|
257
|
+
function registerConvertCmd(parent) {
|
|
258
|
+
parent
|
|
259
|
+
.command('convert')
|
|
260
|
+
.description('Convert between GLB and OBJ formats')
|
|
261
|
+
.requiredOption('--input <path>', 'Input file path (GLB)')
|
|
262
|
+
.requiredOption('--output <path>', 'Output file path (GLB or OBJ)')
|
|
263
|
+
.action(async (opts) => {
|
|
264
|
+
try {
|
|
265
|
+
const { NodeIO } = await import('@gltf-transform/core');
|
|
266
|
+
if (!existsSync(opts.input)) {
|
|
267
|
+
console.error(`Error: input file not found: ${opts.input}`);
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
const inExt = extname(opts.input).toLowerCase();
|
|
271
|
+
const outExt = extname(opts.output).toLowerCase();
|
|
272
|
+
if (!['.glb', '.gltf'].includes(inExt)) {
|
|
273
|
+
console.error('Error: input must be a .glb or .gltf file');
|
|
274
|
+
process.exit(1);
|
|
275
|
+
}
|
|
276
|
+
if (outExt === '.glb' || outExt === '.gltf') {
|
|
277
|
+
// GLB ↔ GLTF conversion
|
|
278
|
+
const io = new NodeIO();
|
|
279
|
+
const doc = await io.read(opts.input);
|
|
280
|
+
ensureOutputDir(opts.output);
|
|
281
|
+
await io.write(opts.output, doc);
|
|
282
|
+
const outSize = statSync(opts.output).size;
|
|
283
|
+
console.log(`Converted: ${opts.input} → ${opts.output} (${sizeLabel(outSize)})`);
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
console.error('Error: output must be .glb or .gltf (OBJ export not yet supported)');
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
catch (e) {
|
|
291
|
+
handleError(e);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
// ─────────────────────────────────────────────────────────
|
|
296
|
+
// optimize
|
|
297
|
+
// ─────────────────────────────────────────────────────────
|
|
298
|
+
function registerOptimizeCmd(parent) {
|
|
299
|
+
parent
|
|
300
|
+
.command('optimize')
|
|
301
|
+
.description('Optimize a GLB to reduce file size (texture resize, dedup)')
|
|
302
|
+
.requiredOption('--input <path>', 'Input GLB file')
|
|
303
|
+
.requiredOption('--output <path>', 'Output optimized GLB file')
|
|
304
|
+
.option('--texture-size <n>', 'Max texture dimension (default: 512)', '512')
|
|
305
|
+
.option('--target-size <s>', 'Target size hint (e.g. 200KB) — informational only')
|
|
306
|
+
.action(async (opts) => {
|
|
307
|
+
try {
|
|
308
|
+
const { NodeIO, TextureChannel } = await import('@gltf-transform/core');
|
|
309
|
+
const { dedup, prune } = await import('@gltf-transform/functions');
|
|
310
|
+
void TextureChannel; // unused but imported for type context
|
|
311
|
+
if (!existsSync(opts.input)) {
|
|
312
|
+
console.error(`Error: input file not found: ${opts.input}`);
|
|
313
|
+
process.exit(1);
|
|
314
|
+
}
|
|
315
|
+
const origSize = statSync(opts.input).size;
|
|
316
|
+
const io = new NodeIO();
|
|
317
|
+
const doc = await io.read(opts.input);
|
|
318
|
+
const maxTexSize = parseInt(opts.textureSize, 10);
|
|
319
|
+
const sharp = (await import('sharp')).default;
|
|
320
|
+
// Resize textures that exceed maxTexSize using sharp
|
|
321
|
+
const textures = doc.getRoot().listTextures();
|
|
322
|
+
for (const tex of textures) {
|
|
323
|
+
const imgData = tex.getImage();
|
|
324
|
+
if (!imgData)
|
|
325
|
+
continue;
|
|
326
|
+
const meta = await sharp(Buffer.from(imgData)).metadata();
|
|
327
|
+
const w = meta.width ?? 0;
|
|
328
|
+
const h = meta.height ?? 0;
|
|
329
|
+
if (w > maxTexSize || h > maxTexSize) {
|
|
330
|
+
const resized = await sharp(Buffer.from(imgData))
|
|
331
|
+
.resize(maxTexSize, maxTexSize, { fit: 'inside', withoutEnlargement: true })
|
|
332
|
+
.png()
|
|
333
|
+
.toBuffer();
|
|
334
|
+
tex.setImage(new Uint8Array(resized)).setMimeType('image/png');
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
await doc.transform(dedup(), prune());
|
|
338
|
+
ensureOutputDir(opts.output);
|
|
339
|
+
await io.write(opts.output, doc);
|
|
340
|
+
const outSize = statSync(opts.output).size;
|
|
341
|
+
const saving = origSize - outSize;
|
|
342
|
+
const savingPct = ((saving / origSize) * 100).toFixed(1);
|
|
343
|
+
console.log(`Optimized: ${opts.output}`);
|
|
344
|
+
console.log(` ${sizeLabel(origSize)} → ${sizeLabel(outSize)} (saved ${sizeLabel(saving)}, ${savingPct}%)`);
|
|
345
|
+
}
|
|
346
|
+
catch (e) {
|
|
347
|
+
handleError(e);
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
// ─────────────────────────────────────────────────────────
|
|
352
|
+
// Register all `playcraft 3d` subcommands
|
|
353
|
+
// ─────────────────────────────────────────────────────────
|
|
354
|
+
export function register3DCommands(program) {
|
|
355
|
+
const threeD = program
|
|
356
|
+
.command('3d')
|
|
357
|
+
.description('3D model utilities — white model library, texture application, GLB optimization');
|
|
358
|
+
registerListModelsCmd(threeD);
|
|
359
|
+
registerInfoCmd(threeD);
|
|
360
|
+
registerApplyTextureCmd(threeD);
|
|
361
|
+
registerConvertCmd(threeD);
|
|
362
|
+
registerOptimizeCmd(threeD);
|
|
363
|
+
}
|