cozyclay 2.1.0 → 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 (583) hide show
  1. package/CHANGELOG.md +65 -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 +22 -6
  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-BlT33CGf.js → first-shot-handoff-CC4xe4HM.js} +1 -1
  33. package/dist/assets/{landing-BSesrCpL.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/llms.txt +14 -0
  40. package/dist/sw.js +3 -6
  41. package/dist/workflow/index.html +5 -4
  42. package/mcp/LIVE-PROTOCOL.md +7 -47
  43. package/mcp/README.md +16 -26
  44. package/mcp/live-hub.mjs +6 -10
  45. package/mcp/package.json +1 -3
  46. package/mcp/tool-handlers.mjs +4 -168
  47. package/package.json +3 -22
  48. package/src/App.jsx +174 -2639
  49. package/src/agent-progress.js +79 -0
  50. package/src/agent-turn-mark.js +30 -0
  51. package/src/analytics.js +1 -19
  52. package/src/app-context.js +5 -0
  53. package/src/app-stage.jsx +2 -34
  54. package/src/asset-pane.jsx +6 -5
  55. package/src/camera-block.js +21 -0
  56. package/src/camera-direction.js +120 -0
  57. package/src/character-kind.js +3 -9
  58. package/src/command-bus.js +8 -6
  59. package/src/commands/cast.js +5 -34
  60. package/src/commands/elements/character.js +1 -1
  61. package/src/commands/elements/motion.js +1 -6
  62. package/src/commands/elements/object.js +6 -2
  63. package/src/commands/elements/shot.js +4 -1
  64. package/src/commands/elements.js +38 -14
  65. package/src/commands/index.js +3 -1
  66. package/src/commands/kits.js +37 -0
  67. package/src/commands/motion-timing.js +57 -0
  68. package/src/commands/motion.js +9 -152
  69. package/src/commands/motion3-compile.js +65 -0
  70. package/src/commands/motion3.js +207 -0
  71. package/src/commands/objects.js +6 -4
  72. package/src/commands/shot.js +49 -11
  73. package/src/commands/stage.js +14 -1
  74. package/src/commands/view.js +4 -1
  75. package/src/document-mirrors.js +42 -0
  76. package/src/document-store.js +4 -0
  77. package/src/domains/cast.js +19 -194
  78. package/src/domains/motion/motion-range-pin-rules.js +1 -1
  79. package/src/domains/motion/take-source.js +51 -0
  80. package/src/domains/motion.js +60 -1838
  81. package/src/domains/scenes.js +12 -55
  82. package/src/domains/shots.js +2 -2
  83. package/src/domains/stage.js +4 -2
  84. package/src/draft-ghost.js +46 -0
  85. package/src/draft-ghost.jsx +129 -0
  86. package/src/dualview.jsx +4 -3
  87. package/src/effects/EffectsLayer.jsx +112 -0
  88. package/src/effects/effect-meshes.js +232 -0
  89. package/src/effects/effect-record.js +103 -0
  90. package/src/effects/emitter.js +59 -0
  91. package/src/effects/simulate.js +261 -0
  92. package/src/effects/state.js +59 -0
  93. package/src/execution-telemetry.js +2 -3
  94. package/src/first-success-guide.jsx +5 -5
  95. package/src/ghost-set.js +257 -0
  96. package/src/held-object-edit.jsx +113 -0
  97. package/src/held-object-pose.js +175 -0
  98. package/src/hierarchy-model.js +1 -1
  99. package/src/hierarchy-panel.jsx +1 -1
  100. package/src/imported-mesh.jsx +103 -0
  101. package/src/kits/held.js +84 -0
  102. package/src/kits/index.js +70 -0
  103. package/src/kits/smoking.js +57 -0
  104. package/src/light-blockers.js +93 -0
  105. package/src/motion/beat-board-model.js +84 -0
  106. package/src/motion/beat-board.css +124 -0
  107. package/src/motion/beat-board.jsx +59 -0
  108. package/src/motion/center-of-mass.js +66 -0
  109. package/src/motion/fix-collisions.js +850 -0
  110. package/src/motion/physics-review.js +118 -0
  111. package/src/{ardy → motion}/physics-support.js +1 -76
  112. package/src/motion/rig-snapshot.js +22 -0
  113. package/src/motion/take-segments.jsx +140 -0
  114. package/src/{ardy → motion}/timeline-coordinates.js +0 -21
  115. package/src/{ardy → motion}/timeline.css +1 -2
  116. package/src/{ardy → motion}/timeline.jsx +48 -419
  117. package/src/motion/waypoints.js +76 -0
  118. package/src/motion3/beats.js +83 -0
  119. package/src/motion3/body.js +158 -0
  120. package/src/motion3/bones.js +47 -0
  121. package/src/motion3/carried.js +47 -0
  122. package/src/motion3/compile-job.js +75 -0
  123. package/src/motion3/compile-worker.js +10 -0
  124. package/src/motion3/compiler/anchors.js +105 -0
  125. package/src/motion3/compiler/arm-orient.js +128 -0
  126. package/src/motion3/compiler/channels.js +125 -0
  127. package/src/motion3/compiler/compile.js +205 -0
  128. package/src/motion3/compiler/gait-rhythm.js +97 -0
  129. package/src/motion3/compiler/grip.js +171 -0
  130. package/src/motion3/compiler/head-steady.js +48 -0
  131. package/src/motion3/compiler/hold.js +134 -0
  132. package/src/motion3/compiler/idle.js +113 -0
  133. package/src/motion3/compiler/limbs.js +170 -0
  134. package/src/motion3/compiler/math.js +72 -0
  135. package/src/motion3/compiler/pose.js +162 -0
  136. package/src/motion3/compiler/quat.js +49 -0
  137. package/src/motion3/compiler/skeleton.js +74 -0
  138. package/src/motion3/compiler/validate-common.js +32 -0
  139. package/src/motion3/compiler/validate-goals.js +118 -0
  140. package/src/motion3/compiler/validate-objects.js +143 -0
  141. package/src/motion3/compiler/validate-parts.js +116 -0
  142. package/src/motion3/compiler/validate.js +122 -0
  143. package/src/motion3/compiler/vocab.js +73 -0
  144. package/src/motion3/compiler/walk-pelvis.js +275 -0
  145. package/src/motion3/compiler/walk.js +207 -0
  146. package/src/motion3/compiler/wrist-clamp.js +32 -0
  147. package/src/motion3/delta.js +47 -0
  148. package/src/motion3/describe.js +174 -0
  149. package/src/motion3/docs.js +27 -0
  150. package/src/motion3/edit-carry.js +21 -0
  151. package/src/motion3/fix/ease.js +16 -0
  152. package/src/motion3/fix/ground.js +37 -0
  153. package/src/motion3/fix/index.js +52 -0
  154. package/src/motion3/fix/push.js +71 -0
  155. package/src/motion3/fix/reach.js +46 -0
  156. package/src/motion3/fix/retime.js +79 -0
  157. package/src/motion3/fix/self.js +211 -0
  158. package/src/motion3/fix/slide.js +70 -0
  159. package/src/motion3/fix/turn.js +51 -0
  160. package/src/motion3/follow.js +39 -0
  161. package/src/motion3/free-motion.js +173 -0
  162. package/src/motion3/grip-choice.js +130 -0
  163. package/src/motion3/hold-plan.js +157 -0
  164. package/src/motion3/json-patch.js +77 -0
  165. package/src/motion3/key-aim.js +105 -0
  166. package/src/motion3/key-edit.js +82 -0
  167. package/src/motion3/lenient.js +127 -0
  168. package/src/motion3/object-keys.js +236 -0
  169. package/src/motion3/object-pose.js +182 -0
  170. package/src/motion3/object-tracks.js +12 -0
  171. package/src/motion3/pipeline.js +102 -0
  172. package/src/motion3/playback.js +51 -0
  173. package/src/motion3/probes.js +91 -0
  174. package/src/motion3/qa/body-checks.js +34 -0
  175. package/src/motion3/qa/check-names.js +14 -0
  176. package/src/motion3/qa/checks.js +121 -0
  177. package/src/motion3/qa/contact-checks.js +51 -0
  178. package/src/motion3/qa/explain.js +80 -0
  179. package/src/motion3/qa/fmt.js +5 -0
  180. package/src/motion3/qa/foot-checks.js +72 -0
  181. package/src/motion3/qa/geometry.js +77 -0
  182. package/src/motion3/qa/goals.js +191 -0
  183. package/src/motion3/qa/ground.js +60 -0
  184. package/src/motion3/qa/hints-contact.js +179 -0
  185. package/src/motion3/qa/hints-goal.js +160 -0
  186. package/src/motion3/qa/hints-knee.js +24 -0
  187. package/src/motion3/qa/hints-motion.js +209 -0
  188. package/src/motion3/qa/hints-object.js +91 -0
  189. package/src/motion3/qa/index.js +42 -0
  190. package/src/motion3/qa/key-checks.js +82 -0
  191. package/src/motion3/qa/near.js +96 -0
  192. package/src/motion3/qa/object-checks.js +135 -0
  193. package/src/motion3/qa/patch.js +161 -0
  194. package/src/motion3/qa/severity.js +38 -0
  195. package/src/motion3/qa/spec-checks.js +65 -0
  196. package/src/motion3/qa/style.js +218 -0
  197. package/src/motion3/qa/wrist-checks.js +69 -0
  198. package/src/motion3/qa/wrist-obs.js +60 -0
  199. package/src/motion3/refs.js +120 -0
  200. package/src/motion3/repair-tune.js +121 -0
  201. package/src/motion3/repair.js +83 -0
  202. package/src/motion3/report.js +67 -0
  203. package/src/motion3/rigs/index.js +14 -0
  204. package/src/motion3/rigs/x-bot-tpose-body.json +1 -0
  205. package/src/motion3/rigs/x-bot-tpose.json +1 -0
  206. package/src/motion3/rigs/y-bot-tpose-body.json +1 -0
  207. package/src/motion3/rigs/y-bot-tpose.json +1 -0
  208. package/src/motion3/scene.js +60 -0
  209. package/src/motion3/schema/brief.js +89 -0
  210. package/src/motion3/schema/fields.js +44 -0
  211. package/src/motion3/schema/spec-examples.js +46 -0
  212. package/src/motion3/schema/topics.js +111 -0
  213. package/src/motion3/schema/types.js +147 -0
  214. package/src/motion3/search.js +117 -0
  215. package/src/motion3/second-hand.js +112 -0
  216. package/src/motion3/skills/basic.js +82 -0
  217. package/src/motion3/skills/book.js +76 -0
  218. package/src/motion3/skills/car.js +145 -0
  219. package/src/motion3/skills/frames-auto.js +29 -0
  220. package/src/motion3/skills/index.js +83 -0
  221. package/src/motion3/skills/info.js +65 -0
  222. package/src/motion3/skills/smoke-kit.js +109 -0
  223. package/src/motion3/skills/smoke.js +160 -0
  224. package/src/motion3/spec-store.js +59 -0
  225. package/src/motion3/take-edit.js +155 -0
  226. package/src/motion3/take.js +63 -0
  227. package/src/motion3/timeline.js +127 -0
  228. package/src/motion3/views.js +121 -0
  229. package/src/motion3/walk-plan.js +119 -0
  230. package/src/object-catalog.jsx +7 -1
  231. package/src/object-colors.js +98 -0
  232. package/src/object-gizmo-parts.js +115 -0
  233. package/src/object-gizmo.jsx +37 -122
  234. package/src/object-glow.js +139 -0
  235. package/src/object-hidden.js +33 -0
  236. package/src/object-look.js +196 -0
  237. package/src/object-size.js +10 -0
  238. package/src/object-travel.js +71 -6
  239. package/src/panels/CameraPanel.jsx +7 -7
  240. package/src/panels/HdrEnvironmentControls.jsx +59 -0
  241. package/src/panels/LightPanel.jsx +25 -4
  242. package/src/panels/ObjectLookPanel.jsx +88 -0
  243. package/src/panels/PoseImagePanel.jsx +1 -1
  244. package/src/panels/PosePanel.jsx +2 -17
  245. package/src/panels/RigControlPanel.jsx +5 -53
  246. package/src/panels/SubjectBox.jsx +4 -1
  247. package/src/panels/details.css +14 -0
  248. package/src/posestudio.jsx +4 -27
  249. package/src/project-browser.jsx +3 -4
  250. package/src/project-resources.js +18 -31
  251. package/src/project.js +13 -104
  252. package/src/props.jsx +25 -280
  253. package/src/rail-dot-drag.js +23 -0
  254. package/src/render-passes.js +10 -0
  255. package/src/resource-status.jsx +1 -0
  256. package/src/result-modal.jsx +4 -19
  257. package/src/room.jsx +20 -12
  258. package/src/scene-assets.js +8 -6
  259. package/src/scene-light-place.js +72 -0
  260. package/src/scene-lights.jsx +148 -0
  261. package/src/scene-objects.js +31 -139
  262. package/src/scenes.js +15 -105
  263. package/src/semantic-edit.js +2 -5
  264. package/src/set-pieces.jsx +183 -0
  265. package/src/settings-menu.jsx +3 -11
  266. package/src/shell/BottomDock.jsx +11 -53
  267. package/src/shell/CompositionGuideButton.jsx +20 -0
  268. package/src/shell/DetailsSlot.jsx +16 -200
  269. package/src/shell/GhostsMenuGroup.jsx +74 -0
  270. package/src/shell/LibrarySlot.jsx +1 -3
  271. package/src/shell/MenuBar.jsx +3 -3
  272. package/src/shell/PreferencesDialog.jsx +15 -264
  273. package/src/shell/PreferencesSlot.jsx +2 -8
  274. package/src/shell/ShotPreviewHeader.jsx +17 -0
  275. package/src/shell/TopBar.jsx +5 -83
  276. package/src/shell/ViewportToolbar.jsx +23 -93
  277. package/src/shell/agent-glass.css +19 -6
  278. package/src/shell/details-selection.js +42 -0
  279. package/src/shell/ghosts-menu.css +22 -0
  280. package/src/shell/glass-regions.css +29 -3
  281. package/src/shell/preferences.css +6 -1
  282. package/src/shell/viewport.css +12 -0
  283. package/src/shot-authoring.js +4 -7
  284. package/src/soft-shadows.js +37 -0
  285. package/src/stage-attach-frames.js +12 -12
  286. package/src/stage-canvas-rigs.jsx +3 -3
  287. package/src/stage-capture.jsx +42 -8
  288. package/src/stage-characters.jsx +22 -216
  289. package/src/stage-editor-gizmos.jsx +6 -6
  290. package/src/stage-environment.js +93 -0
  291. package/src/stage-hdr-builtins.js +107 -0
  292. package/src/stage-hdr-image.js +116 -0
  293. package/src/stage-hdr-library.js +70 -0
  294. package/src/stage-hdr-map.js +94 -0
  295. package/src/stage-hdr.js +130 -0
  296. package/src/stage-lighting.js +149 -0
  297. package/src/stage-occlusion.js +210 -0
  298. package/src/stage-sky.jsx +159 -0
  299. package/src/stage-startup-defaults.js +5 -9
  300. package/src/studio-actions.js +12 -14
  301. package/src/studio-agent-commands.js +46 -16
  302. package/src/studio-agent-motion.js +9 -9
  303. package/src/studio-agent-protocol.js +46 -73
  304. package/src/studio-app-binding.js +29 -45
  305. package/src/studio-asset-list.js +17 -0
  306. package/src/studio-contact-sheet.js +37 -5
  307. package/src/studio-elements.js +27 -7
  308. package/src/studio-entity-bounds.js +18 -0
  309. package/src/studio-error-codes.js +8 -0
  310. package/src/studio-export-framing.js +152 -0
  311. package/src/studio-lighting-check.js +103 -0
  312. package/src/studio-motion-visual.js +150 -0
  313. package/src/studio-schema-errors.js +80 -0
  314. package/src/studio-support-gap.js +74 -0
  315. package/src/styles.css +1 -19
  316. package/src/timeline-defaults.js +5 -0
  317. package/src/timeline-extent.js +8 -20
  318. package/src/tutorial-walk.js +17 -0
  319. package/src/ui-scrub.js +1 -1
  320. package/src/use-shot-camera-tracks.js +12 -0
  321. package/src/view-ghosts.js +97 -0
  322. package/src/view-ghosts.jsx +49 -0
  323. package/src/workflow/AgentPanel.jsx +47 -166
  324. package/src/workflow/AgentProgressCard.jsx +107 -0
  325. package/src/workflow/AgentReplyText.jsx +37 -0
  326. package/src/workflow/AgentTurnSteps.jsx +84 -0
  327. package/src/workflow/LoginPanel.jsx +140 -0
  328. package/src/workflow/WorkflowBuilder.jsx +7 -23
  329. package/src/workflow/agent-client.js +54 -51
  330. package/src/workflow/agent-panel.css +71 -1
  331. package/src/workflow/agent-progress-card.css +167 -0
  332. package/src/workflow/agent-progress-view.js +47 -0
  333. package/src/workflow/agent-turn-steps.css +113 -0
  334. package/src/workflow/agent-turn.js +191 -0
  335. package/src/workflow/login-client.js +34 -0
  336. package/src/workflow/motion-input.js +18 -31
  337. package/src/workflow/node-schema.js +1 -1
  338. package/src/workflow/reply-markdown.js +69 -0
  339. package/src/workflow/scene-asset-sync.js +8 -8
  340. package/tools/agent-session-stats.mjs +61 -0
  341. package/tools/dev-full.mjs +5 -56
  342. package/tools/eslint-baseline.json +12 -13
  343. package/tools/headless-studio.mjs +85 -0
  344. package/tools/knip-baseline.json +0 -179
  345. package/tools/loc-baseline.json +33 -83
  346. package/tools/motion3/extract-body.mjs +103 -0
  347. package/tools/motion3/extract-rig.mjs +85 -0
  348. package/tools/process-supervisor.mjs +0 -88
  349. package/tools/run-tests.mjs +9 -124
  350. package/bin/agent/motion-runtime.mjs +0 -76
  351. package/dist/ardy/cskel27-rest.json +0 -557
  352. package/dist/assets/analytics-B41FmPYI.js +0 -1
  353. package/dist/assets/app-Do-Og5Pu.css +0 -1
  354. package/dist/assets/app-DwP-tl2x.js +0 -4920
  355. package/dist/assets/config-Bx7pYsD9.js +0 -1
  356. package/dist/assets/config-DeoFTYzb.css +0 -1
  357. package/dist/assets/demo-Dt0qZfS7.js +0 -1
  358. package/dist/assets/scene-asset-sync-BCWDJB1d.js +0 -19
  359. package/dist/assets/scene-asset-sync-DjJ_cJ2R.css +0 -1
  360. package/dist/assets/ticket-jqMhqL88.js +0 -1
  361. package/dist/assets/workflow-Br-K7Srs.js +0 -179
  362. package/dist/demo/walk-then-stop.npz +0 -0
  363. package/mcp/ardy-prompts.mjs +0 -218
  364. package/src/ardy/auto-fix-panel.css +0 -108
  365. package/src/ardy/auto-physics.js +0 -1194
  366. package/src/ardy/client.js +0 -153
  367. package/src/ardy/convert.js +0 -313
  368. package/src/ardy/cskel27-neutral.js +0 -39
  369. package/src/ardy/cskel27.js +0 -77
  370. package/src/ardy/export.js +0 -165
  371. package/src/ardy/fix-collisions.js +0 -2257
  372. package/src/ardy/foot-lock.js +0 -230
  373. package/src/ardy/motion-calibration.js +0 -118
  374. package/src/ardy/motion-edit.js +0 -213
  375. package/src/ardy/motion-url.js +0 -55
  376. package/src/ardy/mp4-duration.js +0 -102
  377. package/src/ardy/npz.js +0 -595
  378. package/src/ardy/physics-panel.css +0 -21
  379. package/src/ardy/physics-panel.jsx +0 -89
  380. package/src/ardy/physics-refine.js +0 -111
  381. package/src/ardy/physics-review.js +0 -567
  382. package/src/ardy/physics-temporal.js +0 -54
  383. package/src/ardy/platform-fit-panel.css +0 -64
  384. package/src/ardy/platform-fit-panel.jsx +0 -41
  385. package/src/ardy/platform-fit.js +0 -326
  386. package/src/ardy/playback.js +0 -711
  387. package/src/ardy/pose-pin.js +0 -97
  388. package/src/ardy/prompt-clips.js +0 -30
  389. package/src/ardy/retime.js +0 -211
  390. package/src/ardy/root-drop.js +0 -369
  391. package/src/ardy/timeline-resize.js +0 -13
  392. package/src/ardy/to-cskel27.js +0 -225
  393. package/src/ardy/trim.js +0 -33
  394. package/src/ardy/waypoints.js +0 -369
  395. package/src/ardy-bridge-contract.js +0 -281
  396. package/src/domains/motion/motion-video-capture.js +0 -664
  397. package/src/i2v-motion-client.js +0 -77
  398. package/src/i2v-motion-studio.jsx +0 -180
  399. package/src/line-edit.js +0 -1348
  400. package/src/motion/generation.js +0 -105
  401. package/src/motion-readiness-ui.jsx +0 -67
  402. package/src/motion-readiness.js +0 -40
  403. package/src/motion-resources.js +0 -177
  404. package/src/motion-store.js +0 -65
  405. package/src/motion-trail.js +0 -601
  406. package/src/multimodel-ingest.js +0 -394
  407. package/src/panels/PromptBlocksPanel.jsx +0 -444
  408. package/src/panels/VideoCapturePanel.jsx +0 -181
  409. package/src/pose-extract/browser.js +0 -42
  410. package/src/pose-extract/detector.js +0 -100
  411. package/src/pose-extract/fit.js +0 -227
  412. package/src/pose-extract/image-frame.js +0 -51
  413. package/src/pose-extract/index.js +0 -58
  414. package/src/pose-extract/keyframes.js +0 -74
  415. package/src/pose-extract/landmarks.js +0 -162
  416. package/src/pose-extract/mirror.js +0 -153
  417. package/src/pose-extract/take.js +0 -131
  418. package/src/pose-extract/video-frames.js +0 -91
  419. package/src/take-recipe.js +0 -165
  420. package/src/trail-key-conflicts.js +0 -39
  421. package/src/trail-pick.js +0 -59
  422. package/src/use-i2v-capture-modal.js +0 -15
  423. package/tools/ardy/BRIDGE.md +0 -37
  424. package/tools/ardy/GVHMR-NPZ.md +0 -68
  425. package/tools/ardy/README.md +0 -124
  426. package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
  427. package/tools/ardy/artifacts.mjs +0 -34
  428. package/tools/ardy/bridge.mjs +0 -1732
  429. package/tools/ardy/bvh-cskel27.mjs +0 -1265
  430. package/tools/ardy/cclay_constrained_generate.py +0 -561
  431. package/tools/ardy/cclay_gvhmr_worker.py +0 -264
  432. package/tools/ardy/cclay_motion_edit.py +0 -510
  433. package/tools/ardy/cclay_sequence_generate.py +0 -705
  434. package/tools/ardy/cclay_worker.py +0 -304
  435. package/tools/ardy/dump-npz.py +0 -205
  436. package/tools/ardy/extract-rest.mjs +0 -299
  437. package/tools/ardy/extract.mjs +0 -507
  438. package/tools/ardy/footage.mjs +0 -467
  439. package/tools/ardy/gvhmr-floor.mjs +0 -102
  440. package/tools/ardy/gvhmr-to-cskel27.mjs +0 -21
  441. package/tools/ardy/gvhmr_fastpath.py +0 -168
  442. package/tools/ardy/gvhmr_trajectory.py +0 -240
  443. package/tools/ardy/merge-text-encoder.py +0 -160
  444. package/tools/ardy/mocap-metrics.mjs +0 -200
  445. package/tools/ardy/mocap-quality-gate.mjs +0 -110
  446. package/tools/ardy/motion-stabilize.mjs +0 -348
  447. package/tools/ardy/npz.mjs +0 -414
  448. package/tools/ardy/pose-to-npz.mjs +0 -106
  449. package/tools/ardy/prompt-limits.mjs +0 -5
  450. package/tools/ardy/run-edit-on-box.sh +0 -73
  451. package/tools/ardy/run-local.mjs +0 -233
  452. package/tools/ardy/run-on-box.sh +0 -593
  453. package/tools/ardy/run-sequence-on-box.sh +0 -177
  454. package/tools/ardy/runners/gvhmr-worker.mjs +0 -206
  455. package/tools/ardy/runners/index.mjs +0 -18
  456. package/tools/ardy/runners/local.mjs +0 -455
  457. package/tools/ardy/runners/proc.mjs +0 -125
  458. package/tools/ardy/runners/remote.mjs +0 -252
  459. package/tools/ardy/setup-local.mjs +0 -189
  460. package/tools/ardy/setup-text-encoder-on-box.sh +0 -93
  461. package/tools/ardy/setup-text-encoder.py +0 -306
  462. package/tools/ardy/smpl-cskel27.mjs +0 -178
  463. package/tools/ardy/visual-qa.mjs +0 -196
  464. package/tools/ardy/vq-car.mjs +0 -52
  465. package/tools/bench/EXP3.md +0 -61
  466. package/tools/bench/cclay_bench_extract_incam.py +0 -125
  467. package/tools/bench/cclay_bench_extract_obs.py +0 -204
  468. package/tools/bench/cclay_bench_runner.py +0 -42
  469. package/tools/bench/cube-contact.mjs +0 -162
  470. package/tools/bench/exp3.mjs +0 -152
  471. package/tools/bench/extract-bench-lib.mjs +0 -216
  472. package/tools/bench/extract-bench.mjs +0 -302
  473. package/tools/bench/fal-generate.mjs +0 -88
  474. package/tools/bench/fit/README.md +0 -189
  475. package/tools/bench/fit/camera.mjs +0 -59
  476. package/tools/bench/fit/contact.mjs +0 -419
  477. package/tools/bench/fit/footlock.mjs +0 -234
  478. package/tools/bench/fit/motion.mjs +0 -85
  479. package/tools/bench/fit/pin.mjs +0 -27
  480. package/tools/bench/fit/remote.mjs +0 -105
  481. package/tools/bench/fit-bench.mjs +0 -126
  482. package/tools/bench/fit-sanity.mjs +0 -45
  483. package/tools/bench/metrics.mjs +0 -313
  484. package/tools/bench/obs/depth.mjs +0 -258
  485. package/tools/bench/obs/extrinsics.mjs +0 -175
  486. package/tools/bench/obs/fit_mannequin_betas.py +0 -340
  487. package/tools/bench/obs/ground.mjs +0 -294
  488. package/tools/bench/obs/heading.mjs +0 -151
  489. package/tools/bench/obs/ladder.mjs +0 -555
  490. package/tools/bench/obs/mannequin-betas.json +0 -124
  491. package/tools/bench/obs/remote.mjs +0 -83
  492. package/tools/bench/obs/rest_joints.py +0 -52
  493. package/tools/bench/obs/ybot-targets.mjs +0 -49
  494. package/tools/bench/obs-bench.mjs +0 -520
  495. package/tools/bench/score.mjs +0 -343
  496. package/tools/bench/summarize.mjs +0 -86
  497. package/tools/demo-worker/README.md +0 -137
  498. package/tools/demo-worker/api-client.mjs +0 -233
  499. package/tools/demo-worker/generate.mjs +0 -301
  500. package/tools/demo-worker/index.mjs +0 -466
  501. package/tools/demo-worker/sign-core.mjs +0 -112
  502. package/tools/demo-worker/sign.mjs +0 -161
  503. package/tools/gt-render/browser.mjs +0 -159
  504. package/tools/gt-render/camera-math.mjs +0 -276
  505. package/tools/gt-render/page.mjs +0 -267
  506. package/tools/gt-render/render.mjs +0 -514
  507. package/tools/gt-render/scene-box.mjs +0 -31
  508. package/tools/gt-render/take-transform.mjs +0 -87
  509. package/tools/kimodo/compare-seam.mjs +0 -108
  510. package/tools/kimodo/constraints.mjs +0 -244
  511. package/tools/kimodo/edit.mjs +0 -259
  512. package/tools/kimodo/effector-constraints.mjs +0 -200
  513. package/tools/kimodo/generate.mjs +0 -646
  514. package/tools/kimodo/local-output.mjs +0 -265
  515. package/tools/kimodo/measure-pose.mjs +0 -65
  516. package/tools/kimodo/measure-preserve.mjs +0 -899
  517. package/tools/kimodo/measure-waypoints.mjs +0 -55
  518. package/tools/kimodo/mlx-generate.py +0 -111
  519. package/tools/kimodo/pose-constraints.mjs +0 -248
  520. package/tools/kimodo/preserve-mask.mjs +0 -543
  521. package/tools/kimodo/read-npz.mjs +0 -189
  522. package/tools/kimodo/run-edit-on-box.mjs +0 -155
  523. package/tools/kimodo/run-sequence-on-box.mjs +0 -178
  524. package/tools/kimodo/runner.mjs +0 -284
  525. package/tools/kimodo/seam-batch.mjs +0 -77
  526. package/tools/kimodo/setup-local.mjs +0 -30
  527. package/tools/kimodo/setup-on-box.sh +0 -339
  528. package/tools/kimodo/soma77-to-cskel27.mjs +0 -305
  529. package/tools/morphgs/assets/gen_truth.py +0 -39
  530. package/tools/morphgs/assets/mesh_ori_rig.txt +0 -27
  531. package/tools/morphgs/assets/playback-check.mjs +0 -30
  532. package/tools/morphgs/demo-gate.sh +0 -26
  533. package/tools/morphgs/fbx2morphgs.mjs +0 -68
  534. package/tools/morphgs/morphgs-to-cskel27.mjs +0 -105
  535. package/tools/morphgs/patches/preprocess_src-none-mode.patch +0 -76
  536. package/tools/morphgs/setup-on-cluster.sh +0 -126
  537. package/tools/projflow/driver.py +0 -1069
  538. package/tools/projflow/generate.mjs +0 -805
  539. package/tools/projflow/hml22-to-cskel27.mjs +0 -735
  540. package/tools/projflow/line-edit-job.mjs +0 -547
  541. package/tools/projflow/replay.mjs +0 -452
  542. package/tools/projflow/runner.mjs +0 -269
  543. package/tools/projflow/service.mjs +0 -742
  544. package/tools/track/DESIGN.md +0 -513
  545. package/tools/track/backfill-provenance.mjs +0 -116
  546. package/tools/track/budget.mjs +0 -8
  547. package/tools/track/check-rig.mjs +0 -209
  548. package/tools/track/diagnostics.schema.json +0 -60
  549. package/tools/track/export-rig.mjs +0 -256
  550. package/tools/track/fallback.mjs +0 -7
  551. package/tools/track/fk-parity-fixture.mjs +0 -161
  552. package/tools/track/gate.mjs +0 -349
  553. package/tools/track/masks.mjs +0 -318
  554. package/tools/track/metrics.mjs +0 -201
  555. package/tools/track/publish-obs.mjs +0 -108
  556. package/tools/track/py/check_env.py +0 -61
  557. package/tools/track/py/eval_lr.py +0 -229
  558. package/tools/track/py/lr_viterbi.py +0 -294
  559. package/tools/track/py/masks.py +0 -405
  560. package/tools/track/py/objective.py +0 -382
  561. package/tools/track/py/rig.py +0 -508
  562. package/tools/track/py/scene.py +0 -326
  563. package/tools/track/py/test_joint_indices.py +0 -123
  564. package/tools/track/py/test_lr_viterbi.py +0 -195
  565. package/tools/track/py/test_masks.py +0 -229
  566. package/tools/track/py/test_rig.py +0 -288
  567. package/tools/track/py/test_scene.py +0 -354
  568. package/tools/track/py/test_track.py +0 -328
  569. package/tools/track/py/track.py +0 -411
  570. package/tools/track/remote.mjs +0 -140
  571. package/tools/track/rig-dump.mjs +0 -227
  572. package/tools/track/run-box-tests.mjs +0 -14
  573. package/tools/track/run-box.mjs +0 -168
  574. package/tools/track/setup-box.sh +0 -59
  575. package/tools/track/study-2d.mjs +0 -728
  576. /package/src/{ardy → motion}/collision-blockers.js +0 -0
  577. /package/src/{ardy → motion}/ground.js +0 -0
  578. /package/src/{ardy → motion}/ik-drag.js +0 -0
  579. /package/src/{ardy → motion}/ik-key-json.js +0 -0
  580. /package/src/{ardy → motion}/ik.js +0 -0
  581. /package/src/{ardy → motion}/physics-surface.js +0 -0
  582. /package/src/{ardy → motion}/playback-clock.js +0 -0
  583. /package/src/{ardy → motion}/range-pin.js +0 -0
@@ -0,0 +1,210 @@
1
+ // Ambient occlusion for the stage look (stage.lighting.ao): contact darkening
2
+ // where a body meets the floor or a prop, and in creases (an armpit, the back of
3
+ // a knee in a squat). Screen space, because the bodies are skinned and move: a
4
+ // baked aoMap cannot follow a skinned mesh, and contact blobs under the feet
5
+ // miss every crease.
6
+ //
7
+ // One hook on the scene (scene.onAfterRender) runs it after each draw of the
8
+ // stage through a perspective camera, so every path gets it with no code of its
9
+ // own: the viewport panes, the shot preview card, capture_frame, contact sheets
10
+ // and video export (stage-capture.jsx renders the same scene into its target).
11
+ // A draw with an override material (the outline prepass, the depth and normal
12
+ // plates) is left alone, and so is the orthographic plan.
13
+ //
14
+ // Per draw, at half resolution past 960 px wide: a depth-only prepass of the
15
+ // opaque set, the occlusion within AO_RADIUS of each pixel from SAMPLES samples
16
+ // (how far each sample's direction rises above the surface the depth holds,
17
+ // weighted toward the near ones and faded with the fog), a depth-aware 5x5
18
+ // blur, then a multiply over the frame just drawn. The prepass reuses the
19
+ // shadow maps the draw already made. On an Apple M4 Pro it adds about 0.4 ms of
20
+ // GPU time to a 1920x1080 export frame.
21
+ import * as THREE from "three";
22
+ import { FullScreenQuad } from "three/examples/jsm/postprocessing/Pass.js";
23
+ import { GIZMO_LAYER, POSER_LAYER } from "./dualview.jsx";
24
+
25
+ const AO_RADIUS = 0.4;
26
+ const SAMPLES = 16;
27
+ // A sample whose direction rises less than this cosine above the normal is the
28
+ // same surface (floor noise at grazing angles), not an occluder.
29
+ const BIAS = 0.15;
30
+ // Occlusion to darkening, tuned by eye on the clay figure (a squat's creases, feet on the deck).
31
+ const GAIN = 3;
32
+ // Size buckets kept alive at once (the main pane, the preview card, the exports).
33
+ const CACHED_SIZES = 4;
34
+
35
+ const VERTEX = "varying vec2 vUv;\nvoid main() { vUv = uv; gl_Position = vec4(position.xy, 0.0, 1.0); }";
36
+ const OCCLUSION = `
37
+ uniform sampler2D tDepth;
38
+ uniform mat4 projection;
39
+ uniform mat4 inverseProjection;
40
+ uniform vec2 size;
41
+ uniform vec2 fog;
42
+ varying vec2 vUv;
43
+ vec3 viewAt(vec2 uv, float depth) {
44
+ vec4 p = inverseProjection * vec4(vec3(uv, depth) * 2.0 - 1.0, 1.0);
45
+ return p.xyz / p.w;
46
+ }
47
+ vec3 viewAt(vec2 uv) { return viewAt(uv, texture2D(tDepth, uv).x); }
48
+ void main() {
49
+ float depth = texture2D(tDepth, vUv).x;
50
+ if (depth >= 1.0) { gl_FragColor = vec4(1.0); return; }
51
+ vec3 p = viewAt(vUv, depth);
52
+ // The normal of the surface the depth buffer holds (each axis from the nearer
53
+ // neighbour, so an edge does not bend it): flat facets of the low-poly clay
54
+ // then occlude nothing of their own, whatever their smoothed shading normal says.
55
+ vec2 texel = 1.0 / size;
56
+ vec3 right = viewAt(vUv + vec2(texel.x, 0.0)) - p, left = p - viewAt(vUv - vec2(texel.x, 0.0));
57
+ vec3 up = viewAt(vUv + vec2(0.0, texel.y)) - p, down = p - viewAt(vUv - vec2(0.0, texel.y));
58
+ vec3 n = normalize(cross(abs(right.z) < abs(left.z) ? right : left, abs(up.z) < abs(down.z) ? up : down));
59
+ float reach = min(${AO_RADIUS.toFixed(2)} * projection[1][1] * 0.5 * size.y / -p.z, size.y * 0.2);
60
+ float fade = 1.0 - smoothstep(fog.x, fog.y, -p.z);
61
+ if (reach < 1.0 || fade <= 0.0) { gl_FragColor = vec4(1.0); return; }
62
+ float spin = 6.2831853 * fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715))));
63
+ // A weighted mean over the samples near the surface (weight falls to 0 at RADIUS),
64
+ // so an occluder close by counts fully however small it is on screen.
65
+ float occlusion = 0.0, weights = 0.25;
66
+ for (int i = 0; i < ${SAMPLES}; i++) {
67
+ float angle = float(i) * 2.39996323 + spin;
68
+ // snapped to a texel centre, so the depth read and the position rebuilt from it agree
69
+ vec2 uv = (floor((vUv + vec2(cos(angle), sin(angle)) * ((float(i) + 0.5) / ${SAMPLES}.0) * reach / size) * size) + 0.5) / size;
70
+ vec3 v = viewAt(uv) - p;
71
+ float distance = length(v), weight = pow(max(0.0, 1.0 - distance / ${AO_RADIUS.toFixed(2)}), 2.0);
72
+ occlusion += max(dot(v, n) / max(distance, 1e-4) - ${BIAS.toFixed(2)}, 0.0) * weight;
73
+ weights += weight;
74
+ }
75
+ float ao = clamp(1.0 - ${GAIN.toFixed(2)} * occlusion / (weights * ${(1 - BIAS).toFixed(2)}), 0.0, 1.0);
76
+ gl_FragColor = vec4(vec3(mix(1.0, ao, fade)), 1.0);
77
+ }`;
78
+ const BLUR = `
79
+ #include <packing>
80
+ uniform sampler2D tOcclusion;
81
+ uniform sampler2D tDepth;
82
+ uniform vec2 size;
83
+ uniform vec2 clip;
84
+ varying vec2 vUv;
85
+ float viewZ(vec2 uv) { return perspectiveDepthToViewZ(texture2D(tDepth, uv).x, clip.x, clip.y); }
86
+ void main() {
87
+ float z = viewZ(vUv), total = 0.0, weights = 0.0;
88
+ for (int x = -2; x <= 2; x++) for (int y = -2; y <= 2; y++) {
89
+ vec2 uv = vUv + vec2(float(x), float(y)) / size;
90
+ float weight = 1.0 - smoothstep(0.02, 0.06, abs(viewZ(uv) - z) / -z);
91
+ total += texture2D(tOcclusion, uv).r * weight;
92
+ weights += weight;
93
+ }
94
+ gl_FragColor = vec4(vec3(total / max(weights, 1e-4)), 1.0);
95
+ }`;
96
+ const COMPOSITE = `
97
+ uniform sampler2D tOcclusion;
98
+ uniform float strength;
99
+ varying vec2 vUv;
100
+ void main() { gl_FragColor = vec4(vec3(mix(1.0, texture2D(tOcclusion, vUv).r, strength)), 1.0); }`;
101
+
102
+ const pass = (fragmentShader, uniforms, extra = {}) => new THREE.ShaderMaterial({ vertexShader: VERTEX, fragmentShader, uniforms, depthTest: false, depthWrite: false, ...extra });
103
+ const target = (width, height, filter, depth = false) => new THREE.WebGLRenderTarget(width, height, {
104
+ minFilter: filter, magFilter: filter, depthBuffer: depth, generateMipmaps: false,
105
+ ...(depth ? { depthTexture: Object.assign(new THREE.DepthTexture(width, height, THREE.UnsignedIntType), { minFilter: THREE.NearestFilter, magFilter: THREE.NearestFilter }) } : {}),
106
+ });
107
+ // What the prepass leaves out: light rather than surface (glow halos, smoke,
108
+ // see-through and faded parts, ghosts), picture cards whose alpha a solid
109
+ // depth pass would ignore, lines and points.
110
+ const seeThrough = (material) => [material].flat().some((m) => m && (m.transparent || m.depthWrite === false));
111
+ const leftOut = (node) => node.userData?.beautyOnly || node.userData?.cutoutTexture !== undefined || node.isSprite || node.isPoints || node.isLine || (node.isMesh && seeThrough(node.material));
112
+
113
+ /**
114
+ * Put the occlusion pass on `scene`. `settings.strength` (0..1) is read at each
115
+ * draw, so a slider changes it without rebuilding anything. Answers a function
116
+ * that takes the hook off and frees the targets.
117
+ */
118
+ export function installOcclusion(scene, settings) {
119
+ // Depth is all the pass reads: the prepass writes no colour at all.
120
+ const depthOnly = new THREE.MeshBasicMaterial({ side: THREE.DoubleSide, colorWrite: false });
121
+ const occlusion = pass(OCCLUSION, { tDepth: { value: null }, projection: { value: new THREE.Matrix4() }, inverseProjection: { value: new THREE.Matrix4() }, size: { value: new THREE.Vector2() }, fog: { value: new THREE.Vector2() } });
122
+ const blur = pass(BLUR, { tOcclusion: { value: null }, tDepth: { value: null }, size: { value: new THREE.Vector2() }, clip: { value: new THREE.Vector2() } });
123
+ const composite = pass(COMPOSITE, { tOcclusion: { value: null }, strength: { value: 1 } }, {
124
+ blending: THREE.CustomBlending, blendEquation: THREE.AddEquation, blendSrc: THREE.ZeroFactor, blendDst: THREE.SrcColorFactor,
125
+ blendSrcAlpha: THREE.ZeroFactor, blendDstAlpha: THREE.OneFactor, toneMapped: false,
126
+ });
127
+ const quad = new FullScreenQuad(), sizes = new Map(), viewport = new THREE.Vector4(), scissor = new THREE.Vector4();
128
+ let running = false;
129
+ const targetsFor = (width, height) => {
130
+ const key = `${width}x${height}`;
131
+ let set = sizes.get(key);
132
+ if (set) sizes.delete(key);
133
+ else set = { prepass: target(width, height, THREE.NearestFilter, true), raw: target(width, height, THREE.NearestFilter), smooth: target(width, height, THREE.LinearFilter) };
134
+ sizes.set(key, set);
135
+ if (sizes.size > CACHED_SIZES) {
136
+ const [oldest, stale] = sizes.entries().next().value;
137
+ sizes.delete(oldest);
138
+ for (const t of Object.values(stale)) { t.depthTexture?.dispose(); t.dispose(); }
139
+ }
140
+ return set;
141
+ };
142
+ scene.onAfterRender = (renderer, _scene, camera) => {
143
+ if (running || scene.overrideMaterial || !camera?.isPerspectiveCamera || !(settings.strength > 0)) return;
144
+ running = true;
145
+ const output = renderer.getRenderTarget(), autoClear = renderer.autoClear, background = scene.background, mask = camera.layers.mask;
146
+ const shadows = renderer.shadowMap, shadowAuto = shadows.autoUpdate, shadowNeeds = shadows.needsUpdate;
147
+ renderer.getViewport(viewport);
148
+ renderer.getScissor(scissor);
149
+ const pixels = renderer.getCurrentViewport(new THREE.Vector4()), half = pixels.z > 960 ? 0.5 : 1;
150
+ const width = Math.max(1, Math.round(pixels.z * half)), height = Math.max(1, Math.round(pixels.w * half));
151
+ const { prepass, raw, smooth } = targetsFor(width, height), hidden = [];
152
+ try {
153
+ scene.traverse((node) => { if (node.visible && leftOut(node)) { hidden.push(node); node.visible = false; } });
154
+ scene.background = null;
155
+ scene.overrideMaterial = depthOnly;
156
+ camera.layers.disable(GIZMO_LAYER);
157
+ camera.layers.disable(POSER_LAYER);
158
+ shadows.autoUpdate = false;
159
+ renderer.autoClear = true;
160
+ renderer.setRenderTarget(prepass);
161
+ renderer.render(scene, camera);
162
+ for (const node of hidden.splice(0)) node.visible = true;
163
+ scene.background = background;
164
+ scene.overrideMaterial = null;
165
+ camera.layers.mask = mask;
166
+ const u = occlusion.uniforms, b = blur.uniforms, fog = scene.fog;
167
+ u.tDepth.value = b.tDepth.value = prepass.depthTexture;
168
+ u.projection.value.copy(camera.projectionMatrix);
169
+ u.inverseProjection.value.copy(camera.projectionMatrixInverse);
170
+ u.size.value.set(width, height);
171
+ u.fog.value.set(fog?.near ?? 1e5, fog?.far ?? 2e5);
172
+ renderer.setRenderTarget(raw);
173
+ quad.material = occlusion;
174
+ quad.render(renderer);
175
+ b.tOcclusion.value = raw.texture;
176
+ b.size.value.set(width, height);
177
+ b.clip.value.set(camera.near, camera.far);
178
+ renderer.setRenderTarget(smooth);
179
+ quad.material = blur;
180
+ quad.render(renderer);
181
+ composite.uniforms.tOcclusion.value = smooth.texture;
182
+ composite.uniforms.strength.value = Math.min(1, settings.strength);
183
+ renderer.setRenderTarget(output);
184
+ // The canvas takes back the pane's own viewport (setRenderTarget(null) floors it, the draw rounded it).
185
+ if (output === null) {
186
+ renderer.setViewport(viewport);
187
+ renderer.setScissor(scissor);
188
+ }
189
+ renderer.autoClear = false;
190
+ quad.material = composite;
191
+ quad.render(renderer);
192
+ } finally {
193
+ for (const node of hidden) node.visible = true;
194
+ if (renderer.getRenderTarget() !== output) renderer.setRenderTarget(output);
195
+ scene.background = background;
196
+ scene.overrideMaterial = null;
197
+ camera.layers.mask = mask;
198
+ shadows.autoUpdate = shadowAuto;
199
+ shadows.needsUpdate = shadowNeeds;
200
+ renderer.autoClear = autoClear;
201
+ running = false;
202
+ }
203
+ };
204
+ return () => {
205
+ delete scene.onAfterRender;
206
+ for (const set of sizes.values()) for (const t of Object.values(set)) { t.depthTexture?.dispose(); t.dispose(); }
207
+ for (const material of [depthOnly, occlusion, blur, composite]) material.dispose();
208
+ quad.dispose();
209
+ };
210
+ }
@@ -0,0 +1,159 @@
1
+ import { useEffect, useLayoutEffect, useRef } from "react";
2
+ import { useThree } from "@react-three/fiber";
3
+ import * as THREE from "three";
4
+ import { stageLook } from "./stage-lighting.js";
5
+ import { ENV_HEIGHT, ENV_WIDTH, environmentPixels } from "./stage-environment.js";
6
+ import { installOcclusion } from "./stage-occlusion.js";
7
+ import { useHdrMap } from "./stage-hdr-map.js";
8
+ import { refreshHdrLibrary } from "./stage-hdr-library.js";
9
+
10
+ // Rows of the equirectangular gradient: v runs from straight down (0) through
11
+ // the horizon (0.5) to straight up (1).
12
+ const SKY_ROWS = 128;
13
+ const rgb = (hex) => [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16));
14
+
15
+ /** A sky that grades from `bottom` at the horizon (and below it) to `top`
16
+ * overhead, eased so most of the change happens in the lower 60 degrees a shot
17
+ * camera actually sees. Built once per look as an sRGB equirectangular texture;
18
+ * three draws it as the background in the same pass as the scene. */
19
+ function gradientSky(bottom, top) {
20
+ const [from, to] = [rgb(bottom), rgb(top)];
21
+ const data = new Uint8Array(SKY_ROWS * 4);
22
+ for (let row = 0; row < SKY_ROWS; row++) {
23
+ const elevation = ((row + 0.5) / SKY_ROWS - 0.5) * 180;
24
+ const t = elevation <= 0 ? 0 : 1 - (1 - Math.min(1, elevation / 60)) ** 2;
25
+ for (let channel = 0; channel < 3; channel++) data[row * 4 + channel] = Math.round(from[channel] + (to[channel] - from[channel]) * t);
26
+ data[row * 4 + 3] = 255;
27
+ }
28
+ const texture = new THREE.DataTexture(data, 1, SKY_ROWS);
29
+ texture.colorSpace = THREE.SRGBColorSpace;
30
+ texture.mapping = THREE.EquirectangularReflectionMapping;
31
+ texture.magFilter = THREE.LinearFilter;
32
+ texture.minFilter = THREE.LinearFilter;
33
+ texture.needsUpdate = true;
34
+ return texture;
35
+ }
36
+
37
+ /** The look's environment map (stage-environment.js), prefiltered for image-based
38
+ * lighting. Half floats keep the glow around the key above 1 and filter
39
+ * everywhere WebGL2 runs. */
40
+ function environmentMap(gl, spec) {
41
+ const pixels = environmentPixels(spec), half = new Uint16Array(pixels.length);
42
+ for (let i = 0; i < pixels.length; i++) half[i] = THREE.DataUtils.toHalfFloat(pixels[i]);
43
+ const texture = new THREE.DataTexture(half, ENV_WIDTH, ENV_HEIGHT, THREE.RGBAFormat, THREE.HalfFloatType);
44
+ texture.mapping = THREE.EquirectangularReflectionMapping;
45
+ texture.magFilter = THREE.LinearFilter;
46
+ texture.minFilter = THREE.LinearFilter;
47
+ texture.needsUpdate = true;
48
+ try { return prefiltered(gl, texture); } finally { texture.dispose(); }
49
+ }
50
+
51
+ /** An equirectangular map prefiltered for image-based lighting (PMREM). */
52
+ function prefiltered(gl, texture) {
53
+ const generator = new THREE.PMREMGenerator(gl);
54
+ try { return generator.fromEquirectangular(texture); } finally { generator.dispose(); }
55
+ }
56
+
57
+ /**
58
+ * The stage's sky, fog and exposure (stage.lighting through stageLook), set on
59
+ * the one scene and renderer every path draws with: the viewport panes, the
60
+ * shot preview card, capture_frame, contact sheets and video export
61
+ * (stage-capture.jsx). Nothing here is a post-process; it is all the same pass.
62
+ *
63
+ * The open stage runs 500 m; without a falloff the whole deck reads at once and
64
+ * the horizon sits a kilometre away. The fog fades the floor INTO the sky colour,
65
+ * so past its far distance the deck simply ceases to exist with no horizon line,
66
+ * no clip edge and no tone break. Haze (stage.lighting.fog) pulls that fade in.
67
+ *
68
+ * Perspective cameras see the gradient sky when skyTop is set; orthographic ones
69
+ * (the plan) keep the flat horizon colour, since a box-mapped sky has no meaning
70
+ * looking straight down. A caller that swaps the background for a pass of its
71
+ * own (render-passes.js) is left alone.
72
+ *
73
+ * The environment light (scene.environment) and the ambient occlusion pass
74
+ * (stage-occlusion.js) are set here too, for the same paths. Both are off in the
75
+ * studio look, which therefore draws exactly what it drew before them. The map
76
+ * is rebuilt only when its colours or the key's direction change.
77
+ *
78
+ * An HDR environment (stage.lighting.hdr, stage-hdr-map.js) replaces that map
79
+ * once it has loaded: its prefiltered texture is the light (prefiltered again
80
+ * only when the map changes; rotation and intensity are uniforms), and with
81
+ * hdrBackground the map itself is the perspective backdrop, the deck fading
82
+ * into the map's horizon colour. Until a file loads, the look's own sky lights.
83
+ */
84
+ export function StageAtmosphere({ lighting = null, keyLight, theme = "dark", grid = false }) {
85
+ // The scene and renderer are read inside the effects: they are three objects
86
+ // this component configures, not React state.
87
+ const get = useThree((state) => state.get), invalidate = useThree((state) => state.invalidate);
88
+ const look = stageLook({ lighting, keyLight, theme, grid });
89
+ const { sky, skyTop, exposure } = look, { near, far } = look.fog;
90
+ const hazeNear = look.captureFog?.near ?? null, hazeFar = look.captureFog?.far ?? null;
91
+ const envKey = look.env ? JSON.stringify(look.env) : null, { envIntensity, ao } = look;
92
+ const hdrMap = useHdrMap(look.hdr?.id ?? null), hdr = hdrMap ? look.hdr : null;
93
+ const backdrop = hdr?.background ? hdrMap : null, backdropIntensity = backdrop ? hdr.intensity : 1;
94
+ const hdrLight = hdr ? hdr.envIntensity : null, hdrTurn = hdr ? (hdr.rotation * Math.PI) / 180 : 0;
95
+ useEffect(() => { void refreshHdrLibrary(); }, []);
96
+ useLayoutEffect(() => {
97
+ const { scene } = get();
98
+ const flat = backdrop ? new THREE.Color().setRGB(...backdrop.horizon.map((value) => value * backdropIntensity), THREE.LinearSRGBColorSpace) : new THREE.Color(sky);
99
+ const gradient = skyTop ? gradientSky(sky, skyTop) : null, map = backdrop?.texture ?? null;
100
+ scene.background = flat;
101
+ scene.backgroundIntensity = backdropIntensity;
102
+ scene.fog = new THREE.Fog(flat, near, far);
103
+ scene.onBeforeRender = (_renderer, _scene, camera) => {
104
+ if (scene.background === flat || scene.background === gradient || (map && scene.background === map)) scene.background = camera.isPerspectiveCamera ? map ?? gradient ?? flat : flat;
105
+ };
106
+ invalidate();
107
+ return () => {
108
+ scene.onBeforeRender = () => {};
109
+ gradient?.dispose();
110
+ };
111
+ }, [get, invalidate, sky, skyTop, near, far, backdrop, backdropIntensity]);
112
+ useLayoutEffect(() => {
113
+ // The viewport applies exposure in tone mapping. Exports render into a
114
+ // target, which three never tone-maps, so CaptureRig reads it from here
115
+ // and scales the lights for its draw instead (same pass, same scene).
116
+ const { scene, gl } = get();
117
+ gl.toneMappingExposure = exposure;
118
+ // An HDR backdrop is light as well, so exports scale it (and the horizon the deck fades into) too.
119
+ scene.userData.stageCapture = { exposure, fog: hazeNear === null ? null : { near: hazeNear, far: hazeFar }, backdrop: Boolean(backdrop) };
120
+ invalidate();
121
+ }, [get, invalidate, exposure, hazeNear, hazeFar, backdrop]);
122
+ const hdrTexture = hdrMap?.texture ?? null, envSource = hdrTexture ? null : envKey;
123
+ useLayoutEffect(() => {
124
+ if (!hdrTexture && !envSource) return undefined;
125
+ const { scene, gl } = get();
126
+ const target = hdrTexture ? prefiltered(gl, hdrTexture) : environmentMap(gl, JSON.parse(envSource));
127
+ scene.environment = target.texture;
128
+ invalidate();
129
+ return () => {
130
+ if (scene.environment === target.texture) scene.environment = null;
131
+ target.dispose();
132
+ invalidate();
133
+ };
134
+ }, [get, invalidate, hdrTexture, envSource]);
135
+ useLayoutEffect(() => {
136
+ const { scene } = get();
137
+ scene.environmentIntensity = hdrLight ?? envIntensity;
138
+ // The light and the backdrop turn together: +90 deg carries +x to -z.
139
+ scene.environmentRotation.set(0, hdrTurn, 0);
140
+ scene.backgroundRotation.set(0, hdrTurn, 0);
141
+ invalidate();
142
+ }, [get, invalidate, envIntensity, hdrLight, hdrTurn]);
143
+ // The pass stays installed while ao > 0; the slider only changes its strength.
144
+ const occlusion = useRef({ strength: ao }), occluded = ao > 0;
145
+ useLayoutEffect(() => {
146
+ if (!occluded) return undefined;
147
+ const remove = installOcclusion(get().scene, occlusion.current);
148
+ invalidate();
149
+ return () => {
150
+ remove();
151
+ invalidate();
152
+ };
153
+ }, [get, invalidate, occluded]);
154
+ useLayoutEffect(() => {
155
+ occlusion.current.strength = ao;
156
+ invalidate();
157
+ }, [invalidate, ao]);
158
+ return null;
159
+ }
@@ -59,19 +59,15 @@ export function nextCharacterId(list) {
59
59
  }
