cozyclay 2.1.1 → 3.0.1

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 (585) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +12 -30
  3. package/bin/agent/agent-routes.mjs +43 -116
  4. package/bin/agent/agent-runner.mjs +14 -15
  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/session-store.mjs +2 -2
  14. package/bin/agent/studio-context-budget.mjs +93 -0
  15. package/bin/agent/studio-motion-reference.mjs +23 -0
  16. package/bin/agent/studio-prompt.mjs +1 -1
  17. package/bin/agent/studio-rebase-text.mjs +32 -0
  18. package/bin/agent/studio-rebase.mjs +26 -39
  19. package/bin/agent/studio-tools.mjs +9 -28
  20. package/bin/agent/tool-arguments.mjs +49 -0
  21. package/bin/agent/turn-frames.mjs +66 -0
  22. package/bin/codex-auth.mjs +9 -2
  23. package/bin/cozyclay.mjs +10 -116
  24. package/bin/live/cli.mjs +3 -3
  25. package/bin/mcp-runtime.mjs +2 -12
  26. package/bin/package-signature.mjs +0 -4
  27. package/dist/app/index.html +7 -6
  28. package/dist/assets/EXRLoader-CvVIvGVE.js +1 -0
  29. package/dist/assets/analytics-BWCqdgWA.js +1 -0
  30. package/dist/assets/app-DfTCxaKk.js +5087 -0
  31. package/dist/assets/app-f-fkV5uP.css +1 -0
  32. package/dist/assets/compile-worker-BxKXh07s.js +5 -0
  33. package/dist/assets/fflate.module-KbGj5-H6.js +5 -0
  34. package/dist/assets/{first-shot-handoff-msLI71g1.js → first-shot-handoff-DNO8kv_N.js} +1 -1
  35. package/dist/assets/{landing-ChUvB_zL.js → landing-yfZaJkWc.js} +1 -1
  36. package/dist/assets/scene-asset-sync-U3kS2rvK.js +20 -0
  37. package/dist/assets/scene-asset-sync-_S8NQl5B.css +1 -0
  38. package/dist/assets/workflow-D78eiKTa.js +179 -0
  39. package/dist/cozyclay-package.json +1 -1
  40. package/dist/index.html +3 -3
  41. package/dist/sw.js +3 -6
  42. package/dist/workflow/index.html +5 -4
  43. package/mcp/LIVE-PROTOCOL.md +7 -47
  44. package/mcp/README.md +16 -26
  45. package/mcp/live-hub.mjs +6 -10
  46. package/mcp/package.json +1 -3
  47. package/mcp/tool-handlers.mjs +4 -168
  48. package/package.json +3 -22
  49. package/src/App.jsx +151 -2625
  50. package/src/agent-progress.js +80 -0
  51. package/src/agent-turn-mark.js +30 -0
  52. package/src/analytics.js +1 -19
  53. package/src/app-context.js +12 -2
  54. package/src/app-stage.jsx +2 -34
  55. package/src/asset-pane.jsx +6 -5
  56. package/src/camera-block.js +21 -0
  57. package/src/camera-direction.js +120 -0
  58. package/src/character-kind.js +3 -9
  59. package/src/command-bus.js +10 -8
  60. package/src/commands/cast.js +5 -34
  61. package/src/commands/elements/character.js +1 -1
  62. package/src/commands/elements/motion.js +1 -6
  63. package/src/commands/elements/object.js +6 -2
  64. package/src/commands/elements/shot.js +4 -1
  65. package/src/commands/elements.js +38 -14
  66. package/src/commands/index.js +3 -1
  67. package/src/commands/kits.js +37 -0
  68. package/src/commands/motion-timing.js +57 -0
  69. package/src/commands/motion.js +9 -152
  70. package/src/commands/motion3-compile.js +65 -0
  71. package/src/commands/motion3.js +214 -0
  72. package/src/commands/objects.js +6 -4
  73. package/src/commands/shot.js +55 -13
  74. package/src/commands/stage.js +14 -1
  75. package/src/commands/view.js +4 -1
  76. package/src/document-mirrors.js +42 -0
  77. package/src/document-store.js +4 -0
  78. package/src/domains/cast.js +19 -194
  79. package/src/domains/motion/motion-range-pin-rules.js +1 -1
  80. package/src/domains/motion/take-source.js +53 -0
  81. package/src/domains/motion.js +60 -1838
  82. package/src/domains/scenes.js +12 -55
  83. package/src/domains/shots.js +2 -2
  84. package/src/domains/stage.js +4 -2
  85. package/src/draft-ghost.js +47 -0
  86. package/src/draft-ghost.jsx +129 -0
  87. package/src/dualview.jsx +4 -3
  88. package/src/effects/EffectsLayer.jsx +112 -0
  89. package/src/effects/effect-meshes.js +232 -0
  90. package/src/effects/effect-record.js +103 -0
  91. package/src/effects/emitter.js +59 -0
  92. package/src/effects/simulate.js +261 -0
  93. package/src/effects/state.js +59 -0
  94. package/src/execution-telemetry.js +2 -3
  95. package/src/first-success-guide.jsx +5 -5
  96. package/src/ghost-set.js +257 -0
  97. package/src/held-object-edit.jsx +113 -0
  98. package/src/held-object-pose.js +175 -0
  99. package/src/hierarchy-model.js +1 -1
  100. package/src/hierarchy-panel.jsx +1 -1
  101. package/src/imported-mesh.jsx +103 -0
  102. package/src/kits/held.js +84 -0
  103. package/src/kits/index.js +70 -0
  104. package/src/kits/smoking.js +57 -0
  105. package/src/light-blockers.js +93 -0
  106. package/src/motion/beat-board-model.js +84 -0
  107. package/src/motion/beat-board.css +124 -0
  108. package/src/motion/beat-board.jsx +60 -0
  109. package/src/motion/center-of-mass.js +66 -0
  110. package/src/motion/fix-collisions.js +850 -0
  111. package/src/motion/physics-review.js +118 -0
  112. package/src/{ardy → motion}/physics-support.js +1 -76
  113. package/src/motion/rig-snapshot.js +22 -0
  114. package/src/motion/take-segments.jsx +140 -0
  115. package/src/{ardy → motion}/timeline-coordinates.js +0 -21
  116. package/src/{ardy → motion}/timeline.css +1 -2
  117. package/src/{ardy → motion}/timeline.jsx +42 -413
  118. package/src/motion/waypoints.js +76 -0
  119. package/src/motion3/beats.js +83 -0
  120. package/src/motion3/body.js +158 -0
  121. package/src/motion3/bones.js +47 -0
  122. package/src/motion3/carried.js +47 -0
  123. package/src/motion3/compile-job.js +79 -0
  124. package/src/motion3/compile-worker.js +10 -0
  125. package/src/motion3/compiler/anchors.js +113 -0
  126. package/src/motion3/compiler/arm-orient.js +128 -0
  127. package/src/motion3/compiler/channels.js +125 -0
  128. package/src/motion3/compiler/compile.js +205 -0
  129. package/src/motion3/compiler/gait-rhythm.js +97 -0
  130. package/src/motion3/compiler/grip.js +171 -0
  131. package/src/motion3/compiler/head-steady.js +48 -0
  132. package/src/motion3/compiler/hold.js +134 -0
  133. package/src/motion3/compiler/idle.js +113 -0
  134. package/src/motion3/compiler/limbs.js +197 -0
  135. package/src/motion3/compiler/math.js +72 -0
  136. package/src/motion3/compiler/pose.js +177 -0
  137. package/src/motion3/compiler/quat.js +49 -0
  138. package/src/motion3/compiler/skeleton.js +74 -0
  139. package/src/motion3/compiler/validate-common.js +32 -0
  140. package/src/motion3/compiler/validate-goals.js +118 -0
  141. package/src/motion3/compiler/validate-objects.js +144 -0
  142. package/src/motion3/compiler/validate-parts.js +118 -0
  143. package/src/motion3/compiler/validate.js +122 -0
  144. package/src/motion3/compiler/vocab.js +74 -0
  145. package/src/motion3/compiler/walk-pelvis.js +275 -0
  146. package/src/motion3/compiler/walk.js +207 -0
  147. package/src/motion3/compiler/wrist-clamp.js +32 -0
  148. package/src/motion3/delta.js +47 -0
  149. package/src/motion3/describe.js +177 -0
  150. package/src/motion3/docs.js +27 -0
  151. package/src/motion3/edit-carry.js +21 -0
  152. package/src/motion3/fix/ease.js +16 -0
  153. package/src/motion3/fix/ground.js +37 -0
  154. package/src/motion3/fix/index.js +52 -0
  155. package/src/motion3/fix/push.js +71 -0
  156. package/src/motion3/fix/reach.js +46 -0
  157. package/src/motion3/fix/retime.js +79 -0
  158. package/src/motion3/fix/self.js +211 -0
  159. package/src/motion3/fix/slide.js +70 -0
  160. package/src/motion3/fix/turn.js +51 -0
  161. package/src/motion3/follow.js +39 -0
  162. package/src/motion3/free-motion.js +204 -0
  163. package/src/motion3/grip-choice.js +130 -0
  164. package/src/motion3/hold-plan.js +157 -0
  165. package/src/motion3/json-patch.js +77 -0
  166. package/src/motion3/key-aim.js +105 -0
  167. package/src/motion3/key-edit.js +82 -0
  168. package/src/motion3/lenient.js +127 -0
  169. package/src/motion3/object-keys.js +236 -0
  170. package/src/motion3/object-pose.js +182 -0
  171. package/src/motion3/object-tracks.js +12 -0
  172. package/src/motion3/people.js +82 -0
  173. package/src/motion3/pipeline.js +102 -0
  174. package/src/motion3/playback.js +51 -0
  175. package/src/motion3/probes.js +91 -0
  176. package/src/motion3/qa/body-checks.js +34 -0
  177. package/src/motion3/qa/check-names.js +14 -0
  178. package/src/motion3/qa/checks.js +125 -0
  179. package/src/motion3/qa/contact-checks.js +51 -0
  180. package/src/motion3/qa/explain.js +80 -0
  181. package/src/motion3/qa/fmt.js +5 -0
  182. package/src/motion3/qa/foot-checks.js +72 -0
  183. package/src/motion3/qa/geometry.js +77 -0
  184. package/src/motion3/qa/goals.js +191 -0
  185. package/src/motion3/qa/ground.js +60 -0
  186. package/src/motion3/qa/hints-contact.js +195 -0
  187. package/src/motion3/qa/hints-goal.js +160 -0
  188. package/src/motion3/qa/hints-knee.js +24 -0
  189. package/src/motion3/qa/hints-motion.js +209 -0
  190. package/src/motion3/qa/hints-object.js +91 -0
  191. package/src/motion3/qa/index.js +44 -0
  192. package/src/motion3/qa/key-checks.js +82 -0
  193. package/src/motion3/qa/near.js +96 -0
  194. package/src/motion3/qa/object-checks.js +135 -0
  195. package/src/motion3/qa/patch.js +161 -0
  196. package/src/motion3/qa/people-checks.js +37 -0
  197. package/src/motion3/qa/severity.js +38 -0
  198. package/src/motion3/qa/spec-checks.js +65 -0
  199. package/src/motion3/qa/style.js +228 -0
  200. package/src/motion3/qa/wrist-checks.js +69 -0
  201. package/src/motion3/qa/wrist-obs.js +60 -0
  202. package/src/motion3/refs.js +129 -0
  203. package/src/motion3/repair-tune.js +121 -0
  204. package/src/motion3/repair.js +83 -0
  205. package/src/motion3/report.js +67 -0
  206. package/src/motion3/rigs/index.js +14 -0
  207. package/src/motion3/rigs/x-bot-tpose-body.json +1 -0
  208. package/src/motion3/rigs/x-bot-tpose.json +1 -0
  209. package/src/motion3/rigs/y-bot-tpose-body.json +1 -0
  210. package/src/motion3/rigs/y-bot-tpose.json +1 -0
  211. package/src/motion3/scene.js +60 -0
  212. package/src/motion3/schema/brief.js +89 -0
  213. package/src/motion3/schema/fields.js +44 -0
  214. package/src/motion3/schema/spec-examples.js +46 -0
  215. package/src/motion3/schema/topics.js +113 -0
  216. package/src/motion3/schema/types.js +149 -0
  217. package/src/motion3/search.js +117 -0
  218. package/src/motion3/second-hand.js +112 -0
  219. package/src/motion3/settle.js +136 -0
  220. package/src/motion3/skills/basic.js +83 -0
  221. package/src/motion3/skills/book.js +76 -0
  222. package/src/motion3/skills/car.js +145 -0
  223. package/src/motion3/skills/flip.js +200 -0
  224. package/src/motion3/skills/frames-auto.js +29 -0
  225. package/src/motion3/skills/impact.js +109 -0
  226. package/src/motion3/skills/index.js +85 -0
  227. package/src/motion3/skills/info.js +80 -0
  228. package/src/motion3/skills/smoke-kit.js +109 -0
  229. package/src/motion3/skills/smoke.js +160 -0
  230. package/src/motion3/spec-store.js +59 -0
  231. package/src/motion3/take-edit.js +155 -0
  232. package/src/motion3/take.js +63 -0
  233. package/src/motion3/timeline.js +127 -0
  234. package/src/motion3/views.js +121 -0
  235. package/src/motion3/walk-plan.js +119 -0
  236. package/src/object-catalog.jsx +7 -1
  237. package/src/object-colors.js +98 -0
  238. package/src/object-gizmo-parts.js +115 -0
  239. package/src/object-gizmo.jsx +37 -122
  240. package/src/object-glow.js +139 -0
  241. package/src/object-hidden.js +33 -0
  242. package/src/object-look.js +196 -0
  243. package/src/object-size.js +10 -0
  244. package/src/object-travel.js +71 -6
  245. package/src/panels/CameraPanel.jsx +7 -7
  246. package/src/panels/HdrEnvironmentControls.jsx +59 -0
  247. package/src/panels/LightPanel.jsx +25 -4
  248. package/src/panels/ObjectLookPanel.jsx +88 -0
  249. package/src/panels/PosePanel.jsx +2 -17
  250. package/src/panels/RigControlPanel.jsx +5 -53
  251. package/src/panels/SubjectBox.jsx +4 -1
  252. package/src/panels/details.css +14 -0
  253. package/src/posestudio.jsx +4 -27
  254. package/src/project-browser.jsx +3 -4
  255. package/src/project-resources.js +18 -31
  256. package/src/project.js +13 -104
  257. package/src/props.jsx +25 -280
  258. package/src/qa-label.js +25 -0
  259. package/src/rail-dot-drag.js +23 -0
  260. package/src/render-passes.js +10 -0
  261. package/src/resource-status.jsx +1 -0
  262. package/src/result-modal.jsx +4 -19
  263. package/src/room.jsx +20 -12
  264. package/src/scene-assets.js +8 -6
  265. package/src/scene-light-place.js +72 -0
  266. package/src/scene-lights.jsx +148 -0
  267. package/src/scene-objects.js +31 -139
  268. package/src/scenes.js +15 -105
  269. package/src/semantic-edit.js +2 -5
  270. package/src/set-pieces.jsx +183 -0
  271. package/src/settings-menu.jsx +3 -11
  272. package/src/shell/BottomDock.jsx +9 -51
  273. package/src/shell/DetailsSlot.jsx +16 -200
  274. package/src/shell/GhostsMenuGroup.jsx +74 -0
  275. package/src/shell/LibrarySlot.jsx +1 -3
  276. package/src/shell/MenuBar.jsx +3 -3
  277. package/src/shell/PreferencesDialog.jsx +15 -264
  278. package/src/shell/PreferencesSlot.jsx +2 -8
  279. package/src/shell/TopBar.jsx +5 -84
  280. package/src/shell/ViewportToolbar.jsx +10 -70
  281. package/src/shell/agent-glass.css +19 -6
  282. package/src/shell/details-selection.js +42 -0
  283. package/src/shell/ghosts-menu.css +22 -0
  284. package/src/shell/glass-regions.css +29 -3
  285. package/src/shell/preferences.css +6 -1
  286. package/src/shot-authoring.js +4 -7
  287. package/src/soft-shadows.js +37 -0
  288. package/src/stage-attach-frames.js +12 -12
  289. package/src/stage-canvas-rigs.jsx +3 -3
  290. package/src/stage-capture.jsx +42 -8
  291. package/src/stage-characters.jsx +22 -216
  292. package/src/stage-editor-gizmos.jsx +6 -6
  293. package/src/stage-environment.js +93 -0
  294. package/src/stage-hdr-builtins.js +107 -0
  295. package/src/stage-hdr-image.js +116 -0
  296. package/src/stage-hdr-library.js +70 -0
  297. package/src/stage-hdr-map.js +94 -0
  298. package/src/stage-hdr.js +130 -0
  299. package/src/stage-lighting.js +149 -0
  300. package/src/stage-occlusion.js +210 -0
  301. package/src/stage-sky.jsx +159 -0
  302. package/src/stage-startup-defaults.js +5 -9
  303. package/src/studio-actions.js +16 -14
  304. package/src/studio-agent-commands.js +19 -16
  305. package/src/studio-agent-motion.js +9 -9
  306. package/src/studio-agent-protocol.js +47 -77
  307. package/src/studio-app-binding.js +27 -47
  308. package/src/studio-asset-list.js +17 -0
  309. package/src/studio-contact-sheet.js +37 -5
  310. package/src/studio-elements.js +27 -7
  311. package/src/studio-entity-bounds.js +18 -0
  312. package/src/studio-error-codes.js +15 -0
  313. package/src/studio-export-framing.js +152 -0
  314. package/src/studio-lighting-check.js +103 -0
  315. package/src/studio-motion-visual.js +150 -0
  316. package/src/studio-schema-errors.js +80 -0
  317. package/src/studio-support-gap.js +74 -0
  318. package/src/styles.css +1 -19
  319. package/src/timeline-defaults.js +5 -0
  320. package/src/timeline-extent.js +8 -20
  321. package/src/tutorial-walk.js +17 -0
  322. package/src/ui-scrub.js +1 -1
  323. package/src/use-shot-camera-tracks.js +12 -0
  324. package/src/view-ghosts.js +97 -0
  325. package/src/view-ghosts.jsx +49 -0
  326. package/src/workflow/AgentPanel.jsx +47 -166
  327. package/src/workflow/AgentProgressCard.jsx +109 -0
  328. package/src/workflow/AgentReplyText.jsx +37 -0
  329. package/src/workflow/AgentTurnSteps.jsx +84 -0
  330. package/src/workflow/LoginPanel.jsx +140 -0
  331. package/src/workflow/WorkflowBuilder.jsx +7 -23
  332. package/src/workflow/agent-client.js +54 -51
  333. package/src/workflow/agent-panel.css +71 -1
  334. package/src/workflow/agent-progress-card.css +167 -0
  335. package/src/workflow/agent-progress-view.js +47 -0
  336. package/src/workflow/agent-turn-steps.css +113 -0
  337. package/src/workflow/agent-turn.js +191 -0
  338. package/src/workflow/login-client.js +34 -0
  339. package/src/workflow/motion-input.js +18 -31
  340. package/src/workflow/node-schema.js +1 -1
  341. package/src/workflow/reply-markdown.js +69 -0
  342. package/src/workflow/scene-asset-sync.js +8 -8
  343. package/tools/agent-session-stats.mjs +61 -0
  344. package/tools/dev-full.mjs +5 -56
  345. package/tools/eslint-baseline.json +12 -13
  346. package/tools/knip-baseline.json +0 -179
  347. package/tools/loc-baseline.json +31 -81
  348. package/tools/motion3/extract-body.mjs +103 -0
  349. package/tools/motion3/extract-rig.mjs +85 -0
  350. package/tools/process-supervisor.mjs +0 -88
  351. package/tools/run-tests.mjs +9 -125
  352. package/bin/agent/motion-runtime.mjs +0 -76
  353. package/dist/ardy/cskel27-rest.json +0 -557
  354. package/dist/assets/analytics-C9pMpW7s.js +0 -1
  355. package/dist/assets/app-B_2XB9rw.css +0 -1
  356. package/dist/assets/app-DAuvKPP8.js +0 -4920
  357. package/dist/assets/config-Bx7pYsD9.js +0 -1
  358. package/dist/assets/config-DeoFTYzb.css +0 -1
  359. package/dist/assets/demo-CvKuBgy5.js +0 -1
  360. package/dist/assets/scene-asset-sync-DKkpjKBV.js +0 -19
  361. package/dist/assets/scene-asset-sync-DjJ_cJ2R.css +0 -1
  362. package/dist/assets/ticket-BPk2Ni95.js +0 -1
  363. package/dist/assets/workflow-jJRBR94Z.js +0 -179
  364. package/dist/demo/walk-then-stop.npz +0 -0
  365. package/mcp/ardy-prompts.mjs +0 -218
  366. package/src/ardy/auto-fix-panel.css +0 -108
  367. package/src/ardy/auto-physics.js +0 -1194
  368. package/src/ardy/client.js +0 -153
  369. package/src/ardy/convert.js +0 -313
  370. package/src/ardy/cskel27-neutral.js +0 -39
  371. package/src/ardy/cskel27.js +0 -77
  372. package/src/ardy/export.js +0 -165
  373. package/src/ardy/fix-collisions.js +0 -2257
  374. package/src/ardy/foot-lock.js +0 -230
  375. package/src/ardy/motion-calibration.js +0 -118
  376. package/src/ardy/motion-edit.js +0 -213
  377. package/src/ardy/motion-url.js +0 -55
  378. package/src/ardy/mp4-duration.js +0 -102
  379. package/src/ardy/npz.js +0 -595
  380. package/src/ardy/physics-panel.css +0 -21
  381. package/src/ardy/physics-panel.jsx +0 -89
  382. package/src/ardy/physics-refine.js +0 -111
  383. package/src/ardy/physics-review.js +0 -567
  384. package/src/ardy/physics-temporal.js +0 -54
  385. package/src/ardy/platform-fit-panel.css +0 -64
  386. package/src/ardy/platform-fit-panel.jsx +0 -41
  387. package/src/ardy/platform-fit.js +0 -326
  388. package/src/ardy/playback.js +0 -711
  389. package/src/ardy/pose-pin.js +0 -97
  390. package/src/ardy/prompt-clips.js +0 -30
  391. package/src/ardy/retime.js +0 -211
  392. package/src/ardy/root-drop.js +0 -369
  393. package/src/ardy/timeline-resize.js +0 -13
  394. package/src/ardy/to-cskel27.js +0 -225
  395. package/src/ardy/trim.js +0 -33
  396. package/src/ardy/waypoints.js +0 -369
  397. package/src/ardy-bridge-contract.js +0 -281
  398. package/src/domains/motion/motion-video-capture.js +0 -664
  399. package/src/i2v-motion-client.js +0 -77
  400. package/src/i2v-motion-studio.jsx +0 -180
  401. package/src/line-edit.js +0 -1348
  402. package/src/motion/generation.js +0 -105
  403. package/src/motion-readiness-ui.jsx +0 -67
  404. package/src/motion-readiness.js +0 -40
  405. package/src/motion-resources.js +0 -177
  406. package/src/motion-store.js +0 -65
  407. package/src/motion-trail.js +0 -601
  408. package/src/multimodel-ingest.js +0 -394
  409. package/src/panels/PromptBlocksPanel.jsx +0 -444
  410. package/src/panels/VideoCapturePanel.jsx +0 -181
  411. package/src/pose-extract/browser.js +0 -42
  412. package/src/pose-extract/detector.js +0 -100
  413. package/src/pose-extract/fit.js +0 -227
  414. package/src/pose-extract/image-frame.js +0 -51
  415. package/src/pose-extract/index.js +0 -58
  416. package/src/pose-extract/keyframes.js +0 -74
  417. package/src/pose-extract/landmarks.js +0 -162
  418. package/src/pose-extract/mirror.js +0 -153
  419. package/src/pose-extract/take.js +0 -131
  420. package/src/pose-extract/video-frames.js +0 -91
  421. package/src/take-recipe.js +0 -165
  422. package/src/trail-key-conflicts.js +0 -39
  423. package/src/trail-pick.js +0 -59
  424. package/src/use-i2v-capture-modal.js +0 -15
  425. package/tools/ardy/BRIDGE.md +0 -37
  426. package/tools/ardy/GVHMR-NPZ.md +0 -68
  427. package/tools/ardy/README.md +0 -124
  428. package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
  429. package/tools/ardy/artifacts.mjs +0 -34
  430. package/tools/ardy/bridge.mjs +0 -1732
  431. package/tools/ardy/bvh-cskel27.mjs +0 -1265
  432. package/tools/ardy/cclay_constrained_generate.py +0 -561
  433. package/tools/ardy/cclay_gvhmr_worker.py +0 -264
  434. package/tools/ardy/cclay_motion_edit.py +0 -510
  435. package/tools/ardy/cclay_sequence_generate.py +0 -705
  436. package/tools/ardy/cclay_worker.py +0 -304
  437. package/tools/ardy/dump-npz.py +0 -205
  438. package/tools/ardy/extract-rest.mjs +0 -299
  439. package/tools/ardy/extract.mjs +0 -507
  440. package/tools/ardy/footage.mjs +0 -467
  441. package/tools/ardy/gvhmr-floor.mjs +0 -102
  442. package/tools/ardy/gvhmr-to-cskel27.mjs +0 -21
  443. package/tools/ardy/gvhmr_fastpath.py +0 -168
  444. package/tools/ardy/gvhmr_trajectory.py +0 -240
  445. package/tools/ardy/merge-text-encoder.py +0 -160
  446. package/tools/ardy/mocap-metrics.mjs +0 -200
  447. package/tools/ardy/mocap-quality-gate.mjs +0 -110
  448. package/tools/ardy/motion-stabilize.mjs +0 -348
  449. package/tools/ardy/npz.mjs +0 -414
  450. package/tools/ardy/pose-to-npz.mjs +0 -106
  451. package/tools/ardy/prompt-limits.mjs +0 -5
  452. package/tools/ardy/run-edit-on-box.sh +0 -73
  453. package/tools/ardy/run-local.mjs +0 -233
  454. package/tools/ardy/run-on-box.sh +0 -593
  455. package/tools/ardy/run-sequence-on-box.sh +0 -177
  456. package/tools/ardy/runners/gvhmr-worker.mjs +0 -206
  457. package/tools/ardy/runners/index.mjs +0 -18
  458. package/tools/ardy/runners/local.mjs +0 -455
  459. package/tools/ardy/runners/proc.mjs +0 -125
  460. package/tools/ardy/runners/remote.mjs +0 -252
  461. package/tools/ardy/setup-local.mjs +0 -189
  462. package/tools/ardy/setup-text-encoder-on-box.sh +0 -93
  463. package/tools/ardy/setup-text-encoder.py +0 -306
  464. package/tools/ardy/smpl-cskel27.mjs +0 -178
  465. package/tools/ardy/visual-qa.mjs +0 -196
  466. package/tools/ardy/vq-car.mjs +0 -52
  467. package/tools/bench/EXP3.md +0 -61
  468. package/tools/bench/cclay_bench_extract_incam.py +0 -125
  469. package/tools/bench/cclay_bench_extract_obs.py +0 -204
  470. package/tools/bench/cclay_bench_runner.py +0 -42
  471. package/tools/bench/cube-contact.mjs +0 -162
  472. package/tools/bench/exp3.mjs +0 -152
  473. package/tools/bench/extract-bench-lib.mjs +0 -216
  474. package/tools/bench/extract-bench.mjs +0 -302
  475. package/tools/bench/fal-generate.mjs +0 -88
  476. package/tools/bench/fit/README.md +0 -189
  477. package/tools/bench/fit/camera.mjs +0 -59
  478. package/tools/bench/fit/contact.mjs +0 -419
  479. package/tools/bench/fit/footlock.mjs +0 -234
  480. package/tools/bench/fit/motion.mjs +0 -85
  481. package/tools/bench/fit/pin.mjs +0 -27
  482. package/tools/bench/fit/remote.mjs +0 -105
  483. package/tools/bench/fit-bench.mjs +0 -126
  484. package/tools/bench/fit-sanity.mjs +0 -45
  485. package/tools/bench/metrics.mjs +0 -313
  486. package/tools/bench/obs/depth.mjs +0 -258
  487. package/tools/bench/obs/extrinsics.mjs +0 -175
  488. package/tools/bench/obs/fit_mannequin_betas.py +0 -340
  489. package/tools/bench/obs/ground.mjs +0 -294
  490. package/tools/bench/obs/heading.mjs +0 -151
  491. package/tools/bench/obs/ladder.mjs +0 -555
  492. package/tools/bench/obs/mannequin-betas.json +0 -124
  493. package/tools/bench/obs/remote.mjs +0 -83
  494. package/tools/bench/obs/rest_joints.py +0 -52
  495. package/tools/bench/obs/ybot-targets.mjs +0 -49
  496. package/tools/bench/obs-bench.mjs +0 -520
  497. package/tools/bench/score.mjs +0 -343
  498. package/tools/bench/summarize.mjs +0 -86
  499. package/tools/demo-worker/README.md +0 -137
  500. package/tools/demo-worker/api-client.mjs +0 -233
  501. package/tools/demo-worker/generate.mjs +0 -301
  502. package/tools/demo-worker/index.mjs +0 -466
  503. package/tools/demo-worker/sign-core.mjs +0 -112
  504. package/tools/demo-worker/sign.mjs +0 -161
  505. package/tools/gt-render/browser.mjs +0 -182
  506. package/tools/gt-render/camera-math.mjs +0 -276
  507. package/tools/gt-render/page.mjs +0 -267
  508. package/tools/gt-render/render.mjs +0 -514
  509. package/tools/gt-render/scene-box.mjs +0 -31
  510. package/tools/gt-render/take-transform.mjs +0 -87
  511. package/tools/kimodo/compare-seam.mjs +0 -108
  512. package/tools/kimodo/constraints.mjs +0 -244
  513. package/tools/kimodo/edit.mjs +0 -259
  514. package/tools/kimodo/effector-constraints.mjs +0 -200
  515. package/tools/kimodo/generate.mjs +0 -646
  516. package/tools/kimodo/local-output.mjs +0 -265
  517. package/tools/kimodo/measure-pose.mjs +0 -65
  518. package/tools/kimodo/measure-preserve.mjs +0 -899
  519. package/tools/kimodo/measure-waypoints.mjs +0 -55
  520. package/tools/kimodo/mlx-generate.py +0 -111
  521. package/tools/kimodo/pose-constraints.mjs +0 -248
  522. package/tools/kimodo/preserve-mask.mjs +0 -543
  523. package/tools/kimodo/read-npz.mjs +0 -189
  524. package/tools/kimodo/run-edit-on-box.mjs +0 -155
  525. package/tools/kimodo/run-sequence-on-box.mjs +0 -178
  526. package/tools/kimodo/runner.mjs +0 -284
  527. package/tools/kimodo/seam-batch.mjs +0 -77
  528. package/tools/kimodo/setup-local.mjs +0 -30
  529. package/tools/kimodo/setup-on-box.sh +0 -339
  530. package/tools/kimodo/soma77-to-cskel27.mjs +0 -305
  531. package/tools/morphgs/assets/gen_truth.py +0 -39
  532. package/tools/morphgs/assets/mesh_ori_rig.txt +0 -27
  533. package/tools/morphgs/assets/playback-check.mjs +0 -30
  534. package/tools/morphgs/demo-gate.sh +0 -26
  535. package/tools/morphgs/fbx2morphgs.mjs +0 -68
  536. package/tools/morphgs/morphgs-to-cskel27.mjs +0 -105
  537. package/tools/morphgs/patches/preprocess_src-none-mode.patch +0 -76
  538. package/tools/morphgs/setup-on-cluster.sh +0 -126
  539. package/tools/projflow/driver.py +0 -1069
  540. package/tools/projflow/generate.mjs +0 -805
  541. package/tools/projflow/hml22-to-cskel27.mjs +0 -735
  542. package/tools/projflow/line-edit-job.mjs +0 -547
  543. package/tools/projflow/replay.mjs +0 -452
  544. package/tools/projflow/runner.mjs +0 -269
  545. package/tools/projflow/service.mjs +0 -742
  546. package/tools/track/DESIGN.md +0 -513
  547. package/tools/track/backfill-provenance.mjs +0 -116
  548. package/tools/track/budget.mjs +0 -8
  549. package/tools/track/check-rig.mjs +0 -209
  550. package/tools/track/diagnostics.schema.json +0 -60
  551. package/tools/track/export-rig.mjs +0 -256
  552. package/tools/track/fallback.mjs +0 -7
  553. package/tools/track/fk-parity-fixture.mjs +0 -161
  554. package/tools/track/gate.mjs +0 -349
  555. package/tools/track/masks.mjs +0 -318
  556. package/tools/track/metrics.mjs +0 -201
  557. package/tools/track/publish-obs.mjs +0 -108
  558. package/tools/track/py/check_env.py +0 -61
  559. package/tools/track/py/eval_lr.py +0 -229
  560. package/tools/track/py/lr_viterbi.py +0 -294
  561. package/tools/track/py/masks.py +0 -405
  562. package/tools/track/py/objective.py +0 -382
  563. package/tools/track/py/rig.py +0 -508
  564. package/tools/track/py/scene.py +0 -326
  565. package/tools/track/py/test_joint_indices.py +0 -123
  566. package/tools/track/py/test_lr_viterbi.py +0 -195
  567. package/tools/track/py/test_masks.py +0 -229
  568. package/tools/track/py/test_rig.py +0 -288
  569. package/tools/track/py/test_scene.py +0 -354
  570. package/tools/track/py/test_track.py +0 -328
  571. package/tools/track/py/track.py +0 -411
  572. package/tools/track/remote.mjs +0 -140
  573. package/tools/track/rig-dump.mjs +0 -227
  574. package/tools/track/run-box-tests.mjs +0 -14
  575. package/tools/track/run-box.mjs +0 -168
  576. package/tools/track/setup-box.sh +0 -59
  577. package/tools/track/study-2d.mjs +0 -728
  578. /package/src/{ardy → motion}/collision-blockers.js +0 -0
  579. /package/src/{ardy → motion}/ground.js +0 -0
  580. /package/src/{ardy → motion}/ik-drag.js +0 -0
  581. /package/src/{ardy → motion}/ik-key-json.js +0 -0
  582. /package/src/{ardy → motion}/ik.js +0 -0
  583. /package/src/{ardy → motion}/physics-surface.js +0 -0
  584. /package/src/{ardy → motion}/playback-clock.js +0 -0
  585. /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
+ }