@bitmagic/cli 0.1.60-dev.3 → 0.1.60-dev.5

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.
Files changed (65) hide show
  1. package/README.md +67 -3
  2. package/dist/atomic-json.d.ts +18 -0
  3. package/dist/atomic-json.js +39 -0
  4. package/dist/atomic-json.js.map +1 -0
  5. package/dist/cli.d.ts +1 -1
  6. package/dist/commands/dev.js +79 -35
  7. package/dist/commands/dev.js.map +1 -1
  8. package/dist/commands/publish.d.ts +2 -0
  9. package/dist/commands/publish.js +1 -1
  10. package/dist/commands/publish.js.map +1 -1
  11. package/dist/commands/reload.d.ts +1 -1
  12. package/dist/commands/reload.js +14 -5
  13. package/dist/commands/reload.js.map +1 -1
  14. package/dist/commands/verify.js +134 -118
  15. package/dist/commands/verify.js.map +1 -1
  16. package/dist/editor/motion-panel.d.ts +2 -2
  17. package/dist/editor/motion-panel.js +27 -5
  18. package/dist/editor/motion-panel.js.map +1 -1
  19. package/dist/editor/reload-bus.d.ts +11 -23
  20. package/dist/editor/reload-bus.js +55 -8
  21. package/dist/editor/reload-bus.js.map +1 -1
  22. package/dist/editor/server.d.ts +10 -0
  23. package/dist/editor/server.js +46 -0
  24. package/dist/editor/server.js.map +1 -1
  25. package/dist/editor/shell-page.d.ts +10 -0
  26. package/dist/editor/shell-page.js +58 -3
  27. package/dist/editor/shell-page.js.map +1 -1
  28. package/dist/editor/tab-reuse.d.ts +23 -0
  29. package/dist/editor/tab-reuse.js +61 -0
  30. package/dist/editor/tab-reuse.js.map +1 -0
  31. package/dist/local-port.d.ts +8 -1
  32. package/dist/local-port.js +9 -2
  33. package/dist/local-port.js.map +1 -1
  34. package/dist/machine-lock.d.ts +56 -0
  35. package/dist/machine-lock.js +213 -0
  36. package/dist/machine-lock.js.map +1 -0
  37. package/dist/port-lane.d.ts +27 -0
  38. package/dist/port-lane.js +36 -1
  39. package/dist/port-lane.js.map +1 -1
  40. package/dist/process-alive.d.ts +13 -0
  41. package/dist/process-alive.js +24 -0
  42. package/dist/process-alive.js.map +1 -0
  43. package/dist/project/dev-handle.d.ts +6 -34
  44. package/dist/project/dev-handle.js +9 -59
  45. package/dist/project/dev-handle.js.map +1 -1
  46. package/dist/project/dev-ports.d.ts +67 -0
  47. package/dist/project/dev-ports.js +204 -0
  48. package/dist/project/dev-ports.js.map +1 -0
  49. package/dist/project/game-name.d.ts +9 -0
  50. package/dist/project/game-name.js +21 -0
  51. package/dist/project/game-name.js.map +1 -0
  52. package/dist/project/jobs.js +2 -1
  53. package/dist/project/jobs.js.map +1 -1
  54. package/dist/scaffold/project-files.js +8 -8
  55. package/dist/scaffold/project-files.js.map +1 -1
  56. package/dist/smoke/run.d.ts +4 -0
  57. package/dist/smoke/run.js +5 -2
  58. package/dist/smoke/run.js.map +1 -1
  59. package/dist/verify/iterate.d.ts +5 -0
  60. package/dist/verify/iterate.js +23 -20
  61. package/dist/verify/iterate.js.map +1 -1
  62. package/dist/verify/lock.d.ts +32 -0
  63. package/dist/verify/lock.js +54 -0
  64. package/dist/verify/lock.js.map +1 -0
  65. package/package.json +4 -4
@@ -14,6 +14,7 @@ import { assertServesGame, waitForServer } from '../verify/wait-for-server.js';
14
14
  import { computeFingerprint } from '../publish/fingerprint.js';
15
15
  import { writeVerifyRecord, decideVerifyOutcome, mergeVerifyPlatformRuns, recordAndGateVerify, toVerifyPlatformRun, } from '../verify/result.js';
16
16
  import { runVerifyWatch } from '../verify/iterate.js';
17
+ import { withVerifyLock } from '../verify/lock.js';
17
18
  import { allVerifyScreenshotPaths, verifyArtifactPaths } from '../verify/artifacts.js';
18
19
  import { describeMobileDevice } from '../verify/device.js';
19
20
  import { parsePlatformArg, resolveVerifyPlatforms } from '../verify/platform.js';
