cozyclay 2.1.1 → 3.0.0

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 (577) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +12 -30
  3. package/bin/agent/agent-routes.mjs +40 -113
  4. package/bin/agent/agent-runner.mjs +9 -10
  5. package/bin/agent/cliproxy-provider.mjs +135 -0
  6. package/bin/agent/credential-store.mjs +30 -7
  7. package/bin/agent/pi-tools.mjs +4 -1
  8. package/bin/agent/provider-keys.mjs +32 -0
  9. package/bin/agent/provider-login.mjs +110 -0
  10. package/bin/agent/provider-routes.mjs +99 -0
  11. package/bin/agent/providers.mjs +19 -97
  12. package/bin/agent/script-tool.mjs +17 -4
  13. package/bin/agent/studio-motion-reference.mjs +23 -0
  14. package/bin/agent/studio-prompt.mjs +1 -1
  15. package/bin/agent/studio-rebase-text.mjs +32 -0
  16. package/bin/agent/studio-rebase.mjs +26 -39
  17. package/bin/agent/studio-tools.mjs +9 -28
  18. package/bin/agent/tool-arguments.mjs +49 -0
  19. package/bin/agent/turn-frames.mjs +66 -0
  20. package/bin/codex-auth.mjs +9 -2
  21. package/bin/cozyclay.mjs +10 -116
  22. package/bin/live/cli.mjs +3 -3
  23. package/bin/mcp-runtime.mjs +2 -12
  24. package/bin/package-signature.mjs +0 -4
  25. package/dist/app/index.html +7 -6
  26. package/dist/assets/EXRLoader-CvVIvGVE.js +1 -0
  27. package/dist/assets/analytics-C9sUnndd.js +1 -0
  28. package/dist/assets/app-CCh--1Hc.js +5087 -0
  29. package/dist/assets/app-f-fkV5uP.css +1 -0
  30. package/dist/assets/compile-worker-CkxiD6lH.js +5 -0
  31. package/dist/assets/fflate.module-KbGj5-H6.js +5 -0
  32. package/dist/assets/{first-shot-handoff-msLI71g1.js → first-shot-handoff-CC4xe4HM.js} +1 -1
  33. package/dist/assets/{landing-ChUvB_zL.js → landing-DjHD8u7k.js} +1 -1
  34. package/dist/assets/scene-asset-sync-Hx_FzdOH.js +20 -0
  35. package/dist/assets/scene-asset-sync-_S8NQl5B.css +1 -0
  36. package/dist/assets/workflow-C4yKH1p4.js +179 -0
  37. package/dist/cozyclay-package.json +1 -1
  38. package/dist/index.html +3 -3
  39. package/dist/sw.js +3 -6
  40. package/dist/workflow/index.html +5 -4
  41. package/mcp/LIVE-PROTOCOL.md +7 -47
  42. package/mcp/README.md +16 -26
  43. package/mcp/live-hub.mjs +6 -10
  44. package/mcp/package.json +1 -3
  45. package/mcp/tool-handlers.mjs +4 -168
  46. package/package.json +3 -22
  47. package/src/App.jsx +150 -2624
  48. package/src/agent-progress.js +79 -0
  49. package/src/agent-turn-mark.js +30 -0
  50. package/src/analytics.js +1 -19
  51. package/src/app-context.js +5 -0
  52. package/src/app-stage.jsx +2 -34
  53. package/src/asset-pane.jsx +6 -5
  54. package/src/camera-block.js +21 -0
  55. package/src/camera-direction.js +120 -0
  56. package/src/character-kind.js +3 -9
  57. package/src/command-bus.js +8 -6
  58. package/src/commands/cast.js +5 -34
  59. package/src/commands/elements/character.js +1 -1
  60. package/src/commands/elements/motion.js +1 -6
  61. package/src/commands/elements/object.js +6 -2
  62. package/src/commands/elements/shot.js +4 -1
  63. package/src/commands/elements.js +38 -14
  64. package/src/commands/index.js +3 -1
  65. package/src/commands/kits.js +37 -0
  66. package/src/commands/motion-timing.js +57 -0
  67. package/src/commands/motion.js +9 -152
  68. package/src/commands/motion3-compile.js +65 -0
  69. package/src/commands/motion3.js +207 -0
  70. package/src/commands/objects.js +6 -4
  71. package/src/commands/shot.js +49 -11
  72. package/src/commands/stage.js +14 -1
  73. package/src/commands/view.js +4 -1
  74. package/src/document-mirrors.js +42 -0
  75. package/src/document-store.js +4 -0
  76. package/src/domains/cast.js +19 -194
  77. package/src/domains/motion/motion-range-pin-rules.js +1 -1
  78. package/src/domains/motion/take-source.js +51 -0
  79. package/src/domains/motion.js +60 -1838
  80. package/src/domains/scenes.js +12 -55
  81. package/src/domains/shots.js +2 -2
  82. package/src/domains/stage.js +4 -2
  83. package/src/draft-ghost.js +46 -0
  84. package/src/draft-ghost.jsx +129 -0
  85. package/src/dualview.jsx +4 -3
  86. package/src/effects/EffectsLayer.jsx +112 -0
  87. package/src/effects/effect-meshes.js +232 -0
  88. package/src/effects/effect-record.js +103 -0
  89. package/src/effects/emitter.js +59 -0
  90. package/src/effects/simulate.js +261 -0
  91. package/src/effects/state.js +59 -0
  92. package/src/execution-telemetry.js +2 -3
  93. package/src/first-success-guide.jsx +5 -5
  94. package/src/ghost-set.js +257 -0
  95. package/src/held-object-edit.jsx +113 -0
  96. package/src/held-object-pose.js +175 -0
  97. package/src/hierarchy-model.js +1 -1
  98. package/src/hierarchy-panel.jsx +1 -1
  99. package/src/imported-mesh.jsx +103 -0
  100. package/src/kits/held.js +84 -0
  101. package/src/kits/index.js +70 -0
  102. package/src/kits/smoking.js +57 -0
  103. package/src/light-blockers.js +93 -0
  104. package/src/motion/beat-board-model.js +84 -0
  105. package/src/motion/beat-board.css +124 -0
  106. package/src/motion/beat-board.jsx +59 -0
  107. package/src/motion/center-of-mass.js +66 -0
  108. package/src/motion/fix-collisions.js +850 -0
  109. package/src/motion/physics-review.js +118 -0
  110. package/src/{ardy → motion}/physics-support.js +1 -76
  111. package/src/motion/rig-snapshot.js +22 -0
  112. package/src/motion/take-segments.jsx +140 -0
  113. package/src/{ardy → motion}/timeline-coordinates.js +0 -21
  114. package/src/{ardy → motion}/timeline.css +1 -2
  115. package/src/{ardy → motion}/timeline.jsx +42 -413
  116. package/src/motion/waypoints.js +76 -0
  117. package/src/motion3/beats.js +83 -0
  118. package/src/motion3/body.js +158 -0
  119. package/src/motion3/bones.js +47 -0
  120. package/src/motion3/carried.js +47 -0
  121. package/src/motion3/compile-job.js +75 -0
  122. package/src/motion3/compile-worker.js +10 -0
  123. package/src/motion3/compiler/anchors.js +105 -0
  124. package/src/motion3/compiler/arm-orient.js +128 -0
  125. package/src/motion3/compiler/channels.js +125 -0
  126. package/src/motion3/compiler/compile.js +205 -0
  127. package/src/motion3/compiler/gait-rhythm.js +97 -0
  128. package/src/motion3/compiler/grip.js +171 -0
  129. package/src/motion3/compiler/head-steady.js +48 -0
  130. package/src/motion3/compiler/hold.js +134 -0
  131. package/src/motion3/compiler/idle.js +113 -0
  132. package/src/motion3/compiler/limbs.js +170 -0
  133. package/src/motion3/compiler/math.js +72 -0
  134. package/src/motion3/compiler/pose.js +162 -0
  135. package/src/motion3/compiler/quat.js +49 -0
  136. package/src/motion3/compiler/skeleton.js +74 -0
  137. package/src/motion3/compiler/validate-common.js +32 -0
  138. package/src/motion3/compiler/validate-goals.js +118 -0
  139. package/src/motion3/compiler/validate-objects.js +143 -0
  140. package/src/motion3/compiler/validate-parts.js +116 -0
  141. package/src/motion3/compiler/validate.js +122 -0
  142. package/src/motion3/compiler/vocab.js +73 -0
  143. package/src/motion3/compiler/walk-pelvis.js +275 -0
  144. package/src/motion3/compiler/walk.js +207 -0
  145. package/src/motion3/compiler/wrist-clamp.js +32 -0
  146. package/src/motion3/delta.js +47 -0
  147. package/src/motion3/describe.js +174 -0
  148. package/src/motion3/docs.js +27 -0
  149. package/src/motion3/edit-carry.js +21 -0
  150. package/src/motion3/fix/ease.js +16 -0
  151. package/src/motion3/fix/ground.js +37 -0
  152. package/src/motion3/fix/index.js +52 -0
  153. package/src/motion3/fix/push.js +71 -0
  154. package/src/motion3/fix/reach.js +46 -0
  155. package/src/motion3/fix/retime.js +79 -0
  156. package/src/motion3/fix/self.js +211 -0
  157. package/src/motion3/fix/slide.js +70 -0
  158. package/src/motion3/fix/turn.js +51 -0
  159. package/src/motion3/follow.js +39 -0
  160. package/src/motion3/free-motion.js +173 -0
  161. package/src/motion3/grip-choice.js +130 -0
  162. package/src/motion3/hold-plan.js +157 -0
  163. package/src/motion3/json-patch.js +77 -0
  164. package/src/motion3/key-aim.js +105 -0
  165. package/src/motion3/key-edit.js +82 -0
  166. package/src/motion3/lenient.js +127 -0
  167. package/src/motion3/object-keys.js +236 -0
  168. package/src/motion3/object-pose.js +182 -0
  169. package/src/motion3/object-tracks.js +12 -0
  170. package/src/motion3/pipeline.js +102 -0
  171. package/src/motion3/playback.js +51 -0
  172. package/src/motion3/probes.js +91 -0
  173. package/src/motion3/qa/body-checks.js +34 -0
  174. package/src/motion3/qa/check-names.js +14 -0
  175. package/src/motion3/qa/checks.js +121 -0
  176. package/src/motion3/qa/contact-checks.js +51 -0
  177. package/src/motion3/qa/explain.js +80 -0
  178. package/src/motion3/qa/fmt.js +5 -0
  179. package/src/motion3/qa/foot-checks.js +72 -0
  180. package/src/motion3/qa/geometry.js +77 -0
  181. package/src/motion3/qa/goals.js +191 -0
  182. package/src/motion3/qa/ground.js +60 -0
  183. package/src/motion3/qa/hints-contact.js +179 -0
  184. package/src/motion3/qa/hints-goal.js +160 -0
  185. package/src/motion3/qa/hints-knee.js +24 -0
  186. package/src/motion3/qa/hints-motion.js +209 -0
  187. package/src/motion3/qa/hints-object.js +91 -0
  188. package/src/motion3/qa/index.js +42 -0
  189. package/src/motion3/qa/key-checks.js +82 -0
  190. package/src/motion3/qa/near.js +96 -0
  191. package/src/motion3/qa/object-checks.js +135 -0
  192. package/src/motion3/qa/patch.js +161 -0
  193. package/src/motion3/qa/severity.js +38 -0
  194. package/src/motion3/qa/spec-checks.js +65 -0
  195. package/src/motion3/qa/style.js +218 -0
  196. package/src/motion3/qa/wrist-checks.js +69 -0
  197. package/src/motion3/qa/wrist-obs.js +60 -0
  198. package/src/motion3/refs.js +120 -0
  199. package/src/motion3/repair-tune.js +121 -0
  200. package/src/motion3/repair.js +83 -0
  201. package/src/motion3/report.js +67 -0
  202. package/src/motion3/rigs/index.js +14 -0
  203. package/src/motion3/rigs/x-bot-tpose-body.json +1 -0
  204. package/src/motion3/rigs/x-bot-tpose.json +1 -0
  205. package/src/motion3/rigs/y-bot-tpose-body.json +1 -0
  206. package/src/motion3/rigs/y-bot-tpose.json +1 -0
  207. package/src/motion3/scene.js +60 -0
  208. package/src/motion3/schema/brief.js +89 -0
  209. package/src/motion3/schema/fields.js +44 -0
  210. package/src/motion3/schema/spec-examples.js +46 -0
  211. package/src/motion3/schema/topics.js +111 -0
  212. package/src/motion3/schema/types.js +147 -0
  213. package/src/motion3/search.js +117 -0
  214. package/src/motion3/second-hand.js +112 -0
  215. package/src/motion3/skills/basic.js +82 -0
  216. package/src/motion3/skills/book.js +76 -0
  217. package/src/motion3/skills/car.js +145 -0
  218. package/src/motion3/skills/frames-auto.js +29 -0
  219. package/src/motion3/skills/index.js +83 -0
  220. package/src/motion3/skills/info.js +65 -0
  221. package/src/motion3/skills/smoke-kit.js +109 -0
  222. package/src/motion3/skills/smoke.js +160 -0
  223. package/src/motion3/spec-store.js +59 -0
  224. package/src/motion3/take-edit.js +155 -0
  225. package/src/motion3/take.js +63 -0
  226. package/src/motion3/timeline.js +127 -0
  227. package/src/motion3/views.js +121 -0
  228. package/src/motion3/walk-plan.js +119 -0
  229. package/src/object-catalog.jsx +7 -1
  230. package/src/object-colors.js +98 -0
  231. package/src/object-gizmo-parts.js +115 -0
  232. package/src/object-gizmo.jsx +37 -122
  233. package/src/object-glow.js +139 -0
  234. package/src/object-hidden.js +33 -0
  235. package/src/object-look.js +196 -0
  236. package/src/object-size.js +10 -0
  237. package/src/object-travel.js +71 -6
  238. package/src/panels/CameraPanel.jsx +7 -7
  239. package/src/panels/HdrEnvironmentControls.jsx +59 -0
  240. package/src/panels/LightPanel.jsx +25 -4
  241. package/src/panels/ObjectLookPanel.jsx +88 -0
  242. package/src/panels/PosePanel.jsx +2 -17
  243. package/src/panels/RigControlPanel.jsx +5 -53
  244. package/src/panels/SubjectBox.jsx +4 -1
  245. package/src/panels/details.css +14 -0
  246. package/src/posestudio.jsx +4 -27
  247. package/src/project-browser.jsx +3 -4
  248. package/src/project-resources.js +18 -31
  249. package/src/project.js +13 -104
  250. package/src/props.jsx +25 -280
  251. package/src/rail-dot-drag.js +23 -0
  252. package/src/render-passes.js +10 -0
  253. package/src/resource-status.jsx +1 -0
  254. package/src/result-modal.jsx +4 -19
  255. package/src/room.jsx +20 -12
  256. package/src/scene-assets.js +8 -6
  257. package/src/scene-light-place.js +72 -0
  258. package/src/scene-lights.jsx +148 -0
  259. package/src/scene-objects.js +31 -139
  260. package/src/scenes.js +15 -105
  261. package/src/semantic-edit.js +2 -5
  262. package/src/set-pieces.jsx +183 -0
  263. package/src/settings-menu.jsx +3 -11
  264. package/src/shell/BottomDock.jsx +9 -51
  265. package/src/shell/DetailsSlot.jsx +16 -200
  266. package/src/shell/GhostsMenuGroup.jsx +74 -0
  267. package/src/shell/LibrarySlot.jsx +1 -3
  268. package/src/shell/MenuBar.jsx +3 -3
  269. package/src/shell/PreferencesDialog.jsx +15 -264
  270. package/src/shell/PreferencesSlot.jsx +2 -8
  271. package/src/shell/TopBar.jsx +5 -84
  272. package/src/shell/ViewportToolbar.jsx +10 -70
  273. package/src/shell/agent-glass.css +19 -6
  274. package/src/shell/details-selection.js +42 -0
  275. package/src/shell/ghosts-menu.css +22 -0
  276. package/src/shell/glass-regions.css +29 -3
  277. package/src/shell/preferences.css +6 -1
  278. package/src/shot-authoring.js +4 -7
  279. package/src/soft-shadows.js +37 -0
  280. package/src/stage-attach-frames.js +12 -12
  281. package/src/stage-canvas-rigs.jsx +3 -3
  282. package/src/stage-capture.jsx +42 -8
  283. package/src/stage-characters.jsx +22 -216
  284. package/src/stage-editor-gizmos.jsx +6 -6
  285. package/src/stage-environment.js +93 -0
  286. package/src/stage-hdr-builtins.js +107 -0
  287. package/src/stage-hdr-image.js +116 -0
  288. package/src/stage-hdr-library.js +70 -0
  289. package/src/stage-hdr-map.js +94 -0
  290. package/src/stage-hdr.js +130 -0
  291. package/src/stage-lighting.js +149 -0
  292. package/src/stage-occlusion.js +210 -0
  293. package/src/stage-sky.jsx +159 -0
  294. package/src/stage-startup-defaults.js +5 -9
  295. package/src/studio-actions.js +12 -14
  296. package/src/studio-agent-commands.js +19 -16
  297. package/src/studio-agent-motion.js +9 -9
  298. package/src/studio-agent-protocol.js +41 -71
  299. package/src/studio-app-binding.js +25 -45
  300. package/src/studio-asset-list.js +17 -0
  301. package/src/studio-contact-sheet.js +37 -5
  302. package/src/studio-elements.js +27 -7
  303. package/src/studio-entity-bounds.js +18 -0
  304. package/src/studio-error-codes.js +8 -0
  305. package/src/studio-export-framing.js +152 -0
  306. package/src/studio-lighting-check.js +103 -0
  307. package/src/studio-motion-visual.js +150 -0
  308. package/src/studio-schema-errors.js +80 -0
  309. package/src/studio-support-gap.js +74 -0
  310. package/src/styles.css +1 -19
  311. package/src/timeline-defaults.js +5 -0
  312. package/src/timeline-extent.js +8 -20
  313. package/src/tutorial-walk.js +17 -0
  314. package/src/ui-scrub.js +1 -1
  315. package/src/use-shot-camera-tracks.js +12 -0
  316. package/src/view-ghosts.js +97 -0
  317. package/src/view-ghosts.jsx +49 -0
  318. package/src/workflow/AgentPanel.jsx +47 -166
  319. package/src/workflow/AgentProgressCard.jsx +107 -0
  320. package/src/workflow/AgentReplyText.jsx +37 -0
  321. package/src/workflow/AgentTurnSteps.jsx +84 -0
  322. package/src/workflow/LoginPanel.jsx +140 -0
  323. package/src/workflow/WorkflowBuilder.jsx +7 -23
  324. package/src/workflow/agent-client.js +54 -51
  325. package/src/workflow/agent-panel.css +71 -1
  326. package/src/workflow/agent-progress-card.css +167 -0
  327. package/src/workflow/agent-progress-view.js +47 -0
  328. package/src/workflow/agent-turn-steps.css +113 -0
  329. package/src/workflow/agent-turn.js +191 -0
  330. package/src/workflow/login-client.js +34 -0
  331. package/src/workflow/motion-input.js +18 -31
  332. package/src/workflow/node-schema.js +1 -1
  333. package/src/workflow/reply-markdown.js +69 -0
  334. package/src/workflow/scene-asset-sync.js +8 -8
  335. package/tools/agent-session-stats.mjs +61 -0
  336. package/tools/dev-full.mjs +5 -56
  337. package/tools/eslint-baseline.json +12 -13
  338. package/tools/knip-baseline.json +0 -179
  339. package/tools/loc-baseline.json +31 -81
  340. package/tools/motion3/extract-body.mjs +103 -0
  341. package/tools/motion3/extract-rig.mjs +85 -0
  342. package/tools/process-supervisor.mjs +0 -88
  343. package/tools/run-tests.mjs +8 -124
  344. package/bin/agent/motion-runtime.mjs +0 -76
  345. package/dist/ardy/cskel27-rest.json +0 -557
  346. package/dist/assets/analytics-C9pMpW7s.js +0 -1
  347. package/dist/assets/app-B_2XB9rw.css +0 -1
  348. package/dist/assets/app-DAuvKPP8.js +0 -4920
  349. package/dist/assets/config-Bx7pYsD9.js +0 -1
  350. package/dist/assets/config-DeoFTYzb.css +0 -1
  351. package/dist/assets/demo-CvKuBgy5.js +0 -1
  352. package/dist/assets/scene-asset-sync-DKkpjKBV.js +0 -19
  353. package/dist/assets/scene-asset-sync-DjJ_cJ2R.css +0 -1
  354. package/dist/assets/ticket-BPk2Ni95.js +0 -1
  355. package/dist/assets/workflow-jJRBR94Z.js +0 -179
  356. package/dist/demo/walk-then-stop.npz +0 -0
  357. package/mcp/ardy-prompts.mjs +0 -218
  358. package/src/ardy/auto-fix-panel.css +0 -108
  359. package/src/ardy/auto-physics.js +0 -1194
  360. package/src/ardy/client.js +0 -153
  361. package/src/ardy/convert.js +0 -313
  362. package/src/ardy/cskel27-neutral.js +0 -39
  363. package/src/ardy/cskel27.js +0 -77
  364. package/src/ardy/export.js +0 -165
  365. package/src/ardy/fix-collisions.js +0 -2257
  366. package/src/ardy/foot-lock.js +0 -230
  367. package/src/ardy/motion-calibration.js +0 -118
  368. package/src/ardy/motion-edit.js +0 -213
  369. package/src/ardy/motion-url.js +0 -55
  370. package/src/ardy/mp4-duration.js +0 -102
  371. package/src/ardy/npz.js +0 -595
  372. package/src/ardy/physics-panel.css +0 -21
  373. package/src/ardy/physics-panel.jsx +0 -89
  374. package/src/ardy/physics-refine.js +0 -111
  375. package/src/ardy/physics-review.js +0 -567
  376. package/src/ardy/physics-temporal.js +0 -54
  377. package/src/ardy/platform-fit-panel.css +0 -64
  378. package/src/ardy/platform-fit-panel.jsx +0 -41
  379. package/src/ardy/platform-fit.js +0 -326
  380. package/src/ardy/playback.js +0 -711
  381. package/src/ardy/pose-pin.js +0 -97
  382. package/src/ardy/prompt-clips.js +0 -30
  383. package/src/ardy/retime.js +0 -211
  384. package/src/ardy/root-drop.js +0 -369
  385. package/src/ardy/timeline-resize.js +0 -13
  386. package/src/ardy/to-cskel27.js +0 -225
  387. package/src/ardy/trim.js +0 -33
  388. package/src/ardy/waypoints.js +0 -369
  389. package/src/ardy-bridge-contract.js +0 -281
  390. package/src/domains/motion/motion-video-capture.js +0 -664
  391. package/src/i2v-motion-client.js +0 -77
  392. package/src/i2v-motion-studio.jsx +0 -180
  393. package/src/line-edit.js +0 -1348
  394. package/src/motion/generation.js +0 -105
  395. package/src/motion-readiness-ui.jsx +0 -67
  396. package/src/motion-readiness.js +0 -40
  397. package/src/motion-resources.js +0 -177
  398. package/src/motion-store.js +0 -65
  399. package/src/motion-trail.js +0 -601
  400. package/src/multimodel-ingest.js +0 -394
  401. package/src/panels/PromptBlocksPanel.jsx +0 -444
  402. package/src/panels/VideoCapturePanel.jsx +0 -181
  403. package/src/pose-extract/browser.js +0 -42
  404. package/src/pose-extract/detector.js +0 -100
  405. package/src/pose-extract/fit.js +0 -227
  406. package/src/pose-extract/image-frame.js +0 -51
  407. package/src/pose-extract/index.js +0 -58
  408. package/src/pose-extract/keyframes.js +0 -74
  409. package/src/pose-extract/landmarks.js +0 -162
  410. package/src/pose-extract/mirror.js +0 -153
  411. package/src/pose-extract/take.js +0 -131
  412. package/src/pose-extract/video-frames.js +0 -91
  413. package/src/take-recipe.js +0 -165
  414. package/src/trail-key-conflicts.js +0 -39
  415. package/src/trail-pick.js +0 -59
  416. package/src/use-i2v-capture-modal.js +0 -15
  417. package/tools/ardy/BRIDGE.md +0 -37
  418. package/tools/ardy/GVHMR-NPZ.md +0 -68
  419. package/tools/ardy/README.md +0 -124
  420. package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
  421. package/tools/ardy/artifacts.mjs +0 -34
  422. package/tools/ardy/bridge.mjs +0 -1732
  423. package/tools/ardy/bvh-cskel27.mjs +0 -1265
  424. package/tools/ardy/cclay_constrained_generate.py +0 -561
  425. package/tools/ardy/cclay_gvhmr_worker.py +0 -264
  426. package/tools/ardy/cclay_motion_edit.py +0 -510
  427. package/tools/ardy/cclay_sequence_generate.py +0 -705
  428. package/tools/ardy/cclay_worker.py +0 -304
  429. package/tools/ardy/dump-npz.py +0 -205
  430. package/tools/ardy/extract-rest.mjs +0 -299
  431. package/tools/ardy/extract.mjs +0 -507
  432. package/tools/ardy/footage.mjs +0 -467
  433. package/tools/ardy/gvhmr-floor.mjs +0 -102
  434. package/tools/ardy/gvhmr-to-cskel27.mjs +0 -21
  435. package/tools/ardy/gvhmr_fastpath.py +0 -168
  436. package/tools/ardy/gvhmr_trajectory.py +0 -240
  437. package/tools/ardy/merge-text-encoder.py +0 -160
  438. package/tools/ardy/mocap-metrics.mjs +0 -200
  439. package/tools/ardy/mocap-quality-gate.mjs +0 -110
  440. package/tools/ardy/motion-stabilize.mjs +0 -348
  441. package/tools/ardy/npz.mjs +0 -414
  442. package/tools/ardy/pose-to-npz.mjs +0 -106
  443. package/tools/ardy/prompt-limits.mjs +0 -5
  444. package/tools/ardy/run-edit-on-box.sh +0 -73
  445. package/tools/ardy/run-local.mjs +0 -233
  446. package/tools/ardy/run-on-box.sh +0 -593
  447. package/tools/ardy/run-sequence-on-box.sh +0 -177
  448. package/tools/ardy/runners/gvhmr-worker.mjs +0 -206
  449. package/tools/ardy/runners/index.mjs +0 -18
  450. package/tools/ardy/runners/local.mjs +0 -455
  451. package/tools/ardy/runners/proc.mjs +0 -125
  452. package/tools/ardy/runners/remote.mjs +0 -252
  453. package/tools/ardy/setup-local.mjs +0 -189
  454. package/tools/ardy/setup-text-encoder-on-box.sh +0 -93
  455. package/tools/ardy/setup-text-encoder.py +0 -306
  456. package/tools/ardy/smpl-cskel27.mjs +0 -178
  457. package/tools/ardy/visual-qa.mjs +0 -196
  458. package/tools/ardy/vq-car.mjs +0 -52
  459. package/tools/bench/EXP3.md +0 -61
  460. package/tools/bench/cclay_bench_extract_incam.py +0 -125
  461. package/tools/bench/cclay_bench_extract_obs.py +0 -204
  462. package/tools/bench/cclay_bench_runner.py +0 -42
  463. package/tools/bench/cube-contact.mjs +0 -162
  464. package/tools/bench/exp3.mjs +0 -152
  465. package/tools/bench/extract-bench-lib.mjs +0 -216
  466. package/tools/bench/extract-bench.mjs +0 -302
  467. package/tools/bench/fal-generate.mjs +0 -88
  468. package/tools/bench/fit/README.md +0 -189
  469. package/tools/bench/fit/camera.mjs +0 -59
  470. package/tools/bench/fit/contact.mjs +0 -419
  471. package/tools/bench/fit/footlock.mjs +0 -234
  472. package/tools/bench/fit/motion.mjs +0 -85
  473. package/tools/bench/fit/pin.mjs +0 -27
  474. package/tools/bench/fit/remote.mjs +0 -105
  475. package/tools/bench/fit-bench.mjs +0 -126
  476. package/tools/bench/fit-sanity.mjs +0 -45
  477. package/tools/bench/metrics.mjs +0 -313
  478. package/tools/bench/obs/depth.mjs +0 -258
  479. package/tools/bench/obs/extrinsics.mjs +0 -175
  480. package/tools/bench/obs/fit_mannequin_betas.py +0 -340
  481. package/tools/bench/obs/ground.mjs +0 -294
  482. package/tools/bench/obs/heading.mjs +0 -151
  483. package/tools/bench/obs/ladder.mjs +0 -555
  484. package/tools/bench/obs/mannequin-betas.json +0 -124
  485. package/tools/bench/obs/remote.mjs +0 -83
  486. package/tools/bench/obs/rest_joints.py +0 -52
  487. package/tools/bench/obs/ybot-targets.mjs +0 -49
  488. package/tools/bench/obs-bench.mjs +0 -520
  489. package/tools/bench/score.mjs +0 -343
  490. package/tools/bench/summarize.mjs +0 -86
  491. package/tools/demo-worker/README.md +0 -137
  492. package/tools/demo-worker/api-client.mjs +0 -233
  493. package/tools/demo-worker/generate.mjs +0 -301
  494. package/tools/demo-worker/index.mjs +0 -466
  495. package/tools/demo-worker/sign-core.mjs +0 -112
  496. package/tools/demo-worker/sign.mjs +0 -161
  497. package/tools/gt-render/browser.mjs +0 -182
  498. package/tools/gt-render/camera-math.mjs +0 -276
  499. package/tools/gt-render/page.mjs +0 -267
  500. package/tools/gt-render/render.mjs +0 -514
  501. package/tools/gt-render/scene-box.mjs +0 -31
  502. package/tools/gt-render/take-transform.mjs +0 -87
  503. package/tools/kimodo/compare-seam.mjs +0 -108
  504. package/tools/kimodo/constraints.mjs +0 -244
  505. package/tools/kimodo/edit.mjs +0 -259
  506. package/tools/kimodo/effector-constraints.mjs +0 -200
  507. package/tools/kimodo/generate.mjs +0 -646
  508. package/tools/kimodo/local-output.mjs +0 -265
  509. package/tools/kimodo/measure-pose.mjs +0 -65
  510. package/tools/kimodo/measure-preserve.mjs +0 -899
  511. package/tools/kimodo/measure-waypoints.mjs +0 -55
  512. package/tools/kimodo/mlx-generate.py +0 -111
  513. package/tools/kimodo/pose-constraints.mjs +0 -248
  514. package/tools/kimodo/preserve-mask.mjs +0 -543
  515. package/tools/kimodo/read-npz.mjs +0 -189
  516. package/tools/kimodo/run-edit-on-box.mjs +0 -155
  517. package/tools/kimodo/run-sequence-on-box.mjs +0 -178
  518. package/tools/kimodo/runner.mjs +0 -284
  519. package/tools/kimodo/seam-batch.mjs +0 -77
  520. package/tools/kimodo/setup-local.mjs +0 -30
  521. package/tools/kimodo/setup-on-box.sh +0 -339
  522. package/tools/kimodo/soma77-to-cskel27.mjs +0 -305
  523. package/tools/morphgs/assets/gen_truth.py +0 -39
  524. package/tools/morphgs/assets/mesh_ori_rig.txt +0 -27
  525. package/tools/morphgs/assets/playback-check.mjs +0 -30
  526. package/tools/morphgs/demo-gate.sh +0 -26
  527. package/tools/morphgs/fbx2morphgs.mjs +0 -68
  528. package/tools/morphgs/morphgs-to-cskel27.mjs +0 -105
  529. package/tools/morphgs/patches/preprocess_src-none-mode.patch +0 -76
  530. package/tools/morphgs/setup-on-cluster.sh +0 -126
  531. package/tools/projflow/driver.py +0 -1069
  532. package/tools/projflow/generate.mjs +0 -805
  533. package/tools/projflow/hml22-to-cskel27.mjs +0 -735
  534. package/tools/projflow/line-edit-job.mjs +0 -547
  535. package/tools/projflow/replay.mjs +0 -452
  536. package/tools/projflow/runner.mjs +0 -269
  537. package/tools/projflow/service.mjs +0 -742
  538. package/tools/track/DESIGN.md +0 -513
  539. package/tools/track/backfill-provenance.mjs +0 -116
  540. package/tools/track/budget.mjs +0 -8
  541. package/tools/track/check-rig.mjs +0 -209
  542. package/tools/track/diagnostics.schema.json +0 -60
  543. package/tools/track/export-rig.mjs +0 -256
  544. package/tools/track/fallback.mjs +0 -7
  545. package/tools/track/fk-parity-fixture.mjs +0 -161
  546. package/tools/track/gate.mjs +0 -349
  547. package/tools/track/masks.mjs +0 -318
  548. package/tools/track/metrics.mjs +0 -201
  549. package/tools/track/publish-obs.mjs +0 -108
  550. package/tools/track/py/check_env.py +0 -61
  551. package/tools/track/py/eval_lr.py +0 -229
  552. package/tools/track/py/lr_viterbi.py +0 -294
  553. package/tools/track/py/masks.py +0 -405
  554. package/tools/track/py/objective.py +0 -382
  555. package/tools/track/py/rig.py +0 -508
  556. package/tools/track/py/scene.py +0 -326
  557. package/tools/track/py/test_joint_indices.py +0 -123
  558. package/tools/track/py/test_lr_viterbi.py +0 -195
  559. package/tools/track/py/test_masks.py +0 -229
  560. package/tools/track/py/test_rig.py +0 -288
  561. package/tools/track/py/test_scene.py +0 -354
  562. package/tools/track/py/test_track.py +0 -328
  563. package/tools/track/py/track.py +0 -411
  564. package/tools/track/remote.mjs +0 -140
  565. package/tools/track/rig-dump.mjs +0 -227
  566. package/tools/track/run-box-tests.mjs +0 -14
  567. package/tools/track/run-box.mjs +0 -168
  568. package/tools/track/setup-box.sh +0 -59
  569. package/tools/track/study-2d.mjs +0 -728
  570. /package/src/{ardy → motion}/collision-blockers.js +0 -0
  571. /package/src/{ardy → motion}/ground.js +0 -0
  572. /package/src/{ardy → motion}/ik-drag.js +0 -0
  573. /package/src/{ardy → motion}/ik-key-json.js +0 -0
  574. /package/src/{ardy → motion}/ik.js +0 -0
  575. /package/src/{ardy → motion}/physics-surface.js +0 -0
  576. /package/src/{ardy → motion}/playback-clock.js +0 -0
  577. /package/src/{ardy → motion}/range-pin.js +0 -0
