rnxsim 0.1.313 → 0.1.315
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/LICENSE +11 -10
- package/README.md +5 -0
- package/cli/app-config.ts +65 -0
- package/cli/app-fonts.ts +408 -0
- package/cli/app-project.ts +231 -0
- package/cli/app-splash.ts +185 -0
- package/cli/app-state-reset.ts +24 -0
- package/cli/auth.ts +155 -0
- package/cli/bin.ts +594 -0
- package/cli/bridge-diagnostics.ts +226 -0
- package/cli/bridge-flow-runner.ts +2830 -0
- package/cli/browser-evals.ts +96 -0
- package/cli/commands/agent-wrapper.ts +986 -0
- package/cli/commands/agent.ts +423 -0
- package/cli/commands/app-fonts.ts +98 -0
- package/cli/commands/assert.ts +541 -0
- package/cli/commands/auth.ts +59 -0
- package/cli/commands/camera.ts +266 -0
- package/cli/commands/cleanup.ts +169 -0
- package/cli/commands/compat.ts +87 -0
- package/cli/commands/config.ts +32 -0
- package/cli/commands/control.ts +2142 -0
- package/cli/commands/cpu-profile.ts +269 -0
- package/cli/commands/daemon-mac-app.ts +169 -0
- package/cli/commands/daemon.ts +874 -0
- package/cli/commands/debug.ts +719 -0
- package/cli/commands/desktop.ts +39 -0
- package/cli/commands/detect.ts +197 -0
- package/cli/commands/detox.ts +385 -0
- package/cli/commands/device.ts +133 -0
- package/cli/commands/diagnose.ts +589 -0
- package/cli/commands/electron.ts +95 -0
- package/cli/commands/film.ts +379 -0
- package/cli/commands/flow.ts +1124 -0
- package/cli/commands/inspect/actions.ts +622 -0
- package/cli/commands/inspect/core.ts +2405 -0
- package/cli/commands/inspect/count.ts +17 -0
- package/cli/commands/inspect/describe.ts +192 -0
- package/cli/commands/inspect/env.ts +23 -0
- package/cli/commands/inspect/find.ts +171 -0
- package/cli/commands/inspect/get-layout.ts +39 -0
- package/cli/commands/inspect/keyboard.ts +52 -0
- package/cli/commands/inspect/list.ts +58 -0
- package/cli/commands/inspect/memory.ts +215 -0
- package/cli/commands/inspect/redaction.ts +39 -0
- package/cli/commands/inspect/resolve-target.ts +82 -0
- package/cli/commands/inspect/screens.ts +78 -0
- package/cli/commands/inspect/settle.ts +22 -0
- package/cli/commands/inspect/settling.ts +158 -0
- package/cli/commands/inspect/shared.ts +353 -0
- package/cli/commands/inspect/sleep.ts +14 -0
- package/cli/commands/inspect/tree.ts +32 -0
- package/cli/commands/inspect/url.ts +17 -0
- package/cli/commands/inspect/wait-event.ts +210 -0
- package/cli/commands/inspect/wait-idle.ts +24 -0
- package/cli/commands/inspect/wait-ready.ts +74 -0
- package/cli/commands/inspect/wait-selector.ts +54 -0
- package/cli/commands/inspect/wait.ts +31 -0
- package/cli/commands/inspect.ts +4519 -0
- package/cli/commands/install-desktop.ts +351 -0
- package/cli/commands/login.ts +331 -0
- package/cli/commands/logout.ts +31 -0
- package/cli/commands/maestro-generate.ts +361 -0
- package/cli/commands/maestro.ts +453 -0
- package/cli/commands/mode.ts +57 -0
- package/cli/commands/no-bridge-hint.ts +80 -0
- package/cli/commands/perf.ts +66 -0
- package/cli/commands/permissions.ts +203 -0
- package/cli/commands/profile.ts +108 -0
- package/cli/commands/react.ts +353 -0
- package/cli/commands/record.ts +1434 -0
- package/cli/commands/report-issue.ts +305 -0
- package/cli/commands/reset.ts +85 -0
- package/cli/commands/runtime.ts +351 -0
- package/cli/commands/screenshot-command.ts +106 -0
- package/cli/commands/screenshot-layers.ts +143 -0
- package/cli/commands/screenshot-mode.ts +37 -0
- package/cli/commands/screenshot.ts +488 -0
- package/cli/commands/screenshots-capture.ts +607 -0
- package/cli/commands/screenshots.ts +127 -0
- package/cli/commands/serve.ts +168 -0
- package/cli/commands/setup.ts +545 -0
- package/cli/commands/shell-boolean-mode.ts +81 -0
- package/cli/commands/skills.ts +467 -0
- package/cli/commands/slides.ts +361 -0
- package/cli/commands/state.ts +87 -0
- package/cli/commands/storage.ts +58 -0
- package/cli/commands/telemetry.ts +54 -0
- package/cli/commands/three-mode.ts +763 -0
- package/cli/commands/timeline.ts +122 -0
- package/cli/commands/upgrade.ts +208 -0
- package/cli/commands/upload.ts +1225 -0
- package/cli/commands/version.ts +54 -0
- package/cli/commands/what-happened.ts +327 -0
- package/cli/current-sim.ts +204 -0
- package/cli/desktop-companion.ts +300 -0
- package/cli/drivers/electron.ts +70 -0
- package/cli/drivers/index.ts +20 -0
- package/cli/drivers/playwright-provisioning.ts +180 -0
- package/cli/drivers/playwright.ts +698 -0
- package/cli/drivers/registry.ts +65 -0
- package/cli/drivers/types.ts +102 -0
- package/cli/flow-file.ts +142 -0
- package/cli/flow-live-status.ts +120 -0
- package/cli/flow-session.ts +187 -0
- package/cli/help.ts +80 -0
- package/cli/hidden-runtime-alias.ts +19 -0
- package/cli/hints.ts +216 -0
- package/cli/inspect-notice-state.ts +114 -0
- package/cli/maestro-js.ts +334 -0
- package/cli/open-url.ts +8 -0
- package/cli/parent-pid.ts +204 -0
- package/cli/parse-args.ts +211 -0
- package/cli/prompt.ts +51 -0
- package/cli/recording-access.ts +107 -0
- package/cli/registry.ts +1 -0
- package/cli/resolve-assets.ts +63 -0
- package/cli/run-registry.ts +226 -0
- package/cli/runtime-notes.ts +66 -0
- package/cli/runtime-summary.ts +25 -0
- package/cli/setup-repository.ts +187 -0
- package/cli/telemetry.ts +187 -0
- package/cli/ws-bridge.ts +798 -0
- package/dist-cli/bin.js +5 -5
- package/dist-cli/chunks/{agent-XZ2KTPCU.js → agent-CBU2WDKV.js} +2 -2
- package/dist-cli/chunks/{agent-wrapper-JJYYW2WH.js → agent-wrapper-UNB4R27L.js} +2 -2
- package/dist-cli/chunks/{app-fonts-IXRNQG6B.js → app-fonts-YMLRF3JD.js} +2 -2
- package/dist-cli/chunks/{assert-54T5SK5F.js → assert-5N6HUCL5.js} +2 -2
- package/dist-cli/chunks/{auth-FI5UDI45.js → auth-BHDR3A6G.js} +2 -2
- package/dist-cli/chunks/{beta-JV6UKADW.js → beta-LWUVCPYK.js} +2 -2
- package/dist-cli/chunks/camera-CNO6B2JZ.js +33 -0
- package/dist-cli/chunks/{chunk-ZMJD5GEC.js → chunk-25VCR44B.js} +1 -1
- package/dist-cli/chunks/{chunk-WUSWBCWA.js → chunk-26CSIR76.js} +8 -9
- package/dist-cli/chunks/{chunk-WINYQ44O.js → chunk-26WMWUH4.js} +1 -1
- package/dist-cli/chunks/{chunk-BTWORNNG.js → chunk-2JZ53D4A.js} +1 -1
- package/dist-cli/chunks/{chunk-DCEMHR2Y.js → chunk-2POODCTW.js} +2 -2
- package/dist-cli/chunks/{chunk-5TEF3ET3.js → chunk-4LTBK3ZN.js} +2 -2
- package/dist-cli/chunks/{chunk-NMF2ZMZQ.js → chunk-54MIRTFE.js} +4 -4
- package/dist-cli/chunks/{chunk-WEXDAC74.js → chunk-56H6EVO4.js} +2 -2
- package/dist-cli/chunks/{chunk-F5ZRSS3C.js → chunk-657SQG4D.js} +1 -1
- package/dist-cli/chunks/{chunk-IJ5CAZZC.js → chunk-6ZMRJPKI.js} +1 -1
- package/dist-cli/chunks/{chunk-W6K4EFPH.js → chunk-AOGGOC4V.js} +2 -2
- package/dist-cli/chunks/chunk-BPCDO7ST.js +15 -0
- package/dist-cli/chunks/{chunk-D4FFVGI5.js → chunk-C5O5CTXJ.js} +1 -1
- package/dist-cli/chunks/{chunk-RTN5C5RL.js → chunk-CMM64PPR.js} +1 -1
- package/dist-cli/chunks/{chunk-BBULZ7CG.js → chunk-D3M36D7O.js} +62 -87
- package/dist-cli/chunks/{chunk-5DHC6KHQ.js → chunk-DNCNFZ7V.js} +1 -1
- package/dist-cli/chunks/{chunk-YIFT42WN.js → chunk-FNTTIR2P.js} +2 -2
- package/dist-cli/chunks/chunk-H362IX2I.js +4 -0
- package/dist-cli/chunks/{chunk-IJO63TDP.js → chunk-J6HMTUZH.js} +2 -2
- package/dist-cli/chunks/{chunk-RSZWCKNT.js → chunk-JZ6VTZ5B.js} +3 -3
- package/dist-cli/chunks/{chunk-MJRLLB4R.js → chunk-KQSJVNFE.js} +4 -4
- package/dist-cli/chunks/chunk-L4JZKVCC.js +6 -0
- package/dist-cli/chunks/{chunk-VFCMSYZK.js → chunk-LP2BTEJ6.js} +2 -2
- package/dist-cli/chunks/{chunk-2YR5BGA5.js → chunk-LT6UC7I3.js} +2 -2
- package/dist-cli/chunks/{chunk-OVFJFXUD.js → chunk-MCKQH4AM.js} +2 -2
- package/dist-cli/chunks/{chunk-TZFFR3SD.js → chunk-MF7WUHQ3.js} +2 -2
- package/dist-cli/chunks/{chunk-46EUUFJ5.js → chunk-MSSY4EEZ.js} +1 -1
- package/dist-cli/chunks/{chunk-WMIIKMGK.js → chunk-N5ZCYP5F.js} +2 -2
- package/dist-cli/chunks/{chunk-3NV2NCNX.js → chunk-NCKAD3V7.js} +2 -2
- package/dist-cli/chunks/{chunk-VNQEB4L7.js → chunk-O2KRHVTU.js} +2 -2
- package/dist-cli/chunks/{chunk-OZSSI4WN.js → chunk-OF2HTH25.js} +2 -2
- package/dist-cli/chunks/{chunk-GGRX24GF.js → chunk-OXS2IWO2.js} +2 -2
- package/dist-cli/chunks/{chunk-WWZIXIRD.js → chunk-PVL7XSPN.js} +1 -1
- package/dist-cli/chunks/{chunk-DZS6WPUI.js → chunk-PVPAP3TV.js} +1 -1
- package/dist-cli/chunks/{chunk-5YJCOWCH.js → chunk-PXNAZYGV.js} +1 -1
- package/dist-cli/chunks/chunk-QR3HTFRB.js +5 -0
- package/dist-cli/chunks/{chunk-5TPRP5QT.js → chunk-QR7MWAWE.js} +1 -1
- package/dist-cli/chunks/chunk-RJIEZ2NY.js +4 -0
- package/dist-cli/chunks/{chunk-UC6U3MML.js → chunk-RVM4EZ4E.js} +2 -2
- package/dist-cli/chunks/{chunk-2D2UPBBR.js → chunk-SZKSFMWS.js} +1 -1
- package/dist-cli/chunks/{chunk-7GN3LVWB.js → chunk-TEL7G234.js} +2 -2
- package/dist-cli/chunks/{chunk-GASE6UBA.js → chunk-U2HNIHBY.js} +1 -1
- package/dist-cli/chunks/chunk-UROXJVY3.js +27 -0
- package/dist-cli/chunks/{chunk-HI5TFJWN.js → chunk-WM32MGTJ.js} +2 -2
- package/dist-cli/chunks/{chunk-VZXWHRUZ.js → chunk-WMZ7NJAO.js} +89 -133
- package/dist-cli/chunks/{chunk-XEVZYVIW.js → chunk-WUXCH6WN.js} +10 -9
- package/dist-cli/chunks/chunk-XOXGC3EH.js +9 -0
- package/dist-cli/chunks/chunk-XSSHENNY.js +4 -0
- package/dist-cli/chunks/{chunk-OHAZNXLK.js → chunk-XZ6O67AR.js} +1 -1
- package/dist-cli/chunks/{chunk-QKDWYITG.js → chunk-Y2MWQKOS.js} +3 -3
- package/dist-cli/chunks/{chunk-YDGQTMQL.js → chunk-YY24QI25.js} +1 -1
- package/dist-cli/chunks/{chunk-WF3T4SVI.js → chunk-ZWX6DDIS.js} +2 -2
- package/dist-cli/chunks/{cleanup-P27PA6JI.js → cleanup-F3NUJ3SL.js} +2 -2
- package/dist-cli/chunks/cli-version-XIW6O3FG.js +4 -0
- package/dist-cli/chunks/{compat-ZD65FED3.js → compat-AX2SEPTH.js} +2 -2
- package/dist-cli/chunks/{config-XMJRNM2A.js → config-4QQO5JVU.js} +2 -2
- package/dist-cli/chunks/{control-KMIQT3QP.js → control-OCKKSOWA.js} +2 -2
- package/dist-cli/chunks/daemon-N5VLMYKQ.js +4 -0
- package/dist-cli/chunks/{debug-PT4HOP7N.js → debug-T6MWVEVX.js} +5 -5
- package/dist-cli/chunks/{desktop-S3FG72AK.js → desktop-NCW7R7CB.js} +3 -3
- package/dist-cli/chunks/{detox-B3D4IFCN.js → detox-G6GITO2X.js} +2 -2
- package/dist-cli/chunks/{device-XBNDSB2R.js → device-7BTEJCTH.js} +2 -2
- package/dist-cli/chunks/{diagnose-HMQXJE5N.js → diagnose-SC5LACE3.js} +2 -2
- package/dist-cli/chunks/{disk-cleanup-BLCZ5BSZ.js → disk-cleanup-KK56QQZA.js} +2 -2
- package/dist-cli/chunks/drivers-NPGVBWMG.js +4 -0
- package/dist-cli/chunks/{film-BJGTBYZB.js → film-WGYEHGIA.js} +3 -3
- package/dist-cli/chunks/flow-KQBIBM3N.js +4 -0
- package/dist-cli/chunks/help-G7CAKI65.js +4 -0
- package/dist-cli/chunks/{hidden-runtime-alias-ANOYADHM.js → hidden-runtime-alias-CWTZWH3L.js} +2 -2
- package/dist-cli/chunks/home-paths-EBT4XHAS.js +4 -0
- package/dist-cli/chunks/inspect-CCPZXVDU.js +4 -0
- package/dist-cli/chunks/install-desktop-NZ3VGTQZ.js +4 -0
- package/dist-cli/chunks/{login-FJ737MWG.js → login-GSYOTGVQ.js} +4 -4
- package/dist-cli/chunks/{logout-ZCNMMHMY.js → logout-JPRIR7YK.js} +2 -2
- package/dist-cli/chunks/{maestro-SZTNKLDF.js → maestro-LXKDNO5M.js} +3 -3
- package/dist-cli/chunks/{maestro-generate-DCFAIZ4H.js → maestro-generate-5ZIKOU53.js} +3 -3
- package/dist-cli/chunks/{mode-GRMQCRXR.js → mode-N3QHXCES.js} +2 -2
- package/dist-cli/chunks/{optional-demo-registry-W36EWFFB.js → optional-demo-registry-XDH6X24N.js} +2 -2
- package/dist-cli/chunks/{perf-QYBAAUZG.js → perf-VOPL26FM.js} +2 -2
- package/dist-cli/chunks/{permissions-3QCQ6VF4.js → permissions-CZFKM4UD.js} +2 -2
- package/dist-cli/chunks/{record-QPPC2S4E.js → record-FODSOGMA.js} +3 -3
- package/dist-cli/chunks/{report-issue-7NMFP4HK.js → report-issue-FBPCZKOV.js} +2 -2
- package/dist-cli/chunks/reset-3H35GFO3.js +4 -0
- package/dist-cli/chunks/runtime-4OVFRXV5.js +4 -0
- package/dist-cli/chunks/{screenshot-command-67AECJFB.js → screenshot-command-7S3FYVO7.js} +7 -7
- package/dist-cli/chunks/{screenshot-layers-ASWBYPJL.js → screenshot-layers-CDYHPNC3.js} +3 -3
- package/dist-cli/chunks/{screenshots-capture-PXA3HFQK.js → screenshots-capture-EBIIY2YU.js} +2 -2
- package/dist-cli/chunks/serve-NELNSCZP.js +44 -0
- package/dist-cli/chunks/{setup-7DWPMRSB.js → setup-DF5ZDDIV.js} +2 -2
- package/dist-cli/chunks/{skills-S3Y22TUA.js → skills-E2QZF6WZ.js} +2 -2
- package/dist-cli/chunks/state-MBZDKZ7U.js +14 -0
- package/dist-cli/chunks/{storage-XUIMJWAJ.js → storage-J5BMNVXM.js} +6 -6
- package/dist-cli/chunks/store-U3DXC7HH.js +4 -0
- package/dist-cli/chunks/telemetry-XIPJVT5E.js +4 -0
- package/dist-cli/chunks/{timeline-TMPLQPSP.js → timeline-MC6N5BHY.js} +2 -2
- package/dist-cli/chunks/{upgrade-7HDSIM7K.js → upgrade-7UTU53DH.js} +2 -2
- package/dist-cli/chunks/upload-2M2KOTYI.js +4 -0
- package/dist-cli/chunks/version-VNACNYQK.js +6 -0
- package/dist-cli/chunks/{web-DG3WBYD3.js → web-MGJ2TUKR.js} +2 -2
- package/dist-cli/chunks/{what-happened-XFVUTZR7.js → what-happened-7DLKH3OD.js} +3 -3
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract.cjs +20 -0
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +1 -1
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +67 -28
- package/dist-lib/host/bridge-host.cjs +140 -12
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +2815 -40
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/menu.cjs +1 -1
- package/dist-lib/menu.mjs +1 -1
- package/dist-lib/metro.cjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
- package/dist-lib/sdk.cjs +2549 -2061
- package/dist-lib/sdk.mjs +2543 -2061
- package/dist-lib/skills.cjs +480 -280
- package/dist-lib/vite.cjs +1 -1
- package/package.json +8 -2
- package/src/bridge-constants.ts +3 -4
- package/src/bridge-contract.ts +251 -0
- package/src/connect.ts +83 -0
- package/src/disk-cleanup.ts +30 -0
- package/src/home-paths.ts +81 -38
- package/src/host/bridge-host.ts +134 -6
- package/src/index.ts +27 -1
- package/src/sdk.ts +8 -0
- package/src/sim-client.ts +660 -0
- package/dist-cli/chunks/camera-VL73YIKP.js +0 -22
- package/dist-cli/chunks/chunk-4NPPOV2N.js +0 -5
- package/dist-cli/chunks/chunk-FSUYIVJ6.js +0 -9
- package/dist-cli/chunks/chunk-G2WW6L2C.js +0 -23
- package/dist-cli/chunks/chunk-KTHV3RUS.js +0 -26
- package/dist-cli/chunks/chunk-LF2ZVT7O.js +0 -6
- package/dist-cli/chunks/chunk-NFK7T35W.js +0 -4
- package/dist-cli/chunks/chunk-TIVZIMMW.js +0 -4
- package/dist-cli/chunks/cli-version-WWLPBDQ7.js +0 -4
- package/dist-cli/chunks/daemon-G2ME7NLB.js +0 -4
- package/dist-cli/chunks/drivers-LDECZGP2.js +0 -4
- package/dist-cli/chunks/flow-UEQNVTU7.js +0 -4
- package/dist-cli/chunks/help-T5FYSVGB.js +0 -4
- package/dist-cli/chunks/home-paths-GT3LFNOR.js +0 -4
- package/dist-cli/chunks/inspect-ZA6XF5LD.js +0 -4
- package/dist-cli/chunks/install-desktop-TIMUDHPL.js +0 -4
- package/dist-cli/chunks/runtime-XOAXMSTU.js +0 -4
- package/dist-cli/chunks/serve-BI2NBAXG.js +0 -44
- package/dist-cli/chunks/store-JTHEJLAZ.js +0 -4
- package/dist-cli/chunks/telemetry-ZYJGD2DB.js +0 -4
- package/dist-cli/chunks/upload-GMSZPWM6.js +0 -4
- package/dist-cli/chunks/version-HOCHZ37L.js +0 -6
|
@@ -0,0 +1,763 @@
|
|
|
1
|
+
// sootsim mode three — toggle the live browser 3d device stage.
|
|
2
|
+
//
|
|
3
|
+
// wraps the existing `toggle-three-mode` shell action so CLI, rail, and
|
|
4
|
+
// menu all use the same plugin-owned state path.
|
|
5
|
+
|
|
6
|
+
import { createHash } from 'node:crypto'
|
|
7
|
+
import { mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
8
|
+
import { dirname, extname, resolve } from 'node:path'
|
|
9
|
+
import {
|
|
10
|
+
callInBridgeWrite,
|
|
11
|
+
createBridgeFromParsed,
|
|
12
|
+
parseBridgeCliArgs,
|
|
13
|
+
type WsBridge,
|
|
14
|
+
} from '../ws-bridge'
|
|
15
|
+
import { runShellBooleanMode, type ShellBooleanModeOptions } from './shell-boolean-mode'
|
|
16
|
+
|
|
17
|
+
type ThreeModeConfig = Record<string, unknown>
|
|
18
|
+
type ThreeModeState = Record<string, unknown> & {
|
|
19
|
+
ready?: boolean
|
|
20
|
+
disabled?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const THREE_MODE_READY_TIMEOUT_MS = 12_000
|
|
24
|
+
// a supersampled path-traced frame (soft-orbs export) legitimately spends
|
|
25
|
+
// minutes on shader compile + sample convergence on slower GPUs; raster
|
|
26
|
+
// timeline batches stay far under this either way.
|
|
27
|
+
const THREE_MODE_RENDER_TIMEOUT_MS = 600_000
|
|
28
|
+
const TIMELINE_ACTIONS = new Set(['load', 'seek', 'play', 'pause', 'render', 'measure'])
|
|
29
|
+
|
|
30
|
+
export async function runThreeMode(args: string[], opts: ShellBooleanModeOptions) {
|
|
31
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
32
|
+
console.log(`
|
|
33
|
+
rnxsim mode three — toggle the 3d device stage
|
|
34
|
+
|
|
35
|
+
usage:
|
|
36
|
+
rnxsim mode three [on|off|toggle]
|
|
37
|
+
rnxsim mode three configure [options]
|
|
38
|
+
rnxsim mode three load <timeline.json>
|
|
39
|
+
rnxsim mode three seek <time-ms>
|
|
40
|
+
rnxsim mode three play|pause
|
|
41
|
+
rnxsim mode three render <timeline.json> --output <dir> [options]
|
|
42
|
+
rnxsim mode three measure <timeline.json> [options]
|
|
43
|
+
|
|
44
|
+
arguments:
|
|
45
|
+
on enable 3d mode
|
|
46
|
+
off disable 3d mode
|
|
47
|
+
toggle flip the current state (default)
|
|
48
|
+
|
|
49
|
+
configure options:
|
|
50
|
+
--device <id> set the phone body: iphone-17-air | iphone-17-pro |
|
|
51
|
+
pixel-10 (a film authors its own via initial.device)
|
|
52
|
+
--background <id> set the 3d background
|
|
53
|
+
--gradient <stops> custom gradient bg: comma-separated css hex stops,
|
|
54
|
+
e.g. "#0a0e1a,#16243f,#05080f" (overrides --background;
|
|
55
|
+
evenly spaced, or use "color@offset" for explicit %)
|
|
56
|
+
--gradient-angle <deg> custom gradient angle (default 180 = top→bottom)
|
|
57
|
+
--colorway <id> set the phone colorway
|
|
58
|
+
--script <id> apply a camera preset pose
|
|
59
|
+
--progress <0..1> script progress to sample (default: 1)
|
|
60
|
+
--animate run the script instead of sampling one frame
|
|
61
|
+
--slow run the script slowly when --animate is set
|
|
62
|
+
--environment on|off enable or disable environment objects
|
|
63
|
+
--env-shape <id> orb | pill | diamond | pane
|
|
64
|
+
--env-spread <id> tight | medium | wide
|
|
65
|
+
--env-size <id> small | medium | large
|
|
66
|
+
--env-color <id> peach | gold | mint | sky | violet
|
|
67
|
+
--env-glow on|off enable or disable environment glow
|
|
68
|
+
--env-plastic on|off enable or disable environment plastic material
|
|
69
|
+
--setting <id> none | room | white-backdrop
|
|
70
|
+
--focus on|off enable or disable Focus depth of field
|
|
71
|
+
--focus-amount <id> subtle | medium | strong | intense | extreme
|
|
72
|
+
--focus-range <id> tight | balanced | wide
|
|
73
|
+
--reset-pose return to the default hero pose before other changes
|
|
74
|
+
|
|
75
|
+
render options:
|
|
76
|
+
--output <dir> write frame-%05d.png + timeline-render.json
|
|
77
|
+
--fps <number> frames per second (default: 30)
|
|
78
|
+
--width <px> exact output width (default: 1920)
|
|
79
|
+
--height <px> exact output height (default: 1080)
|
|
80
|
+
--from <ms> first timeline time (default: 0)
|
|
81
|
+
--to <ms> exclusive end time (default: timeline duration)
|
|
82
|
+
--overwrite replace this command's existing frames in the dir
|
|
83
|
+
|
|
84
|
+
measure options (projected bounds only — no pixels rendered):
|
|
85
|
+
--fps <number> sample rate (default: 30)
|
|
86
|
+
--width <px> output width the bounds project into (default: 1920)
|
|
87
|
+
--height <px> output height the bounds project into (default: 1080)
|
|
88
|
+
--from <ms> first timeline time (default: 0)
|
|
89
|
+
--to <ms> exclusive end time (default: timeline duration)
|
|
90
|
+
--count <n> sample n frames from --from at the --fps spacing
|
|
91
|
+
--at <ms[,ms...]> sample exact timeline times (overrides from/to/count)
|
|
92
|
+
--output <file.json> write the measurement JSON here instead of stdout
|
|
93
|
+
|
|
94
|
+
examples:
|
|
95
|
+
rnxsim mode three
|
|
96
|
+
rnxsim mode three on
|
|
97
|
+
rnxsim mode three configure --script hero-arc --background gradient-tide
|
|
98
|
+
rnxsim mode three configure --environment on --env-shape pane --setting room
|
|
99
|
+
rnxsim mode three render ./cinematic.json --output ./frames --fps 30
|
|
100
|
+
rnxsim mode three measure ./cinematic.json --width 450 --height 900 --at 0,1200
|
|
101
|
+
rnxsim mode three off
|
|
102
|
+
`)
|
|
103
|
+
process.exit(0)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const timelineAction = findTimelineAction(args)
|
|
107
|
+
if (timelineAction) {
|
|
108
|
+
await runThreeModeTimelineAction(timelineAction, args, opts)
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (shouldConfigure(args)) {
|
|
113
|
+
await runThreeModeConfigure(args, opts)
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const result = await runShellBooleanMode(args, opts, {
|
|
118
|
+
modeKey: 'threeMode',
|
|
119
|
+
displayName: 'three-mode',
|
|
120
|
+
actionId: 'toggle-three-mode',
|
|
121
|
+
})
|
|
122
|
+
if (result.target) {
|
|
123
|
+
await waitForThreeModeRuntimeReady(args, opts)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function shouldConfigure(args: string[]) {
|
|
128
|
+
return (
|
|
129
|
+
args[0] === 'configure' ||
|
|
130
|
+
args.some((arg) =>
|
|
131
|
+
[
|
|
132
|
+
'--device',
|
|
133
|
+
'--background',
|
|
134
|
+
'--gradient',
|
|
135
|
+
'--gradient-angle',
|
|
136
|
+
'--colorway',
|
|
137
|
+
'--script',
|
|
138
|
+
'--progress',
|
|
139
|
+
'--animate',
|
|
140
|
+
'--slow',
|
|
141
|
+
'--environment',
|
|
142
|
+
'--env-shape',
|
|
143
|
+
'--env-spread',
|
|
144
|
+
'--env-size',
|
|
145
|
+
'--env-color',
|
|
146
|
+
'--env-glow',
|
|
147
|
+
'--env-plastic',
|
|
148
|
+
'--setting',
|
|
149
|
+
'--focus',
|
|
150
|
+
'--focus-amount',
|
|
151
|
+
'--focus-range',
|
|
152
|
+
'--reset-pose',
|
|
153
|
+
].includes(arg),
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function runThreeModeConfigure(args: string[], opts: ShellBooleanModeOptions) {
|
|
159
|
+
const parsed = parseBridgeCliArgs(args, {
|
|
160
|
+
port: opts.port,
|
|
161
|
+
stripBooleanFlags: ['--animate', '--slow', '--reset-pose'],
|
|
162
|
+
stripValueFlags: [
|
|
163
|
+
'--device',
|
|
164
|
+
'--background',
|
|
165
|
+
'--gradient',
|
|
166
|
+
'--gradient-angle',
|
|
167
|
+
'--colorway',
|
|
168
|
+
'--script',
|
|
169
|
+
'--progress',
|
|
170
|
+
'--environment',
|
|
171
|
+
'--env-shape',
|
|
172
|
+
'--env-spread',
|
|
173
|
+
'--env-size',
|
|
174
|
+
'--env-color',
|
|
175
|
+
'--env-glow',
|
|
176
|
+
'--env-plastic',
|
|
177
|
+
'--setting',
|
|
178
|
+
'--focus',
|
|
179
|
+
'--focus-amount',
|
|
180
|
+
'--focus-range',
|
|
181
|
+
],
|
|
182
|
+
})
|
|
183
|
+
const options = buildConfigureOptions(args)
|
|
184
|
+
const bridge = createBridgeFromParsed({
|
|
185
|
+
...parsed,
|
|
186
|
+
commandTimeoutMs: THREE_MODE_READY_TIMEOUT_MS + 1000,
|
|
187
|
+
})
|
|
188
|
+
try {
|
|
189
|
+
await waitForThreeModeRuntimeReadyOnBridge(bridge)
|
|
190
|
+
const state = await callInBridgeWrite<Record<string, unknown>>(
|
|
191
|
+
bridge,
|
|
192
|
+
'SootSim.bridges.threeMode.configure',
|
|
193
|
+
options,
|
|
194
|
+
)
|
|
195
|
+
console.log(` three-mode: configured (${summarizeState(state)})`)
|
|
196
|
+
} finally {
|
|
197
|
+
bridge.close()
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function waitForThreeModeRuntimeReady(
|
|
202
|
+
args: string[],
|
|
203
|
+
opts: ShellBooleanModeOptions,
|
|
204
|
+
) {
|
|
205
|
+
const parsed = parseBridgeCliArgs(args, { port: opts.port })
|
|
206
|
+
const bridge = createBridgeFromParsed({
|
|
207
|
+
...parsed,
|
|
208
|
+
commandTimeoutMs: THREE_MODE_READY_TIMEOUT_MS + 1000,
|
|
209
|
+
})
|
|
210
|
+
try {
|
|
211
|
+
await waitForThreeModeRuntimeReadyOnBridge(bridge)
|
|
212
|
+
} finally {
|
|
213
|
+
bridge.close()
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export async function waitForThreeModeRuntimeReadyOnBridge(bridge: WsBridge) {
|
|
218
|
+
const state = (await bridge.send({
|
|
219
|
+
type: 'evaluate',
|
|
220
|
+
code: `(async () => {
|
|
221
|
+
const deadline = Date.now() + ${THREE_MODE_READY_TIMEOUT_MS}
|
|
222
|
+
while (Date.now() < deadline) {
|
|
223
|
+
const settings = window.SootSim?.bridges?.settings?.get?.()
|
|
224
|
+
if (settings && settings.threeMode !== true) {
|
|
225
|
+
return { ready: false, disabled: true }
|
|
226
|
+
}
|
|
227
|
+
const bridge = window.SootSim?.bridges?.threeMode
|
|
228
|
+
const state = typeof bridge?.getState === 'function' ? bridge.getState() : null
|
|
229
|
+
if (state?.ready === true) return state
|
|
230
|
+
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
231
|
+
}
|
|
232
|
+
const bridge = window.SootSim?.bridges?.threeMode
|
|
233
|
+
const state = typeof bridge?.getState === 'function' ? bridge.getState() : null
|
|
234
|
+
return state || { ready: false }
|
|
235
|
+
})()`,
|
|
236
|
+
})) as ThreeModeState | null
|
|
237
|
+
|
|
238
|
+
if (state?.ready === true) return state
|
|
239
|
+
if (state?.disabled) {
|
|
240
|
+
throw new Error('3d mode is off; run `rnxsim mode three on` before configure')
|
|
241
|
+
}
|
|
242
|
+
throw new Error('3d mode runtime did not become ready')
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
type TimelineSampleResult = {
|
|
246
|
+
durationMs: number
|
|
247
|
+
timeMs: number
|
|
248
|
+
shotId: string | null
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
type TimelineRenderedFrame = {
|
|
252
|
+
index: number
|
|
253
|
+
timeMs: number
|
|
254
|
+
width: number
|
|
255
|
+
height: number
|
|
256
|
+
dataUrl: string
|
|
257
|
+
alpha: {
|
|
258
|
+
min: number
|
|
259
|
+
max: number
|
|
260
|
+
transparentPixels: number
|
|
261
|
+
}
|
|
262
|
+
phoneCollision: {
|
|
263
|
+
hull: Array<{ x: number; y: number }>
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
type TimelineRenderedFrames = {
|
|
268
|
+
frames: TimelineRenderedFrame[]
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
type TimelineMeasuredFrame = {
|
|
272
|
+
index: number
|
|
273
|
+
timeMs: number
|
|
274
|
+
camera: {
|
|
275
|
+
position: [number, number, number]
|
|
276
|
+
target: [number, number, number]
|
|
277
|
+
fov: number
|
|
278
|
+
}
|
|
279
|
+
targets: Record<
|
|
280
|
+
string,
|
|
281
|
+
{
|
|
282
|
+
hull: Array<{ x: number; y: number }>
|
|
283
|
+
bounds: { left: number; right: number; top: number; bottom: number }
|
|
284
|
+
}
|
|
285
|
+
>
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
type TimelineMeasuredFrames = {
|
|
289
|
+
fps: number
|
|
290
|
+
from: number
|
|
291
|
+
to: number
|
|
292
|
+
width: number
|
|
293
|
+
height: number
|
|
294
|
+
frames: TimelineMeasuredFrame[]
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function findTimelineAction(args: string[]): string | null {
|
|
298
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
299
|
+
const arg = args[index]
|
|
300
|
+
if (arg === '--sim' || arg === '--port' || arg === '-p') {
|
|
301
|
+
index += 1
|
|
302
|
+
continue
|
|
303
|
+
}
|
|
304
|
+
if (arg.startsWith('-')) continue
|
|
305
|
+
return TIMELINE_ACTIONS.has(arg) ? arg : null
|
|
306
|
+
}
|
|
307
|
+
return null
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function readFlagValue(args: string[], flag: string) {
|
|
311
|
+
const index = args.indexOf(flag)
|
|
312
|
+
return index >= 0 ? args[index + 1] : undefined
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function readNumberFlag(args: string[], flag: string, fallback: number, minimum = 0) {
|
|
316
|
+
const raw = readFlagValue(args, flag)
|
|
317
|
+
if (raw === undefined) return fallback
|
|
318
|
+
const value = Number.parseFloat(raw)
|
|
319
|
+
if (!Number.isFinite(value) || value < minimum) {
|
|
320
|
+
throw new Error(`${flag} expects a number greater than or equal to ${minimum}`)
|
|
321
|
+
}
|
|
322
|
+
return value
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
326
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function mimeTypeForAsset(path: string, contentType: unknown) {
|
|
330
|
+
const extension = extname(path).toLowerCase()
|
|
331
|
+
if (contentType === 'recording') {
|
|
332
|
+
if (extension === '.mp4' || extension === '.m4v') return 'video/mp4'
|
|
333
|
+
return 'video/webm'
|
|
334
|
+
}
|
|
335
|
+
if (extension === '.jpg' || extension === '.jpeg') return 'image/jpeg'
|
|
336
|
+
if (extension === '.webp') return 'image/webp'
|
|
337
|
+
return 'image/png'
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function inlineTimelineAssets(value: unknown, baseDirectory: string): unknown {
|
|
341
|
+
if (Array.isArray(value)) {
|
|
342
|
+
return value.map((entry) => inlineTimelineAssets(entry, baseDirectory))
|
|
343
|
+
}
|
|
344
|
+
if (!isRecord(value)) return value
|
|
345
|
+
|
|
346
|
+
const result: Record<string, unknown> = {}
|
|
347
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
348
|
+
result[key] = inlineTimelineAssets(entry, baseDirectory)
|
|
349
|
+
}
|
|
350
|
+
if (
|
|
351
|
+
(result.type === 'recording' ||
|
|
352
|
+
result.type === 'screenshot' ||
|
|
353
|
+
result.kind === 'screen') &&
|
|
354
|
+
typeof result.src === 'string' &&
|
|
355
|
+
!/^(?:blob:|data:|https?:)/i.test(result.src)
|
|
356
|
+
) {
|
|
357
|
+
const assetPath = resolve(baseDirectory, result.src)
|
|
358
|
+
const bytes = readFileSync(assetPath)
|
|
359
|
+
result.src = `data:${mimeTypeForAsset(assetPath, result.type === 'recording' ? 'recording' : 'screenshot')};base64,${bytes.toString('base64')}`
|
|
360
|
+
}
|
|
361
|
+
return result
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function readTimelineFile(path: string) {
|
|
365
|
+
const absolutePath = resolve(path)
|
|
366
|
+
const parsed: unknown = JSON.parse(readFileSync(absolutePath, 'utf8'))
|
|
367
|
+
return {
|
|
368
|
+
absolutePath,
|
|
369
|
+
timeline: inlineTimelineAssets(parsed, dirname(absolutePath)),
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async function ensureThreeModeRuntimeOnBridge(bridge: WsBridge) {
|
|
374
|
+
await callInBridgeWrite(bridge, 'SootSim.bridges.settings.set', 'threeMode', true)
|
|
375
|
+
await waitForThreeModeRuntimeReadyOnBridge(bridge)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function runThreeModeTimelineAction(
|
|
379
|
+
action: string,
|
|
380
|
+
args: string[],
|
|
381
|
+
opts: ShellBooleanModeOptions,
|
|
382
|
+
) {
|
|
383
|
+
const parsed = parseBridgeCliArgs(args, {
|
|
384
|
+
port: opts.port,
|
|
385
|
+
commandTimeoutMs: THREE_MODE_RENDER_TIMEOUT_MS,
|
|
386
|
+
stripBooleanFlags: ['--overwrite'],
|
|
387
|
+
stripValueFlags: [
|
|
388
|
+
'--output',
|
|
389
|
+
'--fps',
|
|
390
|
+
'--width',
|
|
391
|
+
'--height',
|
|
392
|
+
'--from',
|
|
393
|
+
'--to',
|
|
394
|
+
'--count',
|
|
395
|
+
'--at',
|
|
396
|
+
],
|
|
397
|
+
})
|
|
398
|
+
const bridge = createBridgeFromParsed(parsed)
|
|
399
|
+
try {
|
|
400
|
+
await ensureThreeModeRuntimeOnBridge(bridge)
|
|
401
|
+
if (action === 'seek') {
|
|
402
|
+
const rawTime = parsed.positional[1]
|
|
403
|
+
const timeMs = rawTime === undefined ? Number.NaN : Number.parseFloat(rawTime)
|
|
404
|
+
if (!Number.isFinite(timeMs)) throw new Error('three-mode seek expects time in ms')
|
|
405
|
+
const sample = await callInBridgeWrite<TimelineSampleResult>(
|
|
406
|
+
bridge,
|
|
407
|
+
'SootSim.bridges.threeMode.seek',
|
|
408
|
+
timeMs,
|
|
409
|
+
)
|
|
410
|
+
console.log(
|
|
411
|
+
` three-mode: ${Math.round(sample.timeMs)}ms / ${Math.round(sample.durationMs)}ms (${sample.shotId ?? 'no shot'})`,
|
|
412
|
+
)
|
|
413
|
+
return
|
|
414
|
+
}
|
|
415
|
+
if (action === 'play' || action === 'pause') {
|
|
416
|
+
const state = await callInBridgeWrite<Record<string, unknown>>(
|
|
417
|
+
bridge,
|
|
418
|
+
`SootSim.bridges.threeMode.${action}`,
|
|
419
|
+
)
|
|
420
|
+
const timeline = isRecord(state.timeline) ? state.timeline : {}
|
|
421
|
+
console.log(
|
|
422
|
+
` three-mode: ${action} (${Math.round(Number(timeline.timeMs ?? 0))}ms)`,
|
|
423
|
+
)
|
|
424
|
+
return
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const timelinePath = parsed.positional[1]
|
|
428
|
+
if (!timelinePath)
|
|
429
|
+
throw new Error(`three-mode ${action} expects a timeline JSON path`)
|
|
430
|
+
const loadedFile = readTimelineFile(timelinePath)
|
|
431
|
+
const loaded = await callInBridgeWrite<TimelineSampleResult>(
|
|
432
|
+
bridge,
|
|
433
|
+
'SootSim.bridges.threeMode.loadTimeline',
|
|
434
|
+
loadedFile.timeline,
|
|
435
|
+
)
|
|
436
|
+
if (action === 'load') {
|
|
437
|
+
console.log(
|
|
438
|
+
` three-mode: loaded ${loadedFile.absolutePath} (${Math.round(loaded.durationMs)}ms)`,
|
|
439
|
+
)
|
|
440
|
+
return
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (action === 'measure') {
|
|
444
|
+
const fps = readNumberFlag(args, '--fps', 30, 0.001)
|
|
445
|
+
const width = Math.round(readNumberFlag(args, '--width', 1_920, 1))
|
|
446
|
+
const height = Math.round(readNumberFlag(args, '--height', 1_080, 1))
|
|
447
|
+
const atValue = readFlagValue(args, '--at')
|
|
448
|
+
let measured: TimelineMeasuredFrames
|
|
449
|
+
if (atValue !== undefined) {
|
|
450
|
+
const times = atValue.split(',').map((token) => {
|
|
451
|
+
const value = Number.parseFloat(token)
|
|
452
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
453
|
+
throw new Error(`--at expects comma-separated times in ms, got "${token}"`)
|
|
454
|
+
}
|
|
455
|
+
return Math.min(value, loaded.durationMs)
|
|
456
|
+
})
|
|
457
|
+
const frames: TimelineMeasuredFrame[] = []
|
|
458
|
+
for (const timeMs of times) {
|
|
459
|
+
const single = await callInBridgeWrite<TimelineMeasuredFrames>(
|
|
460
|
+
bridge,
|
|
461
|
+
'SootSim.bridges.threeMode.measure',
|
|
462
|
+
{ fps, width, height, from: timeMs, count: 1 },
|
|
463
|
+
)
|
|
464
|
+
frames.push({ ...single.frames[0], index: frames.length })
|
|
465
|
+
}
|
|
466
|
+
measured = {
|
|
467
|
+
fps,
|
|
468
|
+
from: times[0],
|
|
469
|
+
to: times[times.length - 1],
|
|
470
|
+
width,
|
|
471
|
+
height,
|
|
472
|
+
frames,
|
|
473
|
+
}
|
|
474
|
+
} else {
|
|
475
|
+
const from = readNumberFlag(args, '--from', 0, 0)
|
|
476
|
+
const countValue = readFlagValue(args, '--count')
|
|
477
|
+
const options: {
|
|
478
|
+
fps: number
|
|
479
|
+
width: number
|
|
480
|
+
height: number
|
|
481
|
+
from: number
|
|
482
|
+
to?: number
|
|
483
|
+
count?: number
|
|
484
|
+
} = { fps, width, height, from }
|
|
485
|
+
if (countValue === undefined) {
|
|
486
|
+
const to = readNumberFlag(args, '--to', loaded.durationMs, 0)
|
|
487
|
+
if (to <= from || to > loaded.durationMs) {
|
|
488
|
+
throw new Error(
|
|
489
|
+
`--to must be greater than --from and at most ${loaded.durationMs}`,
|
|
490
|
+
)
|
|
491
|
+
}
|
|
492
|
+
options.to = to
|
|
493
|
+
} else {
|
|
494
|
+
options.count = Math.round(readNumberFlag(args, '--count', 1, 1))
|
|
495
|
+
}
|
|
496
|
+
measured = await callInBridgeWrite<TimelineMeasuredFrames>(
|
|
497
|
+
bridge,
|
|
498
|
+
'SootSim.bridges.threeMode.measure',
|
|
499
|
+
options,
|
|
500
|
+
)
|
|
501
|
+
}
|
|
502
|
+
const json = `${JSON.stringify(measured, null, 2)}\n`
|
|
503
|
+
const outputValue = readFlagValue(args, '--output')
|
|
504
|
+
if (outputValue) {
|
|
505
|
+
const outputPath = resolve(outputValue)
|
|
506
|
+
mkdirSync(dirname(outputPath), { recursive: true })
|
|
507
|
+
writeFileSync(outputPath, json)
|
|
508
|
+
// stderr keeps a caller piping stdout (phone-film bounds) JSON-clean.
|
|
509
|
+
process.stderr.write(
|
|
510
|
+
` three-mode: measured ${measured.frames.length} frames → ${outputPath}\n`,
|
|
511
|
+
)
|
|
512
|
+
} else {
|
|
513
|
+
process.stdout.write(json)
|
|
514
|
+
}
|
|
515
|
+
return
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const outputValue = readFlagValue(args, '--output')
|
|
519
|
+
if (!outputValue) throw new Error('three-mode render requires --output <dir>')
|
|
520
|
+
const outputDirectory = resolve(outputValue)
|
|
521
|
+
const fps = readNumberFlag(args, '--fps', 30, 0.001)
|
|
522
|
+
const width = Math.round(readNumberFlag(args, '--width', 1_920, 1))
|
|
523
|
+
const height = Math.round(readNumberFlag(args, '--height', 1_080, 1))
|
|
524
|
+
const from = readNumberFlag(args, '--from', 0, 0)
|
|
525
|
+
const to = readNumberFlag(args, '--to', loaded.durationMs, 0)
|
|
526
|
+
if (to <= from || to > loaded.durationMs) {
|
|
527
|
+
throw new Error(`--to must be greater than --from and at most ${loaded.durationMs}`)
|
|
528
|
+
}
|
|
529
|
+
const frameDurationMs = 1_000 / fps
|
|
530
|
+
const frameCount = Math.ceil((to - from) / frameDurationMs)
|
|
531
|
+
mkdirSync(outputDirectory, { recursive: true })
|
|
532
|
+
const ownedOutputs = readdirSync(outputDirectory).filter(
|
|
533
|
+
(name) => /^frame-\d{5}\.png$/.test(name) || name === 'timeline-render.json',
|
|
534
|
+
)
|
|
535
|
+
if (ownedOutputs.length > 0 && !args.includes('--overwrite')) {
|
|
536
|
+
throw new Error(
|
|
537
|
+
`${outputDirectory} already has timeline frames; pass --overwrite to replace them`,
|
|
538
|
+
)
|
|
539
|
+
}
|
|
540
|
+
for (const name of ownedOutputs) unlinkSync(resolve(outputDirectory, name))
|
|
541
|
+
|
|
542
|
+
const manifestFrames: Array<{
|
|
543
|
+
file: string
|
|
544
|
+
timeMs: number
|
|
545
|
+
sha256: string
|
|
546
|
+
alpha: TimelineRenderedFrame['alpha']
|
|
547
|
+
phoneCollision: TimelineRenderedFrame['phoneCollision']
|
|
548
|
+
}> = []
|
|
549
|
+
const batchSize = 4
|
|
550
|
+
for (let frameIndex = 0; frameIndex < frameCount; frameIndex += batchSize) {
|
|
551
|
+
const count = Math.min(batchSize, frameCount - frameIndex)
|
|
552
|
+
const batchFrom = from + frameIndex * frameDurationMs
|
|
553
|
+
const batchTo = Math.min(to, batchFrom + count * frameDurationMs)
|
|
554
|
+
const rendered = await callInBridgeWrite<TimelineRenderedFrames>(
|
|
555
|
+
bridge,
|
|
556
|
+
'SootSim.bridges.threeMode.renderFrames',
|
|
557
|
+
{ fps, from: batchFrom, to: batchTo, width, height, count },
|
|
558
|
+
)
|
|
559
|
+
if (rendered.frames.length !== count) {
|
|
560
|
+
throw new Error(
|
|
561
|
+
`3d renderer returned ${rendered.frames.length} frames for a ${count}-frame batch`,
|
|
562
|
+
)
|
|
563
|
+
}
|
|
564
|
+
for (const frame of rendered.frames) {
|
|
565
|
+
const globalIndex = frameIndex + frame.index
|
|
566
|
+
const file = `frame-${String(globalIndex).padStart(5, '0')}.png`
|
|
567
|
+
const separator = frame.dataUrl.indexOf(',')
|
|
568
|
+
if (
|
|
569
|
+
separator < 0 ||
|
|
570
|
+
!frame.dataUrl.slice(0, separator).includes('image/png;base64')
|
|
571
|
+
) {
|
|
572
|
+
throw new Error(`3d renderer returned a non-PNG frame at ${frame.timeMs}ms`)
|
|
573
|
+
}
|
|
574
|
+
const bytes = Buffer.from(frame.dataUrl.slice(separator + 1), 'base64')
|
|
575
|
+
writeFileSync(resolve(outputDirectory, file), bytes)
|
|
576
|
+
manifestFrames.push({
|
|
577
|
+
file,
|
|
578
|
+
timeMs: frame.timeMs,
|
|
579
|
+
sha256: createHash('sha256').update(bytes).digest('hex'),
|
|
580
|
+
alpha: frame.alpha,
|
|
581
|
+
phoneCollision: frame.phoneCollision,
|
|
582
|
+
})
|
|
583
|
+
}
|
|
584
|
+
process.stderr.write(
|
|
585
|
+
`\r three-mode: rendered ${Math.min(frameIndex + count, frameCount)}/${frameCount}`,
|
|
586
|
+
)
|
|
587
|
+
}
|
|
588
|
+
process.stderr.write('\n')
|
|
589
|
+
|
|
590
|
+
const manifest = {
|
|
591
|
+
version: 2,
|
|
592
|
+
timeline: loadedFile.absolutePath,
|
|
593
|
+
fps,
|
|
594
|
+
from,
|
|
595
|
+
to,
|
|
596
|
+
width,
|
|
597
|
+
height,
|
|
598
|
+
frameCount,
|
|
599
|
+
transparent: manifestFrames.every((frame) => frame.alpha.transparentPixels > 0),
|
|
600
|
+
frames: manifestFrames,
|
|
601
|
+
}
|
|
602
|
+
writeFileSync(
|
|
603
|
+
resolve(outputDirectory, 'timeline-render.json'),
|
|
604
|
+
`${JSON.stringify(manifest, null, 2)}\n`,
|
|
605
|
+
)
|
|
606
|
+
console.log(
|
|
607
|
+
` three-mode: wrote ${frameCount} deterministic RGBA frames to ${outputDirectory}`,
|
|
608
|
+
)
|
|
609
|
+
} finally {
|
|
610
|
+
bridge.close()
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
export function buildConfigureOptions(args: string[]): ThreeModeConfig {
|
|
615
|
+
const value = (flag: string) => args.find((_, i) => args[i - 1] === flag)
|
|
616
|
+
const options: ThreeModeConfig = {}
|
|
617
|
+
|
|
618
|
+
// the body itself. a timeline authors this through initial.device; this
|
|
619
|
+
// flag is for driving the live stage by hand.
|
|
620
|
+
const device = value('--device')
|
|
621
|
+
if (device) {
|
|
622
|
+
if (
|
|
623
|
+
device !== 'iphone-17-air' &&
|
|
624
|
+
device !== 'iphone-17-pro' &&
|
|
625
|
+
device !== 'pixel-10'
|
|
626
|
+
) {
|
|
627
|
+
throw new Error(
|
|
628
|
+
`--device expects iphone-17-air, iphone-17-pro, or pixel-10, got "${device}"`,
|
|
629
|
+
)
|
|
630
|
+
}
|
|
631
|
+
options.device = device
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const colorway = value('--colorway')
|
|
635
|
+
if (colorway) options.colorway = colorway
|
|
636
|
+
|
|
637
|
+
const background = value('--background')
|
|
638
|
+
if (background) options.background = background
|
|
639
|
+
|
|
640
|
+
// custom gradient: "color[@offset],color[@offset],…". offsets are
|
|
641
|
+
// 0..1 or 0..100 (auto-detected); omitted offsets spread evenly.
|
|
642
|
+
const gradient = value('--gradient')
|
|
643
|
+
if (gradient) {
|
|
644
|
+
const raw = gradient
|
|
645
|
+
.split(',')
|
|
646
|
+
.map((s) => s.trim())
|
|
647
|
+
.filter(Boolean)
|
|
648
|
+
const stops = raw.map((token, i) => {
|
|
649
|
+
const [color, offsetStr] = token.split('@')
|
|
650
|
+
let offset =
|
|
651
|
+
offsetStr !== undefined
|
|
652
|
+
? Number.parseFloat(offsetStr)
|
|
653
|
+
: raw.length > 1
|
|
654
|
+
? i / (raw.length - 1)
|
|
655
|
+
: 0
|
|
656
|
+
if (offset > 1) offset = offset / 100
|
|
657
|
+
if (!Number.isFinite(offset)) offset = raw.length > 1 ? i / (raw.length - 1) : 0
|
|
658
|
+
return { color: color.trim(), offset }
|
|
659
|
+
})
|
|
660
|
+
const angleStr = value('--gradient-angle')
|
|
661
|
+
const angleDeg = angleStr === undefined ? undefined : Number.parseFloat(angleStr)
|
|
662
|
+
if (angleStr !== undefined && !Number.isFinite(angleDeg)) {
|
|
663
|
+
throw new Error(`--gradient-angle expects a number, got "${angleStr}"`)
|
|
664
|
+
}
|
|
665
|
+
options.customGradient = { stops, ...(angleDeg !== undefined ? { angleDeg } : {}) }
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const setting = value('--setting')
|
|
669
|
+
if (setting) {
|
|
670
|
+
if (setting !== 'none' && setting !== 'room' && setting !== 'white-backdrop') {
|
|
671
|
+
throw new Error(`--setting expects none, room, or white-backdrop, got "${setting}"`)
|
|
672
|
+
}
|
|
673
|
+
options.setting = setting
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const focus: Record<string, unknown> = {}
|
|
677
|
+
const focusEnabled = parseBooleanValue(value('--focus'), '--focus')
|
|
678
|
+
if (focusEnabled !== undefined) focus.enabled = focusEnabled
|
|
679
|
+
for (const [flag, key] of [
|
|
680
|
+
['--focus-amount', 'amount'],
|
|
681
|
+
['--focus-range', 'range'],
|
|
682
|
+
] as const) {
|
|
683
|
+
const setting = value(flag)
|
|
684
|
+
if (setting) focus[key] = setting
|
|
685
|
+
}
|
|
686
|
+
if (Object.keys(focus).length > 0) options.focus = focus
|
|
687
|
+
|
|
688
|
+
if (args.includes('--reset-pose')) options.resetPose = true
|
|
689
|
+
|
|
690
|
+
const environment = parseBooleanValue(value('--environment'), '--environment')
|
|
691
|
+
const pins: Record<string, string> = {}
|
|
692
|
+
const effects: Record<string, boolean> = {}
|
|
693
|
+
for (const [flag, key] of [
|
|
694
|
+
['--env-shape', 'shape'],
|
|
695
|
+
['--env-spread', 'spread'],
|
|
696
|
+
['--env-size', 'size'],
|
|
697
|
+
['--env-color', 'color'],
|
|
698
|
+
] as const) {
|
|
699
|
+
const pin = value(flag)
|
|
700
|
+
if (pin) pins[key] = pin
|
|
701
|
+
}
|
|
702
|
+
const glow = parseBooleanValue(value('--env-glow'), '--env-glow')
|
|
703
|
+
if (glow !== undefined) effects.glow = glow
|
|
704
|
+
const plastic = parseBooleanValue(value('--env-plastic'), '--env-plastic')
|
|
705
|
+
if (plastic !== undefined) effects.plastic = plastic
|
|
706
|
+
|
|
707
|
+
if (
|
|
708
|
+
environment !== undefined ||
|
|
709
|
+
Object.keys(pins).length > 0 ||
|
|
710
|
+
Object.keys(effects).length > 0
|
|
711
|
+
) {
|
|
712
|
+
options.environment =
|
|
713
|
+
environment === false
|
|
714
|
+
? false
|
|
715
|
+
: {
|
|
716
|
+
pins,
|
|
717
|
+
effects,
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const scriptId = value('--script')
|
|
722
|
+
if (scriptId) {
|
|
723
|
+
const progressValue = value('--progress')
|
|
724
|
+
const progress = progressValue === undefined ? 1 : Number.parseFloat(progressValue)
|
|
725
|
+
if (!Number.isFinite(progress)) {
|
|
726
|
+
throw new Error(`--progress expects a number from 0 to 1, got "${progressValue}"`)
|
|
727
|
+
}
|
|
728
|
+
options.script = {
|
|
729
|
+
id: scriptId,
|
|
730
|
+
progress,
|
|
731
|
+
animate: args.includes('--animate'),
|
|
732
|
+
slow: args.includes('--slow'),
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
return options
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function parseBooleanValue(value: string | undefined, flag: string): boolean | undefined {
|
|
740
|
+
if (value === undefined) return undefined
|
|
741
|
+
const normalized = value.toLowerCase()
|
|
742
|
+
if (['on', 'true', 'yes', '1'].includes(normalized)) return true
|
|
743
|
+
if (['off', 'false', 'no', '0'].includes(normalized)) return false
|
|
744
|
+
throw new Error(`${flag} expects on or off, got "${value}"`)
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function summarizeState(state: Record<string, unknown>) {
|
|
748
|
+
const parts = [
|
|
749
|
+
`ready=${Boolean(state.ready)}`,
|
|
750
|
+
`background=${String(state.background ?? 'unknown')}`,
|
|
751
|
+
`colorway=${String(state.colorway ?? 'unknown')}`,
|
|
752
|
+
`environment=${state.environmentActive ? 'on' : 'off'}`,
|
|
753
|
+
`setting=${String(state.setting ?? 'none')}`,
|
|
754
|
+
`focus=${
|
|
755
|
+
state.focus &&
|
|
756
|
+
typeof state.focus === 'object' &&
|
|
757
|
+
(state.focus as { enabled?: boolean }).enabled
|
|
758
|
+
? 'on'
|
|
759
|
+
: 'off'
|
|
760
|
+
}`,
|
|
761
|
+
]
|
|
762
|
+
return parts.join(', ')
|
|
763
|
+
}
|