@@ -181,131 +182,146 @@ export const verifyCommand = defineCommand({
181
182
  if (!keeping.has(stale))
182
183
  fs.rmSync(stale, { force: true });
183
184
  }
184
- // A free port INSIDE the CORS-allowed window, never an OS-assigned ephemeral one. This matters
185
- // more here than anywhere: verify drives an ORDINARY browser, so an out-of-window origin means
186
- // the CDN refuses the game's forged `.vwld`, the engine builds an empty voxel world, and verify
187
- // then reports a game that loads and renders while the player falls through the floor forever.
188
- // Verify's whole job is to answer "did this break", so it must not be the thing breaking it.
189
- //
190
- // A `bitmagic dev` serving this project holds two ports in this same lane, and the README tells
191
- // creators to leave it open — so its own numbers are the likeliest collision verify has, and
192
- // the lane split only rules out the OTHER release line's dev server, not this one's. Without
193
- // excluding them the only thing between a verify run and the dev server it is testing alongside
194
- // is the scan's random offset and vite's `--strictPort` failing loudly. See local-port.ts.
195
- const dev = readLiveDevHandle(context.root);
196
- const port = await reserveLanePort('Stop whatever is holding them and verify again.', dev === null ? new Set() : new Set([dev.gamePort, dev.editorPort]));
197
- const runSettings = {
198
- port,
199
- renderer,
200
- settleMs,
201
- fast,
202
- orientation: plan.orientation,
203
- };
204
- let server;
205
- try {
206
- server = spawn(vite, ['--port', String(port), '--strictPort'], {
207
- cwd: context.root,
208
- stdio: ['ignore', 'ignore', 'pipe'],
209
- });
210
- let viteStderr = '';
211
- server.stderr?.on('data', (chunk) => {
212
- viteStderr += String(chunk);
213
- });
214
- await Promise.race([
215
- waitForServer(port, 30_000),
216
- watchForEarlyExit(server, () => viteStderr),
217
- ]);
218
- // The race proves something answered — this proves it was OUR vite, not another project's
219
- // dev server sharing the port on the other loopback family. See assertServesGame.
220
- await assertServesGame(port, context.metadata.gameId);
221
- if (watch) {
222
- await runVerifyWatch({
223
- ...runSettings,
224
- root: context.root,
225
- tsc,
226
- artifactDir,
227
- platforms: plan.platforms,
228
- mobilePrimary: plan.mobilePrimary,
229
- output,
230
- describeRunState,
185
+ // Everything from the port on happens INSIDE the verify lock (verify/lock.ts) for a one-shot
186
+ // run: another project's verify measuring fps while this one's Chrome boots would grade the
187
+ // machine, not the game. The build above stays outside — short, and every `bitmagic dev` runs
188
+ // an unlocked `tsc --watch` beside us anyway. `--watch` holds it per iteration instead
189
+ // (verify/iterate.ts), so two watch loops in two projects take turns rather than one waiting
190
+ // for a loop that never ends.
191
+ const serveAndVerify = async () => {
192
+ // A free port INSIDE the CORS-allowed window, never an OS-assigned ephemeral one. This matters
193
+ // more here than anywhere: verify drives an ORDINARY browser, so an out-of-window origin means
194
+ // the CDN refuses the game's forged `.vwld`, the engine builds an empty voxel world, and verify
195
+ // then reports a game that loads and renders while the player falls through the floor forever.
196
+ // Verify's whole job is to answer "did this break", so it must not be the thing breaking it.
197
+ //
198
+ // A `bitmagic dev` serving this project holds two ports in this same lane, and the README tells
199
+ // creators to leave it open — so its own numbers are the likeliest collision verify has, and
200
+ // the lane split only rules out the OTHER release line's dev server, not this one's. Without
201
+ // excluding them the only thing between a verify run and the dev server it is testing alongside
202
+ // is the scan's random offset and vite's `--strictPort` failing loudly. See local-port.ts.
203
+ const dev = readLiveDevHandle(context.root);
204
+ const port = await reserveLanePort('Stop whatever is holding them and verify again.', dev === null ? new Set() : new Set([dev.gamePort, dev.editorPort]));
205
+ const runSettings = {
206
+ port,
207
+ renderer,
208
+ settleMs,
209
+ fast,
210
+ orientation: plan.orientation,
211
+ };
212
+ let server;
213
+ try {
214
+ server = spawn(vite, ['--port', String(port), '--strictPort'], {
215
+ cwd: context.root,
216
+ stdio: ['ignore', 'ignore', 'pipe'],
231
217
  });
232
- return;
233
- }
234
- const runs = [];
235
- const artifacts = [];
236
- // The parity verdict is about the game's CODE, not about the emulated device, so a `both`
237
- // run reports it once. The lead platform runs first, so its verdict is the one kept.
238
- let mobileParity;
239
- for (const platform of plan.platforms) {
240
- const paths = verifyArtifactPaths(artifactDir, platform);
241
- const checkOptions = verifyCheckOptions(runSettings, platform, paths);
242
- let observations = await runHeadlessCheck({
243
- ...checkOptions,
244
- launch: browserLaunchPlan(process.env, renderer),
218
+ let viteStderr = '';
219
+ server.stderr?.on('data', (chunk) => {
220
+ viteStderr += String(chunk);
245
221
  });
246
- let result = classifyVerifyRun(observations, { platform, mobilePrimary: plan.mobilePrimary });
247
- let retriedWithSoftwareGl = false;
248
- if (!result.ok && shouldRetrySoftwareGl(observations, process.env)) {
249
- // A lost context is a machine/driver problem, not a game problem (browser-gl.ts) — one
250
- // automatic software-GL re-run answers "is the game broken or is this GPU stack broken"
251
- // without the creator having to know the env var exists. The retry's console goes to
252
- // its own file so the first run's evidence survives.
253
- output.info('The WebGL context was lost — retrying once with software rendering…');
254
- observations = await runHeadlessCheck({
222
+ await Promise.race([
223
+ waitForServer(port, 30_000),
224
+ watchForEarlyExit(server, () => viteStderr),
225
+ ]);
226
+ // The race proves something answered — this proves it was OUR vite, not another project's
227
+ // dev server sharing the port on the other loopback family. See assertServesGame.
228
+ await assertServesGame(port, context.metadata.gameId);
229
+ if (watch) {
230
+ await runVerifyWatch({
231
+ ...runSettings,
232
+ root: context.root,
233
+ tsc,
234
+ artifactDir,
235
+ platforms: plan.platforms,
236
+ mobilePrimary: plan.mobilePrimary,
237
+ output,
238
+ describeRunState,
239
+ lock: (fn) => withVerifyLock(context.root, 'verify --watch', output, fn),
240
+ });
241
+ return;
242
+ }
243
+ const runs = [];
244
+ const artifacts = [];
245
+ // The parity verdict is about the game's CODE, not about the emulated device, so a `both`
246
+ // run reports it once. The lead platform runs first, so its verdict is the one kept.
247
+ let mobileParity;
248
+ for (const platform of plan.platforms) {
249
+ const paths = verifyArtifactPaths(artifactDir, platform);
250
+ const checkOptions = verifyCheckOptions(runSettings, platform, paths);
251
+ let observations = await runHeadlessCheck({
255
252
  ...checkOptions,
256
- consolePath: paths.retryConsolePath,
257
- launch: softwareGlLaunchPlan(process.env, renderer),
253
+ launch: browserLaunchPlan(process.env, renderer),
258
254
  });
259
- result = classifyVerifyRun(observations, { platform, mobilePrimary: plan.mobilePrimary });
260
- retriedWithSoftwareGl = true;
255
+ let result = classifyVerifyRun(observations, { platform, mobilePrimary: plan.mobilePrimary });
256
+ let retriedWithSoftwareGl = false;
257
+ if (!result.ok && shouldRetrySoftwareGl(observations, process.env)) {
258
+ // A lost context is a machine/driver problem, not a game problem (browser-gl.ts) — one
259
+ // automatic software-GL re-run answers "is the game broken or is this GPU stack broken"
260
+ // without the creator having to know the env var exists. The retry's console goes to
261
+ // its own file so the first run's evidence survives.
262
+ output.info('The WebGL context was lost — retrying once with software rendering…');
263
+ observations = await runHeadlessCheck({
264
+ ...checkOptions,
265
+ consolePath: paths.retryConsolePath,
266
+ launch: softwareGlLaunchPlan(process.env, renderer),
267
+ });
268
+ result = classifyVerifyRun(observations, { platform, mobilePrimary: plan.mobilePrimary });
269
+ retriedWithSoftwareGl = true;
270
+ }
271
+ const observedBackend = observations.renderer?.backend ?? 'unobserved';
272
+ const label = plan.platforms.length > 1 ? `${platform}: ` : '';
273
+ output.info(`${label}Renderer: ${renderer} requested → ${observedBackend} backend`);
274
+ output.info(`${label}${describeRunState(observations)}`);
275
+ if (mobileParity === undefined)
276
+ mobileParity = observations.mobileParity;
277
+ runs.push(toVerifyPlatformRun(platform, result, observations, retriedWithSoftwareGl, paths, fast));
278
+ artifacts.push(paths.consolePath);
279
+ if (!fast)
280
+ artifacts.push(paths.screenshotPath);
261
281
  }
262
- const observedBackend = observations.renderer?.backend ?? 'unobserved';
263
- const label = plan.platforms.length > 1 ? `${platform}: ` : '';
264
- output.info(`${label}Renderer: ${renderer} requested → ${observedBackend} backend`);
265
- output.info(`${label}${describeRunState(observations)}`);
266
- if (mobileParity === undefined)
267
- mobileParity = observations.mobileParity;
268
- runs.push(toVerifyPlatformRun(platform, result, observations, retriedWithSoftwareGl, paths, fast));
269
- artifacts.push(paths.consolePath);
270
- if (!fast)
271
- artifacts.push(paths.screenshotPath);
282
+ const merged = mergeVerifyPlatformRuns(runs, mobileParity);
283
+ const outcome = decideVerifyOutcome(merged.result, computeFingerprint(context.root), new Date().toISOString(), merged.extras);
284
+ recordAndGateVerify(artifactDir, outcome, artifacts, {
285
+ write: writeVerifyRecord,
286
+ // Under --json both streams are prose and belong on stderr; the result document below
287
+ // is the only thing stdout carries.
288
+ info: (line) => output.info(line),
289
+ error: (line) => console.error(line),
290
+ });
291
+ // Only reached when the run PASSED — recordAndGateVerify throws on failure, and the
292
+ // wrapper then emits the error document. Both paths leave stdout parseable.
293
+ // The result document keeps reporting ABSOLUTE paths, unchanged: it is consumed in the
294
+ // moment by whatever ran the command, unlike `result.json`, which persists and therefore
295
+ // records file names only.
296
+ const leadPaths = verifyArtifactPaths(artifactDir, runs[0].platform);
297
+ output.result({
298
+ ok: true,
299
+ failures: outcome.record.failures,
300
+ warnings: outcome.record.warnings,
301
+ fingerprint: outcome.record.fingerprint,
302
+ platform: outcome.record.platform,
303
+ touchControls: outcome.record.touchControls,
304
+ platforms: outcome.record.platforms,
305
+ mobileParity: outcome.record.mobileParity,
306
+ renderer: outcome.record.renderer,
307
+ retriedWithSoftwareGl: outcome.record.retriedWithSoftwareGl,
308
+ settle: outcome.record.settle,
309
+ snapshot: outcome.record.snapshot,
310
+ events: outcome.record.events,
311
+ artifactDir,
312
+ consolePath: leadPaths.consolePath,
313
+ screenshotPath: fast ? undefined : leadPaths.screenshotPath,
314
+ });
272
315
  }
273
- const merged = mergeVerifyPlatformRuns(runs, mobileParity);
274
- const outcome = decideVerifyOutcome(merged.result, computeFingerprint(context.root), new Date().toISOString(), merged.extras);
275
- recordAndGateVerify(artifactDir, outcome, artifacts, {
276
- write: writeVerifyRecord,
277
- // Under --json both streams are prose and belong on stderr; the result document below
278
- // is the only thing stdout carries.
279
- info: (line) => output.info(line),
280
- error: (line) => console.error(line),
281
- });
282
- // Only reached when the run PASSED — recordAndGateVerify throws on failure, and the
283
- // wrapper then emits the error document. Both paths leave stdout parseable.
284
- // The result document keeps reporting ABSOLUTE paths, unchanged: it is consumed in the
285
- // moment by whatever ran the command, unlike `result.json`, which persists and therefore
286
- // records file names only.
287
- const leadPaths = verifyArtifactPaths(artifactDir, runs[0].platform);
288
- output.result({
289
- ok: true,
290
- failures: outcome.record.failures,
291
- warnings: outcome.record.warnings,
292
- fingerprint: outcome.record.fingerprint,
293
- platform: outcome.record.platform,
294
- touchControls: outcome.record.touchControls,
295
- platforms: outcome.record.platforms,
296
- mobileParity: outcome.record.mobileParity,
297
- renderer: outcome.record.renderer,
298
- retriedWithSoftwareGl: outcome.record.retriedWithSoftwareGl,
299
- settle: outcome.record.settle,
300
- snapshot: outcome.record.snapshot,
301
- events: outcome.record.events,
302
- artifactDir,
303
- consolePath: leadPaths.consolePath,
304
- screenshotPath: fast ? undefined : leadPaths.screenshotPath,
305
- });
316
+ finally {
317
+ server?.kill('SIGTERM');
318
+ }
319
+ };
320
+ if (watch) {
321
+ await serveAndVerify();
306
322
  }
307
- finally {
308
- server?.kill('SIGTERM');
323
+ else {
324
+ await withVerifyLock(context.root, 'verify', output, serveAndVerify);
309
325
  }
310
326
  },
311
327
  });
@@ -1 +1 @@
1
- {"version":3,"file":"verify.js","sourceRoot":"","sources":["../../src/commands/verify.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"verify.js","sourceRoot":"","sources":["../../src/commands/verify.ts"],"names":[],"mappings":"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"}
@@ -12,9 +12,9 @@
12
12
  * dot-escape, every colour a `--bm-*` token — `shell-page.test.ts` enforces all three.
13
13
  */
14
14
  export declare const MOTION_PANEL_CSS = "\n /* Level with the other dialogs: each opens from its own control and they never stack. */\n #motion-overlay { z-index: 10; }\n #motion-dialog { width: min(520px, 94vw); padding: 20px; }\n #motion-dialog .bm-label { display: block; margin: 14px 0 6px; }\n #motion-stage { position: relative; aspect-ratio: 16 / 9; border-radius: 8px; overflow: hidden;\n background: var(--bm-surface-alt); border: 1px solid var(--bm-border-muted); }\n #motion-preview { width: 100%; height: 100%; object-fit: contain; background: #000000; display: block; }\n #motion-preview.mirror { transform: scaleX(-1); }\n #motion-timer { position: absolute; top: 10px; left: 10px; display: none; padding: 3px 10px;\n border-radius: 999px; background: var(--bm-pink); color: #ffffff; font-size: 12px;\n font-family: var(--bm-font-mono); }\n #motion-timer.show { display: inline-block; }\n #motion-countdown { position: absolute; inset: 0; display: none; flex-direction: column; align-items: center;\n justify-content: center; gap: 6px; background: var(--bm-scrim); pointer-events: none; }\n #motion-countdown.show { display: flex; }\n #motion-countdown-number { font-size: 96px; font-weight: 700; line-height: 1; color: #ffffff; }\n #motion-countdown-hint { font-size: 13px; color: #ffffff; opacity: 0.85; }\n #motion-hint { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;\n text-align: center; padding: 20px; font-size: 13px; color: var(--bm-text-muted); }\n #motion-hint.hidden { display: none; }\n #motion-trim { display: none; margin-top: 10px; }\n #motion-trim.show { display: block; }\n #motion-trimbar { position: relative; height: 14px; border-radius: 7px; background: var(--bm-surface-alt);\n touch-action: none; }\n #motion-trim-kept { position: absolute; top: 0; bottom: 0; border-radius: 7px; background: var(--bm-aqua); }\n #motion-trim-playhead { position: absolute; top: 0; bottom: 0; width: 2px; background: var(--bm-pink);\n pointer-events: none; }\n .motion-trim-handle { position: absolute; top: 50%; width: 14px; height: 22px;\n transform: translate(-50%, -50%); border-radius: 5px; background: var(--bm-text);\n cursor: ew-resize; touch-action: none; }\n #motion-trim-row { display: flex; align-items: center; justify-content: space-between; gap: 8px;\n margin-top: 6px; min-height: 24px; }\n #motion-trim-note { font-size: 11px; color: var(--bm-text-muted); }\n #motion-trim-actions { display: flex; gap: 6px; }\n #motion-sources { display: flex; gap: 8px; margin-top: 10px; flex-wrap: wrap; }\n #motion-file { display: none; }\n #motion-guidance { margin: 10px 0 0; font-size: 12px; color: var(--bm-text-muted); }\n #motion-options { display: grid; grid-template-columns: 1fr auto; gap: 10px 12px; align-items: end; }\n #motion-name, #motion-slot { width: 100%; }\n .motion-check { display: flex; align-items: center; gap: 8px; margin-top: 12px; font-size: 13px; cursor: pointer; }\n .motion-check input { accent-color: var(--bm-aqua); }\n #motion-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; }\n #motion-note { display: none; margin-top: 12px; padding: 12px; border-radius: 6px; font-size: 12px;\n background: var(--bm-surface-alt); border: 1px solid var(--bm-border-muted);\n color: var(--bm-text-muted); }\n #motion-note.show { display: block; }\n #motion-note code { display: block; margin-top: 6px; color: var(--bm-aqua);\n font-family: var(--bm-font-mono); word-break: break-all; }\n";
15
- export declare const MOTION_PANEL_HTML = " <div id=\"motion-overlay\" class=\"bm-overlay\">\n <div id=\"motion-dialog\" class=\"bm-dialog\">\n <h2>Record a motion</h2>\n <p>Perform the move on camera, or pick a video. Uthana turns it into an animation clip for\n this game's characters. Costs sparks.</p>\n <div id=\"motion-stage\">\n <video id=\"motion-preview\" playsinline muted></video>\n <span id=\"motion-timer\">0:00</span>\n <div id=\"motion-countdown\"><span id=\"motion-countdown-number\">3</span><span id=\"motion-countdown-hint\">Get in frame</span></div>\n <div id=\"motion-hint\">Whole body in frame, one person, plain background, good light.\n 2&ndash;60 seconds.</div>\n </div>\n <div id=\"motion-trim\">\n <div id=\"motion-trimbar\" title=\"Drag the handles to choose what the animation is made from\">\n <div id=\"motion-trim-kept\"></div>\n <div id=\"motion-trim-playhead\"></div>\n <div id=\"motion-trim-start\" class=\"motion-trim-handle\"></div>\n <div id=\"motion-trim-end\" class=\"motion-trim-handle\"></div>\n </div>\n <div id=\"motion-trim-row\">\n <span id=\"motion-trim-note\"></span>\n <span id=\"motion-trim-actions\">\n <button id=\"motion-trim-preview\" class=\"bm-pill\" type=\"button\">&#9654; Play</button>\n <button id=\"motion-trim-full\" class=\"bm-pill bm-pill--ghost\" type=\"button\" hidden>Use full clip</button>\n </span>\n </div>\n </div>\n <div id=\"motion-sources\">\n <button id=\"motion-record\" class=\"bm-pill bm-pill--primary\" type=\"button\">Record with webcam</button>\n <button id=\"motion-stop\" class=\"bm-pill\" type=\"button\" hidden>Stop</button>\n <button id=\"motion-pick\" class=\"bm-pill\" type=\"button\">Choose a video&hellip;</button>\n <button id=\"motion-retake\" class=\"bm-pill bm-pill--ghost\" type=\"button\" hidden>Start over</button>\n <input id=\"motion-file\" type=\"file\" accept=\".mp4,.mov,.avi,.webm,video/mp4,video/quicktime,video/webm\">\n </div>\n <p id=\"motion-guidance\"></p>\n <div id=\"motion-options\">\n <div>\n <label class=\"bm-label\" for=\"motion-name\">Name</label>\n <input id=\"motion-name\" class=\"bm-input\" type=\"text\" placeholder=\"e.g. Victory dance\" maxlength=\"80\">\n </div>\n <div>\n <label class=\"bm-label\" for=\"motion-slot\">Use as</label>\n <select id=\"motion-slot\" class=\"bm-input\">\n <option value=\"\">One-shot clip</option>\n <option value=\"idle\">Idle</option>\n <option value=\"walk\">Walk</option>\n <option value=\"run\">Run</option>\n </select>\n </div>\n </div>\n <label class=\"motion-check\"><input id=\"motion-loop\" type=\"checkbox\"> Loops</label>\n <div id=\"motion-actions\">\n <button id=\"motion-close\" class=\"bm-pill bm-pill--ghost\" type=\"button\">Close</button>\n <button id=\"motion-submit\" class=\"bm-pill bm-pill--primary\" type=\"button\" disabled>Make the animation</button>\n </div>\n <div id=\"motion-note\">\n <span id=\"motion-note-text\"></span>\n <code id=\"motion-command\"></code>\n </div>\n </div>\n </div>\n";
15
+ export declare const MOTION_PANEL_HTML = " <div id=\"motion-overlay\" class=\"bm-overlay\">\n <div id=\"motion-dialog\" class=\"bm-dialog\">\n <h2>Record a motion</h2>\n <p>Perform the move on camera, or pick a video. Uthana turns it into an animation clip for\n this game's characters. Costs sparks.</p>\n <div id=\"motion-stage\">\n <video id=\"motion-preview\" playsinline muted></video>\n <span id=\"motion-timer\">0:00</span>\n <div id=\"motion-countdown\"><span id=\"motion-countdown-number\">3</span><span id=\"motion-countdown-hint\">Get in frame</span></div>\n <div id=\"motion-hint\">Whole body in frame, one person, plain background, good light.\n 2&ndash;60 seconds.</div>\n </div>\n <div id=\"motion-trim\">\n <div id=\"motion-trimbar\" title=\"Drag the handles to choose what the animation is made from\">\n <div id=\"motion-trim-kept\"></div>\n <div id=\"motion-trim-playhead\"></div>\n <div id=\"motion-trim-start\" class=\"motion-trim-handle\"></div>\n <div id=\"motion-trim-end\" class=\"motion-trim-handle\"></div>\n </div>\n <div id=\"motion-trim-row\">\n <span id=\"motion-trim-note\"></span>\n <span id=\"motion-trim-actions\">\n <button id=\"motion-trim-preview\" class=\"bm-pill\" type=\"button\">&#9654; Play</button>\n <button id=\"motion-trim-full\" class=\"bm-pill bm-pill--ghost\" type=\"button\" hidden>Use full clip</button>\n </span>\n </div>\n </div>\n <div id=\"motion-sources\">\n <button id=\"motion-record\" class=\"bm-pill bm-pill--primary\" type=\"button\">Record with webcam</button>\n <button id=\"motion-start\" class=\"bm-pill bm-pill--primary\" type=\"button\" hidden>Start recording</button>\n <button id=\"motion-stop\" class=\"bm-pill\" type=\"button\" hidden>Stop</button>\n <button id=\"motion-pick\" class=\"bm-pill\" type=\"button\">Choose a video&hellip;</button>\n <button id=\"motion-retake\" class=\"bm-pill bm-pill--ghost\" type=\"button\" hidden>Start over</button>\n <input id=\"motion-file\" type=\"file\" accept=\".mp4,.mov,.avi,.webm,video/mp4,video/quicktime,video/webm\">\n </div>\n <p id=\"motion-guidance\"></p>\n <div id=\"motion-options\">\n <div>\n <label class=\"bm-label\" for=\"motion-name\">Name</label>\n <input id=\"motion-name\" class=\"bm-input\" type=\"text\" placeholder=\"e.g. Victory dance\" maxlength=\"80\">\n </div>\n <div>\n <label class=\"bm-label\" for=\"motion-slot\">Use as</label>\n <select id=\"motion-slot\" class=\"bm-input\">\n <option value=\"\">One-shot clip</option>\n <option value=\"idle\">Idle</option>\n <option value=\"walk\">Walk</option>\n <option value=\"run\">Run</option>\n </select>\n </div>\n </div>\n <label class=\"motion-check\"><input id=\"motion-loop\" type=\"checkbox\"> Loops</label>\n <div id=\"motion-actions\">\n <button id=\"motion-close\" class=\"bm-pill bm-pill--ghost\" type=\"button\">Close</button>\n <button id=\"motion-submit\" class=\"bm-pill bm-pill--primary\" type=\"button\" disabled>Make the animation</button>\n </div>\n <div id=\"motion-note\">\n <span id=\"motion-note-text\"></span>\n <code id=\"motion-command\"></code>\n </div>\n </div>\n </div>\n";
16
16
  /**
17
17
  * Behaviour, spliced into the shell's IIFE. Runs after the shell's own helpers are defined, so
18
18
  * `setStatus`, `start`, `autoReload`, `banner` and `frame` are in scope.
19
19
  */
20
- export declare const MOTION_PANEL_JS = "\n // ---- Record motion: webcam or file -> Uthana video-to-motion -> assets[] ----------------------\n var motionToggle = document.getElementById('motion');\n var motionOverlay = document.getElementById('motion-overlay');\n var motionPreview = document.getElementById('motion-preview');\n var motionTimer = document.getElementById('motion-timer');\n var motionCountdown = document.getElementById('motion-countdown');\n var motionCountdownNumber = document.getElementById('motion-countdown-number');\n /** Seconds shown on the live preview before the recorder starts, so the first frames are the pose. */\n var MOTION_COUNTDOWN_S = 3;\n var motionCountdownToken = 0;\n var motionHint = document.getElementById('motion-hint');\n var motionRecordBtn = document.getElementById('motion-record');\n var motionStopBtn = document.getElementById('motion-stop');\n var motionPickBtn = document.getElementById('motion-pick');\n var motionRetakeBtn = document.getElementById('motion-retake');\n var motionFile = document.getElementById('motion-file');\n var motionGuidance = document.getElementById('motion-guidance');\n var motionName = document.getElementById('motion-name');\n var motionSlot = document.getElementById('motion-slot');\n var motionLoop = document.getElementById('motion-loop');\n var motionSubmit = document.getElementById('motion-submit');\n var motionNote = document.getElementById('motion-note');\n var MOTION_MIN_S = 2;\n var MOTION_MAX_S = 60;\n var MOTION_MAX_BYTES = 200 * 1024 * 1024;\n var motionBlob = null;\n // Whether motionBlob came from the webcam recorder (auto-trim applies) or a picked file (it does not).\n var motionFromRecording = false;\n var motionSeconds = 0;\n var motionStream = null;\n var motionRecorder = null;\n var motionChunks = [];\n var motionTickTimer = null;\n var motionStartedAt = 0;\n var motionObjectUrl = null;\n // ---- Trim window: seeded by the sidecar's trim analysis, adjusted on the handles ------------\n var motionTrimWrap = document.getElementById('motion-trim');\n var motionTrimBar = document.getElementById('motion-trimbar');\n var motionTrimKept = document.getElementById('motion-trim-kept');\n var motionTrimPlayhead = document.getElementById('motion-trim-playhead');\n var motionTrimStartHandle = document.getElementById('motion-trim-start');\n var motionTrimEndHandle = document.getElementById('motion-trim-end');\n var motionTrimNote = document.getElementById('motion-trim-note');\n var motionTrimFullBtn = document.getElementById('motion-trim-full');\n var motionTrimPreviewBtn = document.getElementById('motion-trim-preview');\n // True while \"Preview the kept range\" is playing trimStart -> trimEnd.\n var motionPreviewingTrim = false;\n var MOTION_TRIM_MIN_KEPT_S = 2;\n var motionTrimDuration = 0;\n var motionTrimStart = 0;\n var motionTrimEnd = 0;\n var motionPendingId = null;\n var motionAnalysisState = 'idle';\n var motionAnalysisToken = 0;\n var motionAnalyzePollTimer = null;\n\n function motionHasTrim() {\n return motionTrimDuration > 0 && motionTrimEnd > motionTrimStart\n && (motionTrimStart >= 0.1 || motionTrimDuration - motionTrimEnd >= 0.1);\n }\n\n function renderMotionTrim() {\n if (motionTrimDuration <= 0) { motionTrimWrap.classList.remove('show'); return; }\n motionTrimWrap.classList.add('show');\n var pct = function (seconds) {\n return Math.min(Math.max(seconds / motionTrimDuration, 0), 1) * 100 + '%';\n };\n motionTrimKept.style.left = pct(motionTrimStart);\n motionTrimKept.style.width = Math.max(0, (motionTrimEnd - motionTrimStart) / motionTrimDuration) * 100 + '%';\n motionTrimStartHandle.style.left = pct(motionTrimStart);\n motionTrimEndHandle.style.left = pct(motionTrimEnd);\n motionTrimFullBtn.hidden = !motionHasTrim();\n motionTrimPreviewBtn.textContent = motionPreviewingTrim\n ? '\u23F9 Stop'\n : (motionHasTrim() ? '\u25B6 Preview the kept range' : '\u25B6 Play');\n var note;\n if (motionAnalysisState === 'running') {\n note = 'Analyzing the clip for a good trim\u2026';\n } else if (!motionHasTrim()) {\n note = 'Using the full clip.';\n } else {\n var parts = [];\n if (motionTrimStart >= 0.1) parts.push(motionTrimStart.toFixed(1) + 's from the start');\n if (motionTrimDuration - motionTrimEnd >= 0.1) parts.push((motionTrimDuration - motionTrimEnd).toFixed(1) + 's from the end');\n note = 'Trimming ' + parts.join(' and ') + '.';\n }\n motionTrimNote.textContent = note;\n }\n\n function resetMotionTrim() {\n motionPreviewingTrim = false;\n motionAnalysisToken++;\n if (motionAnalyzePollTimer) { clearTimeout(motionAnalyzePollTimer); motionAnalyzePollTimer = null; }\n motionPendingId = null;\n motionAnalysisState = 'idle';\n motionTrimDuration = 0;\n motionTrimStart = 0;\n motionTrimEnd = 0;\n motionTrimWrap.classList.remove('show');\n }\n\n function seedMotionTrim(durationSeconds) {\n if (!(durationSeconds > 0)) return;\n motionTrimDuration = durationSeconds;\n motionTrimStart = 0;\n motionTrimEnd = durationSeconds;\n // Review playback is the trim: the Play/Preview button below drives the\n // video, and a native full-clip scrubber under a trim bar read as \"it\n // plays the whole video anyway\". Controls stay native only while the\n // duration is unknown and the bar cannot render.\n motionPreview.controls = false;\n renderMotionTrim();\n }\n\n function applyMotionSuggestion(suggestion) {\n if (!suggestion) { renderMotionTrim(); return; }\n if (suggestion.durationSeconds > 0) {\n motionTrimDuration = suggestion.durationSeconds;\n motionTrimStart = 0;\n motionTrimEnd = suggestion.durationSeconds;\n }\n if (suggestion.confidence === 'high'\n && typeof suggestion.suggestedStartSeconds === 'number'\n && typeof suggestion.suggestedEndSeconds === 'number'\n && suggestion.suggestedEndSeconds > suggestion.suggestedStartSeconds) {\n motionTrimStart = suggestion.suggestedStartSeconds;\n motionTrimEnd = Math.min(suggestion.suggestedEndSeconds, motionTrimDuration || suggestion.suggestedEndSeconds);\n }\n renderMotionTrim();\n }\n\n function beginMotionTrimDrag(which, event) {\n if (motionTrimDuration <= 0) return;\n var handle = event.currentTarget;\n handle.setPointerCapture(event.pointerId);\n var move = function (e) {\n var rect = motionTrimBar.getBoundingClientRect();\n if (rect.width === 0) return;\n var at = Math.min(Math.max((e.clientX - rect.left) / rect.width, 0), 1) * motionTrimDuration;\n if (which === 'start') motionTrimStart = Math.min(at, motionTrimEnd - MOTION_TRIM_MIN_KEPT_S);\n else motionTrimEnd = Math.max(at, motionTrimStart + MOTION_TRIM_MIN_KEPT_S);\n // Show the exact frame the cut lands on while the handle moves.\n motionPreview.pause();\n motionPreviewingTrim = false;\n motionPreview.currentTime = which === 'start' ? motionTrimStart : motionTrimEnd;\n renderMotionTrim();\n };\n var up = function () {\n handle.removeEventListener('pointermove', move);\n handle.removeEventListener('pointerup', up);\n handle.removeEventListener('pointercancel', up);\n };\n handle.addEventListener('pointermove', move);\n handle.addEventListener('pointerup', up);\n handle.addEventListener('pointercancel', up);\n event.preventDefault();\n }\n\n function pollMotionAnalysis(token) {\n var tick = async function () {\n if (token !== motionAnalysisToken) return;\n var payload;\n try {\n payload = await (await fetch('/api/editor/motion-analyze-status')).json();\n } catch (error) {\n motionAnalyzePollTimer = setTimeout(tick, 2000);\n return;\n }\n if (token !== motionAnalysisToken) return;\n if (payload && payload.status === 'done') {\n motionAnalysisState = 'done';\n applyMotionSuggestion(payload.suggestion);\n return;\n }\n if (payload && payload.status === 'running') {\n motionAnalyzePollTimer = setTimeout(tick, 2000);\n return;\n }\n // failed / unavailable / none: no suggestion, the full-range handles stand.\n motionAnalysisState = 'failed';\n renderMotionTrim();\n };\n motionAnalyzePollTimer = setTimeout(tick, 2000);\n }\n\n /**\n * Hand the clip to the sidecar ahead of submission: it uploads once and runs the unbilled trim\n * analysis while the creator reviews. Degraded, never blocking \u2014 any failure just leaves the\n * handles at full range, and Submit works throughout.\n */\n async function startMotionAnalysis(blob) {\n var token = ++motionAnalysisToken;\n motionPendingId = null;\n motionAnalysisState = 'running';\n renderMotionTrim();\n var result = null;\n try {\n var response = await fetch('/api/editor/motion-analyze', {\n method: 'POST',\n headers: { 'Content-Type': blob.type || 'video/webm' },\n body: blob\n });\n result = await response.json();\n } catch (error) { /* the sidecar said nothing; treat as unavailable */ }\n if (token !== motionAnalysisToken) return;\n if (!result || result.ok !== true || result.analysing !== true) {\n motionAnalysisState = 'idle';\n renderMotionTrim();\n return;\n }\n motionPendingId = result.pendingId || null;\n pollMotionAnalysis(token);\n }\n\n function motionSupported() {\n return !!(window.isSecureContext && navigator.mediaDevices && navigator.mediaDevices.getUserMedia\n && typeof MediaRecorder !== 'undefined');\n }\n\n /** mp4 first (Safari), then the WebM the rest produce; asset-forger re-encodes either. */\n function pickMotionMime() {\n var candidates = ['video/mp4', 'video/webm;codecs=vp9', 'video/webm'];\n for (var i = 0; i < candidates.length; i++) {\n if (MediaRecorder.isTypeSupported(candidates[i])) return candidates[i];\n }\n return '';\n }\n\n function motionCameraError(error) {\n var name = error && error.name;\n if (name === 'NotAllowedError' || name === 'SecurityError') return 'Camera access was refused. Allow it in the address bar, or choose a video file.';\n if (name === 'NotFoundError' || name === 'OverconstrainedError') return 'No camera found. Choose a video file instead.';\n if (name === 'NotReadableError') return 'The camera is in use by another app. Close it, or choose a video file.';\n return 'Could not start the camera: ' + ((error && error.message) || 'unknown error');\n }\n\n function formatMotionClock(seconds) {\n var s = Math.max(0, Math.floor(seconds));\n var rest = s % 60;\n return Math.floor(s / 60) + ':' + (rest < 10 ? '0' : '') + rest;\n }\n\n function showMotionNote(text, command) {\n document.getElementById('motion-note-text').textContent = text;\n document.getElementById('motion-command').textContent = command || '';\n motionNote.classList.add('show');\n }\n\n function releaseMotionObjectUrl() {\n if (motionObjectUrl) { URL.revokeObjectURL(motionObjectUrl); motionObjectUrl = null; }\n }\n\n /** Stop the recorder if one is still live \u2014 it may already have stopped itself. */\n function discardMotionRecorder() {\n if (motionRecorder && motionRecorder.state !== 'inactive') {\n try { motionRecorder.stop(); } catch (error) { /* already stopped */ }\n }\n }\n\n function stopMotionStream() {\n if (motionStream) {\n motionStream.getTracks().forEach(function (track) { track.stop(); });\n motionStream = null;\n }\n if (motionTickTimer) { clearInterval(motionTickTimer); motionTickTimer = null; }\n motionCountdownToken++;\n motionCountdown.classList.remove('show');\n motionTimer.classList.remove('show');\n motionPreview.classList.remove('mirror');\n }\n\n function setMotionButtons(state) {\n motionRecordBtn.hidden = state !== 'idle';\n motionPickBtn.hidden = state !== 'idle';\n motionStopBtn.hidden = state !== 'recording';\n motionRetakeBtn.hidden = state !== 'ready';\n motionSubmit.disabled = state !== 'ready';\n }\n\n function resetMotionPanel() {\n resetMotionTrim();\n stopMotionStream();\n discardMotionRecorder();\n motionRecorder = null;\n motionChunks = [];\n motionBlob = null;\n motionFromRecording = false;\n motionSeconds = 0;\n releaseMotionObjectUrl();\n motionPreview.srcObject = null;\n motionPreview.removeAttribute('src');\n motionPreview.controls = false;\n motionPreview.muted = true;\n motionHint.classList.remove('hidden');\n var supported = motionSupported();\n motionGuidance.textContent = supported ? '' : 'Webcam recording needs a secure page with a camera; choose a video file instead.';\n motionRecordBtn.disabled = !supported;\n motionNote.classList.remove('show');\n setMotionButtons('idle');\n }\n\n function openMotionPanel() {\n resetMotionPanel();\n motionOverlay.classList.add('show');\n motionName.focus();\n }\n\n /** Returns whether it was open \u2014 the Escape handler's contract, like closeAssetDetail. */\n function closeMotionPanel() {\n if (!motionOverlay.classList.contains('show')) return false;\n stopMotionStream();\n discardMotionRecorder();\n motionOverlay.classList.remove('show');\n frame.focus();\n return true;\n }\n\n /** The clip is in hand: show it back, and say whether it is within Uthana's 2-60 s window. */\n function acceptMotionClip(blob, seconds) {\n motionBlob = blob;\n motionSeconds = seconds;\n releaseMotionObjectUrl();\n motionObjectUrl = URL.createObjectURL(blob);\n motionPreview.srcObject = null;\n motionPreview.src = motionObjectUrl;\n motionPreview.controls = true;\n motionPreview.muted = true;\n motionHint.classList.add('hidden');\n var note;\n if (blob.size > MOTION_MAX_BYTES) note = 'That video is over 200 MB. Trim it to the seconds that matter.';\n else if (seconds > 0 && seconds < MOTION_MIN_S) note = 'Too short: Uthana needs at least ' + MOTION_MIN_S + ' seconds.';\n else if (seconds > MOTION_MAX_S) note = 'Too long: Uthana takes at most ' + MOTION_MAX_S + ' seconds. Trim it, or record again.';\n var ok = !note;\n motionGuidance.textContent = ok\n ? (seconds > 0 ? formatMotionClock(seconds) + ' of video ready.' : 'Video ready.')\n : note;\n setMotionButtons('ready');\n motionSubmit.disabled = !ok;\n resetMotionTrim();\n if (ok) {\n seedMotionTrim(seconds);\n // A fresh WebM often reports no duration; the preview's metadata is the fallback scale.\n motionPreview.onloadedmetadata = function () {\n if (motionTrimDuration === 0 && isFinite(motionPreview.duration) && motionPreview.duration > 0) {\n seedMotionTrim(motionPreview.duration);\n }\n };\n void startMotionAnalysis(blob);\n }\n }\n\n async function startMotionRecording() {\n if (!motionSupported()) return;\n motionNote.classList.remove('show');\n try {\n motionStream = await navigator.mediaDevices.getUserMedia({\n video: { width: { ideal: 1280 }, height: { ideal: 720 }, facingMode: 'user' }, audio: false\n });\n } catch (error) {\n motionGuidance.textContent = motionCameraError(error);\n return;\n }\n releaseMotionObjectUrl();\n motionPreview.removeAttribute('src');\n motionPreview.controls = false;\n motionPreview.srcObject = motionStream;\n motionPreview.classList.add('mirror');\n motionHint.classList.add('hidden');\n try { await motionPreview.play(); } catch (error) { /* autoplay refused; the stream still shows on interaction */ }\n var mime = pickMotionMime();\n try {\n motionRecorder = mime ? new MediaRecorder(motionStream, { mimeType: mime }) : new MediaRecorder(motionStream);\n } catch (error) {\n stopMotionStream();\n motionGuidance.textContent = 'This browser cannot record video. Choose a video file instead.';\n return;\n }\n // Count down on the live preview; Stop during the count aborts without recording.\n var token = ++motionCountdownToken;\n setMotionButtons('recording');\n motionGuidance.textContent = 'Get in frame \u2014 recording starts after the count.';\n motionCountdown.classList.add('show');\n for (var left = MOTION_COUNTDOWN_S; left > 0; left--) {\n motionCountdownNumber.textContent = String(left);\n await new Promise(function (resolve) { setTimeout(resolve, 1000); });\n if (token !== motionCountdownToken) return;\n }\n motionCountdown.classList.remove('show');\n motionChunks = [];\n motionRecorder.ondataavailable = function (event) { if (event.data && event.data.size > 0) motionChunks.push(event.data); };\n motionRecorder.onstop = function () {\n var seconds = (Date.now() - motionStartedAt) / 1000;\n var type = motionRecorder.mimeType || mime || 'video/webm';\n stopMotionStream();\n motionFromRecording = true;\n acceptMotionClip(new Blob(motionChunks, { type: type }), seconds);\n };\n motionStartedAt = Date.now();\n motionRecorder.start(1000);\n motionTimer.textContent = '0:00';\n motionTimer.classList.add('show');\n motionGuidance.textContent = 'Recording. Stop when the move is done (auto-stops at ' + MOTION_MAX_S + ' s).';\n motionTickTimer = setInterval(function () {\n var elapsed = (Date.now() - motionStartedAt) / 1000;\n motionTimer.textContent = formatMotionClock(elapsed);\n if (elapsed >= MOTION_MAX_S) stopMotionRecording();\n }, 250);\n }\n\n function stopMotionRecording() {\n if (motionRecorder && motionRecorder.state === 'recording') { motionRecorder.stop(); return; }\n // Stop pressed during the countdown: nothing was recorded, back to the start.\n if (motionCountdown.classList.contains('show')) resetMotionPanel();\n }\n\n function pickMotionFile(file) {\n if (!file) return;\n // Read the duration off the file itself; a picked video knows its length, a fresh WebM often\n // does not (Infinity), so 0 means \"unknown\" and the server's ffprobe is the judge.\n var probe = document.createElement('video');\n probe.preload = 'metadata';\n var url = URL.createObjectURL(file);\n var settled = false;\n var finish = function (seconds) {\n if (settled) return;\n settled = true;\n URL.revokeObjectURL(url);\n motionFromRecording = false;\n acceptMotionClip(file, seconds);\n };\n probe.onloadedmetadata = function () { finish(isFinite(probe.duration) ? probe.duration : 0); };\n probe.onerror = function () { finish(0); };\n probe.src = url;\n setTimeout(function () { finish(0); }, 3000);\n }\n\n var motionPollTimer = null;\n function pollMotionJob() {\n if (motionPollTimer) clearTimeout(motionPollTimer);\n var tick = async function () {\n var job;\n try {\n job = (await (await fetch('/api/editor/motion-status')).json()).job;\n } catch (error) {\n motionPollTimer = setTimeout(tick, 3000);\n return;\n }\n if (!job) return;\n if (job.status === 'running') {\n showMotionNote(job.message, '');\n setStatus('saving', 'Making the animation\u2026');\n motionPollTimer = setTimeout(tick, 3000);\n return;\n }\n if (job.status === 'failed') {\n showMotionNote('Could not make the animation: ' + job.message, '');\n setStatus('error', 'Animation failed');\n setMotionButtons('ready');\n return;\n }\n showMotionNote(job.message + ' Reloading to show it.', '');\n setStatus('saved', 'Animation added');\n setMotionButtons('ready');\n if (autoReload && !voxelSessionActive && !voxelSessionPending) start();\n else banner.classList.add('show');\n };\n void tick();\n }\n\n async function submitMotion() {\n if (!motionBlob) return;\n var name = motionName.value.trim();\n if (name === '') { motionName.focus(); return; }\n // Whatever the handles show is what gets made; a still-running analysis must not move them.\n motionAnalysisToken++;\n if (motionAnalyzePollTimer) { clearTimeout(motionAnalyzePollTimer); motionAnalyzePollTimer = null; }\n var query = '?name=' + encodeURIComponent(name)\n + '&slot=' + encodeURIComponent(motionSlot.value)\n + '&loop=' + (motionLoop.checked ? '1' : '0');\n if (motionHasTrim()) {\n query += '&trimStart=' + encodeURIComponent(String(Math.round(motionTrimStart * 100) / 100))\n + '&trimEnd=' + encodeURIComponent(String(Math.round(motionTrimEnd * 100) / 100));\n }\n if (motionPendingId) {\n query += '&pendingId=' + encodeURIComponent(motionPendingId);\n }\n motionSubmit.disabled = true;\n showMotionNote('Uploading the video\u2026', '');\n var response, result;\n try {\n response = await fetch('/api/editor/motion-video' + query, {\n method: 'POST',\n headers: { 'Content-Type': motionBlob.type || 'video/webm' },\n body: motionBlob\n });\n result = await response.json();\n } catch (error) {\n showMotionNote('Could not send the video to the sidecar.', '');\n motionSubmit.disabled = false;\n return;\n }\n if (!response.ok || result.ok !== true) {\n showMotionNote(result.error || 'The sidecar refused the video.', '');\n motionSubmit.disabled = false;\n return;\n }\n if (result.running !== true) {\n // The one case the sidecar cannot run itself: nobody is logged in.\n showMotionNote(result.reason || '', result.command || '');\n setStatus('saved', 'Video saved');\n return;\n }\n showMotionNote(result.note || 'Making the animation \u2014 this takes a few minutes. You can keep editing.', '');\n setStatus('saving', 'Making the animation\u2026');\n pollMotionJob();\n }\n\n motionToggle.addEventListener('click', openMotionPanel);\n document.getElementById('motion-close').addEventListener('click', function () { closeMotionPanel(); });\n motionOverlay.addEventListener('click', function (event) {\n if (event.target === motionOverlay) closeMotionPanel();\n });\n motionRecordBtn.addEventListener('click', function () { void startMotionRecording(); });\n motionStopBtn.addEventListener('click', stopMotionRecording);\n motionRetakeBtn.addEventListener('click', resetMotionPanel);\n motionPickBtn.addEventListener('click', function () { motionFile.value = ''; motionFile.click(); });\n motionFile.addEventListener('change', function () { pickMotionFile(motionFile.files && motionFile.files[0]); });\n motionSubmit.addEventListener('click', function () { void submitMotion(); });\n motionTrimStartHandle.addEventListener('pointerdown', function (event) { beginMotionTrimDrag('start', event); });\n motionTrimEndHandle.addEventListener('pointerdown', function (event) { beginMotionTrimDrag('end', event); });\n motionTrimFullBtn.addEventListener('click', function () {\n motionTrimStart = 0;\n motionTrimEnd = motionTrimDuration;\n renderMotionTrim();\n });\n // \"Preview the kept range\" plays exactly trimStart -> trimEnd once and stops\n // on the last kept frame: what was just watched IS what the animation is\n // made from.\n motionTrimPreviewBtn.addEventListener('click', function () {\n if (motionPreviewingTrim) {\n motionPreview.pause();\n motionPreviewingTrim = false;\n renderMotionTrim();\n return;\n }\n motionPreview.currentTime = motionTrimStart;\n motionPreviewingTrim = true;\n renderMotionTrim();\n var played = motionPreview.play();\n if (played && played.catch) {\n played.catch(function () { motionPreviewingTrim = false; renderMotionTrim(); });\n }\n });\n motionPreview.addEventListener('pause', function () {\n if (motionPreviewingTrim) { motionPreviewingTrim = false; renderMotionTrim(); }\n });\n motionPreview.addEventListener('ended', function () {\n if (motionPreviewingTrim) { motionPreviewingTrim = false; renderMotionTrim(); }\n });\n motionPreview.addEventListener('timeupdate', function () {\n if (motionTrimDuration > 0) {\n motionTrimPlayhead.style.left = Math.min(Math.max(motionPreview.currentTime / motionTrimDuration, 0), 1) * 100 + '%';\n }\n if (motionPreviewingTrim && motionPreview.currentTime >= motionTrimEnd) {\n motionPreview.pause();\n motionPreview.currentTime = motionTrimEnd;\n motionPreviewingTrim = false;\n renderMotionTrim();\n }\n });\n";
20
+ export declare const MOTION_PANEL_JS = "\n // ---- Record motion: webcam or file -> Uthana video-to-motion -> assets[] ----------------------\n var motionToggle = document.getElementById('motion');\n var motionOverlay = document.getElementById('motion-overlay');\n var motionPreview = document.getElementById('motion-preview');\n var motionTimer = document.getElementById('motion-timer');\n var motionCountdown = document.getElementById('motion-countdown');\n var motionCountdownNumber = document.getElementById('motion-countdown-number');\n /** Seconds shown on the live preview before the recorder starts, so the first frames are the pose. */\n var MOTION_COUNTDOWN_S = 3;\n var motionCountdownToken = 0;\n var motionHint = document.getElementById('motion-hint');\n var motionRecordBtn = document.getElementById('motion-record');\n var motionStartBtn = document.getElementById('motion-start');\n var motionStopBtn = document.getElementById('motion-stop');\n var motionPickBtn = document.getElementById('motion-pick');\n var motionRetakeBtn = document.getElementById('motion-retake');\n var motionFile = document.getElementById('motion-file');\n var motionGuidance = document.getElementById('motion-guidance');\n var motionName = document.getElementById('motion-name');\n var motionSlot = document.getElementById('motion-slot');\n var motionLoop = document.getElementById('motion-loop');\n var motionSubmit = document.getElementById('motion-submit');\n var motionNote = document.getElementById('motion-note');\n var MOTION_MIN_S = 2;\n var MOTION_MAX_S = 60;\n var MOTION_MAX_BYTES = 200 * 1024 * 1024;\n var motionBlob = null;\n // Whether motionBlob came from the webcam recorder (auto-trim applies) or a picked file (it does not).\n var motionFromRecording = false;\n var motionSeconds = 0;\n var motionStream = null;\n // True while the camera is open for framing, before recording starts.\n var motionFraming = false;\n var motionRecorder = null;\n var motionChunks = [];\n var motionTickTimer = null;\n var motionStartedAt = 0;\n var motionObjectUrl = null;\n // ---- Trim window: seeded by the sidecar's trim analysis, adjusted on the handles ------------\n var motionTrimWrap = document.getElementById('motion-trim');\n var motionTrimBar = document.getElementById('motion-trimbar');\n var motionTrimKept = document.getElementById('motion-trim-kept');\n var motionTrimPlayhead = document.getElementById('motion-trim-playhead');\n var motionTrimStartHandle = document.getElementById('motion-trim-start');\n var motionTrimEndHandle = document.getElementById('motion-trim-end');\n var motionTrimNote = document.getElementById('motion-trim-note');\n var motionTrimFullBtn = document.getElementById('motion-trim-full');\n var motionTrimPreviewBtn = document.getElementById('motion-trim-preview');\n // True while \"Preview the kept range\" is playing trimStart -> trimEnd.\n var motionPreviewingTrim = false;\n var MOTION_TRIM_MIN_KEPT_S = 2;\n var motionTrimDuration = 0;\n var motionTrimStart = 0;\n var motionTrimEnd = 0;\n var motionPendingId = null;\n var motionAnalysisState = 'idle';\n var motionAnalysisToken = 0;\n var motionAnalyzePollTimer = null;\n\n function motionHasTrim() {\n return motionTrimDuration > 0 && motionTrimEnd > motionTrimStart\n && (motionTrimStart >= 0.1 || motionTrimDuration - motionTrimEnd >= 0.1);\n }\n\n function renderMotionTrim() {\n if (motionTrimDuration <= 0) { motionTrimWrap.classList.remove('show'); return; }\n motionTrimWrap.classList.add('show');\n var pct = function (seconds) {\n return Math.min(Math.max(seconds / motionTrimDuration, 0), 1) * 100 + '%';\n };\n motionTrimKept.style.left = pct(motionTrimStart);\n motionTrimKept.style.width = Math.max(0, (motionTrimEnd - motionTrimStart) / motionTrimDuration) * 100 + '%';\n motionTrimStartHandle.style.left = pct(motionTrimStart);\n motionTrimEndHandle.style.left = pct(motionTrimEnd);\n motionTrimFullBtn.hidden = !motionHasTrim();\n motionTrimPreviewBtn.textContent = motionPreviewingTrim\n ? '\u23F9 Stop'\n : (motionHasTrim() ? '\u25B6 Preview the kept range' : '\u25B6 Play');\n var note;\n if (motionAnalysisState === 'running') {\n note = 'Analyzing the clip for a good trim\u2026';\n } else if (!motionHasTrim()) {\n note = 'Using the full clip.';\n } else {\n var parts = [];\n if (motionTrimStart >= 0.1) parts.push(motionTrimStart.toFixed(1) + 's from the start');\n if (motionTrimDuration - motionTrimEnd >= 0.1) parts.push((motionTrimDuration - motionTrimEnd).toFixed(1) + 's from the end');\n note = 'Trimming ' + parts.join(' and ') + '.';\n }\n motionTrimNote.textContent = note;\n }\n\n function resetMotionTrim() {\n motionPreviewingTrim = false;\n motionAnalysisToken++;\n if (motionAnalyzePollTimer) { clearTimeout(motionAnalyzePollTimer); motionAnalyzePollTimer = null; }\n motionPendingId = null;\n motionAnalysisState = 'idle';\n motionTrimDuration = 0;\n motionTrimStart = 0;\n motionTrimEnd = 0;\n motionTrimWrap.classList.remove('show');\n }\n\n function seedMotionTrim(durationSeconds) {\n if (!(durationSeconds > 0)) return;\n motionTrimDuration = durationSeconds;\n motionTrimStart = 0;\n motionTrimEnd = durationSeconds;\n // Review playback is the trim: the Play/Preview button below drives the\n // video, and a native full-clip scrubber under a trim bar read as \"it\n // plays the whole video anyway\". Controls stay native only while the\n // duration is unknown and the bar cannot render.\n motionPreview.controls = false;\n renderMotionTrim();\n }\n\n function applyMotionSuggestion(suggestion) {\n if (!suggestion) { renderMotionTrim(); return; }\n if (suggestion.durationSeconds > 0) {\n motionTrimDuration = suggestion.durationSeconds;\n motionTrimStart = 0;\n motionTrimEnd = suggestion.durationSeconds;\n }\n if (suggestion.confidence === 'high'\n && typeof suggestion.suggestedStartSeconds === 'number'\n && typeof suggestion.suggestedEndSeconds === 'number'\n && suggestion.suggestedEndSeconds > suggestion.suggestedStartSeconds) {\n motionTrimStart = suggestion.suggestedStartSeconds;\n motionTrimEnd = Math.min(suggestion.suggestedEndSeconds, motionTrimDuration || suggestion.suggestedEndSeconds);\n }\n renderMotionTrim();\n }\n\n function beginMotionTrimDrag(which, event) {\n if (motionTrimDuration <= 0) return;\n var handle = event.currentTarget;\n handle.setPointerCapture(event.pointerId);\n var move = function (e) {\n var rect = motionTrimBar.getBoundingClientRect();\n if (rect.width === 0) return;\n var at = Math.min(Math.max((e.clientX - rect.left) / rect.width, 0), 1) * motionTrimDuration;\n if (which === 'start') motionTrimStart = Math.min(at, motionTrimEnd - MOTION_TRIM_MIN_KEPT_S);\n else motionTrimEnd = Math.max(at, motionTrimStart + MOTION_TRIM_MIN_KEPT_S);\n // Show the exact frame the cut lands on while the handle moves.\n motionPreview.pause();\n motionPreviewingTrim = false;\n motionPreview.currentTime = which === 'start' ? motionTrimStart : motionTrimEnd;\n renderMotionTrim();\n };\n var up = function () {\n handle.removeEventListener('pointermove', move);\n handle.removeEventListener('pointerup', up);\n handle.removeEventListener('pointercancel', up);\n };\n handle.addEventListener('pointermove', move);\n handle.addEventListener('pointerup', up);\n handle.addEventListener('pointercancel', up);\n event.preventDefault();\n }\n\n function pollMotionAnalysis(token) {\n var tick = async function () {\n if (token !== motionAnalysisToken) return;\n var payload;\n try {\n payload = await (await fetch('/api/editor/motion-analyze-status')).json();\n } catch (error) {\n motionAnalyzePollTimer = setTimeout(tick, 2000);\n return;\n }\n if (token !== motionAnalysisToken) return;\n if (payload && payload.status === 'done') {\n motionAnalysisState = 'done';\n applyMotionSuggestion(payload.suggestion);\n return;\n }\n if (payload && payload.status === 'running') {\n motionAnalyzePollTimer = setTimeout(tick, 2000);\n return;\n }\n // failed / unavailable / none: no suggestion, the full-range handles stand.\n motionAnalysisState = 'failed';\n renderMotionTrim();\n };\n motionAnalyzePollTimer = setTimeout(tick, 2000);\n }\n\n /**\n * Hand the clip to the sidecar ahead of submission: it uploads once and runs the unbilled trim\n * analysis while the creator reviews. Degraded, never blocking \u2014 any failure just leaves the\n * handles at full range, and Submit works throughout.\n */\n async function startMotionAnalysis(blob) {\n var token = ++motionAnalysisToken;\n motionPendingId = null;\n motionAnalysisState = 'running';\n renderMotionTrim();\n var result = null;\n try {\n var response = await fetch('/api/editor/motion-analyze', {\n method: 'POST',\n headers: { 'Content-Type': blob.type || 'video/webm' },\n body: blob\n });\n result = await response.json();\n } catch (error) { /* the sidecar said nothing; treat as unavailable */ }\n if (token !== motionAnalysisToken) return;\n if (!result || result.ok !== true || result.analysing !== true) {\n motionAnalysisState = 'idle';\n renderMotionTrim();\n return;\n }\n motionPendingId = result.pendingId || null;\n pollMotionAnalysis(token);\n }\n\n function motionSupported() {\n return !!(window.isSecureContext && navigator.mediaDevices && navigator.mediaDevices.getUserMedia\n && typeof MediaRecorder !== 'undefined');\n }\n\n /** mp4 first (Safari), then the WebM the rest produce; asset-forger re-encodes either. */\n function pickMotionMime() {\n var candidates = ['video/mp4', 'video/webm;codecs=vp9', 'video/webm'];\n for (var i = 0; i < candidates.length; i++) {\n if (MediaRecorder.isTypeSupported(candidates[i])) return candidates[i];\n }\n return '';\n }\n\n function motionCameraError(error) {\n var name = error && error.name;\n if (name === 'NotAllowedError' || name === 'SecurityError') return 'Camera access was refused. Allow it in the address bar, or choose a video file.';\n if (name === 'NotFoundError' || name === 'OverconstrainedError') return 'No camera found. Choose a video file instead.';\n if (name === 'NotReadableError') return 'The camera is in use by another app. Close it, or choose a video file.';\n return 'Could not start the camera: ' + ((error && error.message) || 'unknown error');\n }\n\n function formatMotionClock(seconds) {\n var s = Math.max(0, Math.floor(seconds));\n var rest = s % 60;\n return Math.floor(s / 60) + ':' + (rest < 10 ? '0' : '') + rest;\n }\n\n function showMotionNote(text, command) {\n document.getElementById('motion-note-text').textContent = text;\n document.getElementById('motion-command').textContent = command || '';\n motionNote.classList.add('show');\n }\n\n function releaseMotionObjectUrl() {\n if (motionObjectUrl) { URL.revokeObjectURL(motionObjectUrl); motionObjectUrl = null; }\n }\n\n /** Stop the recorder if one is still live \u2014 it may already have stopped itself. */\n function discardMotionRecorder() {\n if (motionRecorder && motionRecorder.state !== 'inactive') {\n try { motionRecorder.stop(); } catch (error) { /* already stopped */ }\n }\n }\n\n function stopMotionStream() {\n if (motionStream) {\n motionStream.getTracks().forEach(function (track) { track.stop(); });\n motionStream = null;\n }\n if (motionTickTimer) { clearInterval(motionTickTimer); motionTickTimer = null; }\n motionCountdownToken++;\n motionCountdown.classList.remove('show');\n motionTimer.classList.remove('show');\n motionPreview.classList.remove('mirror');\n }\n\n function setMotionButtons(state) {\n motionRecordBtn.hidden = state !== 'idle';\n motionPickBtn.hidden = state !== 'idle';\n motionStartBtn.hidden = state !== 'framing';\n motionStopBtn.hidden = state !== 'recording' && state !== 'framing';\n motionStopBtn.textContent = state === 'framing' ? 'Cancel' : 'Stop';\n motionRetakeBtn.hidden = state !== 'ready';\n motionSubmit.disabled = state !== 'ready';\n }\n\n function resetMotionPanel() {\n motionFraming = false;\n resetMotionTrim();\n stopMotionStream();\n discardMotionRecorder();\n motionRecorder = null;\n motionChunks = [];\n motionBlob = null;\n motionFromRecording = false;\n motionSeconds = 0;\n releaseMotionObjectUrl();\n motionPreview.srcObject = null;\n motionPreview.removeAttribute('src');\n motionPreview.controls = false;\n motionPreview.muted = true;\n motionHint.classList.remove('hidden');\n var supported = motionSupported();\n motionGuidance.textContent = supported ? '' : 'Webcam recording needs a secure page with a camera; choose a video file instead.';\n motionRecordBtn.disabled = !supported;\n motionNote.classList.remove('show');\n setMotionButtons('idle');\n }\n\n function openMotionPanel() {\n resetMotionPanel();\n motionOverlay.classList.add('show');\n motionName.focus();\n }\n\n /** Returns whether it was open \u2014 the Escape handler's contract, like closeAssetDetail. */\n function closeMotionPanel() {\n if (!motionOverlay.classList.contains('show')) return false;\n stopMotionStream();\n discardMotionRecorder();\n motionOverlay.classList.remove('show');\n frame.focus();\n return true;\n }\n\n /** The clip is in hand: show it back, and say whether it is within Uthana's 2-60 s window. */\n function acceptMotionClip(blob, seconds) {\n motionBlob = blob;\n motionSeconds = seconds;\n releaseMotionObjectUrl();\n motionObjectUrl = URL.createObjectURL(blob);\n motionPreview.srcObject = null;\n motionPreview.src = motionObjectUrl;\n motionPreview.controls = true;\n motionPreview.muted = true;\n motionHint.classList.add('hidden');\n var note;\n if (blob.size > MOTION_MAX_BYTES) note = 'That video is over 200 MB. Trim it to the seconds that matter.';\n else if (seconds > 0 && seconds < MOTION_MIN_S) note = 'Too short: Uthana needs at least ' + MOTION_MIN_S + ' seconds.';\n else if (seconds > MOTION_MAX_S) note = 'Too long: Uthana takes at most ' + MOTION_MAX_S + ' seconds. Trim it, or record again.';\n var ok = !note;\n motionGuidance.textContent = ok\n ? (seconds > 0 ? formatMotionClock(seconds) + ' of video ready.' : 'Video ready.')\n : note;\n setMotionButtons('ready');\n motionSubmit.disabled = !ok;\n resetMotionTrim();\n if (ok) {\n seedMotionTrim(seconds);\n // A fresh WebM often reports no duration; the preview's metadata is the fallback scale.\n motionPreview.onloadedmetadata = function () {\n if (motionTrimDuration === 0 && isFinite(motionPreview.duration) && motionPreview.duration > 0) {\n seedMotionTrim(motionPreview.duration);\n }\n };\n void startMotionAnalysis(blob);\n }\n }\n\n /**\n * Open the camera without recording \u2014 the framing step: the creator sees\n * themselves and gets their whole body in frame for as long as they need.\n * Start then runs the countdown and records.\n */\n async function openMotionCamera() {\n if (!motionSupported()) return;\n motionNote.classList.remove('show');\n try {\n motionStream = await navigator.mediaDevices.getUserMedia({\n video: { width: { ideal: 1280 }, height: { ideal: 720 }, facingMode: 'user' }, audio: false\n });\n } catch (error) {\n motionGuidance.textContent = motionCameraError(error);\n return;\n }\n motionFraming = true;\n releaseMotionObjectUrl();\n motionPreview.removeAttribute('src');\n motionPreview.controls = false;\n motionPreview.srcObject = motionStream;\n motionPreview.classList.add('mirror');\n motionHint.classList.add('hidden');\n try { await motionPreview.play(); } catch (error) { /* autoplay refused; the stream still shows on interaction */ }\n motionGuidance.textContent = 'Get your whole body in frame, then press Start recording.';\n setMotionButtons('framing');\n }\n\n async function startMotionRecording() {\n if (!motionStream || !motionFraming) return;\n motionFraming = false;\n var mime = pickMotionMime();\n try {\n motionRecorder = mime ? new MediaRecorder(motionStream, { mimeType: mime }) : new MediaRecorder(motionStream);\n } catch (error) {\n stopMotionStream();\n motionGuidance.textContent = 'This browser cannot record video. Choose a video file instead.';\n return;\n }\n // Count down on the live preview; Stop during the count aborts without recording.\n var token = ++motionCountdownToken;\n setMotionButtons('recording');\n motionGuidance.textContent = 'Get in frame \u2014 recording starts after the count.';\n motionCountdown.classList.add('show');\n for (var left = MOTION_COUNTDOWN_S; left > 0; left--) {\n motionCountdownNumber.textContent = String(left);\n await new Promise(function (resolve) { setTimeout(resolve, 1000); });\n if (token !== motionCountdownToken) return;\n }\n motionCountdown.classList.remove('show');\n motionChunks = [];\n motionRecorder.ondataavailable = function (event) { if (event.data && event.data.size > 0) motionChunks.push(event.data); };\n motionRecorder.onstop = function () {\n var seconds = (Date.now() - motionStartedAt) / 1000;\n var type = motionRecorder.mimeType || mime || 'video/webm';\n stopMotionStream();\n motionFromRecording = true;\n acceptMotionClip(new Blob(motionChunks, { type: type }), seconds);\n };\n motionStartedAt = Date.now();\n motionRecorder.start(1000);\n motionTimer.textContent = '0:00';\n motionTimer.classList.add('show');\n motionGuidance.textContent = 'Recording. Stop when the move is done (auto-stops at ' + MOTION_MAX_S + ' s).';\n motionTickTimer = setInterval(function () {\n var elapsed = (Date.now() - motionStartedAt) / 1000;\n motionTimer.textContent = formatMotionClock(elapsed);\n if (elapsed >= MOTION_MAX_S) stopMotionRecording();\n }, 250);\n }\n\n function stopMotionRecording() {\n if (motionRecorder && motionRecorder.state === 'recording') { motionRecorder.stop(); return; }\n // Cancel during framing, or Stop during the countdown: nothing was\n // recorded, back to the start.\n if (motionFraming || motionCountdown.classList.contains('show')) resetMotionPanel();\n }\n\n function pickMotionFile(file) {\n if (!file) return;\n // Read the duration off the file itself; a picked video knows its length, a fresh WebM often\n // does not (Infinity), so 0 means \"unknown\" and the server's ffprobe is the judge.\n var probe = document.createElement('video');\n probe.preload = 'metadata';\n var url = URL.createObjectURL(file);\n var settled = false;\n var finish = function (seconds) {\n if (settled) return;\n settled = true;\n URL.revokeObjectURL(url);\n motionFromRecording = false;\n acceptMotionClip(file, seconds);\n };\n probe.onloadedmetadata = function () { finish(isFinite(probe.duration) ? probe.duration : 0); };\n probe.onerror = function () { finish(0); };\n probe.src = url;\n setTimeout(function () { finish(0); }, 3000);\n }\n\n var motionPollTimer = null;\n function pollMotionJob() {\n if (motionPollTimer) clearTimeout(motionPollTimer);\n var tick = async function () {\n var job;\n try {\n job = (await (await fetch('/api/editor/motion-status')).json()).job;\n } catch (error) {\n motionPollTimer = setTimeout(tick, 3000);\n return;\n }\n if (!job) return;\n if (job.status === 'running') {\n showMotionNote(job.message, '');\n setStatus('saving', 'Making the animation\u2026');\n motionPollTimer = setTimeout(tick, 3000);\n return;\n }\n if (job.status === 'failed') {\n showMotionNote('Could not make the animation: ' + job.message, '');\n setStatus('error', 'Animation failed');\n setMotionButtons('ready');\n return;\n }\n showMotionNote(job.message + ' Reloading to show it.', '');\n setStatus('saved', 'Animation added');\n setMotionButtons('ready');\n if (autoReload && !voxelSessionActive && !voxelSessionPending) start();\n else banner.classList.add('show');\n };\n void tick();\n }\n\n async function submitMotion() {\n if (!motionBlob) return;\n var name = motionName.value.trim();\n if (name === '') { motionName.focus(); return; }\n // Whatever the handles show is what gets made; a still-running analysis must not move them.\n motionAnalysisToken++;\n if (motionAnalyzePollTimer) { clearTimeout(motionAnalyzePollTimer); motionAnalyzePollTimer = null; }\n var query = '?name=' + encodeURIComponent(name)\n + '&slot=' + encodeURIComponent(motionSlot.value)\n + '&loop=' + (motionLoop.checked ? '1' : '0');\n if (motionHasTrim()) {\n query += '&trimStart=' + encodeURIComponent(String(Math.round(motionTrimStart * 100) / 100))\n + '&trimEnd=' + encodeURIComponent(String(Math.round(motionTrimEnd * 100) / 100));\n }\n if (motionPendingId) {\n query += '&pendingId=' + encodeURIComponent(motionPendingId);\n }\n motionSubmit.disabled = true;\n showMotionNote('Uploading the video\u2026', '');\n var response, result;\n try {\n response = await fetch('/api/editor/motion-video' + query, {\n method: 'POST',\n headers: { 'Content-Type': motionBlob.type || 'video/webm' },\n body: motionBlob\n });\n result = await response.json();\n } catch (error) {\n showMotionNote('Could not send the video to the sidecar.', '');\n motionSubmit.disabled = false;\n return;\n }\n if (!response.ok || result.ok !== true) {\n showMotionNote(result.error || 'The sidecar refused the video.', '');\n motionSubmit.disabled = false;\n return;\n }\n if (result.running !== true) {\n // The one case the sidecar cannot run itself: nobody is logged in.\n showMotionNote(result.reason || '', result.command || '');\n setStatus('saved', 'Video saved');\n return;\n }\n showMotionNote(result.note || 'Making the animation \u2014 this takes a few minutes. You can keep editing.', '');\n setStatus('saving', 'Making the animation\u2026');\n pollMotionJob();\n }\n\n motionToggle.addEventListener('click', openMotionPanel);\n document.getElementById('motion-close').addEventListener('click', function () { closeMotionPanel(); });\n motionOverlay.addEventListener('click', function (event) {\n if (event.target === motionOverlay) closeMotionPanel();\n });\n motionRecordBtn.addEventListener('click', function () { void openMotionCamera(); });\n motionStartBtn.addEventListener('click', function () { void startMotionRecording(); });\n motionStopBtn.addEventListener('click', stopMotionRecording);\n motionRetakeBtn.addEventListener('click', resetMotionPanel);\n motionPickBtn.addEventListener('click', function () { motionFile.value = ''; motionFile.click(); });\n motionFile.addEventListener('change', function () { pickMotionFile(motionFile.files && motionFile.files[0]); });\n motionSubmit.addEventListener('click', function () { void submitMotion(); });\n motionTrimStartHandle.addEventListener('pointerdown', function (event) { beginMotionTrimDrag('start', event); });\n motionTrimEndHandle.addEventListener('pointerdown', function (event) { beginMotionTrimDrag('end', event); });\n motionTrimFullBtn.addEventListener('click', function () {\n motionTrimStart = 0;\n motionTrimEnd = motionTrimDuration;\n renderMotionTrim();\n });\n // \"Preview the kept range\" plays exactly trimStart -> trimEnd once and stops\n // on the last kept frame: what was just watched IS what the animation is\n // made from.\n motionTrimPreviewBtn.addEventListener('click', function () {\n if (motionPreviewingTrim) {\n motionPreview.pause();\n motionPreviewingTrim = false;\n renderMotionTrim();\n return;\n }\n motionPreview.currentTime = motionTrimStart;\n motionPreviewingTrim = true;\n renderMotionTrim();\n var played = motionPreview.play();\n if (played && played.catch) {\n played.catch(function () { motionPreviewingTrim = false; renderMotionTrim(); });\n }\n });\n motionPreview.addEventListener('pause', function () {\n if (motionPreviewingTrim) { motionPreviewingTrim = false; renderMotionTrim(); }\n });\n motionPreview.addEventListener('ended', function () {\n if (motionPreviewingTrim) { motionPreviewingTrim = false; renderMotionTrim(); }\n });\n motionPreview.addEventListener('timeupdate', function () {\n if (motionTrimDuration > 0) {\n motionTrimPlayhead.style.left = Math.min(Math.max(motionPreview.currentTime / motionTrimDuration, 0), 1) * 100 + '%';\n }\n if (motionPreviewingTrim && motionPreview.currentTime >= motionTrimEnd) {\n motionPreview.pause();\n motionPreview.currentTime = motionTrimEnd;\n motionPreviewingTrim = false;\n renderMotionTrim();\n }\n });\n";
@@ -90,6 +90,7 @@ export const MOTION_PANEL_HTML = ` <div id="motion-overlay" class="bm-overlay">
90
90
  </div>
91
91
  <div id="motion-sources">
92
92
  <button id="motion-record" class="bm-pill bm-pill--primary" type="button">Record with webcam</button>
93
+ <button id="motion-start" class="bm-pill bm-pill--primary" type="button" hidden>Start recording</button>
93
94
  <button id="motion-stop" class="bm-pill" type="button" hidden>Stop</button>
94
95
  <button id="motion-pick" class="bm-pill" type="button">Choose a video&hellip;</button>
95
96
  <button id="motion-retake" class="bm-pill bm-pill--ghost" type="button" hidden>Start over</button>
@@ -140,6 +141,7 @@ export const MOTION_PANEL_JS = `
140
141
  var motionCountdownToken = 0;
141
142
  var motionHint = document.getElementById('motion-hint');
142
143
  var motionRecordBtn = document.getElementById('motion-record');
144
+ var motionStartBtn = document.getElementById('motion-start');
143
145
  var motionStopBtn = document.getElementById('motion-stop');
144
146
  var motionPickBtn = document.getElementById('motion-pick');
145
147
  var motionRetakeBtn = document.getElementById('motion-retake');
@@ -158,6 +160,8 @@ export const MOTION_PANEL_JS = `
158
160
  var motionFromRecording = false;
159
161
  var motionSeconds = 0;
160
162
  var motionStream = null;
163
+ // True while the camera is open for framing, before recording starts.
164
+ var motionFraming = false;
161
165
  var motionRecorder = null;
162
166
  var motionChunks = [];
163
167
  var motionTickTimer = null;
@@ -402,12 +406,15 @@ export const MOTION_PANEL_JS = `
402
406
  function setMotionButtons(state) {
403
407
  motionRecordBtn.hidden = state !== 'idle';
404
408
  motionPickBtn.hidden = state !== 'idle';
405
- motionStopBtn.hidden = state !== 'recording';
409
+ motionStartBtn.hidden = state !== 'framing';
410
+ motionStopBtn.hidden = state !== 'recording' && state !== 'framing';
411
+ motionStopBtn.textContent = state === 'framing' ? 'Cancel' : 'Stop';
406
412
  motionRetakeBtn.hidden = state !== 'ready';
407
413
  motionSubmit.disabled = state !== 'ready';
408
414
  }
409
415
 
410
416
  function resetMotionPanel() {
417
+ motionFraming = false;
411
418
  resetMotionTrim();
412
419
  stopMotionStream();
413
420
  discardMotionRecorder();
@@ -479,7 +486,12 @@ export const MOTION_PANEL_JS = `
479
486
  }
480
487
  }
481
488
 
482
- async function startMotionRecording() {
489
+ /**
490
+ * Open the camera without recording — the framing step: the creator sees
491
+ * themselves and gets their whole body in frame for as long as they need.
492
+ * Start then runs the countdown and records.
493
+ */
494
+ async function openMotionCamera() {
483
495
  if (!motionSupported()) return;
484
496
  motionNote.classList.remove('show');
485
497
  try {
@@ -490,6 +502,7 @@ export const MOTION_PANEL_JS = `
490
502
  motionGuidance.textContent = motionCameraError(error);
491
503
  return;
492
504
  }
505
+ motionFraming = true;
493
506
  releaseMotionObjectUrl();
494
507
  motionPreview.removeAttribute('src');
495
508
  motionPreview.controls = false;
@@ -497,6 +510,13 @@ export const MOTION_PANEL_JS = `
497
510
  motionPreview.classList.add('mirror');
498
511
  motionHint.classList.add('hidden');
499
512
  try { await motionPreview.play(); } catch (error) { /* autoplay refused; the stream still shows on interaction */ }
513
+ motionGuidance.textContent = 'Get your whole body in frame, then press Start recording.';
514
+ setMotionButtons('framing');
515
+ }
516
+
517
+ async function startMotionRecording() {
518
+ if (!motionStream || !motionFraming) return;
519
+ motionFraming = false;
500
520
  var mime = pickMotionMime();
501
521
  try {
502
522
  motionRecorder = mime ? new MediaRecorder(motionStream, { mimeType: mime }) : new MediaRecorder(motionStream);
@@ -539,8 +559,9 @@ export const MOTION_PANEL_JS = `
539
559
 
540
560
  function stopMotionRecording() {
541
561
  if (motionRecorder && motionRecorder.state === 'recording') { motionRecorder.stop(); return; }
542
- // Stop pressed during the countdown: nothing was recorded, back to the start.
543
- if (motionCountdown.classList.contains('show')) resetMotionPanel();
562
+ // Cancel during framing, or Stop during the countdown: nothing was
563
+ // recorded, back to the start.
564
+ if (motionFraming || motionCountdown.classList.contains('show')) resetMotionPanel();
544
565
  }
545
566
 
546
567
  function pickMotionFile(file) {
@@ -650,7 +671,8 @@ export const MOTION_PANEL_JS = `
650
671
  motionOverlay.addEventListener('click', function (event) {
651
672
  if (event.target === motionOverlay) closeMotionPanel();
652
673
  });
653
- motionRecordBtn.addEventListener('click', function () { void startMotionRecording(); });
674
+ motionRecordBtn.addEventListener('click', function () { void openMotionCamera(); });
675
+ motionStartBtn.addEventListener('click', function () { void startMotionRecording(); });
654
676
  motionStopBtn.addEventListener('click', stopMotionRecording);
655
677
  motionRetakeBtn.addEventListener('click', resetMotionPanel);
656
678
  motionPickBtn.addEventListener('click', function () { motionFile.value = ''; motionFile.click(); });
@@ -1 +1 @@
1
- {"version":3,"file":"motion-panel.js","sourceRoot":"","sources":["../../src/editor/motion-panel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,CAAC,MAAM,gBAAgB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiD/B,CAAC;AAEF,MAAM,CAAC,MAAM,iBAAiB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6DhC,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2jB9B,CAAC"}
1
+ {"version":3,"file":"motion-panel.js","sourceRoot":"","sources":["../../src/editor/motion-panel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,CAAC,MAAM,gBAAgB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiD/B,CAAC;AAEF,MAAM,CAAC,MAAM,iBAAiB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8DhC,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAglB9B,CAAC"}