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
@@ -0,0 +1,18 @@
1
+ // The world box an object row reports, in inspect_studio entities, the per-turn context and an
2
+ // action readback alike: a body's own box at the playhead (entityWorldBounds, metres, 4 decimals);
3
+ // for a node with no body (an empty or a light) the union of every body under it, so a group
4
+ // built from parts shows where the whole thing ends; null when nothing there can be measured.
5
+ import { entityWorldBounds } from "./studio-agent-commands.js";
6
+ import { descendantsOf, isEmptyObject } from "./scene-objects.js";
7
+
8
+ const AXES = ["x", "y", "z"];
9
+
10
+ /** World AABB {min,max} of scene object `object` in refreshed Studio state `s`, or null. */
11
+ export function objectRowBounds(object, s, ports) {
12
+ const state = { ...s, frame: s.view.frame }, measure = o => entityWorldBounds(o, state, { bounds: ports.bounds });
13
+ if (!isEmptyObject(object)) return measure(object);
14
+ const boxes = descendantsOf(s.objects, object.id).filter(o => !isEmptyObject(o)).map(measure).filter(Boolean);
15
+ if (!boxes.length) return null;
16
+ return { min: Object.fromEntries(AXES.map(a => [a, Math.min(...boxes.map(b => b.min[a]))])),
17
+ max: Object.fromEntries(AXES.map(a => [a, Math.max(...boxes.map(b => b.max[a]))])) };
18
+ }
@@ -0,0 +1,8 @@
1
+ // The error codes a Studio receipt or failure may carry. Shared by the browser
2
+ // and sidecar; no imports.
3
+ export const STUDIO_ERROR_CODES = Object.freeze([
4
+ "INVALID_ARGUMENT", "INVALID_CONTEXT", "INVALID_IDENTITY", "INVALID_TURN_ID", "INVALID_SESSION_ID", "INVALID_RECEIPT", "INVALID_RANGE", "INVALID_REQUEST",
5
+ "UNKNOWN_TOOL", "UNKNOWN_VARIANT", "DUPLICATE_NAME", "CONTEXT_LIMIT", "CONTEXT_TOO_LARGE", "AMBIGUOUS_TARGET", "AMBIGUOUS_BASIS", "TARGET_NOT_READY", "TARGET_BUSY",
6
+ "STALE_TARGET", "STALE_SCENE", "STALE_ENVIRONMENT", "STALE_CURSOR", "CAPABILITY_MISSING", "LIVE_HUB_UNAVAILABLE", "AUTH_REQUIRED", "GENERATION_LIMIT", "RATE_LIMITED", "BACKEND_UNAVAILABLE",
7
+ "VERIFICATION_FAILED", "REPAIR_REGRESSED", "CANCELLED", "UNCERTAIN_APPLY", "UNDO_CONFLICT", "CONFIRMATION_REQUIRED", "TIMEOUT", "UNDO_EXPIRED",
8
+ ]);
@@ -0,0 +1,152 @@
1
+ // What the export camera shows of a take (verify_result motion, exportFraming): at each sampled frame
2
+ // the camera the export renders there (ports.sampleStateAt: the shot's camera where a shot covers the
3
+ // frame, else the Studio camera as it stands), and through it the character's joints (as motion.sample
4
+ // reads them) and the take's objects (moved by the take's tracks, object-travel.js), projected as
5
+ // frame_shot projects a subject (studio-agent-commands.js projectSubject). Measurements, not verdicts:
6
+ // how much of the character and of each object is inside the frame, and over which sampled frames a
7
+ // part (head, hands, feet) or an object (the outline of each of its bodies) is cut or out. Frames are
8
+ // absolute timeline frames; a span joins consecutive samples ([first, last] sampled frame).
9
+ import { Euler, Vector3 } from "three";
10
+ import { StudioProtocolError } from "./studio-agent-protocol.js";
11
+ import { projectSubject, entityWorldBounds } from "./studio-agent-commands.js";
12
+ import { focalMmToFov, SENSOR_FORMATS } from "./shot.js";
13
+ import { shotAtFrame } from "./cuts.js";
14
+ import { sceneObjectsAt } from "./object-travel.js";
15
+ import { descendantsOf, isEmptyObject } from "./scene-objects.js";
16
+ import { skeletonFor } from "./motion3/rigs/index.js";
17
+ import { fk } from "./motion3/compiler/skeleton.js";
18
+ import { add, apply, ry } from "./motion3/compiler/math.js";
19
+
20
+ const SAMPLES = 24, TAKE_SCHEMA = "cozyclay.take.v3", BARE_OBJECT = /^object:(.+?)(\.(?:center|top|base))?$/s;
21
+ const PARTS = Object.freeze({ head: ["Head", "HeadTop_End"], hand_l: ["LeftHand", "LeftHandMiddle4", "LeftHandIndex4"], hand_r: ["RightHand", "RightHandMiddle4", "RightHandIndex4"],
22
+ foot_l: ["LeftFoot", "LeftToe_End"], foot_r: ["RightFoot", "RightToe_End"] });
23
+ const r2 = v => Math.round(v * 100) / 100;
24
+
25
+ /** Up to n frames spread evenly over [lo, hi], both ends included. */
26
+ export function exportSampleFrames(lo, hi, n = SAMPLES) {
27
+ if (hi <= lo) return [lo];
28
+ const count = Math.min(n, hi - lo + 1);
29
+ return [...new Set(Array.from({ length: count }, (_, i) => Math.round(lo + (i * (hi - lo)) / (count - 1))))];
30
+ }
31
+
32
+ /** [[first, last], ...] of the sampled frames whose flag is set, consecutive samples joined. */
33
+ export function sampleSpans(frames, flags) {
34
+ const spans = [];
35
+ frames.forEach((frame, i) => {
36
+ if (!flags[i]) return;
37
+ if (i > 0 && flags[i - 1]) spans.at(-1)[1] = frame; else spans.push([frame, frame]);
38
+ });
39
+ return spans;
40
+ }
41
+
42
+ /** World joint positions of a v3 take at timeline frame f, in take bone order (motion.sample's reading). */
43
+ function jointsAt(sk, take, f) {
44
+ const B = take.bones.length, ff = Math.max(0, Math.min(take.frameCount - 1, Math.round(f)));
45
+ const quats = Array.from({ length: B }, (_, b) => Array.from(take.rot.subarray((ff * B + b) * 4, (ff * B + b) * 4 + 4)));
46
+ const state = fk(sk, quats, Array.from(take.hips.subarray(ff * 3, ff * 3 + 3)));
47
+ const yaw = ry(take.placement?.yaw ?? 0), at = take.placement?.position ?? [0, 0, 0];
48
+ return state.P.map(p => add(at, apply(yaw, p)));
49
+ }
50
+
51
+ /** Scene object ids a spec names: spec.objects keys, object:<id> refs and any string value that is an object id. */
52
+ export function takeObjectIds(spec, objects) {
53
+ const known = new Set(objects.map(o => o.id)), ids = new Set(Object.keys(spec?.objects ?? {}).filter(id => known.has(id)));
54
+ const walk = value => {
55
+ if (typeof value === "string") { const id = BARE_OBJECT.exec(value)?.[1] ?? value; if (known.has(id)) ids.add(id); }
56
+ else if (Array.isArray(value)) value.forEach(walk);
57
+ else if (value && typeof value === "object") Object.values(value).forEach(walk);
58
+ };
59
+ walk(spec);
60
+ return [...ids];
61
+ }
62
+
63
+ /** How a set of world points lands in the camera: the projection and how much of its screen box is inside the frame. */
64
+ function landing(points, view) {
65
+ const forward = new Vector3().subVectors(view.lookAt, view.position);
66
+ const behind = points.filter(p => p.clone().sub(view.position).dot(forward) <= 0).length;
67
+ if (behind === points.length) return { behind: true, cut: true, fraction: 0, screenFraction: 0 };
68
+ const { screenBounds: b, checks } = projectSubject(points, view.camera, view.fov, view.aspect);
69
+ if (behind) return { behind: true, cut: true, fraction: null, screenFraction: checks.screenFraction };
70
+ // the share of each screen axis inside [-1, 1]; a box flat along an axis is in or out whole
71
+ const share = ({ min, max }) => max - min > 1e-6 ? Math.max(0, Math.min(1, max) - Math.max(-1, min)) / (max - min) : Math.abs(min) <= 1 ? 1 : 0;
72
+ return { behind: false, cut: checks.clipped, fraction: share(b.x) * share(b.y), screenFraction: checks.screenFraction };
73
+ }
74
+
75
+ const corners = box => [box.min.x, box.max.x].flatMap(x => [box.min.y, box.max.y].flatMap(y => [box.min.z, box.max.z].map(z => new Vector3(x, y, z))));
76
+ const RING = Array.from({ length: 8 }, (_, i) => [Math.cos(i * Math.PI / 4), Math.sin(i * Math.PI / 4)]);
77
+ // A body's outline in its own frame (base pivot, local +Y up, 1 m primitives scaled): rims for the round
78
+ // primitives, so a disc seen face on is not measured by the corners of its box; box corners otherwise.
79
+ const OUTLINES = { cylinder: [[0, 1], [1, 1]], cone: [[0, 1], [1, 0]], sphere: [[0, 0], [0.5, 1], [1, 0]], capsule: [[0, 0], [0.25, 1], [0.75, 1], [1, 0]] };
80
+ /** World points outlining a body as drawn at its posed record: the same placement geometry() uses for a box. */
81
+ function bodyPoints(object, state, ports) {
82
+ const outline = OUTLINES[object.renderer];
83
+ if (object.attach || object.path || !object.footprint) { const box = entityWorldBounds(object, state, { bounds: ports.bounds }); return box ? corners(box) : []; }
84
+ const w = object.footprint.width * object.scaleX / 2, d = object.footprint.depth * object.scaleZ / 2, h = object.height * object.scaleY;
85
+ const local = outline ? outline.flatMap(([y, r]) => r ? RING.map(([c, s]) => [w * r * c, y * h, d * r * s]) : [[0, y * h, 0]])
86
+ : [-w, w].flatMap(x => [0, h].flatMap(y => [-d, d].map(z => [x, y, z])));
87
+ const rotation = new Euler((object.rotX ?? 0) * Math.PI / 180, (object.rot ?? 0) * Math.PI / 180, (object.rotZ ?? 0) * Math.PI / 180, "XYZ");
88
+ return local.map(([x, y, z]) => new Vector3(x, y, z).applyEuler(rotation).add(new Vector3(object.x, object.y ?? 0, object.z)));
89
+ }
90
+ const lowest = (rows, frames) => rows.reduce((best, row, i) => row !== null && (best === null || row < best[0]) ? [r2(row), frames[i]] : best, null);
91
+
92
+ /**
93
+ * exportFraming for one character's installed v3 take. scene: the binding's command state (objects,
94
+ * characters, camera, filmback, shotDocument); ports: { sampleStateAt, bounds }; range: verify_result's
95
+ * range ("whole_clip" or { startFrame, endFrameExclusive }).
96
+ */
97
+ export function exportFraming({ character, take, scene, ports, range }) {
98
+ const sensorId = scene.filmback?.sensorId, aspect = scene.filmback?.aspectRatio;
99
+ if (!SENSOR_FORMATS[sensorId] || !(aspect > 0)) throw new StudioProtocolError("TARGET_NOT_READY", "The shot camera and filmback must be available.");
100
+ if (typeof ports.sampleStateAt !== "function") throw new StudioProtocolError("CAPABILITY_MISSING", "This editor cannot sample the export camera at timeline frames.");
101
+ const sk = skeletonFor(take.rig ?? character.model), last = take.frameCount - 1;
102
+ const lo = range && range !== "whole_clip" ? Math.max(0, range.startFrame) : 0, hi = range && range !== "whole_clip" ? Math.min(last, range.endFrameExclusive - 1) : last;
103
+ const frames = exportSampleFrames(lo, hi), shots = scene.shotDocument?.shots ?? [];
104
+ const tracks = new Map(Object.entries(take.objects ?? {}).map(([id, track]) => [id, { frameCount: take.frameCount, characterId: character.id, ...track }]));
105
+ const objectIds = takeObjectIds(take.spec, scene.objects), partIndex = Object.fromEntries(Object.entries(PARTS).map(([part, bones]) => [part, bones.map(b => sk.index[b]).filter(i => i !== undefined)]));
106
+ const shotIds = new Set(), sources = new Set(), body = [], parts = Object.fromEntries(Object.keys(PARTS).map(p => [p, []])), objects = objectIds.map(() => []);
107
+ for (const frame of frames) {
108
+ const sampled = ports.sampleStateAt(frame, scene), camera = sampled?.camera ?? scene.camera;
109
+ if (!camera) throw new StudioProtocolError("TARGET_NOT_READY", "The shot camera must be available.");
110
+ const shot = shotAtFrame(shots, frame);
111
+ if (shot) shotIds.add(shot.id);
112
+ sources.add(shot ? "shot" : "studio");
113
+ const view = { camera, position: new Vector3(camera.position.x, camera.position.y, camera.position.z), lookAt: new Vector3(camera.lookAt.x, camera.lookAt.y, camera.lookAt.z),
114
+ fov: focalMmToFov(camera.focalMm, sensorId, aspect), aspect };
115
+ const joints = jointsAt(sk, take, frame).map(([x, y, z]) => new Vector3(x, y, z));
116
+ body.push(landing(joints, view));
117
+ for (const [part, index] of Object.entries(partIndex)) parts[part].push(landing(index.map(i => joints[i]), view));
118
+ const posed = sceneObjectsAt(scene.objects, frame, { frameCount: take.frameCount, fps: 24, objectTracks: tracks }), state = { ...scene, objects: posed, frame };
119
+ objectIds.forEach((id, k) => {
120
+ const object = posed.find(o => o.id === id), bodies = object && (isEmptyObject(object) ? descendantsOf(posed, id).filter(o => !isEmptyObject(o)) : [object]);
121
+ const points = (bodies ?? []).filter(o => !o.hidden).flatMap(o => bodyPoints(o, state, ports));
122
+ objects[k].push(points.length ? landing(points, view) : null);
123
+ });
124
+ }
125
+ const spans = (rows, key) => sampleSpans(frames, rows.map(row => !!row?.[key]));
126
+ const cut = Object.fromEntries(Object.entries(parts).map(([part, rows]) => [part, spans(rows, "cut")]).filter(([, s]) => s.length));
127
+ const fractions = body.map(row => row.screenFraction);
128
+ return {
129
+ coverage: "sampled-export-camera-joints-and-object-outlines",
130
+ camera: sources.size === 1 ? [...sources][0] : "mixed", shotIds: [...shotIds], sampledFrames: frames,
131
+ character: { id: character.id, screenFraction: [r2(Math.min(...fractions)), r2(Math.max(...fractions))], inFrame: lowest(body.map(row => row.fraction), frames),
132
+ ...(spans(body, "behind").length ? { behindCamera: spans(body, "behind") } : {}), ...(Object.keys(cut).length ? { cut } : {}) },
133
+ objects: objectIds.map((id, k) => {
134
+ const rows = objects[k], out = sampleSpans(frames, rows.map(row => row !== null && row.fraction === 0));
135
+ const partly = sampleSpans(frames, rows.map(row => row !== null && row.cut && row.fraction !== 0));
136
+ return { id, name: scene.objects.find(o => o.id === id)?.name ?? id, inFrame: lowest(rows.map(row => row?.fraction ?? null), frames), ...(out.length ? { out } : {}), ...(partly.length ? { cut: partly } : {}) };
137
+ }),
138
+ };
139
+ }
140
+
141
+ /** verify_result motion: result.exportFraming for every subject character with an installed v3 take (one: the row itself). */
142
+ export function exportFramingInto(result, { args, s, receipt, scene, ports }) {
143
+ const rows = [];
144
+ for (const id of (args.targets ?? receipt?.affectedIds ?? []).filter(id => s.characters.some(c => c.id === id))) {
145
+ const take = s.targets.get(id)?.motion, character = s.characters.find(c => c.id === id);
146
+ if (take?.schema !== TAKE_SCHEMA || !take.rot || !take.hips) continue;
147
+ // what could not be measured says why in its own row, beside the motion check's reasons
148
+ try { rows.push(exportFraming({ character, take, scene, ports, range: args.range })); }
149
+ catch (error) { rows.push({ character: { id }, reason: String(error?.message ?? error) }); }
150
+ }
151
+ if (rows.length) result.exportFraming = rows.length === 1 ? rows[0] : rows;
152
+ }
@@ -0,0 +1,103 @@
1
+ // verify_result checks ["lighting"]: how the shot frame is exposed, measured from
2
+ // the pixels the export path renders (the same frame capture_frame and the
3
+ // contact sheets show), not from the light settings. The agent reads the
4
+ // numbers; it does not have to judge darkness from a thumbnail.
5
+ import { PerspectiveCamera, Vector3 } from "three";
6
+ import { StudioProtocolError } from "./studio-agent-protocol.js";
7
+ import { entityWorldBounds } from "./studio-agent-commands.js";
8
+ import { focalMmToFov } from "./shot.js";
9
+ import { normalizeLighting } from "./stage-lighting.js";
10
+ import { readyHdrMap } from "./stage-hdr-map.js";
11
+
12
+ // Rec. 709 weights on the 8-bit sRGB values: luma as a monitor shows it.
13
+ const lumaOf = (data, i) => 0.2126 * data[i] + 0.7152 * data[i + 1] + 0.0722 * data[i + 2];
14
+ const round = (value, digits) => Number(value.toFixed(digits));
15
+ // A clipped pixel has a channel at full scale; a crushed one is black in every channel.
16
+ const CLIP = 254, CRUSH = 3;
17
+
18
+ /** Percentile of a 256-bin luma histogram, as 0..1. */
19
+ function percentile(histogram, count, fraction) {
20
+ let seen = 0;
21
+ for (let bin = 0; bin < 256; bin++) if ((seen += histogram[bin]) >= fraction * count) return bin / 255;
22
+ return 1;
23
+ }
24
+
25
+ /**
26
+ * Exposure statistics of one RGBA frame (rows bottom-up, as the GPU reads them
27
+ * back) and of pixel boxes inside it: meanLuma 0..1, clippedPct and crushedPct
28
+ * of the whole frame; per box its meanLuma and contrast (the spread between
29
+ * the 5th and 95th luma percentile, 0..1). A box is { id, x0, y0, x1, y1 } in
30
+ * pixels with y up, or { id } alone when the subject is out of frame.
31
+ */
32
+ export function frameLighting({ data, width, height }, boxes = []) {
33
+ let sum = 0, clipped = 0, crushed = 0;
34
+ for (let i = 0; i < width * height * 4; i += 4) {
35
+ sum += lumaOf(data, i);
36
+ const high = Math.max(data[i], data[i + 1], data[i + 2]);
37
+ if (high >= CLIP) clipped++;
38
+ else if (high <= CRUSH) crushed++;
39
+ }
40
+ const pixels = width * height;
41
+ const subjects = boxes.map(({ id, x0, y0, x1, y1 }) => {
42
+ if (x0 === undefined || x1 < x0 || y1 < y0) return { id, meanLuma: null, contrast: null };
43
+ const histogram = new Uint32Array(256);
44
+ let total = 0, count = 0;
45
+ for (let y = y0; y <= y1; y++) for (let x = x0; x <= x1; x++) {
46
+ const luma = lumaOf(data, (y * width + x) * 4);
47
+ histogram[Math.min(255, Math.round(luma))]++;
48
+ total += luma;
49
+ count++;
50
+ }
51
+ return { id, meanLuma: round(total / count / 255, 3), contrast: round(percentile(histogram, count, 0.95) - percentile(histogram, count, 0.05), 3) };
52
+ });
53
+ return { meanLuma: round(sum / pixels / 255, 3), clippedPct: round((100 * clipped) / pixels, 1), crushedPct: round((100 * crushed) / pixels, 1), subjects };
54
+ }
55
+
56
+ /** A world box projected through the shot camera to a clamped pixel box (y up), or null off screen. */
57
+ function pixelBox(bounds, state, width, height) {
58
+ const { camera, filmback } = state, aspect = width / height;
59
+ const view = new PerspectiveCamera((focalMmToFov(camera.focalMm, filmback.sensorId, aspect) * 180) / Math.PI, aspect, 0.01, 10000);
60
+ view.position.set(camera.position.x, camera.position.y, camera.position.z);
61
+ view.lookAt(camera.lookAt.x, camera.lookAt.y, camera.lookAt.z);
62
+ view.updateMatrixWorld(true);
63
+ const forward = new Vector3(camera.lookAt.x - camera.position.x, camera.lookAt.y - camera.position.y, camera.lookAt.z - camera.position.z);
64
+ const corners = [bounds.min.x, bounds.max.x].flatMap((x) => [bounds.min.y, bounds.max.y].flatMap((y) => [bounds.min.z, bounds.max.z].map((z) => new Vector3(x, y, z))))
65
+ .filter((corner) => corner.clone().sub(view.position).dot(forward) > 0).map((corner) => corner.project(view));
66
+ if (!corners.length) return null;
67
+ const px = (ndc, size) => Math.round(((ndc + 1) / 2) * size);
68
+ const box = { x0: px(Math.min(...corners.map((c) => c.x)), width), x1: px(Math.max(...corners.map((c) => c.x)), width) - 1,
69
+ y0: px(Math.min(...corners.map((c) => c.y)), height), y1: px(Math.max(...corners.map((c) => c.y)), height) - 1 };
70
+ const clamped = { x0: Math.max(0, box.x0), x1: Math.min(width - 1, box.x1), y0: Math.max(0, box.y0), y1: Math.min(height - 1, box.y1) };
71
+ return clamped.x1 >= clamped.x0 && clamped.y1 >= clamped.y0 ? clamped : null;
72
+ }
73
+
74
+ /** The lighting check for verify_result: the shot frame at the playhead, its
75
+ * subjects the target characters (every visible one when no target is a
76
+ * character). Fails as TARGET_NOT_READY when nothing can be rendered. */
77
+ export function lightingChecks(ids, state, ports) {
78
+ if (typeof ports.renderFrameBuffer !== "function") throw new StudioProtocolError("CAPABILITY_MISSING", "This editor cannot render the shot frame.");
79
+ if (!state.camera || !(state.camera.focalMm > 0)) throw new StudioProtocolError("TARGET_NOT_READY", "The shot camera must be available.");
80
+ const named = state.characters.filter((character) => ids.includes(character.id));
81
+ const characters = named.length ? named : state.characters.filter((character) => !character.hidden);
82
+ let image;
83
+ try { image = ports.renderFrameBuffer(state.frame); }
84
+ catch (error) { throw new StudioProtocolError("TARGET_NOT_READY", `The shot frame could not be rendered: ${error.message}`); }
85
+ const boxes = characters.map((character) => {
86
+ const bounds = entityWorldBounds(character, state, ports);
87
+ return { id: character.id, ...(bounds && !character.hidden ? pixelBox(bounds, state, image.width, image.height) : {}) };
88
+ });
89
+ // What lit the frame beyond the lights: the look, its environment light and its occlusion pass.
90
+ // An HDR environment is named with whether its map had loaded for this frame (until then the look's sky lit it).
91
+ const { preset, envLight, ao, hdr, hdrRotation, hdrIntensity, hdrBackground } = normalizeLighting(state.stage?.lighting);
92
+ return { coverage: "rendered-shot-frame", frame: state.frame, ...frameLighting(image, boxes),
93
+ look: { preset, envLight, ao, ...(hdr ? { hdr, hdrLoaded: Boolean(readyHdrMap(hdr)), hdrRotation, hdrIntensity, hdrBackground } : {}) } };
94
+ }
95
+
96
+ /** Puts the lighting check on a verify_result answer, or its reason in unsupportedReasons. */
97
+ export function verifyLighting(result, ids, state, ports) {
98
+ try { result.checks = { ...result.checks, lighting: lightingChecks(ids, state, ports) }; }
99
+ catch (error) {
100
+ if (!(error instanceof StudioProtocolError)) throw error;
101
+ result.unsupportedReasons.lighting = error.message;
102
+ }
103
+ }
@@ -0,0 +1,150 @@
1
+ // verify_result's visual: the one image the agent asks to see. "frame" and "plan" are captures;
2
+ // "contact_sheet" tiles frames spread evenly, picked from the installed take's beats
3
+ // (frames: "beats") or named (frames: [f, ...]), seen by the shot camera or by views of its own
4
+ // (views, focus, trail); "trace" plots hand heights and distances of the take over the range (C11).
5
+ // The beats, the views and the trace read one lenient compile of the take's stored spec, as motion.qa does.
6
+ import { StudioProtocolError } from "./studio-agent-protocol.js";
7
+ import { buildContactSheet, paintDots, sampleContactSheetFrames, sheetGrid, viewTileSide } from "./studio-contact-sheet.js";
8
+ import { compileSpec } from "./motion3/pipeline.js";
9
+ import { motionScene } from "./motion3/scene.js";
10
+ import { skeletonFor } from "./motion3/rigs/index.js";
11
+ import { beatFrames, grabReleases, traceSeries, TRACE_COLORS } from "./motion3/beats.js";
12
+ import { depthViews, projectPoint, trailPoints, viewCameras } from "./motion3/views.js";
13
+ import { checkRef } from "./motion3/refs.js";
14
+ import { renderWithGhosts } from "./ghost-set.js";
15
+ import { GHOST_COLORS, sheetGhostSettings } from "./view-ghosts.js";
16
+ import { publishSheet } from "./agent-progress.js";
17
+ import { clampFrames } from "./motion3/lenient.js";
18
+ import { sheetCaption } from "./workflow/agent-progress-view.js";
19
+ // verify_result's lighting check also reads the rendered shot frame; the binding imports it from here.
20
+ export { verifyLighting } from "./studio-lighting-check.js";
21
+
22
+ const TIMELINE_FPS = 24, TRACE_WIDTH = 960, TRACE_HEIGHT = 360, PAD = 12;
23
+ const rgb = hex => [1, 3, 5].map(i => parseInt(hex.slice(i, i + 2), 16));
24
+
25
+ /** [first, last] absolute frames of a verify_result range ("whole_clip" or none: the timeline). */
26
+ const rangeOf = (range, frameCount) => range && range !== "whole_clip" ? [range.startFrame, range.endFrameExclusive - 1] : [0, frameCount - 1];
27
+
28
+ /** The first subject character with a compiled take, compiled again (null when none or it fails). */
29
+ function takeCompile(args, s, receipt) {
30
+ for (const id of args.targets ?? receipt?.affectedIds ?? []) {
31
+ const character = s.characters.find(c => c.id === id), take = s.targets.get(id)?.motion;
32
+ const sk = take?.schema === "cozyclay.take.v3" && take.spec ? skeletonFor(take.rig ?? character?.model) : null;
33
+ if (!character || !sk) continue;
34
+ try {
35
+ return { ...compileSpec({ fps: TIMELINE_FPS, ...take.spec }, { sk, scene: motionScene(s.objects) }, { lenient: true }), characterId: id };
36
+ } catch { return null; }
37
+ }
38
+ return null;
39
+ }
40
+
41
+ /**
42
+ * Line series on a dark plot, as RGBA rows bottom-up (WebGL read-back order, what the encodePng
43
+ * port flips): x is frames lo..hi, y one metre scale for every series. Grey lines mark each second
44
+ * and every 0.1 m; `marks` are frames drawn as light verticals (beat starts, grab, release).
45
+ */
46
+ export function traceRgba(series, [lo, hi], { width = TRACE_WIDTH, height = TRACE_HEIGHT, marks = [] } = {}) {
47
+ const data = new Uint8Array(width * height * 4);
48
+ const put = (x, y, [r, g, b]) => {
49
+ if (x < 0 || y < 0 || x >= width || y >= height) return;
50
+ const i = (y * width + x) * 4; data[i] = r; data[i + 1] = g; data[i + 2] = b; data[i + 3] = 255;
51
+ };
52
+ for (let y = 0; y < height; y++) for (let x = 0; x < width; x++) put(x, y, [22, 24, 29]);
53
+ let bottom = Math.min(...series.map(s => s.min)), top = Math.max(...series.map(s => s.max));
54
+ if (!(top > bottom)) { bottom = (Number.isFinite(bottom) ? bottom : 0) - 0.05; top = bottom + 0.1; }
55
+ const X = f => Math.round(PAD + ((f - lo) / Math.max(1, hi - lo)) * (width - 2 * PAD));
56
+ const Y = v => Math.round(PAD + ((v - bottom) / (top - bottom)) * (height - 2 * PAD));
57
+ for (let m = Math.ceil(bottom * 10); m <= top * 10; m++) for (let x = PAD; x < width - PAD; x++) put(x, Y(m / 10), m === 0 ? [90, 94, 104] : [44, 47, 55]);
58
+ for (let f = Math.ceil(lo / TIMELINE_FPS) * TIMELINE_FPS; f <= hi; f += TIMELINE_FPS) for (let y = PAD; y < height - PAD; y++) put(X(f), y, [44, 47, 55]);
59
+ for (const f of marks) if (f >= lo && f <= hi) for (let y = PAD; y < height - PAD; y++) put(X(f), y, [150, 154, 164]);
60
+ for (const s of series) {
61
+ const color = rgb(s.color);
62
+ for (let i = 1; i < s.values.length; i++) {
63
+ const a = s.values[i - 1], b = s.values[i];
64
+ if (a === null || b === null) continue;
65
+ const x0 = X(lo + i - 1), y0 = Y(a), x1 = X(lo + i), y1 = Y(b), n = Math.max(1, Math.abs(x1 - x0), Math.abs(y1 - y0));
66
+ for (let k = 0; k <= n; k++) {
67
+ const x = Math.round(x0 + ((x1 - x0) * k) / n), y = Math.round(y0 + ((y1 - y0) * k) / n);
68
+ put(x, y, color); put(x + 1, y, color); put(x, y + 1, color); put(x + 1, y + 1, color);
69
+ }
70
+ }
71
+ }
72
+ return { data, width, height };
73
+ }
74
+
75
+ const listOf = value => value === undefined ? [] : [value].flat();
76
+ const round2 = p => p.map(v => Math.round(v * 100) / 100);
77
+
78
+ /**
79
+ * A contact sheet from views of its own (motion3/views.js), a row per view, never the shot
80
+ * camera: each view is placed once for every frame, framed on args.focus (default the whole
81
+ * character) and the trails. A trail is a dot per frame of its ref over the range, so spacing is
82
+ * speed: frames up to the tile's frame solid, later ones faint, the tile's frame ringed. With
83
+ * args.ghosts every tile also draws the take's poses around its frame (ghost-set.js). args.views
84
+ * are the rendered ones (depthViews); `added` names the view added to them and why.
85
+ */
86
+ function viewSheet(args, r, { s, frames, span, grid, picks, ports, added }) {
87
+ if (!r) throw new StudioProtocolError("TARGET_NOT_READY", "views frame a character's compiled take; name a character that has one (run motion.compile first).");
88
+ const focus = listOf(args.focus), refs = listOf(args.trail), ctx = { sk: r.out.ctx.sk, scene: r.out.ctx.scene };
89
+ for (const [field, list] of [["focus", focus], ["trail", refs]]) list.forEach((ref, i) => checkRef(r.expanded, message => { throw new StudioProtocolError("INVALID_ARGUMENT", message); }, ctx, `${field}[${i}]`, ref));
90
+ const trails = refs.map((ref, i) => ({ ref, color: TRACE_COLORS[i], points: trailPoints(r.out, ref, span) }));
91
+ const side = viewTileSide(grid), cameras = viewCameras(r.out, { views: args.views, frames, focus, trails });
92
+ if (!cameras.length) throw new StudioProtocolError("TARGET_NOT_READY", "views found no compiled frame of the take in the range.");
93
+ const cells = cameras.flatMap(cam => frames.map(frame => ({ cam, frame }))), target = args.ghosts ? s.targets.get(r.characterId) : null;
94
+ if (args.ghosts && !target?.rig) throw new StudioProtocolError("TARGET_NOT_READY", "ghosts pose a copy of the character's rig; it has not loaded in the editor yet.");
95
+ const sheet = buildContactSheet(cells, ({ cam, frame }) => {
96
+ const render = () => ports.renderFrameBuffer(frame, { eye: cam.eye, quaternion: cam.quaternion, fovDeg: cam.fovDeg, output: { width: side, height: side } });
97
+ const tile = target ? renderWithGhosts({ rig: target.rig, take: target.motion, objects: s.objects }, frame, render) : render();
98
+ const dots = trails.flatMap(t => t.points.map(({ f, p }) => [f, projectPoint(cam, p, tile.width, tile.height), rgb(t.color)])
99
+ .filter(([, q]) => q).map(([f, [x, y], color]) => ({ x, y, color, r: f === frame ? 4 : 1.5, alpha: f <= frame ? 1 : 0.35, ring: f === frame })));
100
+ return { ...tile, data: paintDots(tile.data, tile.width, tile.height, dots) };
101
+ }, grid);
102
+ const views = cameras.map(cam => ({ name: cam.name, eye: round2(cam.eye), target: round2(cam.target) }));
103
+ const ref = { frames, layout: grid.layout, views, ...(added ? { addedView: added } : {}), focus: focus.length ? focus : "character", ...(trails.length ? { trails: trails.map(({ ref: of, color }) => ({ ref: of, color })) } : {}), ...(picks ? { picks } : {}),
104
+ ...(target ? { ghosts: { ...sheetGhostSettings(), ...GHOST_COLORS } } : {}) };
105
+ return [{ dataUrl: ports.encodePng(sheet.data, sheet), width: sheet.width, height: sheet.height, frames, layout: grid.layout, views: views.map(v => v.name) }, ref];
106
+ }
107
+
108
+ /** Answers args.visual into result.visualRefs and keeps the image for resolve_studio_image. */
109
+ export function studioVisual(args, { s, receipt, result, images, ports }) {
110
+ const keep = (image, ref) => {
111
+ const imageId = crypto.randomUUID();
112
+ images.set(imageId, { ...image, revision: s.revision, receiptId: result.receiptId });
113
+ result.visualRefs.push({ imageId, ...ref });
114
+ // the user sees what the agent looked at (agent-progress.js: the agent panel's progress card)
115
+ const kind = image.kind ?? (args.visual === "contact_sheet" ? "contact sheet" : args.visual);
116
+ publishSheet({ characterId: args.targets?.[0] ?? receipt?.affectedIds?.[0] ?? null, dataUrl: image.dataUrl, width: image.width, height: image.height,
117
+ caption: sheetCaption({ kind, views: image.views ?? [], frames: image.frames ?? [] }), frames: image.frames ?? [] });
118
+ };
119
+ if (args.visual !== "contact_sheet" && (args.views || args.focus || args.trail)) throw new StudioProtocolError("INVALID_ARGUMENT", 'views, focus and trail shape a contact sheet; send visual: "contact_sheet".');
120
+ if (args.ghosts && (args.visual !== "contact_sheet" || !args.views)) throw new StudioProtocolError("INVALID_ARGUMENT", 'ghosts are drawn on a contact sheet from views of its own; send visual: "contact_sheet" with views, e.g. ["side"].');
121
+ if (!args.views && (args.focus || args.trail)) throw new StudioProtocolError("INVALID_ARGUMENT", 'focus and trail are drawn by views of its own; add views, e.g. ["front", "side"] (the shot camera stays the user\'s).');
122
+ if (args.visual === "contact_sheet") {
123
+ // One image of frames across the range, each rendered through the export
124
+ // path, which puts the playhead pose, shot camera and props back.
125
+ const span = rangeOf(args.range, s.frameCount), r = args.frames === "beats" || args.views ? takeCompile(args, s, receipt) : null;
126
+ let frames = sampleContactSheetFrames(args.range, s.frameCount), picks, adjusted;
127
+ // named frames past the range are drawn at its nearest end, and visualRefs[0].adjusted says so (lenient.js)
128
+ if (Array.isArray(args.frames)) ({ frames, adjusted } = clampFrames(args.frames, span));
129
+ else if (args.frames === "beats") ({ frames, picks } = beatFrames({ range: span, even: frames, timeline: r?.timeline, groups: r?.qa.groups, objects: r?.expanded.objects }));
130
+ // Views of its own always include two that look across each other (motion3/views.js depthViews).
131
+ const depth = args.views ? depthViews(args.views) : null, grid = sheetGrid(frames.length, depth?.views.length ?? 0);
132
+ const said = ref => (adjusted ? { ...ref, adjusted } : ref);
133
+ if (depth) { const [image, ref] = viewSheet({ ...args, views: depth.views }, r, { s, frames, span, grid, picks, ports, added: depth.added }); return keep(image, said(ref)); }
134
+ const sheet = buildContactSheet(frames, ports.renderFrameBuffer, grid);
135
+ return keep({ dataUrl: ports.encodePng(sheet.data, sheet), width: sheet.width, height: sheet.height, frames, layout: grid.layout }, said({ frames, layout: grid.layout, ...(picks ? { picks } : {}) }));
136
+ }
137
+ if (args.visual === "trace") {
138
+ const r = takeCompile(args, s, receipt);
139
+ if (!r) throw new StudioProtocolError("TARGET_NOT_READY", "visual trace plots a character's compiled take; name a character that has one (run motion.compile first).");
140
+ const { range, series } = traceSeries(r.out, r.expanded, rangeOf(args.range, s.frameCount));
141
+ const marks = [...(r.timeline ?? []).map(entry => entry.f?.[0]), ...grabReleases(r.timeline, r.expanded.objects).map(row => row.f)];
142
+ const plot = traceRgba(series, range, { marks });
143
+ return keep({ dataUrl: ports.encodePng(plot.data, plot), width: plot.width, height: plot.height, kind: "trace" },
144
+ { kind: "trace", range, series: series.map(({ values, ...summary }) => summary) });
145
+ }
146
+ // A frame is the shot camera's view; a plan is the Top View, the set from above
147
+ // with names and the shot camera's wedge.
148
+ const capture = args.visual === "plan" ? (ports.capturePlan ?? (() => { throw new StudioProtocolError("CAPABILITY_MISSING", "This editor cannot render the top view."); }))() : ports.capture();
149
+ keep(capture, args.visual === "plan" ? { kind: "plan" } : {});
150
+ }
@@ -0,0 +1,80 @@
1
+ // The refusal texts of validateStudioSchema (studio-agent-protocol.js). Every text starts with the
2
+ // path ("$.args.topic") and says what the schema allows there: the allowed values, the allowed
3
+ // fields, the expected type or bounds. What arrived is named by its kind ("got a string"), or by
4
+ // a short preview of a scalar (at most 40 characters), never as a whole payload such as an image.
5
+ // Shared by the browser and the sidecar: no imports.
6
+ const LIST_MAX = 24, PREVIEW_MAX = 40;
7
+
8
+ const preview = value => {
9
+ const text = JSON.stringify(value) ?? String(value);
10
+ return text.length > PREVIEW_MAX ? `${text.slice(0, PREVIEW_MAX - 3)}...` : text;
11
+ };
12
+ const kindOf = value => (value === null ? "null" : Array.isArray(value) ? "an array" : typeof value === "object" ? "an object"
13
+ : typeof value === "number" && !Number.isFinite(value) ? String(value) : `a ${typeof value}`);
14
+ /** A list of names, cut after LIST_MAX with how many there are in all. */
15
+ const listOf = values => (values.length > LIST_MAX ? `${values.slice(0, LIST_MAX).join(", ")}, ... (${values.length} in all)` : values.join(", "));
16
+ const typeName = schema => (Object.hasOwn(schema, "const") ? JSON.stringify(schema.const) : schema.type === "integer" ? "an integer"
17
+ : schema.type === "array" ? `an array${schema.items?.type ? ` of ${schema.items.type}s` : ""}`
18
+ : schema.type === "object" ? "an object" : schema.type === "null" ? "null" : `a ${schema.type}`);
19
+
20
+ /** Wrong type, or a constant that differs: what the schema wants and what arrived. */
21
+ export const wrongType = (path, schema, value) => `${path}: expected ${typeName(schema)}, got ${Object.hasOwn(schema, "const") ? preview(value) : kindOf(value)}.`;
22
+
23
+ export const unknownField = (path, schema, key) => {
24
+ const allowed = Object.keys(schema.properties);
25
+ return `${path} has unknown field ${preview(key)}; ${allowed.length ? `allowed: ${listOf(allowed)}` : "it takes no fields"}.`;
26
+ };
27
+
28
+ export const missingField = (path, key) => `${path}: missing required field "${key}".`;
29
+
30
+ /** An array of the wrong type or length. */
31
+ export function arrayRefusal(path, schema, value) {
32
+ if (!Array.isArray(value)) return wrongType(path, schema, value);
33
+ const lo = schema.minItems ?? 0, hi = schema.maxItems ?? Infinity;
34
+ const want = lo === hi ? `exactly ${lo}` : hi === Infinity ? `at least ${lo}` : lo === 0 ? `at most ${hi}` : `${lo} to ${hi}`;
35
+ return `${path}: expected ${want} item${lo === hi && lo === 1 ? "" : "s"}, got ${value.length}.`;
36
+ }
37
+
38
+ export const duplicateItem = (path, items) => {
39
+ const seen = new Set(), twice = items.map(item => JSON.stringify(item)).find(text => (seen.has(text) ? true : (seen.add(text), false)));
40
+ return `${path}: items must be unique; ${twice && twice.length <= PREVIEW_MAX ? twice : "one item"} appears twice.`;
41
+ };
42
+
43
+ /** A string of the wrong type, empty, too long, or off its pattern. */
44
+ export function stringRefusal(path, schema, value) {
45
+ if (typeof value !== "string") return wrongType(path, schema, value);
46
+ if (!value.trim()) return `${path}: must not be empty.`;
47
+ const length = [...value].length;
48
+ if (schema.maxLength !== undefined && length > schema.maxLength) return schema.description ? `${path}: over ${schema.maxLength} characters. ${schema.description}` : `${path}: at most ${schema.maxLength} characters, got ${length}.`;
49
+ return `${path}: ${preview(value)} does not match ${schema.pattern}.`;
50
+ }
51
+
52
+ export const enumRefusal = (path, schema, value) => `${path}: ${preview(value)} is not one of: ${listOf(schema.enum)}.`;
53
+
54
+ /** A number of the wrong type, not an integer, or not above an exclusive minimum. */
55
+ export function numberRefusal(path, schema, value) {
56
+ if (typeof value !== "number" || !Number.isFinite(value)) return wrongType(path, schema, value);
57
+ if (schema.type === "integer" && !Number.isSafeInteger(value)) return `${path}: expected an integer, got ${value}.`;
58
+ return `${path}: must be greater than ${schema.exclusiveMinimum}, got ${value}.`;
59
+ }
60
+
61
+ const isPlain = schema => ((schema.oneOf ?? schema.anyOf) ? (schema.oneOf ?? schema.anyOf).every(isPlain) : schema.type !== "object" && schema.type !== "array");
62
+ const bounded = schema => [schema.minimum, schema.maximum].some(bound => bound !== undefined) ? ` within [${[schema.minimum ?? "-inf", schema.maximum ?? "inf"].join(", ")}]` : "";
63
+ /** Each value or type a schema admits, in words: true, false, "ifClean", an integer within [1, 8]. */
64
+ const formsOf = schema => ((schema.oneOf ?? schema.anyOf) ? (schema.oneOf ?? schema.anyOf).flatMap(formsOf)
65
+ : Object.hasOwn(schema, "const") ? [JSON.stringify(schema.const)] : schema.enum ? schema.enum.map(v => JSON.stringify(v))
66
+ : schema.type === "boolean" ? ["true", "false"] : [`${typeName(schema)}${schema.type === "number" || schema.type === "integer" ? bounded(schema) : ""}`]);
67
+
68
+ /**
69
+ * A union no branch matched, when every branch failed at the union's own path and the branches are
70
+ * plain values (or the value is not an object): `$.args.install must be true, false or "ifClean",
71
+ * got "yes".` Otherwise null, and the caller surfaces the deepest branch failure.
72
+ */
73
+ export function unionRefusal(path, failures, value) {
74
+ if (!failures.every(({ error }) => error.details?.path === path)) return null;
75
+ if (value !== null && typeof value === "object" && !failures.every(({ branch }) => isPlain(branch))) return null;
76
+ const forms = [...new Set(failures.flatMap(({ branch }) => formsOf(branch)))];
77
+ const shown = forms.length > LIST_MAX ? [...forms.slice(0, LIST_MAX), `... (${forms.length} in all)`] : forms;
78
+ const words = shown.length > 1 ? `${shown.slice(0, -1).join(", ")} or ${shown.at(-1)}` : shown[0];
79
+ return `${path} must be ${words}, got ${value !== null && typeof value === "object" ? kindOf(value) : preview(value)}.`;
80
+ }
@@ -0,0 +1,74 @@
1
+ // How far each placed object stands off what holds it up: the support gap verify_result's
2
+ // placement check reports beside the footprint overlap (studio-agent-commands.js placementChecks),
3
+ // measured on the same world AABB proxies.
4
+ //
5
+ // A group is one unit: the union AABB of its solid parts, judged through its root (a part names
6
+ // its root group). Its support is the floor, or the highest top among the other solid objects
7
+ // whose footprint overlaps the unit's and whose top lies below the unit's middle height.
8
+ // supportGapM is the unit's bottom above that top (0 when it rests on it or sinks into it).
9
+ // These are measurements, not failures: unsupportedIds lists the gaps over 1 cm.
10
+ //
11
+ // Not measured: characters, lights, effects, a group with no solid part, records on a route or
12
+ // riding a character, and records an installed take moves where their scene pose is never drawn
13
+ // (a follow or hinge entry, or a carry in hand from its first frame). A carry grabbed later
14
+ // stands at its scene pose until the grab, so it is measured there.
15
+ import { descendantsOf, isEffectivelyHidden, isEmptyObject } from './scene-objects.js';
16
+ import { EFFECT_KIND } from './effects/effect-record.js';
17
+ import { isCarried, movesRecord } from './motion3/carried.js';
18
+
19
+ const UNSUPPORTED_M = 0.01, EPS = 1e-6;
20
+ const round = v => Number(v.toFixed(4));
21
+
22
+ /** Ids the installed takes move (spec.objects keys of each target's take v3), from the binding's targets map. */
23
+ function takeMovedIds(takes) {
24
+ const moved = new Set();
25
+ for (const target of takes?.values?.() ?? []) {
26
+ const take = target?.motion;
27
+ if (take?.schema !== 'cozyclay.take.v3') continue;
28
+ for (const [id, entry] of Object.entries(take.spec?.objects ?? {}))
29
+ if (movesRecord(entry) && !(isCarried(entry) && entry.grab > 0)) moved.add(id);
30
+ }
31
+ return moved;
32
+ }
33
+
34
+ /** [object, parent, ..., root] by id, stopping at a missing parent or a loop. */
35
+ function chainOf(id, byId) {
36
+ const chain = [];
37
+ for (let o = byId.get(id); o && !chain.includes(o); o = o.parent ? byId.get(o.parent) : null) chain.push(o);
38
+ return chain;
39
+ }
40
+
41
+ const solid = (o, state) => !isEmptyObject(o) && o.renderer !== EFFECT_KIND && !isEffectivelyHidden(o, state.objects, state.characters);
42
+
43
+ /** Union { min, max } of boxes, or null for none. */
44
+ function union(boxes) {
45
+ if (!boxes.length) return null;
46
+ const pick = (end, f) => Object.fromEntries(['x', 'y', 'z'].map(a => [a, f(...boxes.map(b => b[end][a]))]));
47
+ return { min: pick('min', Math.min), max: pick('max', Math.max) };
48
+ }
49
+
50
+ const footprintsOverlap = (a, b) => ['x', 'z'].every(a2 => Math.min(a.max[a2], b.max[a2]) - Math.max(a.min[a2], b.min[a2]) > EPS);
51
+
52
+ /**
53
+ * { supportGapM: { unitId: metres }, unsupportedIds } for the objects among `ids` (each unit once).
54
+ * boxOf(entity) answers its world { min, max } or null (studio-agent-commands.js entityWorldBounds);
55
+ * takes is the binding's targets map (characterId -> { motion }), or undefined.
56
+ */
57
+ export function supportGaps(ids, state, takes, boxOf) {
58
+ const byId = new Map(state.objects.map(o => [o.id, o])), moved = takeMovedIds(takes);
59
+ const rootOf = id => chainOf(id, byId).at(-1)?.id;
60
+ // A part counts when nothing above it rides a character, a route or a take.
61
+ const free = o => chainOf(o.id, byId).every(c => !c.attach && !c.path && !moved.has(c.id));
62
+ const units = [...new Set(ids.filter(id => byId.has(id)).map(rootOf))];
63
+ const boxes = new Map(state.objects.filter(o => solid(o, state)).map(o => [o.id, boxOf(o)]).filter(([, box]) => box));
64
+ const supportGapM = {};
65
+ for (const root of units) {
66
+ const parts = [byId.get(root), ...descendantsOf(state.objects, root)].filter(o => boxes.has(o.id) && free(o));
67
+ const own = union(parts.map(o => boxes.get(o.id)));
68
+ if (!own) continue;
69
+ const middle = (own.min.y + own.max.y) / 2;
70
+ const tops = [...boxes].filter(([id, box]) => rootOf(id) !== root && box.max.y <= middle && footprintsOverlap(own, box)).map(([, box]) => box.max.y);
71
+ supportGapM[root] = round(Math.max(0, own.min.y - Math.max(state.floorY ?? 0, ...tops)));
72
+ }
73
+ return { supportGapM, unsupportedIds: Object.keys(supportGapM).filter(id => supportGapM[id] > UNSUPPORTED_M) };
74
+ }
package/src/styles.css CHANGED
@@ -1543,25 +1543,6 @@ select {
1543
1543
  font-size: 11px;
1544
1544
  font-weight: 700;
1545
1545
  }