@@ -1,5 +1,8 @@
1
1
  // Shared by the browser and sidecar. No Node, React, renderer or provider imports.
2
2
  import { STUDIO_ELEMENTS, isSettableElement } from "./studio-elements.js";
3
+ import { STUDIO_ERROR_CODES } from "./studio-error-codes.js";
4
+ import { EFFECT_VALUE_SCHEMA } from "./effects/effect-record.js";
5
+ import { wrongType, unknownField, missingField, arrayRefusal, duplicateItem, stringRefusal, enumRefusal, numberRefusal, unionRefusal } from "./studio-schema-errors.js";
3
6
  // Mirrors PREVIS_MODES (project.js) and POSTURES (scenes.js). Neither is imported:
4
7
  // both pull in three.js, which the agent sidecar must not load. The protocol test pins the lists to their owners.
5
8
  const PREVIS_MODES = ["storyboard", "animation"], POSTURES = ["stand", "sit", "lie"];
@@ -10,7 +13,7 @@ export const STUDIO_PROTOCOL_VERSION = "studio-agent-v1";
10
13
  export const STUDIO_CONTEXT_MAX_BYTES = 64 * 1024;
11
14
  export const STUDIO_CONTEXT_LIMITS = Object.freeze({ entities: 24, entityIndex: 400, shots: 8, assets: 48, recentReceipts: 3, jobs: 8 });
