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,130 @@
1
+ // The grip a keyed carry keeps (C18, lane pathgrip). Without at/along a carry keeps the palm-object transform of
2
+ // its grab frame (object-keys.js attach), and nothing chose that grip: the hand met the object at whatever turn the
3
+ // reach left it, and the keys then took the cup to the mouth with the wrist past its comfortable range on 9-47
4
+ // frames in 6 of 8 bench S8 takes (50-58 deg of flex and 70 deg of forearm twist at the mouth). A person picks the
5
+ // grip for where the object is going (end-state comfort, Rosenbaum), so for a keyed carry whose grip is free the
6
+ // grab offset is chosen here over the carry's keyed path:
7
+ // cost at the grab (the hand takes the grip on the body at rest) and along the keys, the arm's wrist need at
8
+ // the elbow the hold solve takes (arm-orient.js landscape) priced against the comfortable ranges
9
+ // (WRIST_COMFORT: z^2 inside, STEEP to the anatomical limits, OVER past them), plus REACH per squared
10
+ // centimetre the arm is short or folded (a residual QA reports) and BODY per squared centimetre the arm
11
+ // or the hand goes into the body (body.js)
12
+ // faithful the keys as the compile resolves them (key-aim.js: an aim key without roll at the roll the arm's
13
+ // strain picks), priced every STEP frames; today's grip that keeps every frame comfortable stays
14
+ // search from today's grip, turns of STEPS degrees about the palm axes (the fingers' line, the palm normal,
15
+ // the knuckle line) while the rough cost (the grab and KEYS keys, an aim key at the best of ROLLS rolls
16
+ // by the strain) drops; the fingers' point stays where the fit put it
17
+ // pick the found grip only when its faithful cost beats today's by MARGIN or SHARE of it (no flip for less)
18
+ // Deterministic. The hand comes into the grip on its reach and leaves it on its release (object-keys.js).
19
+ import { sub, scale, norm, len, apply, mul, T, axisRot, matFromQuat, quatFromMat } from "./compiler/math.js";
20
+ import { qmul, qrot, qdot, axisQuat, arcQuat, qlog, slerp } from "./compiler/quat.js";
21
+ import { aimArm, holdCost } from "./compiler/hold.js";
22
+ import { curveOf, keyedPose } from "./object-keys.js";
23
+ import { landscape, ELBOW_REST, WRIST_COMFORT, WRIST_LIMITS } from "./compiler/arm-orient.js";
24
+ import { bodyFor, segmentDepth } from "./body.js";
25
+ import { reachAt, ELBOW_FOLD } from "./compiler/limbs.js";
26
+ import { compose, relative } from "./carried.js";
27
+
28
+ const THIN = 0.045, ROLLS = 4, KEYS = 8, STEPS = [30, 10], TRIES = 2, STEP = 4, MARGIN = 2, SHARE = 0.1, COMFORTABLE = 1.5, REACH = 20, BODY = 4, STEEP = 8, OVER = 400;
29
+ // the palm frame's columns (carried.js carriedPose): the fingers' line, the palm normal, the knuckle line (reversed)
30
+ const AXES = ["the fingers' line", "the palm normal", "the knuckle line"];
31
+ const col = (R, i) => [R[0][i], R[1][i], R[2][i]];
32
+ const invert = p => { const Rt = T(p.R); return { pos: scale(apply(Rt, p.pos), -1), R: Rt }; };
33
+ const r2 = v => Math.round(v * 100) / 100;
34
+
35
+ /** The rotation a fraction t of the way from A to B. */
36
+ export function between(A, B, t) {
37
+ const a = quatFromMat(A);
38
+ let b = quatFromMat(B);
39
+ if (qdot(a, b) < 0) b = b.map(v => -v);
40
+ return matFromQuat(slerp(a, b, t));
41
+ }
42
+
43
+ /** Price of one wrist turn v (deg) against its comfortable range and anatomical limits. */
44
+ function price(v, [clo, chi], [llo, lhi]) {
45
+ const c = v >= 0 ? chi : -clo, L = v >= 0 ? lhi : -llo, a = Math.abs(v);
46
+ if (a <= c) return (a / c) ** 2;
47
+ const e = (Math.min(a, L) - c) / Math.max(1e-6, L - c);
48
+ return 1 + STEEP * e * e + (a > L ? OVER * ((a - L) / L) ** 2 : 0);
49
+ }
50
+
51
+ /** The arm's comfort-priced cost of holding palm frame `palm` on pose s (the plain arm of `side`, `now` its palm frame there). */
52
+ function comfortCost(sk, s, side, palm, now) {
53
+ const { goal, plane, wristAt } = aimArm(sk, s, side, palm, now, 1), W = wristAt(goal), land = landscape(sk, s, side, goal, W, plane, ELBOW_REST);
54
+ const arm = land.at(land.softmin()), I = sk.index, S = s.P[I[`${side}Arm`]];
55
+ const A = len(sub(s.P[I[`${side}ForeArm`]], S)), B = len(sub(s.P[I[`${side}Hand`]], s.P[I[`${side}ForeArm`]]));
56
+ const fold = Math.max(0, reachAt(A, B, ELBOW_FOLD) - len(sub(W, S))), body = bodyFor(sk);
57
+ const into = body ? Math.max(segmentDepth(body, sk, s, I[`${side}Arm`], S, arm.elbow), segmentDepth(body, sk, s, I[`${side}ForeArm`], arm.elbow, W), segmentDepth(body, sk, s, I[`${side}Hand`], W, palm.pos)) : 0;
58
+ return ["twist", "flex", "dev"].reduce((c, k) => c + price(arm.need[k], WRIST_COMFORT[k], WRIST_LIMITS[k]), 0) + REACH * ((land.miss * 100) ** 2 + (fold * 100) ** 2) + BODY * (into * 100) ** 2;
59
+ }
60
+
61
+ /** The rough cost of key `key` with offset A: a rot key at its rot; an aim key turned from where the carry holds it (key-aim.js), at its roll or at the best of ROLLS rolls by the arm's strain (hold.js holdCost, as the compile picks it), priced by priceOf. */
62
+ function keyCost(sk, s, side, key, now, A, priceOf) {
63
+ if (key.aim === undefined) return priceOf({ pos: key.pos, R: matFromQuat(key.rot) });
64
+ const from = quatFromMat(compose(now, A).R), d = norm(key.aim), q0 = qmul(arcQuat(norm(qrot(from, [0, 1, 0])), d), from);
65
+ const pose = r => ({ pos: key.pos, R: matFromQuat(qmul(axisQuat(d, r), q0)) });
66
+ if (key.roll !== undefined) return priceOf(pose(key.roll));
67
+ let best = null;
68
+ for (let j = 0; j < ROLLS; j++) {
69
+ const p = pose(j * (360 / ROLLS) - 180), strain = holdCost(sk, s, side, compose(p, invert(A)), now);
70
+ if (!best || strain < best.strain) best = { p, strain };
71
+ }
72
+ return priceOf(best.p);
73
+ }
74
+
75
+ /**
76
+ * True when body or group `id` is thin (two half sizes under THIN m: a cup, a can, a bar, a handle); a group by its
77
+ * largest part (a cup with a handle is held by its cup). A box keeps the grip grip.js gives it at the grab.
78
+ */
79
+ export function thinBody(scene, id) {
80
+ const parts = Object.entries(scene?.objects ?? {}).filter(([k, o]) => (k === id || o.ancestors?.includes(id)) && o.half);
81
+ if (!parts.length) return false;
82
+ const half = parts.map(([, o]) => o.half).reduce((m, h) => (h[0] * h[1] * h[2] > m[0] * m[1] * m[2] ? h : m));
83
+ const long = [0, 1, 2].reduce((a, i) => (half[i] > half[a] ? i : a), 0);
84
+ return [0, 1, 2].filter(i => i !== long).every(i => half[i] < THIN);
85
+ }
86
+
87
+ /** At most n of the keys, spread over them (the first and the last kept). */
88
+ const spread = (keys, n) => (keys.length <= n ? keys : Array.from({ length: n }, (_, i) => keys[Math.round((i * (keys.length - 1)) / (n - 1))]));
89
+
90
+ /** How the found rotation R differs from today's R0, in words: "today's grip turned 40 deg about the palm normal". */
91
+ function named(R0, R) {
92
+ const v = qlog(quatFromMat(mul(T(R0), R))), deg = Math.round((2 * len(v) * 180) / Math.PI), i = [0, 1, 2].reduce((m, j) => (Math.abs(v[j]) > Math.abs(v[m]) ? j : m), 0);
93
+ return `today's grip turned ${deg} deg, mostly about ${AXES[i]}`;
94
+ }
95
+
96
+ /**
97
+ * chooseGrip({ sk, o, side, grab, rest, plain, palmOf, map }) -> { offset, name, cost, today }: the grab offset a
98
+ * keyed carry with a free grip keeps (see above). plain(f) solves frame f without keys, palmOf(s) is the carry's palm
99
+ * frame on a pose, rest the body's pose at the grab, map a key frame to its timeline frame.
100
+ */
101
+ export function chooseGrip({ sk, o, side, grab, rest, plain, palmOf, map = f => f }) {
102
+ const solved = new Map(), at = f => { const g = Math.round(f); if (!solved.has(g)) { const s = plain(g); solved.set(g, { s, now: palmOf(s) }); } return solved.get(g); };
103
+ const { s: s0, now: now0 } = at(grab), keys = [...o.keys].sort((a, b) => a.f - b.f), some = spread(keys, KEYS);
104
+ const offsetOf = R => relative({ pos: now0.pos, R }, rest), held = (A, f, pose) => { const { s, now } = at(f); return comfortCost(sk, s, side, compose(pose, invert(A)), now); };
105
+ const grabCost = A => comfortCost(sk, s0, side, compose(rest, invert(A)), now0);
106
+ const rough = R => { const A = offsetOf(R); return grabCost(A) + some.reduce((sum, key) => { const { s, now } = at(map(key.f)); return sum + keyCost(sk, s, side, key, now, A, pose => held(A, map(key.f), pose)); }, 0); };
107
+ const faithful = R => {
108
+ const A = offsetOf(R), t0 = Math.round(map(keys[0].f)), t1 = Math.round(map(keys[keys.length - 1].f));
109
+ const curve = curveOf(o, map, t => quatFromMat(compose(at(t).now, A).R), (t, pos, q, opts) => { const { s, now } = at(t); return holdCost(sk, s, side, compose({ pos, R: matFromQuat(q) }, invert(A)), now, opts); }, 2 * (rest?.half?.[1] ?? 0));
110
+ let cost = grabCost(A), worst = cost;
111
+ for (let f = t0; f <= t1; f += STEP) { const c = held(A, f, keyedPose(curve, f)); cost += c; worst = Math.max(worst, c); }
112
+ return { A, cost, worst };
113
+ };
114
+ // today's grip that keeps every frame comfortable (each turn inside its range) stays as it is
115
+ const today = { name: "today", R: now0.R, ...faithful(now0.R) };
116
+ if (today.worst <= COMFORTABLE) return { offset: today.A, name: "today", cost: r2(today.cost), today: r2(today.cost) };
117
+ let cur = { R: now0.R, rough: rough(now0.R) };
118
+ for (const step of STEPS) {
119
+ for (let tries = 0, better = true; better && tries < TRIES; tries++) {
120
+ better = false;
121
+ for (const i of [0, 1, 2]) for (const sign of [1, -1]) {
122
+ const R = mul(axisRot(col(cur.R, i), sign * step), cur.R), c = rough(R);
123
+ if (c < cur.rough - 1e-6) { cur = { R, rough: c }; better = true; }
124
+ }
125
+ }
126
+ }
127
+ const found = cur.R === now0.R ? today : { name: named(now0.R, cur.R), ...faithful(cur.R) };
128
+ const pick = found.cost < today.cost - Math.max(MARGIN, SHARE * today.cost) ? found : today;
129
+ return { offset: pick.A, name: pick.name, cost: r2(pick.cost), today: r2(today.cost) };
130
+ }
@@ -0,0 +1,157 @@
1
+ // The arms' paths over a keyed span (C14, lane K8). What the arm does on a frame the keys lead
2
+ // (the holding elbow's swivel, compiler/arm-orient.js; the second hand's roll about the object,
3
+ // second-hand.js) is read as a cost per frame on the pose that frame starts from, and the path
4
+ // minimises, over all those frames at once, the cost of each frame plus SMOOTH times the squared
5
+ // acceleration of the point that choice moves (the elbow; the free end of the second hand): where
6
+ // the cost barely cares the arm glides, where it does the arm goes where the wrist needs it. The
7
+ // best choice alone can move 20 deg from one frame to the next while a bat whips through the
8
+ // zone, and an arm that followed it would jerk (QA POP). Solved by NEWTON Gauss-Newton steps from
9
+ // each frame's soft minimum, each a banded (5-diagonal) linear solve. Every frame reads the same
10
+ // plan, so a frame still solves on its own (a windowed compile, a frame solved on demand) and the
11
+ // same spec gives the same take.
12
+ // The holding elbow's path never jumps (lane Q1, steadyPath). Where the cheap poles of two frames
13
+ // in a row lie on either side of a ridge, smoothPath from each frame's soft minimum stays in a
14
+ // local optimum that jumps across it (session 4db5d447, the follow-through: the elbow could hold
15
+ // the bat down-forward to f43 and up-forward from f44, and the planned elbow went 22 cm in one
16
+ // frame, QA POP and FLIP 78 deg). A path that jumps (jumpsIn) is solved again from the cheapest
17
+ // path that never turns more than JUMP a frame (continuousPath, dynamic programming over the
18
+ // poles), and if the cost pulls that back into a jump, with the smoothing BOOST times stronger
19
+ // around it: the elbow goes over the ridge in a few frames (QA-17 run: at most 10 cm a frame,
20
+ // the plane 26 deg a frame) and pays its strain there. A path that does not jump, and one these
21
+ // do not free of its jump (the keys jerk the hand there), is the plan as before, bit for bit.
22
+ import { holdLandscape } from "./compiler/hold.js";
23
+ import { add, sub, scale, dot, angle, len } from "./compiler/math.js";
24
+
25
+ const SMOOTH = 1000, NEWTON = 4, STEP = 0.03, MAX_STEP = 0.6, MIN_CURVE = 0.05, SIGMA = 2;
26
+ // A jump: the pole turns more than JUMP deg in a frame while the elbow moves more than SPIKE times
27
+ // as far as in the frames around and more than FLOOR m (QA POP's floor, accLimb). The continuous
28
+ // path prices each turn TURN per squared radian and OVER_TURN past JUMP; BOOST, AROUND as above.
29
+ const JUMP = 45, SPIKE = 2, FLOOR = 0.05, TURN = 10, OVER_TURN = 400, BOOST = 16, AROUND = 4;
30
+ /** The poles continuousPath reads (radians from the landscape's th 0), 5 deg apart. */
31
+ const POLE_GRID = Array.from({ length: 72 }, (_, i) => (2 * Math.PI * i) / 72 - Math.PI);
32
+
33
+ /** Solve the symmetric 5-diagonal system A x = b; A[i][k] is the entry (i, i + k - 2). */
34
+ function solveBand(A, b) {
35
+ const n = b.length, x = Array.from(b);
36
+ for (let i = 0; i < n; i++) {
37
+ for (let j = i + 1; j <= Math.min(i + 2, n - 1); j++) {
38
+ const m = A[j][i - j + 2] / A[i][2];
39
+ for (let k = i; k <= Math.min(i + 2, n - 1); k++) A[j][k - j + 2] -= m * A[i][k - i + 2];
40
+ x[j] -= m * x[i];
41
+ }
42
+ }
43
+ for (let i = n - 1; i >= 0; i--) {
44
+ for (let k = i + 1; k <= Math.min(i + 2, n - 1); k++) x[i] -= A[i][k - i + 2] * x[k];
45
+ x[i] /= A[i][2];
46
+ }
47
+ return x;
48
+ }
49
+
50
+ /**
51
+ * The angle per frame (radians, frames in a row) that minimises sum cost(i, x_i) + SMOOTH *
52
+ * sum weight_r |P(r) - 2 P(r+1) + P(r+2)|^2 with P(i) = point(i, x_i) (metres), from x0 (each
53
+ * frame's own best, unwrapped here so neighbours differ by less than half a turn); weight 1 when
54
+ * not given.
55
+ */
56
+ export function smoothPath(cost, point, x0, weight = null) {
57
+ const n = x0.length, x = x0.slice();
58
+ for (let i = 1; i < n; i++) x[i] = x[i - 1] + Math.atan2(Math.sin(x[i] - x[i - 1]), Math.cos(x[i] - x[i - 1]));
59
+ for (let it = 0; it < NEWTON && n > 2; it++) {
60
+ const A = x.map(() => new Float64Array(5)), b = new Float64Array(n);
61
+ x.forEach((t, i) => {
62
+ const c0 = cost(i, t), cp = cost(i, t + STEP), cm = cost(i, t - STEP);
63
+ A[i][2] = Math.max(MIN_CURVE, (cp + cm - 2 * c0) / (STEP * STEP));
64
+ b[i] = -(cp - cm) / (2 * STEP);
65
+ });
66
+ // linearised: the point moves along tan = dP/dx
67
+ const P = x.map((t, i) => point(i, t)), tan = x.map((t, i) => scale(sub(point(i, t + STEP), point(i, t - STEP)), 1 / (2 * STEP)));
68
+ for (let r = 0; r + 2 < n; r++) {
69
+ const c = [1, -2, 1], acc = add(sub(P[r], scale(P[r + 1], 2)), P[r + 2]), k = weight ? 2 * SMOOTH * weight[r] : 2 * SMOOTH;
70
+ for (let p = 0; p < 3; p++) {
71
+ b[r + p] -= k * c[p] * dot(acc, tan[r + p]);
72
+ for (let q = 0; q < 3; q++) A[r + p][q - p + 2] += k * c[p] * c[q] * dot(tan[r + p], tan[r + q]);
73
+ }
74
+ }
75
+ const step = solveBand(A, b);
76
+ x.forEach((t, i) => { x[i] = t + Math.max(-MAX_STEP, Math.min(MAX_STEP, step[i])); });
77
+ }
78
+ return x;
79
+ }
80
+
81
+ /**
82
+ * The frames i where the path x jumps from frame i to i + 1: dir (a unit vector, the elbow's pole)
83
+ * turns more than JUMP degrees and the point (the elbow) moves more than SPIKE times as far as in
84
+ * the frame before or after, and more than FLOOR metres. A pole that turns while the arm is
85
+ * straight barely moves the elbow, and a swing moves it fast for several frames: neither is a jump.
86
+ */
87
+ function jumpsIn(x, dir, point) {
88
+ const P = x.map((t, i) => point(i, t)), step = i => (i >= 0 && i + 1 < P.length ? len(sub(P[i + 1], P[i])) : 0);
89
+ return x.slice(1).map((t, i) => i).filter(i => step(i) > FLOOR && step(i) > SPIKE * Math.max(step(i - 1), step(i + 1)) && angle(dir(i, x[i]), dir(i + 1, x[i + 1])) > JUMP);
90
+ }
91
+
92
+ /**
93
+ * The path over the candidate angles `grid` that minimises sum cost(i, x_i) + TURN * sum a_i^2
94
+ * plus OVER_TURN * (a_i - JUMP)^2 past JUMP, a_i the turn (radians) of dir from frame i to i + 1
95
+ * (dynamic programming): the cheapest way through that never jumps, whichever valley it starts in.
96
+ */
97
+ function continuousPath(cost, dir, n, grid) {
98
+ const K = grid.length, lim = JUMP * Math.PI / 180, C = [], U = [];
99
+ for (let i = 0; i < n; i++) { C.push(grid.map(t => cost(i, t))); U.push(grid.map(t => dir(i, t))); }
100
+ let total = C[0].slice();
101
+ const back = [];
102
+ for (let i = 1; i < n; i++) {
103
+ const next = new Array(K), from = new Int32Array(K);
104
+ for (let k = 0; k < K; k++) {
105
+ let best = Infinity;
106
+ for (let j = 0; j < K; j++) {
107
+ const a = Math.acos(Math.max(-1, Math.min(1, dot(U[i - 1][j], U[i][k])))), t = total[j] + TURN * a * a + (a > lim ? OVER_TURN * (a - lim) ** 2 : 0);
108
+ if (t < best) { best = t; from[k] = j; }
109
+ }
110
+ next[k] = best + C[i][k];
111
+ }
112
+ back.push(from);
113
+ total = next;
114
+ }
115
+ const pick = new Array(n);
116
+ pick[n - 1] = total.indexOf(Math.min(...total));
117
+ for (let i = n - 1; i > 0; i--) pick[i - 1] = back[i - 1][pick[i]];
118
+ return pick.map(k => grid[k]);
119
+ }
120
+
121
+ /**
122
+ * smoothPath that never jumps (jumpsIn): dir(i, x) is the world direction choice x gives frame i
123
+ * (the elbow's pole), grid the candidate angles. When the path smoothPath finds from x0 jumps, it
124
+ * starts again from the continuous path (continuousPath); if the cost pulls that back into a jump,
125
+ * the acceleration terms within AROUND frames of it weigh BOOST times more. A path that does not
126
+ * jump is smoothPath's, bit for bit, and so is one these do not free of jumps (the keys themselves
127
+ * jerk the hand there).
128
+ */
129
+ export function steadyPath(cost, point, x0, dir, grid = POLE_GRID) {
130
+ const x = smoothPath(cost, point, x0);
131
+ if (!jumpsIn(x, dir, point).length) return x;
132
+ const y0 = continuousPath(cost, dir, x.length, grid), y = smoothPath(cost, point, y0), jumps = jumpsIn(y, dir, point);
133
+ if (!jumps.length) return y;
134
+ const weight = new Float64Array(Math.max(0, y.length - 2)).map((_, r) => (jumps.some(j => j >= r - AROUND && j <= r + 1 + AROUND) ? BOOST : 1));
135
+ const z = smoothPath(cost, point, y0, weight);
136
+ return jumpsIn(z, dir, point).length ? x : z;
137
+ }
138
+
139
+ /** Values over frames in a row, smoothed by a Gaussian of SIGMA frames (the weights renormalised at the ends). */
140
+ export function smoothed(v) {
141
+ return v.map((_, i) => {
142
+ let sum = 0, wsum = 0;
143
+ for (let j = Math.max(0, i - 3 * SIGMA); j <= Math.min(v.length - 1, i + 3 * SIGMA); j++) { const g = Math.exp(-((j - i) ** 2) / (2 * SIGMA * SIGMA)); sum += g * v[j]; wsum += g; }
144
+ return sum / wsum;
145
+ });
146
+ }
147
+
148
+ /**
149
+ * The holding elbow's planned pole per frame, Map f -> world direction, over `frames` (ascending
150
+ * and contiguous, the frames the keys lead). frameAt(f) -> { s, held }: the plain pose of frame f
151
+ * and what the hand holds there (object-keys.js lead).
152
+ */
153
+ export function polePlan(sk, side, frames, frameAt) {
154
+ const lands = frames.map(f => { const { s, held } = frameAt(f); return holdLandscape(sk, s, side, held); });
155
+ const th = steadyPath((i, t) => lands[i].at(t).cost, (i, t) => lands[i].at(t).elbow, lands.map(l => l.softmin()), (i, t) => lands[i].at(t).pv);
156
+ return new Map(frames.map((f, i) => [f, lands[i].at(th[i]).pv]));
157
+ }
@@ -0,0 +1,77 @@
1
+ // JSON Patch (RFC 6902) on a motion spec: add, remove, replace and test, with "-" to append to
2
+ // an array in add. Used for motion.compile { base, patch } (C7) and by repair (C6). Errors are
3
+ // MOTION_SPEC_INVALID (INVALID_ARGUMENT to the agent) and name the op and what is there:
4
+ // patch[1] (replace /keys/12/f): /keys/12 does not exist (keys has 9 entries)
5
+ // With a notes list (motion.compile, lenient.js), replace on a member an existing object lacks adds it
6
+ // and says so; repair (no notes) stays strict.
7
+
8
+ const OPS = ["add", "remove", "replace", "test"];
9
+ const bad = msg => { throw new Error(`MOTION_SPEC_INVALID: ${msg}`); };
10
+ const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
11
+
12
+ /** "/keys/0/set" -> ["keys", "0", "set"] (~1 is "/", ~0 is "~"). */
13
+ const tokens = path => path.split("/").slice(1).map(t => t.replaceAll("~1", "/").replaceAll("~0", "~"));
14
+ const pointer = parts => parts.map(t => `/${String(t).replaceAll("~", "~0").replaceAll("/", "~1")}`).join("");
15
+
16
+ function what(container, name) {
17
+ if (Array.isArray(container)) return `${name} has ${container.length} entries`;
18
+ const keys = Object.keys(container);
19
+ return keys.length ? `${name} has ${keys.slice(0, 8).join(", ")}${keys.length > 8 ? ", ..." : ""}` : `${name} is empty`;
20
+ }
21
+
22
+ /** The container holding the last token, walking every token before it. */
23
+ function parentOf(doc, parts, tag) {
24
+ let node = doc;
25
+ for (let i = 0; i < parts.length - 1; i++) {
26
+ const t = parts[i], name = i ? parts[i - 1] : "the spec";
27
+ if (node === null || typeof node !== "object" || !Object.hasOwn(node, Array.isArray(node) ? Number(t) : t) || (Array.isArray(node) && !/^\d+$/.test(t)))
28
+ bad(`${tag}: ${pointer(parts.slice(0, i + 1))} does not exist${node && typeof node === "object" ? ` (${what(node, name)})` : ""}`);
29
+ node = node[Array.isArray(node) ? Number(t) : t];
30
+ }
31
+ if (node === null || typeof node !== "object") bad(`${tag}: ${pointer(parts.slice(0, -1))} is not an object or array`);
32
+ return node;
33
+ }
34
+
35
+ function applyOne(doc, op, i, notes) {
36
+ if (!op || typeof op !== "object" || !OPS.includes(op.op)) bad(`patch[${i}]: op must be ${OPS.join(", ")}, got ${JSON.stringify(op?.op)}`);
37
+ const tag = `patch[${i}] (${op.op} ${op.path})`;
38
+ if (typeof op.path !== "string" || (op.path !== "" && !op.path.startsWith("/"))) bad(`${tag}: path must be a JSON Pointer such as "/keys/0/f"`);
39
+ if (op.op !== "remove" && !Object.hasOwn(op, "value")) bad(`${tag}: needs a value`);
40
+ if (op.path === "") {
41
+ if (op.op === "replace") return structuredClone(op.value);
42
+ if (op.op === "test") { if (!same(doc, op.value)) bad(`${tag}: the spec differs from the test value`); return doc; }
43
+ bad(`${tag}: ${op.op} needs a path below the spec`);
44
+ }
45
+ const parts = tokens(op.path), last = parts[parts.length - 1], node = parentOf(doc, parts, tag);
46
+ const name = parts.length > 1 ? parts[parts.length - 2] : "the spec";
47
+ if (Array.isArray(node)) {
48
+ if (last === "-") { if (op.op !== "add") bad(`${tag}: "-" (append) works with add only`); node.push(structuredClone(op.value)); return doc; }
49
+ const k = /^\d+$/.test(last) ? Number(last) : NaN;
50
+ const max = op.op === "add" ? node.length : node.length - 1;
51
+ if (!(k >= 0 && k <= max)) bad(`${tag}: ${op.path} does not exist (${what(node, name)})`);
52
+ if (op.op === "add") node.splice(k, 0, structuredClone(op.value));
53
+ else if (op.op === "remove") node.splice(k, 1);
54
+ else if (op.op === "replace") node[k] = structuredClone(op.value);
55
+ else if (!same(node[k], op.value)) bad(`${tag}: value is ${JSON.stringify(node[k])?.slice(0, 60)}, not ${JSON.stringify(op.value)?.slice(0, 60)}`);
56
+ return doc;
57
+ }
58
+ if (op.op === "replace" && notes && !Object.hasOwn(node, last)) notes.push(`${tag}: ${op.path} did not exist (${what(node, name)}); added`);
59
+ else if (op.op !== "add" && !Object.hasOwn(node, last)) bad(`${tag}: ${op.path} does not exist (${what(node, name)})`);
60
+ if (op.op === "remove") delete node[last];
61
+ else if (op.op === "test") { if (!same(node[last], op.value)) bad(`${tag}: value is ${JSON.stringify(node[last])?.slice(0, 60)}, not ${JSON.stringify(op.value)?.slice(0, 60)}`); }
62
+ else node[last] = structuredClone(op.value);
63
+ return doc;
64
+ }
65
+
66
+ /**
67
+ * Applies ops in order to a copy of doc (doc is never changed) and returns the copy; the first
68
+ * failing op throws MOTION_SPEC_INVALID and nothing is applied. notes (optional) turns a replace on a
69
+ * missing object member into an add and collects a line for it.
70
+ * @param {object} doc
71
+ * @param {{ op: "add" | "remove" | "replace" | "test", path: string, value?: any }[]} ops
72
+ * @param {string[]} [notes]
73
+ */
74
+ export function applyPatch(doc, ops, notes) {
75
+ if (!Array.isArray(ops)) bad("patch must be an array of { op, path, value }");
76
+ return ops.reduce((d, op, i) => applyOne(d, op, i, notes), structuredClone(doc));
77
+ }
@@ -0,0 +1,105 @@
1
+ // C16. Aim keys: a held-object key (C14) may give aim [x, y, z], the world direction the object's
2
+ // +Y points (a bat: knob to barrel), with an optional roll (degrees about that direction,
3
+ // right-handed), in place of the quaternion rot. An aim key turns the object the shortest way
4
+ // from where the plain carry (no keys) holds it at that key's frame onto aim, then by roll.
5
+ // (Turning from the key before instead piles the roll up over a swing: the bat fixture's keys as
6
+ // aims, so chained, missed by 45.5 cm.)
7
+ // Without roll (lane K8) the key takes the roll the arm holds most easily: ROLLS candidate rolls
8
+ // about aim are scored by the strain of the arm that holds them (compiler/hold.js holdCost), a
9
+ // pass over the keys in order picks one per key with a light price on how far the object turns
10
+ // from the key before (so equally easy rolls do not alternate), and a golden-section search
11
+ // settles each pick within its step. roll given (0 included) and rot keys are used exactly.
12
+ // The quaternion is rounded to 1e-5 (the grid motion.keyObject writes) and kept on the hemisphere
13
+ // of the key before it.
14
+ // lean (degrees, lane elbow) lets the compiler lean aim itself up to that far: a cup brought to the mouth
15
+ // is tipped toward the face AND a little toward the hand's side, which lets the elbow stay down (a cup at
16
+ // the mouth aimed only at the face can be held with a comfortable wrist only with the elbow over the
17
+ // shoulder). The aim, and AZIMUTHS directions leaned lean/2 and lean, each at every roll (or at the
18
+ // given roll), are scored by the arm's strain plus the shoulder's price (hold.js holdCost opts.shoulder);
19
+ // the chain and the roll refinement run as above on the picked direction.
20
+ import { qmul, qrot, qdot, qnorm, arcQuat, axisQuat } from "./compiler/quat.js";
21
+ import { add, cross, norm, scale, sub } from "./compiler/math.js";
22
+
23
+ const ROLLS = 24, STEP = 360 / ROLLS, TURN = 0.2, GOLDEN = 12, AZIMUTHS = 8;
24
+ const round5 = v => Math.round(v * 1e5) / 1e5;
25
+ /** The turn between two unit quaternions, radians. */
26
+ const turn = (a, b) => 2 * Math.acos(Math.min(1, Math.abs(qdot(a, b))));
27
+
28
+ /**
29
+ * Where a leaning key puts the object: a lean pivots about the object's top (its +Y end, `top` m above its
30
+ * base pivot), so a cup's rim stays where aim put it (at the lips) and its foot swings; any other key's pos.
31
+ */
32
+ export const leanPos = (key, q, top) => (key.lean > 0 && key.aim && top > 0 ? add(key.pos, scale(sub(norm(key.aim), qrot(q, [0, 1, 0])), top)) : key.pos);
33
+
34
+ /** aim d and the directions leaned lean/2 and lean deg from it toward AZIMUTHS sides. */
35
+ function leanedAims(d, lean) {
36
+ const side = norm(cross(d, Math.abs(d[1]) < 0.9 ? [0, 1, 0] : [1, 0, 0])), up = cross(d, side), out = [d];
37
+ for (const deg of [lean / 2, lean]) for (let j = 0; j < AZIMUTHS; j++) {
38
+ const a = (2 * Math.PI * j) / AZIMUTHS, t = (Math.min(deg, 45) * Math.PI) / 180, w = add(scale(side, Math.cos(a)), scale(up, Math.sin(a)));
39
+ out.push(norm(add(scale(d, Math.cos(t)), scale(w, Math.sin(t)))));
40
+ }
41
+ return out;
42
+ }
43
+
44
+ /** The pick per key: DP over candidates [{ q, cost }] per key, a TURN price on the squared turn between neighbours. */
45
+ function chain(cands) {
46
+ let best = cands[0].map(c => ({ total: c.cost, from: -1 }));
47
+ const back = [best];
48
+ for (let i = 1; i < cands.length; i++) {
49
+ best = cands[i].map(c => {
50
+ let pick = { total: Infinity, from: -1 };
51
+ cands[i - 1].forEach((p, j) => { const t = back[i - 1][j].total + TURN * turn(p.q, c.q) ** 2; if (t < pick.total) pick = { total: t, from: j }; });
52
+ return { total: pick.total + c.cost, from: pick.from };
53
+ });
54
+ back.push(best);
55
+ }
56
+ const picks = [];
57
+ for (let i = cands.length - 1, j = best.reduce((m, b, k) => (b.total < best[m].total ? k : m), 0); i >= 0; j = back[i][j].from, i--) picks[i] = j;
58
+ return picks;
59
+ }
60
+
61
+ /** Golden-section search of f on [lo, hi]. */
62
+ function golden(f, lo, hi) {
63
+ const g = (Math.sqrt(5) - 1) / 2;
64
+ let a = lo, b = hi, c = b - g * (b - a), d = a + g * (b - a), fc = f(c), fd = f(d);
65
+ for (let i = 0; i < GOLDEN; i++) {
66
+ if (fc < fd) { b = d; d = c; fd = fc; c = b - g * (b - a); fc = f(c); } else { a = c; c = d; fc = fd; d = a + g * (b - a); fd = f(d); }
67
+ }
68
+ return (a + b) / 2;
69
+ }
70
+
71
+ /**
72
+ * The rot of each key (keys sorted by f): a rot key's own, an aim key's resolved quaternion
73
+ * [x, y, z, w]. plain(i) is the rotation (a quaternion) the plain carry gives the object at key
74
+ * i's frame; it is asked for aim keys only. strain(i, q), when given, is the arm's cost of
75
+ * holding key i at rotation q; it picks the roll of aim keys that give none.
76
+ */
77
+ export function keyRots(keys, plain, strain) {
78
+ const at = keys.map((k, i) => {
79
+ if (k.aim === undefined) return { fixed: qnorm(k.rot) };
80
+ const from = qnorm(plain(i)), aimed = d => { const q0 = qmul(arcQuat(norm(qrot(from, [0, 1, 0])), d), from); return r => qmul(axisQuat(d, r), q0); };
81
+ if (k.lean > 0 && strain) return { leans: leanedAims(norm(k.aim), k.lean).map(aimed), roll: k.roll };
82
+ const rolled = aimed(norm(k.aim));
83
+ return k.roll !== undefined || !strain ? { fixed: k.roll ? rolled(k.roll) : rolled(0) } : { rolled };
84
+ });
85
+ const rollsOf = a => (a.roll !== undefined ? [a.roll] : Array.from({ length: ROLLS }, (_, j) => j * STEP - 180));
86
+ const cands = at.map((a, i) => (a.fixed ? [{ q: a.fixed, cost: 0, r: 0 }]
87
+ : a.leans ? a.leans.flatMap(rolled => rollsOf(a).map(r => { const q = rolled(r); return { q, cost: strain(i, q), r, rolled, fixedRoll: a.roll !== undefined }; }))
88
+ : Array.from({ length: ROLLS }, (_, j) => { const r = j * STEP - 180, q = a.rolled(r); return { q, cost: strain(i, q), r }; })));
89
+ const picks = chain(cands), chosen = cands.map((c, i) => c[picks[i]]);
90
+ // a leaning key keeps the direction it picked; its roll is refined below like any other (unless given)
91
+ for (const [i, a] of at.entries()) if (a.leans) at[i] = chosen[i].fixedRoll ? { fixed: chosen[i].q } : { rolled: chosen[i].rolled };
92
+ const rots = [];
93
+ let prev = null;
94
+ for (const [i, a] of at.entries()) {
95
+ if (keys[i].aim === undefined) { rots.push(keys[i].rot); prev = a.fixed; continue; }
96
+ let q = (a.fixed ?? a.rolled(golden(r => {
97
+ const qr = a.rolled(r), near = [chosen[i - 1], chosen[i + 1]].filter(Boolean);
98
+ return strain(i, qr) + near.reduce((sum, n) => sum + TURN * turn(n.q, qr) ** 2, 0);
99
+ }, chosen[i].r - STEP / 2, chosen[i].r + STEP / 2))).map(round5);
100
+ if (prev && qdot(q, prev) < 0) q = q.map(v => -v);
101
+ rots.push(q);
102
+ prev = qnorm(q);
103
+ }
104
+ return rots;
105
+ }
@@ -0,0 +1,82 @@
1
+ // motion.keyObject (C14): one key of an object the take moves, upserted into (or removed from) a
2
+ // copy of the take's spec. The caller compiles and installs the copy.
3
+ // carry objects.<id>.keys[{ f, pos, rot }] or [{ f, pos, aim, roll? }] (C16): the world pose of its
4
+ // base (a group: its root), only while it is in hand (grab..release); kept sorted by f; the
5
+ // last one removed drops `keys`
6
+ // hinge spec.keys[{ f, set: { <channel>: degrees } }]: `angle`, or the turn of `rot` (the drawn
7
+ // world rotation) about the hinge axis from the scene pose; a key at f is reused
8
+ // An object a skill moves (exitCar's door hinge) is keyed the same way: a hinge through its channel,
9
+ // a carry by copying the skill's entry into spec.objects with the keys (the spec's own entry wins).
10
+ // Frames are the spec's (absolute). Errors carry the MOTION_SPEC_INVALID: prefix (INVALID_ARGUMENT).
11
+ import { isCarried, isFollow, movesRecord } from "./carried.js";
12
+ import { groupsOf } from "./scene.js";
13
+ import { mul, T, norm, quatFromMat, matFromQuat } from "./compiler/math.js";
14
+
15
+ const bad = msg => { throw new Error(`MOTION_SPEC_INVALID: ${msg}`); };
16
+ const round = (v, d) => Math.round(v * d) / d;
17
+ const framesOf = list => (list.length ? list.map(f => `f${f}`).join(", ") : "none");
18
+
19
+ /** The spec.objects key that moves scene body `id` (itself or its nearest group; a switch moves nothing), or null. */
20
+ function moverOf(spec, scene, id) {
21
+ return [id, ...(scene?.objects?.[id]?.ancestors ?? [])].find(k => movesRecord(spec.objects?.[k])) ?? null;
22
+ }
23
+
24
+ /** Degrees the world rotation `rot` turns about `axis` from the rest rotation (the twist, -180..180). */
25
+ function hingeAngle(rot, rest, axis) {
26
+ const q = quatFromMat(mul(matFromQuat(rot), T(rest.R))), a = norm(axis), sg = q[3] < 0 ? -1 : 1;
27
+ const deg = 2 * Math.atan2(sg * (q[0] * a[0] + q[1] * a[1] + q[2] * a[2]), sg * q[3]) * 180 / Math.PI;
28
+ return round(deg > 180 ? deg - 360 : deg, 100);
29
+ }
30
+
31
+ function carryKey(spec, id, o, { frame, pos, rot, aim, roll, remove }) {
32
+ const lo = o.grab ?? 0, hi = o.release ?? spec.frames - 1, keys = [...(o.keys ?? [])].filter(k => k.f !== frame);
33
+ if (frame < lo || frame > hi) bad(`"${id}" is in hand only f${lo}..f${hi} (grab..release); at f${frame} the take does not hold it, so drag it as a scene object, or change objects.${id}.grab/release first`);
34
+ if (remove) {
35
+ if (keys.length === (o.keys ?? []).length) bad(`"${id}" has no key at f${frame}; its keys: ${framesOf((o.keys ?? []).map(k => k.f))}`);
36
+ } else {
37
+ if (!Array.isArray(pos) || Array.isArray(rot) === Array.isArray(aim)) {
38
+ bad(`keying "${id}" (carried) needs pos [x, y, z] and rot [x, y, z, w] or aim [x, y, z] (+ roll), one of the two: the world pose of its base (a group: its root)`);
39
+ }
40
+ const turn = rot ? { rot: rot.map(v => round(v, 1e5)) } : { aim: aim.map(v => round(v, 1e5)), ...(Number.isFinite(roll) && roll ? { roll: round(roll, 100) } : {}) };
41
+ keys.push({ f: frame, pos: pos.map(v => round(v, 1e4)), ...turn });
42
+ }
43
+ const { keys: _old, ...rest } = o;
44
+ spec.objects[id] = keys.length ? { ...rest, keys: keys.sort((a, b) => a.f - b.f) } : rest;
45
+ return { kind: "carry", f: frame, ...(remove ? { removed: true } : {}), keys: keys.map(k => k.f) };
46
+ }
47
+
48
+ function hingeKey(spec, scene, id, o, { frame, rot, angle, remove }) {
49
+ const ch = o.channel, i = (spec.keys ?? []).findIndex(k => k?.f === frame && !k.src && k.set && ch in k.set);
50
+ if (remove) {
51
+ if (i < 0) bad(`"${id}" (hinge, channel "${ch}") has no key at f${frame}; ${ch} is keyed at ${framesOf((spec.keys ?? []).filter(k => k?.set && ch in k.set).map(k => k.f))}`);
52
+ const { [ch]: _gone, ...set } = spec.keys[i].set;
53
+ spec.keys = Object.keys(set).length ? spec.keys.map((k, j) => (j === i ? { ...k, set, ...(k.step ? { step: k.step.filter(c => c !== ch) } : {}) } : k)) : spec.keys.filter((_, j) => j !== i);
54
+ return { kind: "hinge", channel: ch, f: frame, removed: true };
55
+ }
56
+ const rest = scene?.objects?.[id] ?? groupsOf(scene)[id];
57
+ const value = Number.isFinite(angle) ? round(angle, 100) : Array.isArray(rot) && rest ? hingeAngle(rot, rest, o.axis ?? [0, 1, 0])
58
+ : bad(`keying "${id}" (a hinge on channel "${ch}") needs angle (degrees) or rot [x, y, z, w] (its drawn world rotation; the turn about the hinge axis is kept)`);
59
+ const at = (spec.keys ?? []).findIndex(k => k?.f === frame && !k.src);
60
+ spec.keys = at < 0 ? [...(spec.keys ?? []), { f: frame, set: { [ch]: value } }] : spec.keys.map((k, j) => (j === at ? { ...k, set: { ...k.set, [ch]: value } } : k));
61
+ return { kind: "hinge", channel: ch, f: frame, angle: value };
62
+ }
63
+
64
+ /**
65
+ * keyObject(spec, scene, { objectId, frame, pos?, rot?, aim?, roll?, angle?, remove? }, skillObjects?) -> { spec, keyed }:
66
+ * a copy of the spec with the key written, and what was written ({ objectId, kind, f, ... }).
67
+ * skillObjects are the spec.objects entries its skills add (skills/index.js expandUse).
68
+ */
69
+ export function keyObject(stored, scene, args, skillObjects = {}) {
70
+ const spec = structuredClone(stored), { objectId: id, frame } = args, moves = x => (movesRecord(x) ? x : undefined);
71
+ const o = moves(spec.objects?.[id]) ?? structuredClone(moves(skillObjects[id]));
72
+ if (!o) {
73
+ const mover = moverOf({ objects: { ...skillObjects, ...spec.objects } }, scene, id);
74
+ bad(mover ? `"${id}" moves with "${mover}" (spec.objects.${mover}); key "${mover}" with its root pose`
75
+ : `"${id}" is not in this take's spec.objects (${Object.keys({ ...skillObjects, ...spec.objects }).join(", ") || "none"}): the take neither carries nor hinges it, so drag it as a scene object`);
76
+ }
77
+ if (!(Number.isInteger(frame) && frame >= 0 && frame < spec.frames)) bad(`frame ${frame} is outside the take (0..${spec.frames - 1})`);
78
+ if (isFollow(o)) bad(`"${id}" follows ${o.follow} (spec.objects.${id}); it has no keys: change its follow, offset or aim`);
79
+ if (isCarried(o)) spec.objects = { ...spec.objects, [id]: o };
80
+ const keyed = isCarried(o) ? carryKey(spec, id, o, args) : hingeKey(spec, scene, id, o, args);
81
+ return { spec, keyed: { objectId: id, ...keyed } };
82
+ }