60
60
  return id;
61
61
  }
62
- export const DEMO_MOTION_URL = "/demo/walk-then-stop.npz";
63
- export const DEMO_MOTION_PROMPT = "A person walks forward.";
64
62
  // How long a deletion keeps offering its one-press Undo. Both toasts use the
65
63
  // same window so the two deletion paths feel like one rule.
66
64
  export const OBJECT_DELETE_UNDO_MS = 7000;
67
65
  export const ASSET_DELETE_UNDO_MS = 7000;
68
- // Neutral grey clay: the set is the grey workbench, so the figures are grey
69
- // too, light enough to stand off the deck and the sky.
70
- export const CLAY = "#bdbec3";
71
- export const CLAY_B = "#a4a5aa";
72
- // X Bot's shell is smooth (no raised exoskeleton like Y Bot's), so it gets a
73
- // brighter, whiter clay to keep it readable against the set.
74
- export const CLAY_X = "#d0d0d4";
66
+ // White clay: every figure starts white (the renderer still drops the value a
67
+ // little and shades the joint shells darker, so the body keeps its form).
68
+ export const CLAY = "#ffffff";
69
+ export const CLAY_B = "#ffffff";
70
+ export const CLAY_X = "#ffffff";
75
71
  // Model/role default for a cast member; a user-picked entry.tint always wins
