retiam 1.0.0 → 1.0.1
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/bin/cli.js +213 -209
- package/bin/export.js +322 -322
- package/bin/init.js +425 -425
- package/bin/lint.js +354 -354
- package/bin/render-config.js +87 -0
- package/bin/render.js +517 -600
- package/bin/snapshot.js +231 -231
- package/dist/{UTIF-pBEih-vH.js → UTIF-CuM_xgsi.js} +1 -1
- package/dist/assets/roboto-msdf.json +3642 -3642
- package/dist/{index-CnM0BhUR.js → index-C7UzgTiw.js} +4728 -4728
- package/dist/{main-Bw2LTrnR.js → main-CCBHkqdA.js} +1 -1
- package/dist/retiam.es.js +1 -1
- package/package.json +106 -116
- package/readme.md +155 -143
package/bin/snapshot.js
CHANGED
|
@@ -1,231 +1,231 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// =========================================================================
|
|
4
|
-
// File: bin/snapshot.js - Retiam Agent-Native Visual Keyframe Snapshotter
|
|
5
|
-
// =========================================================================
|
|
6
|
-
|
|
7
|
-
import fs from 'fs';
|
|
8
|
-
import path from 'path';
|
|
9
|
-
import puppeteer from 'puppeteer';
|
|
10
|
-
import { createServer } from 'vite';
|
|
11
|
-
|
|
12
|
-
const projectRoot = process.cwd();
|
|
13
|
-
const args = process.argv.slice(2);
|
|
14
|
-
|
|
15
|
-
let targetFile = null;
|
|
16
|
-
let times = [1.0, 2.5]; // default snapshot times in seconds
|
|
17
|
-
let width = 1280;
|
|
18
|
-
let height = 720;
|
|
19
|
-
let outDir = path.resolve(projectRoot, 'outputs/snapshot');
|
|
20
|
-
let jsonOutput = false;
|
|
21
|
-
|
|
22
|
-
// Check for --help before parsing options
|
|
23
|
-
const isHelp = args.includes('--help') || args.some((arg, idx) => arg === '-h' && (idx + 1 >= args.length || isNaN(parseInt(args[idx + 1], 10))));
|
|
24
|
-
if (isHelp) {
|
|
25
|
-
console.log(`
|
|
26
|
-
📸 Retiam WebGPU Engine - Keyframe Snapshotter
|
|
27
|
-
|
|
28
|
-
Usage:
|
|
29
|
-
npx retiam snapshot <path/to/Scene.ts> [options]
|
|
30
|
-
|
|
31
|
-
Arguments:
|
|
32
|
-
<file> Target scene TypeScript file (e.g. src/scenes/AStarScene.ts)
|
|
33
|
-
|
|
34
|
-
Options:
|
|
35
|
-
--times <sec,sec,...> Comma-separated list of timeline seconds to capture (default: 1.0, 2.5)
|
|
36
|
-
-w, --width <px> Snapshot width in pixels (default: 1280)
|
|
37
|
-
-h, --height <px> Snapshot height in pixels (default: 720)
|
|
38
|
-
-o, --out <dir> Output directory for snapshot images (default: ./outputs/snapshot)
|
|
39
|
-
--json Output machine-readable JSON for agents and automation
|
|
40
|
-
--help Show this help message and exit
|
|
41
|
-
|
|
42
|
-
Examples:
|
|
43
|
-
npx retiam snapshot src/scenes/AStarScene.ts
|
|
44
|
-
npx retiam snapshot src/scenes/AStarScene.ts --times 1.0,3.5,7.0 -w 1920 -h 1080
|
|
45
|
-
npx retiam snapshot src/scenes/AStarScene.ts --times 5.0 --out ./outputs/snapshot --json
|
|
46
|
-
`);
|
|
47
|
-
process.exit(0);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
for (let i = 0; i < args.length; i++) {
|
|
51
|
-
if (args[i] === '--json') {
|
|
52
|
-
jsonOutput = true;
|
|
53
|
-
} else if (args[i] === '-w' || args[i] === '--width') {
|
|
54
|
-
width = parseInt(args[++i], 10);
|
|
55
|
-
} else if (args[i] === '-h' || args[i] === '--height') {
|
|
56
|
-
height = parseInt(args[++i], 10);
|
|
57
|
-
} else if (args[i] === '-o' || args[i] === '--out') {
|
|
58
|
-
outDir = path.resolve(projectRoot, args[++i]);
|
|
59
|
-
} else if (args[i] === '--times') {
|
|
60
|
-
times = args[++i].split(',').map(s => parseFloat(s.trim())).filter(n => !isNaN(n)).sort((a, b) => a - b);
|
|
61
|
-
} else if (!args[i].startsWith('-') && !targetFile) {
|
|
62
|
-
targetFile = args[i];
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (!targetFile) {
|
|
67
|
-
if (jsonOutput) {
|
|
68
|
-
console.log(JSON.stringify({ status: 'error', message: 'No target scene file specified.' }));
|
|
69
|
-
} else {
|
|
70
|
-
console.error('❌ Error: No target scene file specified.');
|
|
71
|
-
console.log('Usage: npx retiam snapshot <path/to/Scene.ts> [--times 1.0,2.5,4.0] [--out ./outputs/snapshot] [--json]');
|
|
72
|
-
console.log('Run "npx retiam snapshot --help" for more details.');
|
|
73
|
-
}
|
|
74
|
-
process.exit(1);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const resolvedScenePath = path.resolve(projectRoot, targetFile);
|
|
78
|
-
if (!fs.existsSync(resolvedScenePath)) {
|
|
79
|
-
if (jsonOutput) {
|
|
80
|
-
console.log(JSON.stringify({ status: 'error', message: `Scene file not found: ${targetFile}` }));
|
|
81
|
-
} else {
|
|
82
|
-
console.error(`❌ Error: Scene file not found: ${resolvedScenePath}`);
|
|
83
|
-
}
|
|
84
|
-
process.exit(1);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (!fs.existsSync(outDir)) {
|
|
88
|
-
fs.mkdirSync(outDir, { recursive: true });
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
let viteServer = null;
|
|
92
|
-
let browser = null;
|
|
93
|
-
|
|
94
|
-
const cleanup = async () => {
|
|
95
|
-
if (browser) try { await browser.close(); } catch (e) {}
|
|
96
|
-
if (viteServer) try { await viteServer.close(); } catch (e) {}
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
process.on('SIGINT', async () => {
|
|
100
|
-
await cleanup();
|
|
101
|
-
process.exit(1);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
(async () => {
|
|
105
|
-
try {
|
|
106
|
-
if (!jsonOutput) {
|
|
107
|
-
console.log(`\n📸 Starting Retiam Keyframe Snapshotter`);
|
|
108
|
-
console.log(` Scene: ${targetFile}`);
|
|
109
|
-
console.log(` Resolution: ${width}x${height}`);
|
|
110
|
-
console.log(` Timestamps: ${times.map(t => `${t}s`).join(', ')}`);
|
|
111
|
-
console.log(` Output Dir: ${outDir}\n`);
|
|
112
|
-
console.log(`🚀 Starting local Vite server...`);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const userViteConfig = path.resolve(projectRoot, 'vite.config.ts');
|
|
116
|
-
viteServer = await createServer({
|
|
117
|
-
configFile: fs.existsSync(userViteConfig) ? userViteConfig : undefined,
|
|
118
|
-
root: projectRoot,
|
|
119
|
-
server: { port: 5199, strictPort: false },
|
|
120
|
-
logLevel: 'error'
|
|
121
|
-
});
|
|
122
|
-
await viteServer.listen();
|
|
123
|
-
|
|
124
|
-
const address = viteServer.httpServer.address();
|
|
125
|
-
const actualPort = (address && typeof address === 'object') ? address.port : 5199;
|
|
126
|
-
|
|
127
|
-
if (!jsonOutput) console.log(`🌐 Launching headless browser with WebGPU...`);
|
|
128
|
-
|
|
129
|
-
const launchArgs = [
|
|
130
|
-
'--enable-unsafe-webgpu',
|
|
131
|
-
'--disable-web-security',
|
|
132
|
-
'--no-sandbox',
|
|
133
|
-
'--enable-experimental-web-platform-features',
|
|
134
|
-
'--autoplay-policy=no-user-gesture-required'
|
|
135
|
-
];
|
|
136
|
-
|
|
137
|
-
try {
|
|
138
|
-
browser = await puppeteer.launch({
|
|
139
|
-
headless: "new",
|
|
140
|
-
args: launchArgs,
|
|
141
|
-
channel: 'chrome'
|
|
142
|
-
});
|
|
143
|
-
} catch (e) {
|
|
144
|
-
// Fallback to default bundled Chromium
|
|
145
|
-
browser = await puppeteer.launch({
|
|
146
|
-
headless: "new",
|
|
147
|
-
args: launchArgs
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const page = await browser.newPage();
|
|
152
|
-
await page.setViewport({ width, height, deviceScaleFactor: 1.0 });
|
|
153
|
-
|
|
154
|
-
page.on('console', msg => {
|
|
155
|
-
if (msg.type() === 'error') {
|
|
156
|
-
console.error(`[BROWSER ERROR] ${msg.text()}`);
|
|
157
|
-
} else {
|
|
158
|
-
console.log(`[BROWSER LOG] ${msg.text()}`);
|
|
159
|
-
}
|
|
160
|
-
});
|
|
161
|
-
page.on('requestfailed', request => {
|
|
162
|
-
console.error(`[REQUEST FAILED] ${request.url()} - ${request.failure()?.errorText}`);
|
|
163
|
-
});
|
|
164
|
-
page.on('response', response => {
|
|
165
|
-
if (response.status() >= 400) {
|
|
166
|
-
console.error(`[HTTP ${response.status()}] ${response.url()}`);
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
const url = `http://localhost:${actualPort}?file=${targetFile}`;
|
|
171
|
-
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
|
172
|
-
|
|
173
|
-
// Wait for canvas to be present
|
|
174
|
-
await page.waitForSelector('canvas', { timeout: 15000 });
|
|
175
|
-
|
|
176
|
-
const baseSceneName = path.basename(targetFile, path.extname(targetFile));
|
|
177
|
-
const snapshots = [];
|
|
178
|
-
|
|
179
|
-
let lastTimeSec = 0;
|
|
180
|
-
for (const timeSec of times) {
|
|
181
|
-
const deltaMs = Math.max(0, (timeSec - lastTimeSec) * 1000);
|
|
182
|
-
if (deltaMs > 0) {
|
|
183
|
-
await new Promise(resolve => setTimeout(resolve, deltaMs));
|
|
184
|
-
}
|
|
185
|
-
lastTimeSec = timeSec;
|
|
186
|
-
|
|
187
|
-
const filename = `snapshot_${baseSceneName}_${timeSec.toFixed(1)}s.png`;
|
|
188
|
-
const filePath = path.join(outDir, filename);
|
|
189
|
-
|
|
190
|
-
await page.screenshot({ path: filePath, type: 'png' });
|
|
191
|
-
|
|
192
|
-
snapshots.push({
|
|
193
|
-
timeSeconds: timeSec,
|
|
194
|
-
filename,
|
|
195
|
-
path: filePath,
|
|
196
|
-
width,
|
|
197
|
-
height
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
if (!jsonOutput) {
|
|
201
|
-
console.log(` ✅ Captured snapshot at ${timeSec.toFixed(1)}s -> ${filename}`);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
await cleanup();
|
|
206
|
-
|
|
207
|
-
if (jsonOutput) {
|
|
208
|
-
console.log(JSON.stringify({
|
|
209
|
-
status: 'success',
|
|
210
|
-
scene: targetFile,
|
|
211
|
-
snapshots
|
|
212
|
-
}, null, 2));
|
|
213
|
-
} else {
|
|
214
|
-
console.log(`\n🎉 All keyframe snapshots successfully generated in: ${outDir}\n`);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
process.exit(0);
|
|
218
|
-
} catch (err) {
|
|
219
|
-
await cleanup();
|
|
220
|
-
if (jsonOutput) {
|
|
221
|
-
console.log(JSON.stringify({
|
|
222
|
-
status: 'error',
|
|
223
|
-
message: err.message,
|
|
224
|
-
stack: err.stack
|
|
225
|
-
}));
|
|
226
|
-
} else {
|
|
227
|
-
console.error('\n❌ Snapshotting failed:', err);
|
|
228
|
-
}
|
|
229
|
-
process.exit(1);
|
|
230
|
-
}
|
|
231
|
-
})();
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// =========================================================================
|
|
4
|
+
// File: bin/snapshot.js - Retiam Agent-Native Visual Keyframe Snapshotter
|
|
5
|
+
// =========================================================================
|
|
6
|
+
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import puppeteer from 'puppeteer';
|
|
10
|
+
import { createServer } from 'vite';
|
|
11
|
+
|
|
12
|
+
const projectRoot = process.cwd();
|
|
13
|
+
const args = process.argv.slice(2);
|
|
14
|
+
|
|
15
|
+
let targetFile = null;
|
|
16
|
+
let times = [1.0, 2.5]; // default snapshot times in seconds
|
|
17
|
+
let width = 1280;
|
|
18
|
+
let height = 720;
|
|
19
|
+
let outDir = path.resolve(projectRoot, 'outputs/snapshot');
|
|
20
|
+
let jsonOutput = false;
|
|
21
|
+
|
|
22
|
+
// Check for --help before parsing options
|
|
23
|
+
const isHelp = args.includes('--help') || args.some((arg, idx) => arg === '-h' && (idx + 1 >= args.length || isNaN(parseInt(args[idx + 1], 10))));
|
|
24
|
+
if (isHelp) {
|
|
25
|
+
console.log(`
|
|
26
|
+
📸 Retiam WebGPU Engine - Keyframe Snapshotter
|
|
27
|
+
|
|
28
|
+
Usage:
|
|
29
|
+
npx retiam snapshot <path/to/Scene.ts> [options]
|
|
30
|
+
|
|
31
|
+
Arguments:
|
|
32
|
+
<file> Target scene TypeScript file (e.g. src/scenes/AStarScene.ts)
|
|
33
|
+
|
|
34
|
+
Options:
|
|
35
|
+
--times <sec,sec,...> Comma-separated list of timeline seconds to capture (default: 1.0, 2.5)
|
|
36
|
+
-w, --width <px> Snapshot width in pixels (default: 1280)
|
|
37
|
+
-h, --height <px> Snapshot height in pixels (default: 720)
|
|
38
|
+
-o, --out <dir> Output directory for snapshot images (default: ./outputs/snapshot)
|
|
39
|
+
--json Output machine-readable JSON for agents and automation
|
|
40
|
+
--help Show this help message and exit
|
|
41
|
+
|
|
42
|
+
Examples:
|
|
43
|
+
npx retiam snapshot src/scenes/AStarScene.ts
|
|
44
|
+
npx retiam snapshot src/scenes/AStarScene.ts --times 1.0,3.5,7.0 -w 1920 -h 1080
|
|
45
|
+
npx retiam snapshot src/scenes/AStarScene.ts --times 5.0 --out ./outputs/snapshot --json
|
|
46
|
+
`);
|
|
47
|
+
process.exit(0);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (let i = 0; i < args.length; i++) {
|
|
51
|
+
if (args[i] === '--json') {
|
|
52
|
+
jsonOutput = true;
|
|
53
|
+
} else if (args[i] === '-w' || args[i] === '--width') {
|
|
54
|
+
width = parseInt(args[++i], 10);
|
|
55
|
+
} else if (args[i] === '-h' || args[i] === '--height') {
|
|
56
|
+
height = parseInt(args[++i], 10);
|
|
57
|
+
} else if (args[i] === '-o' || args[i] === '--out') {
|
|
58
|
+
outDir = path.resolve(projectRoot, args[++i]);
|
|
59
|
+
} else if (args[i] === '--times') {
|
|
60
|
+
times = args[++i].split(',').map(s => parseFloat(s.trim())).filter(n => !isNaN(n)).sort((a, b) => a - b);
|
|
61
|
+
} else if (!args[i].startsWith('-') && !targetFile) {
|
|
62
|
+
targetFile = args[i];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!targetFile) {
|
|
67
|
+
if (jsonOutput) {
|
|
68
|
+
console.log(JSON.stringify({ status: 'error', message: 'No target scene file specified.' }));
|
|
69
|
+
} else {
|
|
70
|
+
console.error('❌ Error: No target scene file specified.');
|
|
71
|
+
console.log('Usage: npx retiam snapshot <path/to/Scene.ts> [--times 1.0,2.5,4.0] [--out ./outputs/snapshot] [--json]');
|
|
72
|
+
console.log('Run "npx retiam snapshot --help" for more details.');
|
|
73
|
+
}
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const resolvedScenePath = path.resolve(projectRoot, targetFile);
|
|
78
|
+
if (!fs.existsSync(resolvedScenePath)) {
|
|
79
|
+
if (jsonOutput) {
|
|
80
|
+
console.log(JSON.stringify({ status: 'error', message: `Scene file not found: ${targetFile}` }));
|
|
81
|
+
} else {
|
|
82
|
+
console.error(`❌ Error: Scene file not found: ${resolvedScenePath}`);
|
|
83
|
+
}
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!fs.existsSync(outDir)) {
|
|
88
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let viteServer = null;
|
|
92
|
+
let browser = null;
|
|
93
|
+
|
|
94
|
+
const cleanup = async () => {
|
|
95
|
+
if (browser) try { await browser.close(); } catch (e) {}
|
|
96
|
+
if (viteServer) try { await viteServer.close(); } catch (e) {}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
process.on('SIGINT', async () => {
|
|
100
|
+
await cleanup();
|
|
101
|
+
process.exit(1);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
(async () => {
|
|
105
|
+
try {
|
|
106
|
+
if (!jsonOutput) {
|
|
107
|
+
console.log(`\n📸 Starting Retiam Keyframe Snapshotter`);
|
|
108
|
+
console.log(` Scene: ${targetFile}`);
|
|
109
|
+
console.log(` Resolution: ${width}x${height}`);
|
|
110
|
+
console.log(` Timestamps: ${times.map(t => `${t}s`).join(', ')}`);
|
|
111
|
+
console.log(` Output Dir: ${outDir}\n`);
|
|
112
|
+
console.log(`🚀 Starting local Vite server...`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const userViteConfig = path.resolve(projectRoot, 'vite.config.ts');
|
|
116
|
+
viteServer = await createServer({
|
|
117
|
+
configFile: fs.existsSync(userViteConfig) ? userViteConfig : undefined,
|
|
118
|
+
root: projectRoot,
|
|
119
|
+
server: { port: 5199, strictPort: false },
|
|
120
|
+
logLevel: 'error'
|
|
121
|
+
});
|
|
122
|
+
await viteServer.listen();
|
|
123
|
+
|
|
124
|
+
const address = viteServer.httpServer.address();
|
|
125
|
+
const actualPort = (address && typeof address === 'object') ? address.port : 5199;
|
|
126
|
+
|
|
127
|
+
if (!jsonOutput) console.log(`🌐 Launching headless browser with WebGPU...`);
|
|
128
|
+
|
|
129
|
+
const launchArgs = [
|
|
130
|
+
'--enable-unsafe-webgpu',
|
|
131
|
+
'--disable-web-security',
|
|
132
|
+
'--no-sandbox',
|
|
133
|
+
'--enable-experimental-web-platform-features',
|
|
134
|
+
'--autoplay-policy=no-user-gesture-required'
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
browser = await puppeteer.launch({
|
|
139
|
+
headless: "new",
|
|
140
|
+
args: launchArgs,
|
|
141
|
+
channel: 'chrome'
|
|
142
|
+
});
|
|
143
|
+
} catch (e) {
|
|
144
|
+
// Fallback to default bundled Chromium
|
|
145
|
+
browser = await puppeteer.launch({
|
|
146
|
+
headless: "new",
|
|
147
|
+
args: launchArgs
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const page = await browser.newPage();
|
|
152
|
+
await page.setViewport({ width, height, deviceScaleFactor: 1.0 });
|
|
153
|
+
|
|
154
|
+
page.on('console', msg => {
|
|
155
|
+
if (msg.type() === 'error') {
|
|
156
|
+
console.error(`[BROWSER ERROR] ${msg.text()}`);
|
|
157
|
+
} else {
|
|
158
|
+
console.log(`[BROWSER LOG] ${msg.text()}`);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
page.on('requestfailed', request => {
|
|
162
|
+
console.error(`[REQUEST FAILED] ${request.url()} - ${request.failure()?.errorText}`);
|
|
163
|
+
});
|
|
164
|
+
page.on('response', response => {
|
|
165
|
+
if (response.status() >= 400) {
|
|
166
|
+
console.error(`[HTTP ${response.status()}] ${response.url()}`);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const url = `http://localhost:${actualPort}?file=${targetFile}`;
|
|
171
|
+
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
|
172
|
+
|
|
173
|
+
// Wait for canvas to be present
|
|
174
|
+
await page.waitForSelector('canvas', { timeout: 15000 });
|
|
175
|
+
|
|
176
|
+
const baseSceneName = path.basename(targetFile, path.extname(targetFile));
|
|
177
|
+
const snapshots = [];
|
|
178
|
+
|
|
179
|
+
let lastTimeSec = 0;
|
|
180
|
+
for (const timeSec of times) {
|
|
181
|
+
const deltaMs = Math.max(0, (timeSec - lastTimeSec) * 1000);
|
|
182
|
+
if (deltaMs > 0) {
|
|
183
|
+
await new Promise(resolve => setTimeout(resolve, deltaMs));
|
|
184
|
+
}
|
|
185
|
+
lastTimeSec = timeSec;
|
|
186
|
+
|
|
187
|
+
const filename = `snapshot_${baseSceneName}_${timeSec.toFixed(1)}s.png`;
|
|
188
|
+
const filePath = path.join(outDir, filename);
|
|
189
|
+
|
|
190
|
+
await page.screenshot({ path: filePath, type: 'png' });
|
|
191
|
+
|
|
192
|
+
snapshots.push({
|
|
193
|
+
timeSeconds: timeSec,
|
|
194
|
+
filename,
|
|
195
|
+
path: filePath,
|
|
196
|
+
width,
|
|
197
|
+
height
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
if (!jsonOutput) {
|
|
201
|
+
console.log(` ✅ Captured snapshot at ${timeSec.toFixed(1)}s -> ${filename}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
await cleanup();
|
|
206
|
+
|
|
207
|
+
if (jsonOutput) {
|
|
208
|
+
console.log(JSON.stringify({
|
|
209
|
+
status: 'success',
|
|
210
|
+
scene: targetFile,
|
|
211
|
+
snapshots
|
|
212
|
+
}, null, 2));
|
|
213
|
+
} else {
|
|
214
|
+
console.log(`\n🎉 All keyframe snapshots successfully generated in: ${outDir}\n`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
process.exit(0);
|
|
218
|
+
} catch (err) {
|
|
219
|
+
await cleanup();
|
|
220
|
+
if (jsonOutput) {
|
|
221
|
+
console.log(JSON.stringify({
|
|
222
|
+
status: 'error',
|
|
223
|
+
message: err.message,
|
|
224
|
+
stack: err.stack
|
|
225
|
+
}));
|
|
226
|
+
} else {
|
|
227
|
+
console.error('\n❌ Snapshotting failed:', err);
|
|
228
|
+
}
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
})();
|