retiam 2.0.1 → 2.0.2
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/THIRD_PARTY_NOTICES.md +3 -3
- package/agent/AGENTS.md +67 -0
- package/agent/CLAUDE.md +2 -0
- package/agent/skill/SKILL.md +111 -0
- package/agent/skill/examples/AreaLesson.ts +48 -0
- package/agent/skill/examples/ExplainMotion.ts +32 -0
- package/agent/skill/examples/NarratedMotion.ts +42 -0
- package/agent/skill/examples/SecantToTangent.ts +142 -0
- package/agent/skill/references/api.md +109 -0
- package/agent/skill/references/explainers.md +121 -0
- package/agent/skill/references/quality.md +148 -0
- package/agent/skill/references/styles.md +58 -0
- package/agent/skill/references/tts.md +72 -0
- package/agent/templates/explainer/lesson.json +25 -0
- package/agent/templates/explainer/review.json +6 -0
- package/agent/templates/explainer/styles.json +92 -0
- package/bin/agent-kit.js +26 -0
- package/bin/cli.js +24 -4
- package/bin/context.js +57 -0
- package/bin/init.js +71 -12
- package/bin/lesson-project.js +30 -0
- package/bin/lesson.js +168 -0
- package/bin/lint.js +6 -6
- package/bin/snapshot-runtime.js +185 -0
- package/bin/snapshot.js +32 -225
- package/dist/THIRD_PARTY_NOTICES.md +3 -3
- package/dist/UTIF-BrQ3Ng5l.min.js +1 -0
- package/dist/{UTIF-2Ear_0Uo.js → UTIF-CN0-pEnd.js} +1 -1
- package/dist/decompress-D7LzbvPD.min.js +15 -0
- package/dist/index-3sAseSJQ.min.js +10472 -0
- package/dist/{index-B2sJBy4c.js → index-yQIdejOj.js} +15 -13
- package/dist/libs/explainer/example.d.ts +3 -0
- package/dist/libs/explainer/index.d.ts +5 -0
- package/dist/libs/explainer/index.js +134 -0
- package/dist/libs/explainer/plan.d.ts +12 -0
- package/dist/libs/explainer/plan.js +197 -0
- package/dist/libs/explainer/runtime.d.ts +24 -0
- package/dist/libs/explainer/styles.d.ts +3 -0
- package/dist/libs/explainer/types.d.ts +119 -0
- package/dist/libs/explainer/visuals.d.ts +18 -0
- package/dist/libs/tts/index.js +386 -1009
- package/dist/libs/vite/index.d.ts +28 -0
- package/dist/libs/vite/index.js +158 -0
- package/dist/{main-D_G4uIju.js → main-BTk_9jtl.js} +1 -1
- package/dist/main-lQwYAed8.min.js +5939 -0
- package/dist/msEdgeV2-BLEnvPJa.js +641 -0
- package/dist/opentype.module-B-1cYAb-.min.js +1 -0
- package/dist/retiam.es.js +1 -1
- package/dist/retiam.es.min.js +1 -0
- package/dist/src/engine/animation/Animation.d.ts +1 -0
- package/package.json +13 -8
- package/readme.md +7 -6
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import puppeteer from 'puppeteer';
|
|
4
|
+
import { createServer } from 'vite';
|
|
5
|
+
|
|
6
|
+
export function parseSnapshotOptions(args, projectRoot = process.cwd()) {
|
|
7
|
+
let file;
|
|
8
|
+
const options = { projectRoot, times: [1, 2.5], width: 1280, height: 720,
|
|
9
|
+
fps: 60, timeout: 120000, outDir: path.resolve(projectRoot, 'outputs/snapshot') };
|
|
10
|
+
const flags = { '-w': 'width', '--width': 'width', '-h': 'height', '--height': 'height',
|
|
11
|
+
'--fps': 'fps', '--timeout': 'timeout', '-o': 'outDir', '--out': 'outDir', '--times': 'times' };
|
|
12
|
+
for (let i = 0; i < args.length; i++) {
|
|
13
|
+
const arg = args[i];
|
|
14
|
+
if (arg === '--json') continue;
|
|
15
|
+
const key = flags[arg];
|
|
16
|
+
if (key) {
|
|
17
|
+
const value = args[++i];
|
|
18
|
+
if (value === undefined || value.startsWith('--')) throw new Error(`${arg} requires a value.`);
|
|
19
|
+
if (key === 'outDir') options.outDir = path.resolve(projectRoot, value);
|
|
20
|
+
else if (key === 'times') {
|
|
21
|
+
const values = value.split(',');
|
|
22
|
+
if (values.some(item => !item.trim() || !Number.isFinite(Number(item)) || Number(item) < 0)) {
|
|
23
|
+
throw new Error('--times must contain finite, nonnegative seconds.');
|
|
24
|
+
}
|
|
25
|
+
options.times = [...new Set(values.map(Number))].sort((a, b) => a - b);
|
|
26
|
+
} else {
|
|
27
|
+
const number = Number(value);
|
|
28
|
+
if (!Number.isSafeInteger(number) || number <= 0) throw new Error(`${arg} must be a positive integer.`);
|
|
29
|
+
options[key] = number;
|
|
30
|
+
}
|
|
31
|
+
} else if (arg.startsWith('-')) throw new Error(`Unknown option: ${arg}`);
|
|
32
|
+
else if (file) throw new Error('Provide one target scene file.');
|
|
33
|
+
else file = arg;
|
|
34
|
+
}
|
|
35
|
+
if (!file) throw new Error('No target scene file specified.');
|
|
36
|
+
const resolved = path.resolve(projectRoot, file);
|
|
37
|
+
if (!fs.existsSync(resolved) || !fs.statSync(resolved).isFile()) throw new Error(`Scene file not found: ${file}`);
|
|
38
|
+
const relative = path.relative(fs.realpathSync(projectRoot), fs.realpathSync(resolved));
|
|
39
|
+
if (relative.startsWith(`..${path.sep}`) || relative === '..' || path.isAbsolute(relative)) {
|
|
40
|
+
throw new Error('The scene must be inside the project directory.');
|
|
41
|
+
}
|
|
42
|
+
if (options.times.some(time => !Number.isFinite(time * 1000))) throw new Error('Snapshot times are too large.');
|
|
43
|
+
return { ...options, file, sceneUrl: '/' + relative.split(path.sep).map(encodeURIComponent).join('/') };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Runs in the browser. No live render loop or project entrypoint is started.
|
|
47
|
+
async function snapshotRuntime(sceneUrl, width, height) {
|
|
48
|
+
const state = window.__retiamSnapshot = { ready: false, error: null, capture: null };
|
|
49
|
+
const fail = error => { state.error = error?.stack || error?.message || String(error); };
|
|
50
|
+
window.addEventListener('error', event => fail(event.error || event.message));
|
|
51
|
+
window.addEventListener('unhandledrejection', event => fail(event.reason));
|
|
52
|
+
try {
|
|
53
|
+
const [{ SceneComposer }, { default: SceneClass }] = await Promise.all([
|
|
54
|
+
import('retiam'), import(/* @vite-ignore */ sceneUrl),
|
|
55
|
+
]);
|
|
56
|
+
if (typeof SceneClass !== 'function') throw new Error('The scene must default-export a Scene class.');
|
|
57
|
+
const canvas = document.querySelector('canvas');
|
|
58
|
+
canvas.width = width;
|
|
59
|
+
canvas.height = height;
|
|
60
|
+
const composer = new SceneComposer();
|
|
61
|
+
composer.enablePlayer(false);
|
|
62
|
+
await composer.init(canvas);
|
|
63
|
+
// Suppress invalidate() starting realtime playback, without pausing scene updates.
|
|
64
|
+
composer.PAUSED = true;
|
|
65
|
+
composer.device.addEventListener('uncapturederror', event => fail(event.error));
|
|
66
|
+
const scene = new SceneClass();
|
|
67
|
+
composer.add(scene);
|
|
68
|
+
Promise.resolve(scene.run()).catch(fail);
|
|
69
|
+
|
|
70
|
+
async function settleAssets() {
|
|
71
|
+
while (composer.pendingTasks.length || scene.pendingTasks.length) {
|
|
72
|
+
await Promise.all([...composer.pendingTasks.splice(0), ...scene.pendingTasks.splice(0)]);
|
|
73
|
+
}
|
|
74
|
+
if (state.error) throw new Error(state.error);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function draw() {
|
|
78
|
+
await settleAssets();
|
|
79
|
+
composer.prepare();
|
|
80
|
+
composer.stateManager.uploadToGPU();
|
|
81
|
+
composer.submit(composer.getCurrentTexture());
|
|
82
|
+
await composer.device.queue.onSubmittedWorkDone();
|
|
83
|
+
if (state.error) throw new Error(state.error);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let currentTime = 0;
|
|
87
|
+
await settleAssets();
|
|
88
|
+
await composer.update(0, 0);
|
|
89
|
+
await draw();
|
|
90
|
+
state.capture = async (seconds, fps) => {
|
|
91
|
+
const target = seconds * 1000;
|
|
92
|
+
const step = 1000 / fps;
|
|
93
|
+
while (currentTime < target) {
|
|
94
|
+
const next = Math.min(target, currentTime + step);
|
|
95
|
+
await composer.update(next, next - currentTime);
|
|
96
|
+
currentTime = next;
|
|
97
|
+
// Submit each step, including compute/physics-dependent geometry.
|
|
98
|
+
await draw();
|
|
99
|
+
}
|
|
100
|
+
await draw();
|
|
101
|
+
// Present only requested frames; simulation steps need no wall-clock waits.
|
|
102
|
+
await new Promise(requestAnimationFrame);
|
|
103
|
+
if (state.error) throw new Error(state.error);
|
|
104
|
+
return currentTime / 1000;
|
|
105
|
+
};
|
|
106
|
+
state.ready = true;
|
|
107
|
+
} catch (error) { fail(error); }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function snapshotHTML(options) {
|
|
111
|
+
const literal = JSON.stringify(options.sceneUrl).replaceAll('<', '\\u003c');
|
|
112
|
+
return `<!doctype html><html><head><link rel="icon" href="data:,"></head>
|
|
113
|
+
<body style="margin:0;overflow:hidden"><canvas style="display:block;width:100vw;height:100vh"></canvas>
|
|
114
|
+
<script type="module">(${snapshotRuntime.toString()})(${literal},${options.width},${options.height});</script></body></html>`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function captureSnapshots(options, { signal } = {}) {
|
|
118
|
+
let server, browser;
|
|
119
|
+
const errors = [];
|
|
120
|
+
const route = '/__retiam_snapshot__.html';
|
|
121
|
+
// Vite and scene messages go to stderr so stdout remains one JSON document.
|
|
122
|
+
const logger = {
|
|
123
|
+
hasWarned: false, info: message => console.error(message), warn: message => console.error(message),
|
|
124
|
+
warnOnce: message => console.error(message), error: message => console.error(message),
|
|
125
|
+
clearScreen() {}, hasErrorLogged() { return false; },
|
|
126
|
+
};
|
|
127
|
+
const close = () => Promise.allSettled([browser?.close(), server?.close()]);
|
|
128
|
+
const abort = () => { void close(); };
|
|
129
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
130
|
+
try {
|
|
131
|
+
signal?.throwIfAborted();
|
|
132
|
+
server = await createServer({
|
|
133
|
+
root: options.projectRoot, logLevel: 'error', customLogger: logger,
|
|
134
|
+
server: { host: '127.0.0.1', port: 0, strictPort: false, open: false, hmr: false, watch: null },
|
|
135
|
+
plugins: [{ name: 'retiam-snapshot', configureServer(vite) {
|
|
136
|
+
vite.middlewares.use((request, response, next) => {
|
|
137
|
+
if (request.url?.split('?')[0] !== route) return next();
|
|
138
|
+
vite.transformIndexHtml(route, snapshotHTML(options)).then(html => {
|
|
139
|
+
response.setHeader('Content-Type', 'text/html');
|
|
140
|
+
response.end(html);
|
|
141
|
+
}).catch(next);
|
|
142
|
+
});
|
|
143
|
+
} }],
|
|
144
|
+
});
|
|
145
|
+
await server.listen();
|
|
146
|
+
signal?.throwIfAborted();
|
|
147
|
+
const launch = { headless: 'new', protocolTimeout: options.timeout,
|
|
148
|
+
args: ['--enable-unsafe-webgpu', '--no-sandbox', '--autoplay-policy=no-user-gesture-required'] };
|
|
149
|
+
if (process.env.PUPPETEER_EXECUTABLE_PATH) {
|
|
150
|
+
browser = await puppeteer.launch({ ...launch, executablePath: process.env.PUPPETEER_EXECUTABLE_PATH });
|
|
151
|
+
} else {
|
|
152
|
+
try { browser = await puppeteer.launch({ ...launch, channel: 'chrome' }); }
|
|
153
|
+
catch { browser = await puppeteer.launch(launch); }
|
|
154
|
+
}
|
|
155
|
+
signal?.throwIfAborted();
|
|
156
|
+
const page = await browser.newPage();
|
|
157
|
+
page.setDefaultTimeout(options.timeout);
|
|
158
|
+
page.on('console', message => console.error(`[Browser ${message.type()}] ${message.text()}`));
|
|
159
|
+
page.on('pageerror', error => errors.push(error.message));
|
|
160
|
+
page.on('requestfailed', request => console.error(`[Request failed] ${request.url()}: ${request.failure()?.errorText}`));
|
|
161
|
+
await page.setViewport({ width: options.width, height: options.height, deviceScaleFactor: 1 });
|
|
162
|
+
await page.goto(`http://127.0.0.1:${server.httpServer.address().port}${route}`, { waitUntil: 'domcontentloaded' });
|
|
163
|
+
await page.waitForFunction(() => window.__retiamSnapshot?.ready || window.__retiamSnapshot?.error);
|
|
164
|
+
const error = await page.evaluate(() => window.__retiamSnapshot.error);
|
|
165
|
+
if (error) throw new Error(error);
|
|
166
|
+
if (errors.length) throw new Error(errors.join('\n'));
|
|
167
|
+
fs.mkdirSync(options.outDir, { recursive: true });
|
|
168
|
+
const canvas = await page.$('canvas');
|
|
169
|
+
const base = path.basename(options.file, path.extname(options.file));
|
|
170
|
+
const snapshots = [];
|
|
171
|
+
for (const seconds of options.times) {
|
|
172
|
+
signal?.throwIfAborted();
|
|
173
|
+
const timeSeconds = await page.evaluate((time, fps) => window.__retiamSnapshot.capture(time, fps), seconds, options.fps);
|
|
174
|
+
if (errors.length) throw new Error(errors.join('\n'));
|
|
175
|
+
const filename = `snapshot_${base}_${seconds}s.png`;
|
|
176
|
+
const location = path.join(options.outDir, filename);
|
|
177
|
+
await canvas.screenshot({ path: location, type: 'png' });
|
|
178
|
+
snapshots.push({ timeSeconds, filename, path: location, width: options.width, height: options.height });
|
|
179
|
+
}
|
|
180
|
+
return { status: 'success', scene: options.file, fps: options.fps, snapshots };
|
|
181
|
+
} finally {
|
|
182
|
+
signal?.removeEventListener('abort', abort);
|
|
183
|
+
await close();
|
|
184
|
+
}
|
|
185
|
+
}
|
package/bin/snapshot.js
CHANGED
|
@@ -1,231 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { captureSnapshots, parseSnapshotOptions } from './snapshot-runtime.js';
|
|
2
3
|
|
|
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
4
|
const args = process.argv.slice(2);
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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];
|
|
5
|
+
const json = args.includes('--json');
|
|
6
|
+
if (args.includes('--help') || (args.includes('-h') && !/^\d+$/.test(args[args.indexOf('-h') + 1] || ''))) {
|
|
7
|
+
console.log(`Usage: retiam snapshot <scene.ts> [options]
|
|
8
|
+
|
|
9
|
+
Runs the named scene directly, advancing its timeline in fixed steps.
|
|
10
|
+
|
|
11
|
+
--times <sec,sec,...> Timeline seconds (default: 1,2.5)
|
|
12
|
+
-w, --width <px> Image width (default: 1280)
|
|
13
|
+
-h, --height <px> Image height (default: 720)
|
|
14
|
+
--fps <number> Timeline step rate (default: 60)
|
|
15
|
+
-o, --out <dir> PNG directory (default: outputs/snapshot)
|
|
16
|
+
--timeout <ms> Browser operation timeout (default: 120000)
|
|
17
|
+
--json One JSON result on stdout; diagnostics on stderr
|
|
18
|
+
--help Show this help
|
|
19
|
+
|
|
20
|
+
Example: retiam snapshot src/scenes/YourScene.ts --times 0,1.5,3 --json`);
|
|
21
|
+
} else {
|
|
22
|
+
if (json) {
|
|
23
|
+
// Project Vite plugins may log directly instead of using Vite's logger.
|
|
24
|
+
for (const method of ['log', 'info', 'debug']) console[method] = (...values) => console.error(...values);
|
|
63
25
|
}
|
|
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 () => {
|
|
26
|
+
const controller = new AbortController();
|
|
27
|
+
for (const signal of ['SIGINT', 'SIGTERM']) process.once(signal, () => controller.abort());
|
|
105
28
|
try {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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);
|
|
29
|
+
const options = parseSnapshotOptions(args);
|
|
30
|
+
const result = await captureSnapshots(options, { signal: controller.signal });
|
|
31
|
+
if (json) process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
32
|
+
else console.log(`Captured ${result.snapshots.length} snapshots of ${result.scene}:\n${result.snapshots.map(item => ` ${item.timeSeconds}s: ${item.path}`).join('\n')}`);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (json) process.stdout.write(JSON.stringify({ status: 'error', message: error.message }) + '\n');
|
|
35
|
+
else console.error(`Snapshot failed: ${error.message}`);
|
|
36
|
+
process.exitCode = 1;
|
|
230
37
|
}
|
|
231
|
-
}
|
|
38
|
+
}
|
|
@@ -311,8 +311,8 @@ After dependency or asset changes, run `npm ci`, then
|
|
|
311
311
|
for packages that omit license files, reviewed README excerpts, and asset provenance are recorded in
|
|
312
312
|
`tools/third-party-sources.json` in the source repository. These internal maintenance records
|
|
313
313
|
are not needed in distributions. Generation uses local files and checked-in originals;
|
|
314
|
-
it does not execute package scripts or fetch new legal terms.
|
|
315
|
-
|
|
316
|
-
|
|
314
|
+
it does not execute package scripts or fetch new legal terms. This is a
|
|
315
|
+
maintenance command for dependency, asset, or license changes; ordinary builds,
|
|
316
|
+
publishing, and package-version changes do not require regenerating the inventory.
|
|
317
317
|
|
|
318
318
|
<!-- retiam-third-party-packages: ["@babel/code-frame@7.27.1","@babel/helper-validator-identifier@7.28.5","@dimforge/rapier2d-compat@0.20.0","@dimforge/rapier3d-compat@0.20.0","@petamoriken/float16@3.9.3","@puppeteer/browsers@1.9.1","@tensorflow/tfjs-backend-cpu@4.20.0","@tensorflow/tfjs-backend-webgl@4.20.0","@tensorflow/tfjs-backend-webgpu@4.20.0","@tensorflow/tfjs-converter@4.20.0","@tensorflow/tfjs-core@4.20.0","@tensorflow/tfjs-data@4.20.0","@tensorflow/tfjs-layers@4.20.0","@tensorflow/tfjs@4.20.0","@tootallnate/quickjs-emscripten@0.23.0","@types/dom-webcodecs@0.1.18","@types/estree@1.0.7","@types/libtess@1.2.0","@types/long@4.0.2","@types/node-fetch@2.6.13","@types/node@22.7.4","@types/offscreencanvas@2019.3.0","@types/offscreencanvas@2019.7.3","@types/prismjs@1.26.6","@types/seedrandom@2.4.34","@types/wicg-file-system-access@2020.9.8","@webgpu/types@0.1.38","@webgpu/types@0.1.40","@zappar/msdf-generator@1.2.4","agent-base@6.0.2","agent-base@7.1.3","ansi-regex@5.0.1","ansi-styles@4.3.0","argparse@1.0.10","argparse@2.0.1","as-number@1.0.0","ast-types@0.13.4","asynckit@0.4.0","axios@1.20.0","b4a@1.6.7","base64-js@1.5.1","basic-ftp@5.0.5","box3d-wasm@0.2.0","buffer-crc32@0.2.13","buffer@5.7.1","buffer@6.0.3","call-bind-apply-helpers@1.0.2","callsites@3.1.0","chalk@4.1.2","chromium-bidi@0.5.8","cli-progress@3.12.0","cliui@7.0.4","cliui@8.0.1","color-convert@2.0.1","color-name@1.1.4","combined-stream@1.0.8","comlink@4.4.2","commander@9.2.0","core-js@3.29.1","cosmiconfig@9.0.0","cross-fetch@4.0.0","data-uri-to-buffer@6.0.2","debug@4.3.4","degenerator@5.0.1","delayed-stream@1.0.0","devtools-protocol@0.0.1232444","dunder-proto@1.0.1","earcut@3.0.2","emoji-regex@8.0.0","end-of-stream@1.4.4","env-paths@2.2.1","error-ex@1.3.2","es-define-property@1.0.1","es-errors@1.3.0","es-object-atoms@1.1.2","es-set-tostringtag@2.1.0","esbuild@0.25.5","esbuild@0.28.1","escalade@3.2.0","escodegen@2.1.0","esm@3.2.25","esprima@4.0.1","estraverse@5.3.0","esutils@2.0.3","events@3.3.0","extract-zip@2.0.1","extrude-polyline@1.0.6","fast-fifo@1.3.2","fd-slicer@1.1.0","fdir@6.4.5","fdir@6.5.0","follow-redirects@1.16.0","form-data@4.0.6","function-bind@1.1.2","get-caller-file@2.0.5","get-intrinsic@1.3.0","get-proto@1.0.1","get-stream@5.2.0","get-uri@6.0.4","gl-vec2@1.3.0","gopd@1.2.0","hachure-fill@0.5.2","has-flag@4.0.0","has-symbols@1.1.0","has-tostringtag@1.0.2","hasown@2.0.4","http-proxy-agent@7.0.2","https-proxy-agent@5.0.1","https-proxy-agent@7.0.6","ieee754@1.2.1","import-fresh@3.3.1","inherits@2.0.4","ip-address@9.0.5","is-arrayish@0.2.1","is-fullwidth-code-point@3.0.0","isomorphic-ws@5.0.0","jolt-physics@1.1.0","js-tokens@4.0.0","js-yaml@4.1.0","jsbn@1.1.0","json-parse-even-better-errors@2.3.1","libtess@1.2.2","lines-and-columns@1.2.4","long@4.0.0","lru-cache@7.18.3","math-intrinsics@1.1.0","mathjax-full@3.2.2","matter-js@0.20.0","mhchemparser@4.2.1","mime-db@1.52.0","mime-types@2.1.35","mitt@3.0.1","mj-context-menu@0.6.1","mkdirp-classic@0.5.3","monaco-editor@0.45.0","mp4-muxer@5.2.2","ms@2.1.2","msedge-tts@2.0.8","nanoid@3.3.11","netmask@2.0.2","node-fetch@2.6.13","node-fetch@2.7.0","oidn-web@0.3.5","once@1.4.0","opentype.js@1.3.4","pac-proxy-agent@7.2.0","pac-resolver@7.0.1","pako@1.0.11","parent-module@1.0.1","parse-json@5.2.0","path-data-parser@0.1.0","pend@1.2.0","picocolors@1.1.1","picomatch@4.0.2","picomatch@4.0.3","planck@1.5.0","points-on-curve@0.2.0","points-on-path@0.2.1","polygon-clipping@0.15.7","polyline-miter-util@1.0.1","postcss@8.5.6","prismjs@1.30.0","progress@2.0.3","proxy-agent@6.3.1","proxy-from-env@1.1.0","proxy-from-env@2.1.0","pump@3.0.2","puppeteer-core@21.11.0","puppeteer@21.11.0","readable-stream@3.6.2","regenerator-runtime@0.13.11","require-directory@2.1.1","resolve-from@4.0.0","robust-predicates@3.0.3","rollup@4.41.1","roughjs@4.6.6","safe-buffer@5.2.1","seedrandom@3.0.5","smart-buffer@4.2.0","socks-proxy-agent@8.0.5","socks@2.8.5","source-map-js@1.2.1","source-map@0.6.1","speech-rule-engine@4.0.7","splaytree@3.2.3","sprintf-js@1.0.3","sprintf-js@1.1.3","stage-js@1.0.2","stream-browserify@3.0.0","streamx@2.22.1","string-width@4.2.3","string.prototype.codepointat@0.2.1","string_decoder@1.3.0","strip-ansi@6.0.1","supports-color@7.2.0","svg-parser@2.0.4","svg-pathdata@7.1.0","tar-fs@3.0.4","tar-stream@3.1.7","text-decoder@1.2.3","through@2.3.8","tiny-inflate@1.0.3","tinyglobby@0.2.15","tr46@0.0.3","tslib@2.8.1","tsx@4.22.4","typescript@5.4.5","unbzip2-stream@1.4.3","undici-types@6.19.8","urlpattern-polyfill@10.0.0","utif@2.0.1","util-deprecate@1.0.2","vite@6.3.6","webidl-conversions@3.0.1","webm-muxer@5.1.4","wgsl_reflect@1.2.3","whatwg-url@5.0.0","wicked-good-xpath@1.3.0","woff2-encoder@2.0.0","wrap-ansi@7.0.0","wrappy@1.0.2","ws@8.16.0","ws@8.22.0","xmldom-sre@0.1.31","y18n@5.0.8","yargs-parser@20.2.9","yargs-parser@21.1.1","yargs@16.2.0","yargs@17.7.2","yauzl@2.10.0","yerror@8.0.0"] -->
|