12
15
  export const STUDIO_TOOL_FAMILIES = Object.freeze(["inspect_studio", "run_action", "verify_result"]);
13
- export const STUDIO_TOOL_ALIASES = Object.freeze(["operate_studio", "arrange_objects", "arrange_characters", "patch_elements", "frame_shot", "generate_motion", "undo_edit"]);
16
+ export const STUDIO_TOOL_ALIASES = Object.freeze(["operate_studio", "arrange_objects", "arrange_characters", "patch_elements", "frame_shot", "undo_edit"]);
14
17
  export const STUDIO_TOOL_LABELS = Object.freeze({
15
18
  inspect_studio: "Read the scene",
16
19
  operate_studio: "Selection and view",
@@ -18,7 +21,6 @@ export const STUDIO_TOOL_LABELS = Object.freeze({
18
21
  arrange_characters: "Arrange characters",
19
22
  patch_elements: "Edit properties",
20
23
  frame_shot: "Frame the shot",
21
- generate_motion: "Generate motion",
22
24
  verify_result: "Verify the result",
23
25
  undo_edit: "Undo an edit",
24
26
  run_action: "Run an editor action",
@@ -28,12 +30,7 @@ export const STUDIO_TOOLS = Object.freeze(Object.keys(STUDIO_TOOL_LABELS));
28
30
  * object→objects, shot→shot, stage→stage. */
29
31
  export const STUDIO_PATCH_KINDS = Object.freeze(["character", "object", "shot", "stage"]);
30
32
  export const STUDIO_PATCH_DOMAINS = Object.freeze({ character: "cast", object: "objects", shot: "shot", stage: "stage" });
31
- export const STUDIO_ERROR_CODES = Object.freeze([
32
- "INVALID_ARGUMENT", "INVALID_CONTEXT", "INVALID_IDENTITY", "INVALID_TURN_ID", "INVALID_SESSION_ID", "INVALID_RECEIPT", "INVALID_RANGE", "INVALID_REQUEST",
33
- "UNKNOWN_TOOL", "UNKNOWN_VARIANT", "DUPLICATE_NAME", "CONTEXT_LIMIT", "CONTEXT_TOO_LARGE", "AMBIGUOUS_TARGET", "AMBIGUOUS_BASIS", "TARGET_NOT_READY", "TARGET_BUSY",
34
- "STALE_TARGET", "STALE_SCENE", "STALE_ENVIRONMENT", "STALE_CURSOR", "CAPABILITY_MISSING", "LIVE_HUB_UNAVAILABLE", "AUTH_REQUIRED", "GENERATION_LIMIT", "RATE_LIMITED", "BACKEND_UNAVAILABLE",
35
- "VERIFICATION_FAILED", "REPAIR_REGRESSED", "CANCELLED", "UNCERTAIN_APPLY", "UNDO_CONFLICT", "CONFIRMATION_REQUIRED", "TIMEOUT", "UNDO_EXPIRED",
36
- ]);
33
+ export { STUDIO_ERROR_CODES };
37
34
  export const STUDIO_VARIANTS = freezeStudioData({
38
35
  selectionKinds: ["scene", "object", "character", "rig", "camera"], modes: ["scene", "pose", "camera", "motion"], shotModes: ["keys", "follow", "rail"],
39
36
  framingSizes: ["extreme close-up", "close-up", "medium close-up", "medium shot", "medium-wide shot", "wide shot", "extreme wide shot"],
@@ -60,8 +57,7 @@ const record = value => value !== null && typeof value === "object" && !Array.is
60
57
  const text = (maxLength = 120) => ({ type: "string", minLength: 1, maxLength });
61
58
  const number = (minimum, maximum) => ({ type: "number", ...(minimum !== undefined ? { minimum } : {}), ...(maximum !== undefined ? { maximum } : {}) });
62
59
  const integer = (minimum = 0, maximum = Number.MAX_SAFE_INTEGER) => ({ ...number(minimum, maximum), type: "integer" });
63
- const bool = { type: "boolean" };
64
- const literal = value => ({ const: value });
60
+ const bool = { type: "boolean" }, literal = value => ({ const: value });
65
61
  const choices = values => ({ type: "string", enum: values });
66
62
  const nullable = schema => ({ oneOf: [schema, { type: "null" }] });
67
63
  const array = (items, maxItems, minItems = 0, uniqueItems = false) => ({ type: "array", items, minItems, maxItems, ...(uniqueItems ? { uniqueItems } : {}) });
@@ -114,9 +110,6 @@ const characterOp = union(
114
110
  object({ op: literal("update"), characterId: id }, { position, facing, scale: positive, name, hidden: bool }),
115
111
  object({ op: literal("remove"), characterId: id }),
116
112
  );
117
- export const BEAT_TEXT_MAX = 500;
118
- const generateSource = object({ kind: literal("generate"), beats: array(object({ text: { ...text(BEAT_TEXT_MAX), description: 'An English description of the body movement starting with "A person". The editor refuses prompts over 500 characters.' } }, { seconds: number(0.5, 60) }), 8, 1) }, { durationSeconds: number(2, 60), seed: integer(-2147483648, 2147483647) });
119
- const source = union(generateSource, object({ kind: literal("reuse"), artifactId: id }));
120
113
 
121
114
  /* ----------------------------------------------- element patches ----
122
115
  * The accepted fields of `patch_elements` are DERIVED from the element
@@ -126,18 +119,20 @@ const source = union(generateSource, object({ kind: literal("reuse"), artifactId
126
119
  * (schedules, routes, pictures) declare their shape here, because the table
127
120
  * records what an element IS, not how JSON carries it. */
128
121
  const dataImage = { ...text(2 * 1024 * 1024), pattern: "^data:image/[A-Za-z0-9.+-]+[;,]" };
129
- const promptBlock = object({ startFrame: integer(), endFrame: integer(1), text: text(2000) }, { id });
122
+
130
123
  const cameraKey = object({ frame: integer(), framing: object({ pos: vec3, yaw: number(), pitch: number(), fovDeg: number(1, 179) }) }, { id });
131
124
  // A mark is an arc fraction, or { t, bank, pitch } when the object should lean
132
125
  // there (degrees, +-90). The route normaliser (src/object-path.js) owns the
133
126
  // rules; this only carries the shape through untouched.
134
127
  const routeMark = union(number(0, 1), object({ t: number(0, 1) }, { bank: number(-90, 90), pitch: number(-90, 90) }));
135
128
  const objectRoute = nullable(object({ points: array(vec3, 64, 2) }, { speed: number(0, 50), faceTravel: bool, loop: bool, extend: bool, marks: array(routeMark, 6) }));
129
+ // A practical light and a glow (src/object-look.js owns the defaults and the repair).
130
+ const objectLight = object({}, { type: choices(["point", "spot"]), color: { ...text(32), pattern: "^#[0-9a-fA-F]{6}$" }, intensity: number(0, 1000), range: number(0, 100), angle: number(1, 89), softness: number(0, 1), shadow: bool, flicker: number(0, 1), aim: nullable(vec3) });
131
+ const objectGlow = nullable(object({}, { color: { ...text(32), pattern: "^#[0-9a-fA-F]{6}$" }, intensity: number(0, 20), halo: number(0, 4) }));
136
132
  const PATCH_VALUE_SCHEMAS = {
137
- "character.pose": nullable(id), "character.identityImage": nullable(dataImage), "character.promptBlocks": array(promptBlock, 64),
133
+ "character.pose": nullable(id), "character.identityImage": nullable(dataImage), "object.light": objectLight, "object.glow": objectGlow, "object.effect": EFFECT_VALUE_SCHEMA,
138
134
  "object.parent": nullable(id), "object.path": objectRoute, "stage.environmentImage": nullable(dataImage),
139
- "shot.cameraKeys": array(cameraKey, 64), "shot.targetModel": nullable(id),
140
- };
135
+ "shot.cameraKeys": array(cameraKey, 64), "shot.targetModel": nullable(id) };
141
136
  /** One declared element as a JSON value schema, or null when the declaration
142
137
  * carries no carriable shape (a structured element without a declared value
143
138
  * schema above). Null elements are omitted from the patch schema, and
@@ -204,8 +199,7 @@ const toolSchemas = {
204
199
  arrange_characters: object({ ops: array(characterOp, 8, 1) }),
205
200
  patch_elements: object({ ops: array(patchOp, 32, 1) }),
206
201
  frame_shot: object({ subjectIds: ids(1), framing }, { shotId: id, caption: { type: "string", maxLength: 500 }, keyAtFrame: integer() }),
207
- generate_motion: object({ characterId: id, source }, { repair: { ...choices(["bounded", "none"]), default: "bounded" } }),
208
- verify_result: object({ checks: array(choices(["placement", "framing", "motion"]), 3, 1, true) }, { receiptId: id, targets: ids(), range: union(literal("whole_clip"), range), visual: { ...choices(["none", "frame", "contact_sheet", "plan"]), default: "none" } }),
202
+ verify_result: object({ checks: { ...array(choices(["placement", "framing", "motion", "lighting"]), 4, 1, true), description: "lighting measures the shot frame rendered at the playhead: checks.lighting { meanLuma 0..1, clippedPct (a channel at full scale), crushedPct (pure black), subjects: [{ id, meanLuma, contrast (5th to 95th percentile luma spread, 0..1) }] inside each target character's projected box, look: { preset, envLight, ao } the stage look's environment light and occlusion strength }. motion also answers exportFraming (one per character with a take): what the camera the export renders shows of the take over sampledFrames: camera (shot where a shot covers the frame, studio = the Studio camera as it stands when none does; shotIds), character { screenFraction [min, max] of the frame height, inFrame [lowest share of its joints' screen box inside the frame, frame], behindCamera, cut { head, hand_l, hand_r, foot_l, foot_r } }, and every object the take names { id, name, inFrame, out, cut }; out, cut and behindCamera are spans [first, last] of sampled frames; { character: { id }, reason } when it cannot be measured" } }, { receiptId: id, targets: ids(), range: union(literal("whole_clip"), range), visual: { ...choices(["none", "frame", "contact_sheet", "plan", "trace"]), default: "none" }, frames: { ...union(choices(["even", "beats"]), array(integer(), 8, 1, true)), default: "even", description: "contact_sheet frames: even spacing, beats (grab/release, beat starts, the worst QA frames, then even fill, answered in visualRefs[0].picks), or up to 8 timeline frames of your choice" }, views: { ...array(choices(["front", "side", "three-quarter", "top"]), 2, 1, true), description: "contact_sheet from cameras of its own around the character instead of the shot camera (which stays as it is): a row per view, each held still over the frames and framed on focus, so small actions fill the tile; side and three-quarter look from the focus's side; a sheet always holds two views at least 60 degrees apart: if the asked ones look along nearly one line, side, front or top is added and named in visualRefs[0].addedView" }, focus: { ...union(text(120), array(text(120), 3, 1, true)), description: "with views: what to frame, as refs (object:<id>, joint:<Bone>, prop:<prop>.<point>, landmark:mouth); default the whole character" }, trail: { ...union(text(120), array(text(120), 3, 1, true)), description: "with views: refs whose path over the range is drawn as one dot per frame (spacing shows speed; later frames faint, the tile's own frame ringed), colours in visualRefs[0].trails" }, ghosts: { ...bool, description: "with views: each tile also shows the first target's poses around its frame as translucent ghosts (onion skin), earlier ones blue, later ones orange, carried props included, so spacing and arcs read in one tile; how many and how far apart is view.setGhosts (while it is off: 3 each side, every 4 frames), answered in visualRefs[0].ghosts" } }),
209
203
  undo_edit: object({ receiptId: id }),
210
204
  run_action: object({ action: id }, { args: openObject, confirmationToken: id }),
211
205
  };
@@ -219,7 +213,7 @@ const entity = object({ id, kind: choices(["object", "character", "rig"]), token
219
213
  name, detailsOmitted: bool, position: vec3, yawDeg: number(), rotationDeg: vec3, scale: union(positive, positiveVec3), bounds: nullable(bounds),
220
214
  libraryKind: id, renderer: id, color: nullable(text(32)), tint: nullable(text(32)), modelId: nullable(text(120)), assetId: id, parentId: nullable(id), attachment: nullable(object({ characterId: id, bone: nullable(id) })), pathPointCount: integer(0, 64), pathMarks: array(union(number(0, 1), object({ t: number(0, 1) }, { bank: number(-90, 90), pitch: number(-90, 90) })), 6),
221
215
  local: object({ position: vec3, rotationDeg: vec3 }),
222
- motion: object({ takeId: nullable(id), frames: integer(), ikKeyCount: integer(), promptBlockCount: integer() }, { poseId: nullable(id), keyIds: ids(8, 0) }),
216
+ motion: object({ takeId: nullable(id), frames: integer(), ikKeyCount: integer() }, { poseId: nullable(id), keyIds: ids(8, 0) }),
223
217
  capabilities: object({ rigReady: bool, ik: bool, measuredFeet: bool }),
224
218
  // `kind` above already names the entity class ("character"); a character's own
225
219
  // kind is `characterKind`: a rigged model, or a rig-free capsule figure.
@@ -298,7 +292,9 @@ const authoredReceipt = { ...receiptBase, authored: literal(true), undo };
298
292
  // A run_action receipt names the registered action and what it did.
299
293
  const actionFields = { action: id, summary: text(240) };
300
294
  const receiptVariants = {
301
- completed: object({ ...receiptBase, status: literal("completed"), kind: choices(["job", "document", "transaction", "mutation"]) }, { ...actionFields, ...batchDetails, output: openObject, jobId: id, txId: id, nextHost: identity }),
295
+ // A mutation inside an agent turn answers "completed" (the turn owns its undo entry); a patch
296
+ // among them still reports each op, so ops ride here too.
297
+ completed: object({ ...receiptBase, status: literal("completed"), kind: choices(["job", "document", "transaction", "mutation"]) }, { ...actionFields, ...batchDetails, output: openObject, jobId: id, txId: id, nextHost: identity, ops: opResults }),
302
298
  started: object({ ...receiptBase, status: literal("started"), kind: literal("job"), jobId: id, authored: literal(false), undo: literal(null) }, { ...actionFields }),
303
299
  applied: object({ ...authoredReceipt, status: literal("applied") }, { mutated: literal(true), ops: opResults, ...batchDetails, ...actionFields }),
304
300
  partial: object({ ...authoredReceipt, status: literal("partial"), ops: opResults }, { mutated: literal(true), ...batchDetails }),
@@ -321,65 +317,66 @@ const failureSchema = object({ ok: literal(false), commandId: id, host: identity
321
317
  // These are JSON Schema data, not validators with hidden browser dependencies.
322
318
  // x-studio-range is the sole relational schema annotation: end > start.
323
319
  export const StudioSchemas = freezeStudioData({ catalogue: STUDIO_CATALOGUE, variants: STUDIO_VARIANTS, Vec3: vec3, FrameRange: range, PositionSpec: position, FacingSpec: facing,
324
- ObjectOp: objectOp, CharacterOp: characterOp, Framing: framing, GenerateSource: generateSource, Source: source, Identity: identity, Host: host, TargetGuard: guardSchema,
320
+ ObjectOp: objectOp, CharacterOp: characterOp, Framing: framing, Identity: identity, Host: host, TargetGuard: guardSchema,
325
321
  Entity: entity, ShotSummary: shotSummary, StudioContextV1: contextSchema, StudioTurn: turnSchema, StudioStop: stopSchema, Receipt: receiptSchema, ReceiptVariants: receiptVariants, Failure: failureSchema, GeometryFacts: geometryFacts });
326
322
 
327
- /** Validate the closed JSON Schema subset used above; return a detached value
328
- * with declared defaults. Errors contain schema paths, never payload values. */
323
+ /** Validate the closed JSON Schema subset used above; return a detached value with declared
324
+ * defaults. Errors name the path and what it allows (studio-schema-errors.js). */
329
325
  export function validateStudioSchema(schema, value, code = "INVALID_ARGUMENT", path = "$") {
330
- if (schema.oneOf) {
331
- const matches = [];
332
- const failures = [];
333
- for (const branch of schema.oneOf) {
326
+ if (schema.oneOf || schema.anyOf) {
327
+ const matches = [], failures = [];
328
+ for (const branch of schema.oneOf ?? schema.anyOf) {
334
329
  try { matches.push(validateStudioSchema(branch, value, code, path)); }
335
330
  catch (error) { if (!(error instanceof StudioProtocolError)) throw error; failures.push({ branch, error }); }
336
331
  }
337
332
  // No variant matched: the generic "one of N" message hides which path and
338
333
  // bound actually offended. Surface the failure from the variant the input
339
334
  // discriminates to (matching target.kind), or else the deepest failing
340
- // path, instead of the union's own shallow message.
335
+ // path, instead of the union's own shallow message. A union of plain values
336
+ // (true, false or "ifClean") that failed at its own path names every form.
341
337
  if (matches.length === 0 && failures.length) {
342
- const kind = record(value) && record(value.target) ? value.target.kind : undefined;
343
- const discriminated = kind !== undefined && failures.find(({ branch }) => branch.properties?.target?.properties?.kind?.const === kind);
338
+ const kind = record(value) && record(value.target) ? value.target.kind : undefined, discriminated = kind !== undefined && failures.find(({ branch }) => branch.properties?.target?.properties?.kind?.const === kind);
339
+ const forms = !discriminated && unionRefusal(path, failures, value);
340
+ if (forms) fail(code, forms, path);
344
341
  const chosen = discriminated || failures.reduce((best, next) => next.error.details.path.length > best.error.details.path.length ? next : best);
345
342
  throw chosen.error;
346
343
  }
347
- if (matches.length !== 1) fail(code, "Expected exactly one supported variant.", path);
344
+ if (matches.length !== 1 && schema.oneOf) fail(code, `${path}: matches ${matches.length} variants; send the fields of exactly one.`, path);
348
345
  return matches[0];
349
346
  }
350
- if (Object.hasOwn(schema, "const")) { if (value !== schema.const) fail(code, "Invalid constant.", path); return value; }
351
- if (schema.type === "null") { if (value !== null) fail(code, "Expected null.", path); return null; }
347
+ if (Object.hasOwn(schema, "const")) { if (value !== schema.const) fail(code, wrongType(path, schema, value), path); return value; }
348
+ if (schema.type === "null") { if (value !== null) fail(code, wrongType(path, schema, value), path); return null; }
352
349
  if (schema.type === "object") {
353
- if (!record(value)) fail(code, "Expected object.", path);
350
+ if (!record(value)) fail(code, wrongType(path, schema, value), path);
354
351
  const open = schema.additionalProperties === true;
355
- for (const key of Object.keys(value)) if (!Object.hasOwn(schema.properties, key) && !open) fail(code, "Unexpected field.", path);
352
+ for (const key of Object.keys(value)) if (!Object.hasOwn(schema.properties, key) && !open) fail(code, unknownField(path, schema, key), path);
356
353
  const result = {};
357
354
  if (open) for (const key of Object.keys(value)) if (!Object.hasOwn(schema.properties, key)) result[key] = structuredClone(value[key]);
358
355
  for (const [key, child] of Object.entries(schema.properties)) {
359
356
  if (Object.hasOwn(value, key)) result[key] = validateStudioSchema(child, value[key], code, `${path}.${key}`);
360
- else if (schema.required.includes(key)) fail(code, "Required field missing.", `${path}.${key}`);
357
+ else if (schema.required.includes(key)) fail(code, missingField(path, key), `${path}.${key}`);
361
358
  else if (Object.hasOwn(child, "default")) result[key] = structuredClone(child.default);
362
359
  }
363
360
  if (schema["x-studio-range"] && result.endFrameExclusive <= result.startFrame) fail(code, "Frame range must be nonempty and half-open.", path);
364
361
  return result;
365
362
  }
366
363
  if (schema.type === "array") {
367
- if (!Array.isArray(value) || value.length < schema.minItems || value.length > schema.maxItems) fail(code, "Array outside supported bounds.", path);
364
+ if (!Array.isArray(value) || value.length < schema.minItems || value.length > schema.maxItems) fail(code, arrayRefusal(path, schema, value), path);
368
365
  const result = value.map((item, i) => validateStudioSchema(schema.items, item, code, `${path}[${i}]`));
369
- if (schema.uniqueItems && new Set(result.map(item => JSON.stringify(item))).size !== result.length) fail(code, "Duplicate array member.", path);
366
+ if (schema.uniqueItems && new Set(result.map(item => JSON.stringify(item))).size !== result.length) fail(code, duplicateItem(path, result), path);
370
367
  return result;
371
368
  }
372
369
  if (schema.type === "string") {
373
- if (typeof value !== "string" || !value.trim() || (schema.maxLength !== undefined && [...value].length > schema.maxLength) || (schema.pattern && !new RegExp(schema.pattern).test(value))) fail(code, typeof value === "string" && schema.description && [...value].length > schema.maxLength ? `Over ${schema.maxLength} characters. ${schema.description}` : "Invalid or oversized string.", path);
374
- if (schema.enum && !schema.enum.includes(value)) fail(code, "Unsupported enum value.", path);
370
+ if (typeof value !== "string" || !value.trim() || (schema.maxLength !== undefined && [...value].length > schema.maxLength) || (schema.pattern && !new RegExp(schema.pattern).test(value))) fail(code, stringRefusal(path, schema, value), path);
371
+ if (schema.enum && !schema.enum.includes(value)) fail(code, enumRefusal(path, schema, value), path);
375
372
  return value;
376
373
  }
377
- if (schema.type === "boolean") { if (typeof value !== "boolean") fail(code, "Expected boolean.", path); return value; }
374
+ if (schema.type === "boolean") { if (typeof value !== "boolean") fail(code, wrongType(path, schema, value), path); return value; }
378
375
  if (schema.type === "number" || schema.type === "integer") {
379
- if (typeof value !== "number" || !Number.isFinite(value) || (schema.type === "integer" && !Number.isSafeInteger(value)) || (schema.exclusiveMinimum !== undefined && value <= schema.exclusiveMinimum)) fail(code, "Number outside supported bounds.", path);
376
+ if (typeof value !== "number" || !Number.isFinite(value) || (schema.type === "integer" && !Number.isSafeInteger(value)) || (schema.exclusiveMinimum !== undefined && value <= schema.exclusiveMinimum)) fail(code, numberRefusal(path, schema, value), path);
380
377
  if ((schema.minimum !== undefined && value < schema.minimum) || (schema.maximum !== undefined && value > schema.maximum)) {
381
378
  const bounds = [schema.minimum, schema.maximum].filter(bound => bound !== undefined);
382
- fail(code, `${path}: Expected a number within [${bounds.join(", ")}].`, path);
379
+ fail(code, `${path}: Expected a number within [${bounds.join(", ")}], got ${value}.`, path);
383
380
  }
384
381
  return value;
385
382
  }
@@ -455,13 +452,6 @@ export function validateStudioContextFreshness(submitted, current) {
455
452
  }
456
453
  const pickIdentity = value => Object.fromEntries(Object.keys(identityFields).map(key => [key, value[key]]));
457
454
 
458
- function validateGenerationTiming(value) {
459
- const timed = value.beats.filter(beat => beat.seconds !== undefined).length;
460
- if ((value.durationSeconds !== undefined && timed !== 0) || (value.durationSeconds === undefined && timed !== value.beats.length)) fail("INVALID_ARGUMENT", "Use total duration or a duration for every beat, not mixed timing.");
461
- const total = value.durationSeconds ?? value.beats.reduce((sum, beat) => sum + beat.seconds, 0);
462
- if (total < 2 || total > 60 || total / value.beats.length < 0.5) fail("INVALID_ARGUMENT", "Generation must be 2-60 seconds with at least 0.5 seconds per beat.");
463
- return total;
464
- }
465
455
  /** Unknown paths are answered with the paths that DO exist for that kind, so a
466
456
  * caller never has to guess the vocabulary from a rejection. */
467
457
  function validatePatchPaths(args) {
@@ -501,10 +491,6 @@ export function validateStudioCommand(command) {
501
491
  // final batch is still checked for overlap.
502
492
  if (args.collisionPolicy === "avoid" && !args.ops.some(op => ["create", "update"].includes(op.op) && op.position?.relativeTo)) fail("INVALID_ARGUMENT", "collisionPolicy avoid needs at least one op with position.relativeTo (it nudges outward from that reference); other ops are placed as given.");
503
493
  }
504
- if (command.name === "generate_motion" && args.source.kind === "generate") {
505
- args.source.beats = args.source.beats.map(beat => ({ ...beat, text: beat.text.trim().replace(/\s+/g, " ") }));
506
- validateGenerationTiming(args.source);
507
- }
508
494
  if (command.name === "frame_shot" && args.framing.exact && JSON.stringify(args.framing.exact.position) === JSON.stringify(args.framing.exact.lookAt)) fail("INVALID_ARGUMENT", "Camera position and aim cannot coincide.");
509
495
  if (command.name === "patch_elements") {
510
496
  for (const op of args.ops) if (!Object.keys(op.set).length) fail("INVALID_ARGUMENT", "Patch has no fields.");
@@ -518,22 +504,6 @@ export function validateStudioCommand(command) {
518
504
  }
519
505
  return { name: command.name, args };
520
506
  }
521
- export function compileStudioBeats(value) {
522
- const source = validateStudioSchema(generateSource, value);
523
- const totalSeconds = validateGenerationTiming(source);
524
- const frameCount = Math.round(totalSeconds * 24);
525
- const blocks = []; let cumulative = 0, startFrame = 0;
526
- for (const [sourceBeat, beat] of source.beats.entries()) {
527
- cumulative += beat.seconds ?? totalSeconds / source.beats.length;
528
- const end = sourceBeat === source.beats.length - 1 ? frameCount : Math.round(cumulative * 24);
529
- const pieces = Math.ceil((end - startFrame) / 120), start = startFrame;
530
- for (let i = 0; i < pieces; i++) {
531
- const endFrameExclusive = start + Math.round((end - start) * (i + 1) / pieces);
532
- blocks.push({ sourceBeat, text: beat.text.trim().replace(/\s+/g, " "), startFrame, endFrameExclusive }); startFrame = endFrameExclusive;
533
- }
534
- }
535
- return freezeStudioData({ fps: 24, frameCount, durationSeconds: frameCount / 24, blocks });
536
- }
537
507
  export function validateReceipt(value) {
538
508
  if (value?.ok === false) {
539
509
  const r = validateStudioSchema(failureSchema, value, "INVALID_RECEIPT");
@@ -3,16 +3,18 @@
3
3
  // history, the action registry); this module owns no React or renderer state.
4
4
  import { createCommandBus } from "./command-bus.js";
5
5
  import { readElementDocument, elementReadback, elementPatchArgs, elementTarget, elementPatchReceipt } from "./commands/elements.js";
6
- import { physicsKeyStamp } from "./ardy/physics-review.js";
6
+ import { physicsKeyStamp } from "./motion/physics-review.js";
7
7
  import { shotAtFrame } from "./cuts.js";
8
8
  import { isProxyFigure } from "./scenes.js";
9
- import { CUTOUT_KIND, MESH_KIND, OBJECT_LIBRARY, supportHeightForObject } from "./scene-objects.js";
9
+ import { supportHeightForObject } from "./scene-objects.js";
10
+ import { studioAssetList } from "./studio-asset-list.js";
10
11
  import { buildStudioContext, physicsFingerprintInput, studioEntityCursor, validateStudioCursor } from "./studio-agent-context.js";
11
- import { createStudioCommandJournal, entityWorldBounds, framingChecks, localTransformReadback, placementChecks, sampledFramingResult, studioObjectCatalogue } from "./studio-agent-commands.js";
12
- import { verifyInstalledTake } from "./studio-agent-motion.js";
13
- import { shotGeometryFacts } from "./studio-geometry-facts.js";
12
+ import { createStudioCommandJournal, framingChecks, localTransformReadback, placementChecks, sampledFramingResult, studioObjectCatalogue } from "./studio-agent-commands.js";
13
+ import { verifyInstalledTake } from "./studio-agent-motion.js"; import { objectRowBounds } from "./studio-entity-bounds.js"; import { exportFramingInto } from "./studio-export-framing.js";
14
+ import { shotGeometryFacts } from "./studio-geometry-facts.js"; import { KIT_CATALOGUE } from "./kits/index.js";
14
15
  import { STUDIO_TOOL_FAMILIES, StudioProtocolError, validateReceipt, validateStudioCommand, validateStudioIdentity } from "./studio-agent-protocol.js";
15
- import { CONTACT_SHEET_LAYOUT, buildContactSheet, sampleContactSheetFrames } from "./studio-contact-sheet.js";
16
+ import { studioVisual, verifyLighting } from "./studio-motion-visual.js";
17
+ import { withEffectState } from "./effects/state.js"; import { hdrEnvironmentCatalogue } from "./stage-hdr-library.js";
16
18
 
17
19
  // App-owned adapter: the shared command modules remain the only
18
20
  // planners and validators. Ports below publish through the native editor stores.
@@ -34,8 +36,7 @@ export function createStudioAppBinding(ports) {
34
36
  const motionContentKey = value => {
35
37
  if (!value || typeof value !== "object") return null;
36
38
  if (typeof value.studioTakeId === "string" && value.studioTakeId) return value.studioTakeId;
37
- if (typeof value.motionRef?.motionId === "string" && value.motionRef.motionId) return value.motionRef.motionId;
38
- return `${value.frames ?? 0}:${value.fps ?? 0}:${value.rotMats?.length ?? 0}:${value.rootPos?.length ?? 0}:${value.posedJoints?.length ?? 0}`;
39
+ return `${value.frames ?? 0}:${value.fps ?? 0}`;
39
40
  };
40
41
  const calibrationContentKey = value => value && typeof value === "object" ? JSON.stringify(value) : null;
41
42
  const tokens = new Map(), receipts = new Map(), images = new Map();
@@ -121,30 +122,18 @@ export function createStudioAppBinding(ports) {
121
122
  return { id: c.id, kind: "character", token: tokens.get(c.id).token, name: c.subject || c.id,
122
123
  position: { x: c.x, y: c.y ?? 0, z: c.z }, yawDeg: c.rot ?? 0, scale: c.scale ?? 1, tint: c.tint ?? null, modelId: c.model ?? null,
123
124
  motion: { takeId: t?.motion?.studioTakeId ?? null, frames: t?.motion?.frames ?? 0,
124
- ikKeyCount: t?.ikState?.keys.size ?? 0, promptBlockCount: c.layer?.promptClips?.length ?? 0 },
125
+ ikKeyCount: t?.ikState?.keys.size ?? 0 },
125
126
  capabilities: { rigReady: Boolean(t?.rig), ik: Boolean(t?.rig?.userData?.poseBind), measuredFeet: false },
126
127
  characterKind: isProxyFigure(c) ? "proxy" : "rig", ...(isProxyFigure(c) ? { posture: c.posture ?? "stand" } : {}) };
127
128
  }), ...s.objects.map(o => { const local = localTransformReadback(o, s.objects); return { id: o.id, kind: "object", token: tokens.get(o.id).token, name: o.name || o.id,
128
129
  position: { x: o.x, y: o.y ?? 0, z: o.z }, yawDeg: o.rot ?? 0,
129
130
  rotationDeg: { x: o.rotX ?? 0, y: o.rot ?? 0, z: o.rotZ ?? 0 }, scale: { x: o.scaleX, y: o.scaleY, z: o.scaleZ },
130
- renderer: o.renderer, color: o.color ?? null, ...(o.assetId ? { assetId: o.assetId } : {}),
131
+ renderer: o.renderer, color: o.color ?? null, bounds: objectRowBounds(o, s, ports), ...(o.assetId ? { assetId: o.assetId } : {}),
131
132
  parentId: o.parent ?? null, attachment: o.attach ?? null, pathPointCount: o.path?.points.length ?? 0,
132
133
  // the dots on the route, with the lean (degrees) a mark carries
133
134
  ...(o.path?.marks?.length ? { pathMarks: o.path.marks.map(mark => typeof mark === "number" ? mark : { t: mark.t, ...(mark.bank !== undefined ? { bank: mark.bank } : {}), ...(mark.pitch !== undefined ? { pitch: mark.pitch } : {}) }) } : {}), ...(local ? { local } : {}) }; })];
134
135
  }
135
136
  const frameRange = row => ({ startFrame: row.startFrame, endFrameExclusive: row.endFrame + 1 });
136
- function assetList(s) {
137
- const catalogue = studioObjectCatalogue().objects.map(({ kind }) => {
138
- const entry = OBJECT_LIBRARY.find(row => row.kind === kind);
139
- return { kind, name: entry?.label ?? kind, type: entry?.group === "Primitives" ? "primitive" : "set-piece" };
140
- });
141
- const imported = new Map();
142
- for (const o of s.objects) {
143
- const assetId = o.renderer === CUTOUT_KIND ? o.sourceAssetId || o.assetId : o.renderer === MESH_KIND ? o.assetId : null;
144
- if (assetId && !imported.has(assetId)) imported.set(assetId, { id: assetId, name: o.name || assetId, type: o.renderer === CUTOUT_KIND ? "image" : "mesh" });
145
- }
146
- return [...catalogue, ...imported.values()];
147
- }
148
137
  function context() {
149
138
  const s = refresh(), entities = entityProjection(s), registry = ports.actions?.();
150
139
  const shot = s.shots.find(row => row.id === s.selectedShotId) ?? shotAtFrame(s.shots, s.view.frame);
@@ -158,7 +147,7 @@ export function createStudioAppBinding(ports) {
158
147
  // buildStudioContext selects the detailed rows and writes the real page.
159
148
  entities, entityPage: { returned: 0, total: 0, truncated: false, nextCursor: null },
160
149
  shots: s.shots.map(row => ({ id: row.id, name: row.name, range: range(row), keyCount: row.cameraKeys.length })), shotsTruncated: false,
161
- assets: assetList(s), recentReceipts: [...receipts.values()].filter(r => r.ok).reverse().slice(0, 3).map(r => ({ id: r.receiptId, summary: r.status, canUndoDirect: ports.canUndo(r) })),
150
+ assets: studioAssetList(s.objects), recentReceipts: [...receipts.values()].filter(r => r.ok).reverse().slice(0, 3).map(r => ({ id: r.receiptId, summary: r.status, canUndoDirect: ports.canUndo(r) })),
162
151
  jobs: [], capabilities: { profile: "studio-slice-1", tools: STUDIO_TOOL_FAMILIES,
163
152
  rigReady: Boolean(s.targets.get(s.activeCharacterId)?.rig), cameraReady: Boolean(s.camera), bridgeReady: s.bridgeReady,
164
153
  ...(s.previsMode ? { storyboard: s.previsMode === "storyboard" } : {}) },
@@ -196,13 +185,13 @@ export function createStudioAppBinding(ports) {
196
185
  if (id === s.host.sceneId) return { selection: s.selection, activeCharacterId: s.activeCharacterId, shotId: s.selectedShotId, view: s.view,
197
186
  ...(patched.length ? { patched } : {}) };
198
187
  const object = s.objects.find(row => row.id === id), entity = object ?? s.characters.find(row => row.id === id);
199
- // Beyond the transform an object's patched paths already carry: the world box it occupies and, for a
200
- // group, who hangs off it. `compact` (a receipt near its size limit) drops back to the paths alone.
201
- // A character's box means cloning and posing its rig, too dear for every receipt: inspect or verify has it.
188
+ // Beyond the transform an object's patched paths already carry: the world box it occupies (an empty: its
189
+ // parts', as inspect's rows) and, for a group, who hangs off it. `compact` (a receipt near its size limit) drops back to the paths alone.
190
+ // A character's box means cloning and posing its rig, too dear for every receipt or inspect row: verify has it.
202
191
  const extras = () => {
203
192
  if (!object || compact) return {};
204
193
  const childIds = s.objects.filter(row => row.parent === id).map(row => row.id);
205
- return { bounds: entityWorldBounds(object, { ...s, frame: s.view.frame }, { bounds: ports.bounds }),
194
+ return { bounds: objectRowBounds(object, s, ports),
206
195
  ...(childIds.length ? { childIds: childIds.slice(0, 12), ...(childIds.length > 12 ? { childCount: childIds.length } : {}) } : {}) };
207
196
  };
208
197
  if (patched.length) return { patched, ...extras() };
@@ -286,11 +275,12 @@ export function createStudioAppBinding(ports) {
286
275
  // A framing check also carries the cut's geometry, read from the scene: side of the action line
287
276
  // and sight lines per shot, the receipt's or targets' shots first (8 at most).
288
277
  const result = { receiptId: receipt?.receiptId ?? null, revision: s.revision, evidenceRevision, stale: evidenceRevision !== s.revision, checks: measured ? { coverage: "current-scene-targets" } : receipt.checks,
289
- verification: receipt?.verification ?? null, semanticStatus: "unavailable", visualRefs: [], unsupportedChecks: [], unsupportedReasons: {}, ...(args.checks.includes("framing") ? { geometry: shotGeometryFacts(scene, { first: receipt ? receipt.affectedIds : args.targets, ports: { bounds: ports.bounds } }) } : {}) };
290
- const reasons = result.unsupportedReasons, verified = [], skipped = [], pending = [];
291
- for (const check of args.checks.filter(check => check !== "motion" && measured)) {
278
+ verification: receipt?.verification ?? null, semanticStatus: "unavailable", visualRefs: [], unsupportedChecks: [], unsupportedReasons: {}, ...(args.checks.includes("framing") ? { geometry: shotGeometryFacts(scene, { first: receipt ? receipt.affectedIds : args.targets, ports: { bounds: ports.bounds } }) } : {}) }, reasons = result.unsupportedReasons, verified = [], skipped = [], pending = [];
279
+ // Lighting is measured from the rendered shot frame (studio-lighting-check.js), never from a receipt.
280
+ if (args.checks.includes("lighting")) verifyLighting(result, args.targets ?? receipt.affectedIds, scene, ports);
281
+ for (const check of args.checks.filter(check => !["motion", "lighting"].includes(check) && measured)) {
292
282
  if (!measured.length) { reasons[check] = "No target is an object or character in the current scene."; continue; }
293
- try { result.checks[check] = check === "placement" ? placementChecks(measured, scene, { bounds: ports.bounds }) : framingChecks(measured, scene, { bounds: ports.bounds }); }
283
+ try { result.checks[check] = check === "placement" ? placementChecks(measured, scene, { bounds: ports.bounds, takes: s.targets }) : framingChecks(measured, scene, { bounds: ports.bounds }); }
294
284
  catch (error) { if (!(error instanceof StudioProtocolError)) throw error; reasons[check] = error.message; }
295
285
  }
296
286
  if (args.checks.includes("framing") && args.range !== undefined && !args.rangeWasDefaulted) {
@@ -309,17 +299,7 @@ export function createStudioAppBinding(ports) {
309
299
  .then(verification => { verified[index] = verification; }, error => { if (!(error instanceof StudioProtocolError)) throw error; skipped.push(`${name}: ${error.message}`); }));
310
300
  });
311
301
  }
312
- // A frame is the shot camera's view; a plan is the Top View, the set from above
313
- // with names and the shot camera's wedge.
314
- if (args.visual !== "none") {
315
- if (args.visual === "contact_sheet") {
316
- // One image of frames across the range, each rendered through the export
317
- // path, which puts the playhead pose, shot camera and props back.
318
- const frames = sampleContactSheetFrames(args.range, s.frameCount), sheet = buildContactSheet(frames, ports.renderFrameBuffer), imageId = crypto.randomUUID();
319
- images.set(imageId, { dataUrl: ports.encodePng(sheet.data, sheet), width: sheet.width, height: sheet.height, frames, layout: CONTACT_SHEET_LAYOUT, revision: s.revision, receiptId: result.receiptId });
320
- result.visualRefs.push({ imageId, frames, layout: CONTACT_SHEET_LAYOUT });
321
- } else { const capture = args.visual === "plan" ? (ports.capturePlan ?? (() => fail("CAPABILITY_MISSING", "This editor cannot render the top view.")))() : ports.capture(); const imageId = crypto.randomUUID(); images.set(imageId, { ...capture, revision: s.revision, receiptId: result.receiptId }); result.visualRefs.push({ imageId, ...(args.visual === "plan" ? { kind: "plan" } : {}) }); }
322
- }
302
+ if (args.checks.includes("motion")) exportFramingInto(result, { args, s, receipt, scene, ports }); if (args.visual !== "none") studioVisual(args, { s, receipt, result, images, ports });
323
303
  const finish = () => {
324
304
  const computed = verified.filter(Boolean);
325
305
  if (computed.length) result.verification = computed.length === 1 ? computed[0] : computed;
@@ -347,7 +327,7 @@ export function createStudioAppBinding(ports) {
347
327
  // the model pays for): the revision is what the agent's next command is
348
328
  // admitted at, so a scope without it leaves that admission stale.
349
329
  const head = { revision: c.revision, host: (({ workspaceId, documentEpoch, sceneId, sceneEpoch }) => ({ workspaceId, documentEpoch, sceneId, sceneEpoch }))(c.host) };
350
- if (command.args.scope === "catalogue") return { ...head, ...studioObjectCatalogue() };
330
+ if (command.args.scope === "catalogue") return { ...head, ...studioObjectCatalogue(), kits: KIT_CATALOGUE, environments: hdrEnvironmentCatalogue() };
351
331
  if (command.args.scope === "document") return { ...head, scope: "document",
352
332
  ...readElementDocument(refresh().document, command.args, c.host.sceneId) };
353
333
  // Discovery for run_action: every registered action with its label, kind,
@@ -370,7 +350,8 @@ export function createStudioAppBinding(ports) {
370
350
  // Stable id order, so an offset cursor survives unrelated edits.
371
351
  const filtered = entityProjection(s).filter(wanted).sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
372
352
  const offset = args.cursor ? validateStudioCursor(args.cursor, c) : 0, limit = command.args.limit;
373
- return { ...head, entities: filtered.slice(offset, offset + limit), total: filtered.length,
353
+ // An effect row also carries its particles at the playhead (src/effects/state.js).
354
+ return { ...head, entities: filtered.slice(offset, offset + limit).map(row => withEffectState(row, s.view.frame, s.objects)), total: filtered.length,
374
355
  nextCursor: offset + limit < filtered.length ? studioEntityCursor(c, offset + limit) : null };
375
356
  },
376
357
  operate_studio: request => execute({ ...request, name: "operate_studio" }),
@@ -378,7 +359,6 @@ export function createStudioAppBinding(ports) {
378
359
  arrange_characters: request => execute({ ...request, name: "arrange_characters" }),
379
360
  patch_elements: request => execute({ ...request, name: "patch_elements" }),
380
361
  frame_shot: request => execute({ ...request, name: "frame_shot" }),
381
- generate_motion: request => execute({ ...request, name: "generate_motion" }),
382
362
  verify_result: request => execute({ ...request, name: "verify_result" }),
383
363
  undo_edit: request => runAction(request, { action: "edit.undo", args: request.args }),
384
364
  run_action: request => execute({ ...request, name: "run_action" }),
@@ -0,0 +1,17 @@
1
+ // The asset summary the agent's turn context lists: every catalogue kind the
2
+ // Studio can create, then each imported picture or model once.
3
+ import { CUTOUT_KIND, MESH_KIND, OBJECT_MENU_ENTRIES } from "./scene-objects.js";
4
+ import { studioObjectCatalogue } from "./studio-agent-commands.js";
5
+
6
+ export function studioAssetList(objects) {
7
+ const catalogue = studioObjectCatalogue().objects.map(({ kind }) => {
8
+ const entry = OBJECT_MENU_ENTRIES.find(row => row.kind === kind);
9
+ return { kind, name: entry?.label ?? kind, type: entry?.group === "Primitives" ? "primitive" : "set-piece" };
10
+ });
11
+ const imported = new Map();
12
+ for (const o of objects) {
13
+ const assetId = o.renderer === CUTOUT_KIND ? o.sourceAssetId || o.assetId : o.renderer === MESH_KIND ? o.assetId : null;
14
+ if (assetId && !imported.has(assetId)) imported.set(assetId, { id: assetId, name: o.name || assetId, type: o.renderer === CUTOUT_KIND ? "image" : "mesh" });
15
+ }
16
+ return [...catalogue, ...imported.values()];
17
+ }
@@ -5,7 +5,7 @@
5
5
  const CONTACT_SHEET_COLUMNS = 3;
6
6
  const CONTACT_SHEET_ROWS = 2;
7
7
  export const CONTACT_SHEET_LAYOUT = "3x2 row-major";
8
- const CONTACT_SHEET_MAX_SIDE = 1600;
8
+ const CONTACT_SHEET_MAX_SIDE = 1600, VIEW_SHEET_MAX_HEIGHT = 900;
9
9
 
10
10
  /** Up to 6 frames spread evenly over a half-open range, first and last
11
11
  * included; "whole_clip" (or no range) is timeline frames 0..frameCount-1. */
@@ -53,21 +53,53 @@ export function tileRgba(tiles, tileWidth, tileHeight, { columns = CONTACT_SHEET
53
53
  return { data, width, height };
54
54
  }
55
55
 
56
+ /** The grid of a sheet: 3x2 row-major for up to 6 frames, 4x2 for 7-8. Views of its own
57
+ * (viewCount >= 1) take a column per frame and a row per view when there are several views
58
+ * or at most 3 frames. viewCount 0 is the shot camera. */
59
+ export function sheetGrid(frameCount, viewCount = 0) {
60
+ if (viewCount > 1 || (viewCount === 1 && frameCount <= 3)) return { columns: frameCount, rows: viewCount, layout: `${frameCount}x${viewCount} row-major, a row per view` };
61
+ return frameCount <= CONTACT_SHEET_COLUMNS * CONTACT_SHEET_ROWS ? { columns: CONTACT_SHEET_COLUMNS, rows: CONTACT_SHEET_ROWS, layout: CONTACT_SHEET_LAYOUT } : { columns: 4, rows: 2, layout: "4x2 row-major" };
62
+ }
63
+
64
+ /** Square tile side for a grid of own-camera views: the sheet stays within
65
+ * maxSide wide and VIEW_SHEET_MAX_HEIGHT high (1600x900 at most). */
66
+ export const viewTileSide = ({ columns, rows }, maxSide = CONTACT_SHEET_MAX_SIDE) => Math.max(1, Math.floor(Math.min(maxSide / columns, VIEW_SHEET_MAX_HEIGHT / rows)));
67
+
68
+ /** Paint dots on a bottom-up RGBA buffer: { x, y (from the bottom row), r, color [r,g,b],
69
+ * alpha 0..1, ring } with ring drawing a 1 px white circle around the dot at r + 2. */
70
+ export function paintDots(data, width, height, dots) {
71
+ const put = (x, y, [r, g, b], a) => {
72
+ if (x < 0 || y < 0 || x >= width || y >= height) return;
73
+ const i = (y * width + x) * 4;
74
+ data[i] = Math.round(data[i] + (r - data[i]) * a); data[i + 1] = Math.round(data[i + 1] + (g - data[i + 1]) * a); data[i + 2] = Math.round(data[i + 2] + (b - data[i + 2]) * a);
75
+ };
76
+ for (const { x, y, r, color, alpha = 1, ring = false } of dots) {
77
+ const cx = Math.round(x), cy = Math.round(y), reach = Math.ceil(r) + (ring ? 3 : 0);
78
+ for (let dy = -reach; dy <= reach; dy++) for (let dx = -reach; dx <= reach; dx++) {
79
+ const d = Math.hypot(dx, dy);
80
+ if (d <= r) put(cx + dx, cy + dy, color, alpha);
81
+ else if (ring && Math.abs(d - r - 2) <= 0.5) put(cx + dx, cy + dy, [255, 255, 255], 1);
82
+ }
83
+ }
84
+ return data;
85
+ }
86
+
56
87
  /** Render each frame (render(frame) -> { data, width, height }, rows bottom-up),
57
88
  * shrink it so the whole sheet's longest side is at most maxSide, and tile
58
- * the frames 3x2 row-major into one bottom-up buffer. */
59
- export function buildContactSheet(frames, render, { maxSide = CONTACT_SHEET_MAX_SIDE } = {}) {
89
+ * the frames row-major (3x2 unless columns and rows say) into one bottom-up buffer.
90
+ * A frame may be any cell the render understands (a frame and a view). */
91
+ export function buildContactSheet(frames, render, { maxSide = CONTACT_SHEET_MAX_SIDE, columns = CONTACT_SHEET_COLUMNS, rows = CONTACT_SHEET_ROWS } = {}) {
60
92
  const tiles = [];
61
93
  let tileWidth, tileHeight;
62
94
  for (const frame of frames) {
63
95
  const shot = render(frame);
64
96
  if (tileWidth === undefined) {
65
- const scale = Math.min(1, maxSide / Math.max(shot.width * CONTACT_SHEET_COLUMNS, shot.height * CONTACT_SHEET_ROWS));
97
+ const scale = Math.min(1, maxSide / Math.max(shot.width * columns, shot.height * rows));
66
98
  tileWidth = Math.max(1, Math.floor(shot.width * scale));
67
99
  tileHeight = Math.max(1, Math.floor(shot.height * scale));
68
100
  }
69
101
  // The renderer reuses one read-back buffer: shrink this frame before the next render.
70
102
  tiles.push(boxDownscaleRgba(shot.data, shot.width, shot.height, tileWidth, tileHeight));
71
103
  }
72
- return tileRgba(tiles, tileWidth, tileHeight, { bottomUp: true });
104
+ return tileRgba(tiles, tileWidth, tileHeight, { columns, rows, bottomUp: true });
73
105
  }
@@ -31,15 +31,13 @@ const entries = [
31
31
  { path: "character.identityImage", type: "image", persisted: true, undoDomain: "cast", normalizer: "createCharacterEntry", note: "data:image only" },
32
32
  { path: "character.pose", type: "id", persisted: true, undoDomain: "cast", normalizer: "createCharacterEntry" },
33
33
  { path: "character.waypoints", type: "array", persisted: true, undoDomain: "cast", normalizer: "createCharacterEntry", actions: ["character.addWaypoint", "character.moveWaypoint", "character.removeWaypoint", "character.clearWaypoints"], note: "root path pins, addressed by frame" },
34
- { path: "character.promptBlocks", type: "array", persisted: true, undoDomain: "cast", normalizer: "createCharacterEntry", note: "stored at layer.promptClips" },
35
- { path: "character.motionRef.url", type: "string", persisted: true, undoDomain: "cast", normalizer: "createCharacterEntry" },
36
- { path: "character.motionRef.motionId", type: "id", persisted: true, undoDomain: "cast", normalizer: "createCharacterEntry" },
34
+
37
35
  { path: "character.sessionMotion", type: "array", persisted: false, undoDomain: "cast", normalizer: "createCharacterEntry", note: "dropped by createCharacterEntry" },
38
36
  { path: "character.ikKeys", type: "array", persisted: false, undoDomain: "cast", normalizer: "createCharacterEntry", actions: ["character.setIkKey", "character.removeIkKey", "character.clearIkKeys"], note: "dropped by createCharacterEntry; keys travel as JSON quaternions/positions" },
39
- { path: "object.renderer", type: "enum", persisted: true, undoDomain: "objects", normalizer: "normalizeSceneObject", enum: ["cube", "sphere", "capsule", "cylinder", "cone", "plane", "chair", "car", "small-plane", "empty"] },
37
+ { path: "object.renderer", type: "enum", persisted: true, undoDomain: "objects", normalizer: "normalizeSceneObject", enum: ["cube", "sphere", "capsule", "cylinder", "cone", "plane", "chair", "car", "small-plane", "empty", "light", "effect"], note: "light is made from catalogue kind point-light or spot-light; effect (smoke, fire, rain, trail...) from kind effect" },
40
38
  { path: "object.position", type: "vec3", persisted: true, undoDomain: "objects", normalizer: "normalizeSceneObject", min: { x: -240, y: 0, z: -240 }, max: { x: 240, y: 240, z: 240 }, note: "y is the object's base: y=0 rests on the floor; height/supportY rise from it" },
41
- { path: "object.rotation", type: "vec3", persisted: true, undoDomain: "objects", normalizer: "normalizeSceneObject", min: { x: -180, y: -180, z: -180 }, max: { x: 180, y: 180, z: 180 }, note: "rot/rotX/rotZ degrees, wrapped at the upper bound" },
42
- { path: "object.scale", type: "vec3", persisted: true, undoDomain: "objects", normalizer: "normalizeSceneObject", min: { x: 0.1, y: 0.1, z: 0.1 }, max: { x: 100, y: 100, z: 100 } },
39
+ { path: "object.rotation", type: "vec3", persisted: true, undoDomain: "objects", normalizer: "normalizeSceneObject", min: { x: -180, y: -180, z: -180 }, max: { x: 180, y: 180, z: 180 }, angle: true, note: "rot/rotX/rotZ degrees, wrapped at the upper bound" },
40
+ { path: "object.scale", type: "vec3", persisted: true, undoDomain: "objects", normalizer: "normalizeSceneObject", min: { x: 0.01, y: 0.01, z: 0.01 }, max: { x: 100, y: 100, z: 100 } },
43
41
  { path: "object.name", type: "string", persisted: true, undoDomain: "objects", normalizer: "normalizeSceneObject" },
44
42
  { path: "object.color", type: "color", persisted: true, undoDomain: "objects", normalizer: "normalizeSceneObject" },
45
43
  { path: "object.opacity", type: "number", persisted: true, undoDomain: "objects", normalizer: "normalizeSceneObject", min: 0.05, max: 1, note: "1 is solid; lower is see-through, e.g. 0.3 for clear glass" },
@@ -48,6 +46,9 @@ const entries = [
48
46
  { path: "object.path", type: "array", persisted: true, undoDomain: "objects", normalizer: "normalizeSceneObject" },
49
47
  { path: "object.remove", type: "boolean", persisted: true, undoDomain: "objects", normalizer: null, note: "lifecycle operation, not a document field" },
50
48
  { path: "object.cutout", type: "image", persisted: true, undoDomain: "objects", normalizer: "normalizeSceneObject", actions: ["asset.import"], note: "assetId-backed cutout record; imported pictures and models are placed by asset.import" },
49
+ { path: "object.light", type: "object", persisted: true, undoDomain: "objects", normalizer: "normalizeSceneObject", note: "renderer light only. { type point|spot, color, intensity 0..1000 candela (lighter 0.45, candle 0.3, desk lamp 5, street lamp 40), range m (0 none), angle 1..89 deg spot half-angle, softness 0..1, shadow (first 2 lights only; inside a solid record it is blocked: receipt LIGHT_BLOCKED), flicker 0..1 (lighter 0.6), aim {x,y,z} world point or null = local -Y }, e.g. { intensity: 40, aim: { x: 0, y: 0, z: 1 } }; a take level track scales it" },
50
+ { path: "object.glow", type: "object", persisted: true, undoDomain: "objects", normalizer: "normalizeSceneObject", note: "any record: self-lit colour plus additive halo, same under any stage light; casts no shadow, so a light inside a glowing bulb shines out. { color, intensity 0..20, halo 0..4 x the record's radius }, e.g. ember { color: \"#ff5a1f\", intensity: 6, halo: 2 }; null removes; a take level track scales it" },
51
+ { path: "object.effect", type: "object", persisted: true, undoDomain: "objects", normalizer: "normalizeSceneObject", note: "renderer effect only (object.add kind \"effect\"). { preset: smoke|exhale|steam|fire|sparks|dust|rain|snow|trail, rate particles/s, life s, size m start diameter, grow end/start size, speed m/s along the record's local +Y (rotX 90 aims +Z), spread 0..1 cone, gravity m/s2 (+ falls, - rises), wind {x,y,z} m/s, color, opacity 0..1, additive (emissive: fire, sparks), seed int, area {x,y,z} m birth box (rain/snow/dust fall through it from its top), width m trail ribbon across local Y }. A new preset loads its defaults and fields in the same set override them, e.g. { preset: \"fire\", rate: 40 }; the record stores the merged numbers. Particles are born where the record is at each frame (route, parent, take carry), only while it and its parents are shown, at rate x the take level. To emit from the body (a breath at the mouth), a motion spec entry objects.<id> = { follow: \"landmark:mouth\", aim: \"face\" } moves it there every frame. Measure with inspect_studio { scope: \"entities\", ids }: effect { frame (the playhead), alive, emitter [x,y,z], bounds { min, max } m }" },
51
52
  { path: "stage.keyLight.x", type: "number", persisted: true, undoDomain: "stage", normalizer: "createSceneStage", min: -30, max: 30 },
52
53
  { path: "stage.keyLight.y", type: "number", persisted: true, undoDomain: "stage", normalizer: "createSceneStage", min: 0.5, max: 30 },
53
54
  { path: "stage.keyLight.z", type: "number", persisted: true, undoDomain: "stage", normalizer: "createSceneStage", min: -30, max: 30 },
@@ -60,6 +61,25 @@ const entries = [
60
61
  { path: "stage.camera", documentPath: "shotAspect", type: "enum", persisted: true, undoDomain: "stage", normalizer: "createSceneStage", enum: ["16:9", "2.39:1", "9:16", "1:1", "4:3", "12:7", "fal 480P"], note: "output aspect ratio" },
61
62
  { path: "stage.cameraPresetId", type: "id", nullable: true, persisted: true, undoDomain: "stage", normalizer: "createSceneStage" },
62
63
  { path: "stage.sensorId", type: "enum", persisted: true, undoDomain: "stage", normalizer: "createSceneStage", enum: ["super16", "super35", "fullFrame", "65mm"] },
64
+ // The stage look (src/stage-lighting.js). stage.applyLighting writes all of it from a recipe; each field stays tweakable.
65
+ { path: "stage.keyLight.color", type: "color", nullable: true, persisted: true, undoDomain: "stage", normalizer: "createSceneStage", note: "key light colour #rrggbb, overrides warmth, e.g. \"#9db6e6\" cool moonlight or \"#ffad5c\" low sun; null = white tinted by warmth" },
66
+ { path: "stage.lighting.preset", type: "enum", persisted: true, readOnly: true, undoDomain: "stage", normalizer: "createSceneStage", enum: ["studio", "day", "overcast", "golden", "dusk", "night", "interior", "stage"], note: "label of the last recipe stage.applyLighting applied; studio is the neutral grey default" },
67
+ { path: "stage.lighting.sky", type: "color", nullable: true, persisted: true, undoDomain: "stage", normalizer: "createSceneStage", note: "background and fog colour at the horizon, e.g. \"#0b1426\" night; null = the UI theme's studio grey" },
68
+ { path: "stage.lighting.skyTop", type: "color", nullable: true, persisted: true, undoDomain: "stage", normalizer: "createSceneStage", note: "sky colour overhead: shot camera and exports grade from sky at the horizon to this, e.g. \"#020408\"; null = flat sky" },
69
+ { path: "stage.lighting.ambient", type: "number", persisted: true, undoDomain: "stage", normalizer: "createSceneStage", min: 0, max: 3, note: "multiplier on the hemisphere and ambient light; 1 = studio, 0.6 = night, 0 = black stage" },
70
+ { path: "stage.lighting.hemiSky", type: "color", persisted: true, undoDomain: "stage", normalizer: "createSceneStage", note: "hemisphere light from above, e.g. \"#34507f\" cool night sky; studio \"#ffffff\"" },
71
+ { path: "stage.lighting.hemiGround", type: "color", persisted: true, undoDomain: "stage", normalizer: "createSceneStage", note: "hemisphere bounce from below, e.g. \"#6e5136\" warm ground; studio \"#5a5d64\"" },
72
+ { path: "stage.lighting.fill", type: "number", persisted: true, undoDomain: "stage", normalizer: "createSceneStage", min: 0, max: 3, note: "multiplier on the fixed fill and rim lights; 1 = studio, 0 = key only" },
73
+ { path: "stage.lighting.fog", type: "number", persisted: true, undoDomain: "stage", normalizer: "createSceneStage", min: 0, max: 1, note: "haze 0..1 fading the set into the sky colour: 0 = far horizon only, 0.45 = from ~18 m to ~38 m, 1 = from 2 m to 14 m" },
74
+ { path: "stage.lighting.exposure", type: "number", persisted: true, undoDomain: "stage", normalizer: "createSceneStage", min: 0.2, max: 3, note: "camera exposure multiplier on lit surfaces; 1 = studio, e.g. 1.4 to lift a dark frame" },
75
+ { path: "stage.lighting.shadowSoftness", type: "number", persisted: true, undoDomain: "stage", normalizer: "createSceneStage", min: 0, max: 1, note: "key shadow penumbra; 0 = crisp (studio), 0.75 = soft moonlight, 1 = overcast" },
76
+ { path: "stage.lighting.envLight", type: "number", persisted: true, undoDomain: "stage", normalizer: "createSceneStage", min: 0, max: 2, note: "sky environment light on every surface: a map from sky, skyTop, hemiGround and a glow toward the key (sun or moon) tints and fills clay from all sides; it takes over the flat hemisphere as it rises. 0 = hemisphere only (studio), 1 = the map alone at the same brightness (every other recipe), e.g. 1.4 for a brighter fill" },
77
+ { path: "stage.lighting.ao", type: "number", persisted: true, undoDomain: "stage", normalizer: "createSceneStage", min: 0, max: 1, note: "ambient occlusion strength: contact darkening within ~0.5 m where a body meets the floor or a prop and in creases (armpits, the back of a squat's knees), in the viewport and every export. 0 = off (studio), 0.8 = most recipes, 1 = overcast" },
78
+ // An HDR environment map (src/stage-hdr.js): the light and, with hdrBackground, the backdrop.
79
+ { path: "stage.lighting.hdr", type: "id", nullable: true, persisted: true, undoDomain: "stage", normalizer: "createSceneStage", note: "HDR environment map that replaces the sky environment light: a built-in (\"builtin:night-street\", \"builtin:overcast-field\", \"builtin:studio-softbox\") or a file the user loaded (\"hdr-<32 hex>\"); inspect_studio { scope: \"catalogue\" } environments lists them with each map's brightest direction. It lights the set only while envLight > 0 (its light = map x hdrIntensity x envLight; studio has envLight 0, so set envLight 1). stage.applyLighting clears it unless keep has \"lighting.hdr\". null = the look's own sky" },
80
+ { path: "stage.lighting.hdrRotation", type: "number", persisted: true, undoDomain: "stage", normalizer: "createSceneStage", min: -180, max: 180, angle: true, note: "degrees the HDR map turns about the vertical axis, light and backdrop together: +90 carries a light at +x (screen right from the default camera) to -z (behind the subject), so a brightest [x, y, z] lands at [x cos r + z sin r, y, z cos r - x sin r]; wrapped at the upper bound" },
81
+ { path: "stage.lighting.hdrIntensity", type: "number", persisted: true, undoDomain: "stage", normalizer: "createSceneStage", min: 0, max: 4, note: "multiplier on the HDR map's radiance in its light and its backdrop; 1 = the file as authored, e.g. 0.5 to calm a bright daylight map" },
82
+ { path: "stage.lighting.hdrBackground", type: "boolean", persisted: true, undoDomain: "stage", normalizer: "createSceneStage", note: "true = the HDR map is the backdrop behind the set (the floor fades into its horizon), in the viewport, shot and every export; false = keep the look's sky as the backdrop and use the map only as light" },
63
83
  { path: "shot.crud", type: "array", persisted: true, undoDomain: "shot", normalizer: null, actions: ["shot.create", "shot.split", "shot.duplicate", "shot.remove", "shot.setRange", "shot.reorder"], note: "create/split/duplicate/reorder/remove/range through the shared action registry" },
64
84
  { path: "shot.cameraKeys", type: "array", persisted: true, undoDomain: "shot", normalizer: null, frameMin: 0 },
65
85
  { path: "shot.cameraRail", type: "array", persisted: true, undoDomain: "shot", normalizer: "repairCamera", actions: ["shot.setCameraRail", "shot.clearCameraRail"], note: "rail points; crane/dolly timing follow the shot's camera block" },
@@ -74,7 +94,7 @@ const entries = [
74
94
  { path: "selection", type: "id", persisted: false, undoDomain: null, normalizer: null },
75
95
  { path: "timeline", type: "number", persisted: false, undoDomain: null, normalizer: null },
76
96
  { path: "view.mode", type: "enum", persisted: false, undoDomain: null, normalizer: null, enum: ["scene", "pose", "camera", "motion"] },
77
- { path: "view.partColoursGuideModeInset", type: "array", persisted: false, undoDomain: null, normalizer: null, actions: ["view.setPartColours", "view.setGuideMode", "view.setInset"], note: "partColours/guideMode/inset; viewer settings, not document fields" },
97
+ { path: "view.partColoursGuideModeInset", type: "array", persisted: false, undoDomain: null, normalizer: null, actions: ["view.setPartColours", "view.setGuideMode", "view.setInset", "view.setGhosts"], note: "partColours/guideMode/inset/ghosts; viewer settings, not document fields" },
78
98
  { path: "read.sceneDescription", type: "string", persisted: false, undoDomain: null, normalizer: null },
79
99
  { path: "read.captureFrame", type: "image", persisted: false, undoDomain: null, normalizer: null },
80
100
  { path: "undo", type: "boolean", persisted: false, undoDomain: null, normalizer: null },