76
72
  // over this at render time.
77
73
  export const defaultCharacterTint = (entry, index) => (entry.model === "x-bot-tpose" ? CLAY_X : index === 0 ? CLAY : CLAY_B);
@@ -38,7 +38,7 @@ const castOverride = {
38
38
  /** A world floor point in metres; y is the floor. */
39
39
  const floorPoint = input({ x: { type: "number" }, z: { type: "number" } });
40
40
  const waypointFrame = { ...frame, minimum: 1 };
41
- /** The tracks an IK key stores (src/ardy/ik.js IK_TRACKS and FK_TRACKS). A
41
+ /** The tracks an IK key stores (src/motion/ik.js IK_TRACKS and FK_TRACKS). A
42
42
  * chain track keys its three bones (upper, lower, end); a joint track one. */
43
43
  export const STUDIO_IK_CHAIN_TRACKS = freezeStudioData(["leftHand", "rightHand", "leftFoot", "rightFoot"]);
44
44
  export const STUDIO_IK_JOINT_TRACKS = freezeStudioData(["hips", "spine", "chest", "neck", "head", "leftShoulder", "rightShoulder"]);
@@ -47,9 +47,9 @@ const perBone = items => ({ type: "array", items, minItems: 1, maxItems: 3 });
47
47
  const quaternion = input({ x: num, y: num, z: num, w: num });
48
48
  /** One track's key: the IK state's { q, p, baseQ, basePos, chainP,
49
49
  * keepTranslations, blend, pin, correctionRange } with quaternions as {x,y,z,w} and positions
50
- * as {x,y,z}; blend is the key's own correction range in frames (src/ardy/ik.js
50
+ * as {x,y,z}; blend is the key's own correction range in frames (src/motion/ik.js
51
51
  * correctionWeight), absent for the editor's default range; pin is the id of
52
- * the range pin that baked the key (src/ardy/range-pin.js). correctionRange
52
+ * the range pin that baked the key (src/motion/range-pin.js). correctionRange
53
53
  * pairs the boundary keys of an explicitly selected interval. */
54
54
  const ikTrackKey = input({}, { q: perBone(quaternion), p: StudioSchemas.Vec3, baseQ: perBone(quaternion), basePos: StudioSchemas.Vec3,
55
55
  chainP: perBone(StudioSchemas.Vec3), keepTranslations: { type: "boolean" }, blend: { type: "integer", minimum: 1, maximum: 240 },
@@ -87,12 +87,10 @@ export const STUDIO_ACTIONS = freezeStudioData([
87
87
  { id: "shot.remove", label: "Delete shot", kind: "mutation", undoDomain: "shot", input: input(shotId),
88
88
  description: "Delete a shot and leave its frames as free-camera time." },
89
89
  { id: "shot.setRange", label: "Set shot range", kind: "mutation", undoDomain: "shot", input: input({ ...shotId, range: StudioSchemas.FrameRange }),
90
- description: "Move a shot's start and end to a half-open frame range. Edges are clamped to the timeline and refused where they would overlap another shot; the receipt's delta shows the range that landed." },
90
+ description: "Move a shot's start and end to a half-open frame range, e.g. { shotId, range: { startFrame: 0, endFrameExclusive: 240 } } for frames 0-239. range: [0, 240] ([start, endExclusive]) and flat { shotId, startFrame: 0, endFrameExclusive: 240 } are taken as the same range. Edges are clamped to the timeline and refused where they would overlap another shot; the receipt's summary shows the range that landed (\"Set shot range: Shot 1 [0, 240).\")." },
91
91
  { id: "shot.reorder", label: "Move shot", kind: "mutation", undoDomain: "shot",
92
92
  input: input(shotId, { startFrame: frame, index: stillIndex }),
93
93
  description: "Move a shot; give exactly one of startFrame or index. With startFrame, a clip moves in time to start there, keeping its length and camera keys (a noop where it would overlap another shot), and a still moves to the place in the still order that frame falls in. With index (stills only), the still moves to that position among the stills (0 is first; past the end means last). Stills stay end to end from frame 0." },
94
- { id: "motion.generateAllBlocks", label: "Generate all blocks", kind: "job", generation: "motion", domain: "motion", background: true, input: input(),
95
- description: "Generate the active character's motion from all of its prompt blocks, like the top-bar Generate Motion button when prompt blocks have text. It starts a job and returns status \"started\"; the take lands in the editor when the job finishes. Counts as the one motion generation of this message." },
96
94
  { id: "character.addWaypoint", label: "Add root waypoint", kind: "mutation", undoDomain: "cast", input: input({ ...characterId, position: floorPoint }, { frame: waypointFrame }),
97
95
  description: `Pin a character's root path: at frame, the character's root stands at position (world x/z metres). Frame 0 is the character's own spot, so pins start at frame 1 and each frame holds one pin. Omit frame to pace the pin at a walk (1.4 m/s) from the previous one. ${WAYPOINT_RULES} Read paths with inspect_studio { scope: "motion" }.` },
98
96
  { id: "character.move", label: "Move character", kind: "mutation", undoDomain: "cast", input: input(characterId, { x: num, z: num, rot: num }),
@@ -110,8 +108,8 @@ export const STUDIO_ACTIONS = freezeStudioData([
110
108
  { id: "character.clearIkKeys", label: "Clear IK keys", kind: "mutation", undoDomain: "motion", input: input(characterId),
111
109
  description: "Delete every IK key of the character, returning it to its take or pose without corrections." },
112
110
  { id: "shot.setCameraRail", label: "Set camera rail", kind: "mutation", undoDomain: "shot",
113
- input: input({ ...shotId, points: { type: "array", items: floorPoint, minItems: 2, maxItems: 512 } }),
114
- description: "Lay a shot's dolly rail through points (world floor x/z metres, in travel order) and put the shot's camera on it, like drawing the rail in the Top-View. It replaces any rail the shot had; an authored rail-follow range is kept, otherwise the camera travels the rail over the whole shot. Read rails with inspect_studio { scope: \"shot\" }." },
111
+ input: input({ ...shotId, points: { type: "array", items: floorPoint, minItems: 2, maxItems: 512 } }, { height: { oneOf: [{ type: "number", minimum: 0.1, maximum: 30 }, { type: "array", items: input({ t: { type: "number", minimum: 0, maximum: 1 }, height: { type: "number", minimum: 0.1, maximum: 30 } }), minItems: 2, maxItems: 8 }] }, aim: { oneOf: [{ type: "array", items: idSchema, minItems: 1, maxItems: 4 }, input({ x: num, y: num, z: num }), { type: "null" }] }, aimHeight: { type: "number", minimum: 0, maximum: 30 }, focalMm: { oneOf: [{ type: "number", minimum: 10, maximum: 300 }, { type: "null" }] } }),
112
+ description: "Lay a shot's dolly rail through points (world floor x/z metres, in travel order) and put the shot's camera on it, like drawing the rail in the Top-View: the camera runs the rail at an even speed over the whole shot (or its authored rail-follow range), so a push-in, pull-out, orbit, track or crane is one call with 2-6 points (a line, or an arc through a few points; the rail is a smooth curve through them). height is the lens height in metres, one number or crane marks [{ t: 0..1 along the rail, height }] (default the follow height, 1.6). aim is what the camera holds: up to 4 character or object ids (their middle, frame by frame: a character's chest, an object's middle) or a world point { x, y, z }; without an aim it follows the scene's first character. aimHeight puts a cast aim at that world height instead of the chest, e.g. 0.9 to hold a whole standing body, 1.6 for faces. focalMm is the shot's lens (default the scene lens). It replaces any rail the shot had and keeps any height, aim or lens you leave out (null clears aim and lens), and it removes the camera keys the rail covers. Answers the rail's length, speed, height, aim and lens; check the framing with verify_result { targets, checks: [\"framing\"], range: the shot's frames } and a contact sheet. Read rails with inspect_studio { scope: \"shot\" }." },
115
113
  { id: "shot.clearCameraRail", label: "Delete camera rail", kind: "mutation", undoDomain: "shot", input: input(shotId),
116
114
  description: "Delete a shot's camera rail; the shot's camera goes back to following its subject at the current distance." },
117
115
  { id: "object.attach", label: "Attach to character", kind: "mutation", undoDomain: "objects",
@@ -125,6 +123,8 @@ export const STUDIO_ACTIONS = freezeStudioData([
125
123
  description: "Show a composition guide over the shot frame: rule of thirds, golden ratio, center cross, safe areas, or off. Overlay only, never in exported pixels; a viewer setting that is never undone." },
126
124
  { id: "view.setInset", label: "Top-View inset", kind: "transient", input: input({ collapsed: { type: "boolean" } }),
127
125
  description: "Fold (collapsed: true) or unfold the Top-View inset pane over the viewport. A viewer setting that is never undone." },
126
+ { id: "view.setGhosts", label: "Ghosts", kind: "transient", input: input({ mode: { type: "string", enum: ["off", "step", "keys"] } }, { count: { type: "integer", minimum: 1, maximum: 6 }, step: { type: "integer", minimum: 1, maximum: 24 } }),
127
+ description: "Onion skin in the editor viewport, like the View menu's Ghosts: translucent poses of the active character's installed take, count (1-6, default 3) before the playhead in blue and as many after it in orange, fading with distance in time; props the take carries (a kit's bat) ghost with the body. mode step spaces them every step frames (1-24, default 4), keys puts them on the spec's key frames, off hides them; e.g. { mode: \"step\", count: 3, step: 4 }. An editor overlay: never in capture_frame, exports or contact sheets (verify_result ghosts: true draws them on a views sheet). A viewer setting: never saved or undone." },
128
128
  { id: "export.shotVideo", exposure: "confirm", confirmationReason: "Exporting writes a video file outside the Studio.", label: "Export video (mp4)", kind: "job", timeoutMs: 300_000, input: input({}, { ...shotId, scope: { type: "string", enum: ["timeline", "shot"], description: "timeline: the whole timeline, every shot back to back; shot: one shot (shotId, or the one under the playhead)." } }),
129
129
  description: "Record a shot to an MP4 (camera move and character motion, no editor chrome), like the Export menu's Video (mp4), and ask the browser to download it. With shotId it records that shot's own range; without it, what the menu records: the whole take when there is motion, else the shot under the playhead (else the first shot), keyed from the current camera if it has no keys. Waits until the file is encoded (up to 5 minutes) and answers status \"completed\" with output.fileName and output.frameCount. One export at a time; not a motion generation." },
130
130
  { id: "scene.create", label: "New scene", kind: "document", input: input(),
@@ -151,9 +151,6 @@ export const STUDIO_ACTIONS = freezeStudioData([
151
151
  description: "Prepare the Studio's Send-to-AI package for the shot under the playhead (else the current camera): the prompt for an external image or video model, written from the shot's camera, cast, poses, environment and style, plus reference frames rendered from the shot camera (its first and last camera key when it has keys). No AI service is called. mode \"image\" is a still, \"video\" a shot with its camera move; model is the image model the prompt is written for (image mode only). Each omitted choice keeps the Studio's current one; a given one becomes it. The Studio opens its result panel, where the user copies the prompt and downloads the frames; the frame images stay there and are not in the answer. Answers status \"completed\" with output.prompt, mode, modelLabel, shot (id, name, range), aspectRatio, cameraMode and referenceFrames (how many frames the panel shows). Changes nothing in the scene; not a motion generation." },
152
152
  // The instruction leads the H3 prompt, which the motion API caps at 4000
153
153
  // characters with its camera-lock text (about 225) appended.
154
- { id: "motion.generateFromVideo", exposure: "confirm", confirmationReason: "AI video motion uses a paid external generation.", label: "Generate motion (AI video)", kind: "job", generation: "motion", timeoutMs: 300_000,
155
- input: input({ instruction: { type: "string", minLength: 1, maxLength: 3700 } }),
156
- description: "Make the active character perform instruction through a hosted AI video model (Fal H3 Max Turbo, 480P), like the agent panel's Generate motion button. It sends the character's pose frame (pose A from the Fal card, else a fresh capture from the shot camera) with the instruction to the hosted model, and each run spends one of the account's daily AI video generations. Run it only when the user explicitly asked for AI-video motion; for ordinary motion use generate_motion or motion.generateAllBlocks. The capture needs the character's full body inside the shot frame and shaded part colours (view.setPartColours { mode: \"shaded\" }); a prompt the user hand-edited in the Fal card is sent instead of instruction. Waits for the clip (up to 5 minutes) and answers status \"completed\" with output.videoUrl, resolution, durationSeconds, ingested, frames, fps and dailyRemaining. The clip is ingested as Video capture footage and the timeline takes its length and frame rate; no take is installed until GVHMR extraction runs on it in the Video capture panel. Counts as the one motion generation of this message." },
157
154
  ]);
158
155
  export const STUDIO_ACTION_IDS = freezeStudioData(STUDIO_ACTIONS.map(action => action.id));
159
156
 
@@ -216,14 +213,15 @@ export function createStudioActionRegistry({ readState } = {}) {
216
213
  list(state = readState?.()) {
217
214
  return [...entries.values()].map(entry => {
218
215
  const verdict = availability(entry, state);
219
- const row = { id: entry.id, label: entry.label, kind: entry.kind, description: entry.description, exposure: entry.exposure, ...(entry.agentHidden ? { agentHidden: true } : {}),
216
+ const row = { id: entry.id, label: entry.label, kind: entry.kind, description: entry.description, exposure: entry.exposure, ...(entry.agentHidden ? { agentHidden: true } : {}), ...(entry.readOnly ? { readOnly: true } : {}),
220
217
  ...(entry.timeoutMs === undefined ? {} : { timeoutMs: entry.timeoutMs }), ...(entry.generation ? { generation: entry.generation } : {}) };
221
218
  return verdict === true ? { ...row, available: true, input: entry.input } : { ...row, available: false, reason: verdict };
222
219
  });
223
220
  },
224
221
  prepare(id, args = {}) {
225
- const entry = registry.get(id);
226
- const validated = validateStudioSchema(entry.input, args ?? {}, "INVALID_ARGUMENT", "$.args");
222
+ const entry = registry.get(id), given = args ?? {};
223
+ // normalizeArgs (optional) rewrites another spelling the action takes into its declared input first.
224
+ const validated = validateStudioSchema(entry.input, entry.normalizeArgs ? entry.normalizeArgs(given) : given, "INVALID_ARGUMENT", "$.args");
227
225
  if (readState) {
228
226
  const verdict = availability(entry, readState());
229
227
  if (verdict !== true) fail("TARGET_NOT_READY", verdict);
@@ -1,7 +1,7 @@
1
1
  // Studio composites prepare private domain drafts; the App owns publication,
2
2
  // history, gesture fences and semantic revision/telemetry. No UI callbacks here.
3
3
  import { Euler, Vector3, Quaternion, PerspectiveCamera } from 'three';
4
- import { createSceneObject, updateSceneObject, removeSceneObject, setSceneObjectParent, descendantsOf, isEffectivelyHidden, supportHeightForObject, OBJECT_LIBRARY } from './scene-objects.js';
4
+ import { createSceneObject, updateSceneObject, removeSceneObject, setSceneObjectParent, descendantsOf, isEffectivelyHidden, supportHeightForObject, OBJECT_MENU_ENTRIES } from './scene-objects.js';
5
5
  import { createCharacterEntry } from './scenes.js';
6
6
  import { createShot, shotAtFrame } from './cuts.js';
7
7
  import { captureFraming } from './camera-move.js';
@@ -11,6 +11,8 @@ import { coplanarConflicts } from './coplanar-depth.js';
11
11
  import { clampWarning } from './receipt-findings.js';
12
12
  import { occludersOf, primarySubjects, sideOfAxis } from './studio-geometry-facts.js';
13
13
  import { StudioProtocolError, StudioSchemas, STUDIO_PATCH_KINDS, STUDIO_PATCH_DESCRIPTORS, validateStudioSchema, validateStudioIdentity, validateReceipt, freezeStudioData } from './studio-agent-protocol.js';
14
+ import { sampleContactSheetFrames } from './studio-contact-sheet.js';
15
+ import { supportGaps } from './studio-support-gap.js';
14
16
 
15
17
  const DEG = Math.PI / 180, EPS = 1e-8, CHARACTER_SUPPORT_TOLERANCE = 5e-3;
16
18
  const fail = (code, message) => { throw new StudioProtocolError(code, message); };
@@ -240,9 +242,8 @@ export function entityWorldBounds(entity, state, ports) {
240
242
  } catch { return null; }
241
243
  }
242
244
  const overlapEvidence = overlaps => ({ coverage: 'same-frame-world-AABB-proxies', overlapIds: [...new Set(overlaps.map(o => o.id))].slice(0, 100), maximumFootprintOverlapM: Math.max(0, ...overlaps.map(o => o.depth)) });
243
- /** Placement evidence for entity ids in the given scene: the same-frame AABB
244
- * overlap an arrangement receipt reports for the entities it moved. */
245
- export const placementChecks = (ids, state, ports) => overlapEvidence(overlapsOf(ids, state, ports));
245
+ // Placement evidence for entity ids: the AABB overlap an arrangement receipt reports, and each object's support gap (studio-support-gap.js).
246
+ export const placementChecks = (ids, state, ports) => ({ ...overlapEvidence(overlapsOf(ids, state, ports)), ...supportGaps(ids, state, ports.takes, entity => entityWorldBounds(entity, state, ports)) });
246
247
  function avoid(entity, relation, state, ports) {
247
248
  const blocked = overlapsFor(entity, state, ports);
248
249
  if (!blocked.length) return entity;
@@ -368,7 +369,7 @@ const LEVELS = { ground: 0.3 / 1.8, low: 0.7 / 1.8, hip: 1.1 / 1.8, eye: 1.65 /
368
369
  const ANGLES = { front: 0, 'front three-quarter': 40, profile: 90, 'rear three-quarter': 140, back: 180 };
369
370
  /** Where subject bound points land through one camera: the evidence a
370
371
  * frame_shot receipt reports, plus the screen box its details keep. */
371
- function projectSubject(points, camera, fov, aspectRatio) {
372
+ export function projectSubject(points, camera, fov, aspectRatio) {
372
373
  const direction = vector(camera.lookAt).sub(vector(camera.position));
373
374
  const projection = new PerspectiveCamera(fov / DEG, aspectRatio, 0.01, 10000);
374
375
  projection.position.copy(vector(camera.position)); projection.lookAt(vector(camera.lookAt)); projection.updateMatrixWorld(true);
@@ -399,6 +400,32 @@ export function framingChecks(ids, state, ports) {
399
400
  const projected = projectSubject(boxes.flat(), state.camera, focalMmToFov(state.camera.focalMm, state.filmback.sensorId, state.filmback.aspectRatio), state.filmback.aspectRatio).checks;
400
401
  return { ...projected, ...sightChecks(subjects, boxes, state.camera.position, state, ports).checks };
401
402
  }
403
+
404
+ /** Framing evidence over explicit timeline samples. The caller supplies the
405
+ * sampled state so camera keys and animated subject bounds use the same frame.
406
+ * A sample is evidence for that frame only; it is never advertised as an
407
+ * all-frame proof. */
408
+ export function sampledFramingChecks(ids, state, ports, frames) {
409
+ if (!Array.isArray(frames) || !frames.length) fail('INVALID_RANGE', 'Framing sampling needs at least one timeline frame.');
410
+ if (typeof ports.sampleStateAt !== 'function') fail('CAPABILITY_MISSING', 'This editor cannot sample framing at timeline frames.');
411
+ const samples = frames.map(frame => {
412
+ const sampledState = ports.sampleStateAt(frame, state);
413
+ if (!sampledState) fail('TARGET_NOT_READY', `The editor could not read timeline frame ${frame}.`);
414
+ const checks = framingChecks(ids, sampledState, ports), codes = [checks.clipped ? 'CLIPPED' : null, checks.behindCamera ? 'BEHIND_CAMERA' : null, checks.occluded ? 'OCCLUDED' : null].filter(Boolean);
415
+ return { frame, checks, codes };
416
+ });
417
+ const sampledChecks = samples.map(({ frame, checks }) => ({ frame, screenFraction: checks.screenFraction, clipped: checks.clipped, behindCamera: checks.behindCamera, occluded: checks.occluded }));
418
+ const issueFrames = samples.filter(sample => sample.codes.length).map(({ frame, checks, codes }) => ({ frame, codes, screenFraction: checks.screenFraction }));
419
+ return { ...samples.at(-1).checks, coverage: 'sampled-timeline-subject-bounds-projection', evaluatedFrames: frames.length, sampledFrames: frames, sampledChecks, issueFrames };
420
+ }
421
+ export function sampledFramingResult(args, scene, ports, frameCount, measured, receipt) {
422
+ const range = args.range === 'whole_clip' ? { startFrame: 0, endFrameExclusive: frameCount } : args.range;
423
+ if (range.endFrameExclusive > frameCount) fail('INVALID_RANGE', 'Framing verification range exceeds the scene timeline.');
424
+ const ids = measured ?? (args.targets ?? receipt?.affectedIds ?? []).filter(id => scene.objects.some(o => o.id === id) || scene.characters.some(c => c.id === id));
425
+ if (!ids.length) return { reason: 'No target is available for sampled framing verification.' };
426
+ try { return sampledFramingChecks(ids, scene, ports, sampleContactSheetFrames(args.range, frameCount)); }
427
+ catch (error) { if (!(error instanceof StudioProtocolError)) throw error; return { reason: error.message }; }
428
+ }
402
429
  export function frameDraft(command, state, ports) {
403
430
  if (state.frameCount <= 0) fail('TARGET_NOT_READY', 'A nonempty timeline is required.');
404
431
  const subject = state.characters.find(e => e.id === command.args.subjectIds[0]);
@@ -409,9 +436,13 @@ export function frameDraft(command, state, ports) {
409
436
  const created = !shot && !state.shotDocument.shots.length && !command.args.shotId;
410
437
  if (created) shot = createShot('Shot 1', 0, state.frameCount - 1);
411
438
  if (!shot) fail('AMBIGUOUS_TARGET', 'No shot owns this frame; select an existing shot explicitly.');
412
- const still = shot.kind === 'still';
439
+ // The shot's camera keys are what the contact sheet and the export render, so
440
+ // every framing lands in them. Without keyAtFrame, a shot that holds one
441
+ // framing (a still, or a clip with at most one key) is reframed whole; a clip
442
+ // that already moves takes the framing as a key at the playhead.
413
443
  const existingKey = shot.cameraKeys[0];
414
- const frame = still ? (existingKey?.frame ?? shot.startFrame) : command.args.keyAtFrame ?? state.frame;
444
+ const hold = shot.kind === 'still' || (command.args.keyAtFrame === undefined && shot.cameraKeys.length <= 1);
445
+ const frame = hold ? (existingKey?.frame ?? shot.startFrame) : command.args.keyAtFrame ?? state.frame;
415
446
  if (frame < shot.startFrame || frame > shot.endFrame || shot.endFrame >= state.frameCount) fail('INVALID_RANGE', 'Framing/key frame must be inside the shot range.');
416
447
  const intent = command.args.framing.intent;
417
448
  let camera = command.args.framing.exact;
@@ -430,19 +461,17 @@ export function frameDraft(command, state, ports) {
430
461
  const direction = vector(camera.lookAt).sub(vector(camera.position));
431
462
  if (direction.length() <= EPS) fail('INVALID_ARGUMENT', 'Camera position and aim must differ.');
432
463
  const framing = captureFraming({ pos: camera.position, yaw: Math.atan2(-direction.x, -direction.z), pitch: Math.atan2(direction.y, Math.hypot(direction.x, direction.z)), fovDeg: fov / DEG });
433
- let keyId;
434
- let keys = shot.cameraKeys;
435
- if (still || command.args.keyAtFrame !== undefined) {
436
- keyId = (still ? existingKey?.id : keys.find(k => k.frame === frame)?.id) ?? createStableItemId('camera-key');
437
- keys = still ? [{ id: keyId, frame, framing }] : [...keys.filter(k => k.frame !== frame), { id: keyId, frame, framing }].sort((a, b) => a.frame - b.frame);
438
- }
464
+ const keyId = (hold ? existingKey?.id : shot.cameraKeys.find(k => k.frame === frame)?.id) ?? createStableItemId('camera-key');
465
+ const keys = hold ? [{ id: keyId, frame, framing }] : [...shot.cameraKeys.filter(k => k.frame !== frame), { id: keyId, frame, framing }].sort((a, b) => a.frame - b.frame);
439
466
  const nextShot = { ...shot, cameraKeys: keys, camera: { ...shot.camera, mode: 'keys' } };
440
467
  const shots = created ? [nextShot] : state.shotDocument.shots.map(s => s.id === shot.id ? nextShot : s);
441
468
  const { screenBounds, checks: projected } = projectSubject(points, camera, fov, state.filmback.aspectRatio), derivedSize = projected.derivedSize;
442
469
  const sight = sightChecks([subject], [points], camera.position, state, ports), checks = { ...projected, ...sight.checks };
443
470
  const resolvedCamera = { ...camera, sensorId: state.filmback.sensorId, slate: derivedSize };
444
- const draft = { shotDocument: { ...state.shotDocument, shots }, camera: resolvedCamera, manual: true };
445
- return { domain: 'shot', draft, affectedIds: [shot.id, ...(keyId ? [keyId] : [])],
471
+ // Not a manual override: the keys drive the live shot camera from here, so a
472
+ // later playhead move shows what the export renders at that frame.
473
+ const draft = { shotDocument: { ...state.shotDocument, shots }, camera: resolvedCamera, manual: false };
474
+ return { domain: 'shot', draft, affectedIds: [shot.id, keyId],
446
475
  checks,
447
476
  warnings: sight.occluders.slice(0, 5).map(item => ({ code: 'OCCLUDED', id: item.byObjectId })), details: { created, shotId: shot.id, keyId, frame, framing, screenBounds, subjectIds: [subject.id] } };
448
477
  }
@@ -487,7 +516,8 @@ export function createStudioCommandJournal({ host, now = Date.now, isRetained =
487
516
  };
488
517
  }
489
518
  export function studioObjectCatalogue() {
490
- return freezeStudioData({ objects: OBJECT_LIBRARY.map(({ kind, footprint, height, supportY }) => ({ kind, footprint: { ...footprint }, height, supportY: supportY ?? height })), imageRefs: [],
519
+ // Every creatable kind: the sized library, the Empty and the two practical lights (zero footprint).
520
+ return freezeStudioData({ objects: OBJECT_MENU_ENTRIES.map(({ kind, footprint, height, supportY }) => ({ kind, footprint: { ...footprint }, height, supportY: supportY ?? height })), imageRefs: [],
491
521
  // The patchable vocabulary, so a caller reads the paths and their declared
492
522
  // ranges instead of guessing them from a rejection.
493
523
  patchable: Object.fromEntries(STUDIO_PATCH_KINDS.map(kind => [kind, STUDIO_PATCH_DESCRIPTORS.filter(descriptor => descriptor.path.startsWith(`${kind}.`))])) });