1546
- /* The photo tile reads as a second, distinct source next to Save pose rather
1547
- than a variant of it, so its dashed outline carries the accent hue. */
1548
- .pose-tile.add.photo {
1549
- border-color: color-mix(in srgb, var(--cyan) 42%, var(--line2));
1550
- color: color-mix(in srgb, var(--cyan) 70%, var(--muted));
1551
- }
1552
- .pose-tile.add.photo .add-plus {
1553
- font-size: 22px;
1554
- }
1555
- .pose-tile.add.photo.busy {
1556
- border-style: solid;
1557
- opacity: 0.75;
1558
- cursor: progress;
1559
- }
1560
- .pose-tile.add.photo:disabled:hover {
1561
- border-color: color-mix(in srgb, var(--cyan) 42%, var(--line2));
1562
- background: transparent;
1563
- }
1564
-
1565
1546
  /* ------------------------------------------------------------ panel ------- */
1566
1547
 
1567
1548
  .panel {
@@ -7296,6 +7277,7 @@ input[type=range] {
7296
7277
  white-space: nowrap;
7297
7278
  }
7298
7279
 
7280
+ /* The panels' native file pickers stay hidden; their own buttons open them. */
7299
7281
  .multimodel-file-input {
7300
7282
  display: none;
7301
7283
  }
@@ -0,0 +1,5 @@
1
+ // The studio timeline's shared numbers: the production clock and the defaults a scene starts from.
2
+ export const TIMELINE_FPS = 24;
3
+ export const DEFAULT_DURATION_S = 15; // pre-motion timeline duration; shown as duration x TIMELINE_FPS frames
4
+ export const DEFAULT_PLAYBACK_SPEED = 1;
5
+ export const MAX_WAYPOINTS = 32; // a root path holds 2..32 distinct waypoint frames