cozyclay 2.1.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (577) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +12 -30
  3. package/bin/agent/agent-routes.mjs +40 -113
  4. package/bin/agent/agent-runner.mjs +9 -10
  5. package/bin/agent/cliproxy-provider.mjs +135 -0
  6. package/bin/agent/credential-store.mjs +30 -7
  7. package/bin/agent/pi-tools.mjs +4 -1
  8. package/bin/agent/provider-keys.mjs +32 -0
  9. package/bin/agent/provider-login.mjs +110 -0
  10. package/bin/agent/provider-routes.mjs +99 -0
  11. package/bin/agent/providers.mjs +19 -97
  12. package/bin/agent/script-tool.mjs +17 -4
  13. package/bin/agent/studio-motion-reference.mjs +23 -0
  14. package/bin/agent/studio-prompt.mjs +1 -1
  15. package/bin/agent/studio-rebase-text.mjs +32 -0
  16. package/bin/agent/studio-rebase.mjs +26 -39
  17. package/bin/agent/studio-tools.mjs +9 -28
  18. package/bin/agent/tool-arguments.mjs +49 -0
  19. package/bin/agent/turn-frames.mjs +66 -0
  20. package/bin/codex-auth.mjs +9 -2
  21. package/bin/cozyclay.mjs +10 -116
  22. package/bin/live/cli.mjs +3 -3
  23. package/bin/mcp-runtime.mjs +2 -12
  24. package/bin/package-signature.mjs +0 -4
  25. package/dist/app/index.html +7 -6
  26. package/dist/assets/EXRLoader-CvVIvGVE.js +1 -0
  27. package/dist/assets/analytics-C9sUnndd.js +1 -0
  28. package/dist/assets/app-CCh--1Hc.js +5087 -0
  29. package/dist/assets/app-f-fkV5uP.css +1 -0
  30. package/dist/assets/compile-worker-CkxiD6lH.js +5 -0
  31. package/dist/assets/fflate.module-KbGj5-H6.js +5 -0
  32. package/dist/assets/{first-shot-handoff-msLI71g1.js → first-shot-handoff-CC4xe4HM.js} +1 -1
  33. package/dist/assets/{landing-ChUvB_zL.js → landing-DjHD8u7k.js} +1 -1
  34. package/dist/assets/scene-asset-sync-Hx_FzdOH.js +20 -0
  35. package/dist/assets/scene-asset-sync-_S8NQl5B.css +1 -0
  36. package/dist/assets/workflow-C4yKH1p4.js +179 -0
  37. package/dist/cozyclay-package.json +1 -1
  38. package/dist/index.html +3 -3
  39. package/dist/sw.js +3 -6
  40. package/dist/workflow/index.html +5 -4
  41. package/mcp/LIVE-PROTOCOL.md +7 -47
  42. package/mcp/README.md +16 -26
  43. package/mcp/live-hub.mjs +6 -10
  44. package/mcp/package.json +1 -3
  45. package/mcp/tool-handlers.mjs +4 -168
  46. package/package.json +3 -22
  47. package/src/App.jsx +150 -2624
  48. package/src/agent-progress.js +79 -0
  49. package/src/agent-turn-mark.js +30 -0
  50. package/src/analytics.js +1 -19
  51. package/src/app-context.js +5 -0
  52. package/src/app-stage.jsx +2 -34
  53. package/src/asset-pane.jsx +6 -5
  54. package/src/camera-block.js +21 -0
  55. package/src/camera-direction.js +120 -0
  56. package/src/character-kind.js +3 -9
  57. package/src/command-bus.js +8 -6
  58. package/src/commands/cast.js +5 -34
  59. package/src/commands/elements/character.js +1 -1
  60. package/src/commands/elements/motion.js +1 -6
  61. package/src/commands/elements/object.js +6 -2
  62. package/src/commands/elements/shot.js +4 -1
  63. package/src/commands/elements.js +38 -14
  64. package/src/commands/index.js +3 -1
  65. package/src/commands/kits.js +37 -0
  66. package/src/commands/motion-timing.js +57 -0
  67. package/src/commands/motion.js +9 -152
  68. package/src/commands/motion3-compile.js +65 -0
  69. package/src/commands/motion3.js +207 -0
  70. package/src/commands/objects.js +6 -4
  71. package/src/commands/shot.js +49 -11
  72. package/src/commands/stage.js +14 -1
  73. package/src/commands/view.js +4 -1
  74. package/src/document-mirrors.js +42 -0
  75. package/src/document-store.js +4 -0
  76. package/src/domains/cast.js +19 -194
  77. package/src/domains/motion/motion-range-pin-rules.js +1 -1
  78. package/src/domains/motion/take-source.js +51 -0
  79. package/src/domains/motion.js +60 -1838
  80. package/src/domains/scenes.js +12 -55
  81. package/src/domains/shots.js +2 -2
  82. package/src/domains/stage.js +4 -2
  83. package/src/draft-ghost.js +46 -0
  84. package/src/draft-ghost.jsx +129 -0
  85. package/src/dualview.jsx +4 -3
  86. package/src/effects/EffectsLayer.jsx +112 -0
  87. package/src/effects/effect-meshes.js +232 -0
  88. package/src/effects/effect-record.js +103 -0
  89. package/src/effects/emitter.js +59 -0
  90. package/src/effects/simulate.js +261 -0
  91. package/src/effects/state.js +59 -0
  92. package/src/execution-telemetry.js +2 -3
  93. package/src/first-success-guide.jsx +5 -5
  94. package/src/ghost-set.js +257 -0
  95. package/src/held-object-edit.jsx +113 -0
  96. package/src/held-object-pose.js +175 -0
  97. package/src/hierarchy-model.js +1 -1
  98. package/src/hierarchy-panel.jsx +1 -1
  99. package/src/imported-mesh.jsx +103 -0
  100. package/src/kits/held.js +84 -0
  101. package/src/kits/index.js +70 -0
  102. package/src/kits/smoking.js +57 -0
  103. package/src/light-blockers.js +93 -0
  104. package/src/motion/beat-board-model.js +84 -0
  105. package/src/motion/beat-board.css +124 -0
  106. package/src/motion/beat-board.jsx +59 -0
  107. package/src/motion/center-of-mass.js +66 -0
  108. package/src/motion/fix-collisions.js +850 -0
  109. package/src/motion/physics-review.js +118 -0
  110. package/src/{ardy → motion}/physics-support.js +1 -76
  111. package/src/motion/rig-snapshot.js +22 -0
  112. package/src/motion/take-segments.jsx +140 -0
  113. package/src/{ardy → motion}/timeline-coordinates.js +0 -21
  114. package/src/{ardy → motion}/timeline.css +1 -2
  115. package/src/{ardy → motion}/timeline.jsx +42 -413
  116. package/src/motion/waypoints.js +76 -0
  117. package/src/motion3/beats.js +83 -0
  118. package/src/motion3/body.js +158 -0
  119. package/src/motion3/bones.js +47 -0
  120. package/src/motion3/carried.js +47 -0
  121. package/src/motion3/compile-job.js +75 -0
  122. package/src/motion3/compile-worker.js +10 -0
  123. package/src/motion3/compiler/anchors.js +105 -0
  124. package/src/motion3/compiler/arm-orient.js +128 -0
  125. package/src/motion3/compiler/channels.js +125 -0
  126. package/src/motion3/compiler/compile.js +205 -0
  127. package/src/motion3/compiler/gait-rhythm.js +97 -0
  128. package/src/motion3/compiler/grip.js +171 -0
  129. package/src/motion3/compiler/head-steady.js +48 -0
  130. package/src/motion3/compiler/hold.js +134 -0
  131. package/src/motion3/compiler/idle.js +113 -0
  132. package/src/motion3/compiler/limbs.js +170 -0
  133. package/src/motion3/compiler/math.js +72 -0
  134. package/src/motion3/compiler/pose.js +162 -0
  135. package/src/motion3/compiler/quat.js +49 -0
  136. package/src/motion3/compiler/skeleton.js +74 -0
  137. package/src/motion3/compiler/validate-common.js +32 -0
  138. package/src/motion3/compiler/validate-goals.js +118 -0
  139. package/src/motion3/compiler/validate-objects.js +143 -0
  140. package/src/motion3/compiler/validate-parts.js +116 -0
  141. package/src/motion3/compiler/validate.js +122 -0
  142. package/src/motion3/compiler/vocab.js +73 -0
  143. package/src/motion3/compiler/walk-pelvis.js +275 -0
  144. package/src/motion3/compiler/walk.js +207 -0
  145. package/src/motion3/compiler/wrist-clamp.js +32 -0
  146. package/src/motion3/delta.js +47 -0
  147. package/src/motion3/describe.js +174 -0
  148. package/src/motion3/docs.js +27 -0
  149. package/src/motion3/edit-carry.js +21 -0
  150. package/src/motion3/fix/ease.js +16 -0
  151. package/src/motion3/fix/ground.js +37 -0
  152. package/src/motion3/fix/index.js +52 -0
  153. package/src/motion3/fix/push.js +71 -0
  154. package/src/motion3/fix/reach.js +46 -0
  155. package/src/motion3/fix/retime.js +79 -0
  156. package/src/motion3/fix/self.js +211 -0
  157. package/src/motion3/fix/slide.js +70 -0
  158. package/src/motion3/fix/turn.js +51 -0
  159. package/src/motion3/follow.js +39 -0
  160. package/src/motion3/free-motion.js +173 -0
  161. package/src/motion3/grip-choice.js +130 -0
  162. package/src/motion3/hold-plan.js +157 -0
  163. package/src/motion3/json-patch.js +77 -0
  164. package/src/motion3/key-aim.js +105 -0
  165. package/src/motion3/key-edit.js +82 -0
  166. package/src/motion3/lenient.js +127 -0
  167. package/src/motion3/object-keys.js +236 -0
  168. package/src/motion3/object-pose.js +182 -0
  169. package/src/motion3/object-tracks.js +12 -0
  170. package/src/motion3/pipeline.js +102 -0
  171. package/src/motion3/playback.js +51 -0
  172. package/src/motion3/probes.js +91 -0
  173. package/src/motion3/qa/body-checks.js +34 -0
  174. package/src/motion3/qa/check-names.js +14 -0
  175. package/src/motion3/qa/checks.js +121 -0
  176. package/src/motion3/qa/contact-checks.js +51 -0
  177. package/src/motion3/qa/explain.js +80 -0
  178. package/src/motion3/qa/fmt.js +5 -0
  179. package/src/motion3/qa/foot-checks.js +72 -0
  180. package/src/motion3/qa/geometry.js +77 -0
  181. package/src/motion3/qa/goals.js +191 -0
  182. package/src/motion3/qa/ground.js +60 -0
  183. package/src/motion3/qa/hints-contact.js +179 -0
  184. package/src/motion3/qa/hints-goal.js +160 -0
  185. package/src/motion3/qa/hints-knee.js +24 -0
  186. package/src/motion3/qa/hints-motion.js +209 -0
  187. package/src/motion3/qa/hints-object.js +91 -0
  188. package/src/motion3/qa/index.js +42 -0
  189. package/src/motion3/qa/key-checks.js +82 -0
  190. package/src/motion3/qa/near.js +96 -0
  191. package/src/motion3/qa/object-checks.js +135 -0
  192. package/src/motion3/qa/patch.js +161 -0
  193. package/src/motion3/qa/severity.js +38 -0
  194. package/src/motion3/qa/spec-checks.js +65 -0
  195. package/src/motion3/qa/style.js +218 -0
  196. package/src/motion3/qa/wrist-checks.js +69 -0
  197. package/src/motion3/qa/wrist-obs.js +60 -0
  198. package/src/motion3/refs.js +120 -0
  199. package/src/motion3/repair-tune.js +121 -0
  200. package/src/motion3/repair.js +83 -0
  201. package/src/motion3/report.js +67 -0
  202. package/src/motion3/rigs/index.js +14 -0
  203. package/src/motion3/rigs/x-bot-tpose-body.json +1 -0
  204. package/src/motion3/rigs/x-bot-tpose.json +1 -0
  205. package/src/motion3/rigs/y-bot-tpose-body.json +1 -0
  206. package/src/motion3/rigs/y-bot-tpose.json +1 -0
  207. package/src/motion3/scene.js +60 -0
  208. package/src/motion3/schema/brief.js +89 -0
  209. package/src/motion3/schema/fields.js +44 -0
  210. package/src/motion3/schema/spec-examples.js +46 -0
  211. package/src/motion3/schema/topics.js +111 -0
  212. package/src/motion3/schema/types.js +147 -0
  213. package/src/motion3/search.js +117 -0
  214. package/src/motion3/second-hand.js +112 -0
  215. package/src/motion3/skills/basic.js +82 -0
  216. package/src/motion3/skills/book.js +76 -0
  217. package/src/motion3/skills/car.js +145 -0
  218. package/src/motion3/skills/frames-auto.js +29 -0
  219. package/src/motion3/skills/index.js +83 -0
  220. package/src/motion3/skills/info.js +65 -0
  221. package/src/motion3/skills/smoke-kit.js +109 -0
  222. package/src/motion3/skills/smoke.js +160 -0
  223. package/src/motion3/spec-store.js +59 -0
  224. package/src/motion3/take-edit.js +155 -0
  225. package/src/motion3/take.js +63 -0
  226. package/src/motion3/timeline.js +127 -0
  227. package/src/motion3/views.js +121 -0
  228. package/src/motion3/walk-plan.js +119 -0
  229. package/src/object-catalog.jsx +7 -1
  230. package/src/object-colors.js +98 -0
  231. package/src/object-gizmo-parts.js +115 -0
  232. package/src/object-gizmo.jsx +37 -122
  233. package/src/object-glow.js +139 -0
  234. package/src/object-hidden.js +33 -0
  235. package/src/object-look.js +196 -0
  236. package/src/object-size.js +10 -0
  237. package/src/object-travel.js +71 -6
  238. package/src/panels/CameraPanel.jsx +7 -7
  239. package/src/panels/HdrEnvironmentControls.jsx +59 -0
  240. package/src/panels/LightPanel.jsx +25 -4
  241. package/src/panels/ObjectLookPanel.jsx +88 -0
  242. package/src/panels/PosePanel.jsx +2 -17
  243. package/src/panels/RigControlPanel.jsx +5 -53
  244. package/src/panels/SubjectBox.jsx +4 -1
  245. package/src/panels/details.css +14 -0
  246. package/src/posestudio.jsx +4 -27
  247. package/src/project-browser.jsx +3 -4
  248. package/src/project-resources.js +18 -31
  249. package/src/project.js +13 -104
  250. package/src/props.jsx +25 -280
  251. package/src/rail-dot-drag.js +23 -0
  252. package/src/render-passes.js +10 -0
  253. package/src/resource-status.jsx +1 -0
  254. package/src/result-modal.jsx +4 -19
  255. package/src/room.jsx +20 -12
  256. package/src/scene-assets.js +8 -6
  257. package/src/scene-light-place.js +72 -0
  258. package/src/scene-lights.jsx +148 -0
  259. package/src/scene-objects.js +31 -139
  260. package/src/scenes.js +15 -105
  261. package/src/semantic-edit.js +2 -5
  262. package/src/set-pieces.jsx +183 -0
  263. package/src/settings-menu.jsx +3 -11
  264. package/src/shell/BottomDock.jsx +9 -51
  265. package/src/shell/DetailsSlot.jsx +16 -200
  266. package/src/shell/GhostsMenuGroup.jsx +74 -0
  267. package/src/shell/LibrarySlot.jsx +1 -3
  268. package/src/shell/MenuBar.jsx +3 -3
  269. package/src/shell/PreferencesDialog.jsx +15 -264
  270. package/src/shell/PreferencesSlot.jsx +2 -8
  271. package/src/shell/TopBar.jsx +5 -84
  272. package/src/shell/ViewportToolbar.jsx +10 -70
  273. package/src/shell/agent-glass.css +19 -6
  274. package/src/shell/details-selection.js +42 -0
  275. package/src/shell/ghosts-menu.css +22 -0
  276. package/src/shell/glass-regions.css +29 -3
  277. package/src/shell/preferences.css +6 -1
  278. package/src/shot-authoring.js +4 -7
  279. package/src/soft-shadows.js +37 -0
  280. package/src/stage-attach-frames.js +12 -12
  281. package/src/stage-canvas-rigs.jsx +3 -3
  282. package/src/stage-capture.jsx +42 -8
  283. package/src/stage-characters.jsx +22 -216
  284. package/src/stage-editor-gizmos.jsx +6 -6
  285. package/src/stage-environment.js +93 -0
  286. package/src/stage-hdr-builtins.js +107 -0
  287. package/src/stage-hdr-image.js +116 -0
  288. package/src/stage-hdr-library.js +70 -0
  289. package/src/stage-hdr-map.js +94 -0
  290. package/src/stage-hdr.js +130 -0
  291. package/src/stage-lighting.js +149 -0
  292. package/src/stage-occlusion.js +210 -0
  293. package/src/stage-sky.jsx +159 -0
  294. package/src/stage-startup-defaults.js +5 -9
  295. package/src/studio-actions.js +12 -14
  296. package/src/studio-agent-commands.js +19 -16
  297. package/src/studio-agent-motion.js +9 -9
  298. package/src/studio-agent-protocol.js +41 -71
  299. package/src/studio-app-binding.js +25 -45
  300. package/src/studio-asset-list.js +17 -0
  301. package/src/studio-contact-sheet.js +37 -5
  302. package/src/studio-elements.js +27 -7
  303. package/src/studio-entity-bounds.js +18 -0
  304. package/src/studio-error-codes.js +8 -0
  305. package/src/studio-export-framing.js +152 -0
  306. package/src/studio-lighting-check.js +103 -0
  307. package/src/studio-motion-visual.js +150 -0
  308. package/src/studio-schema-errors.js +80 -0
  309. package/src/studio-support-gap.js +74 -0
  310. package/src/styles.css +1 -19
  311. package/src/timeline-defaults.js +5 -0
  312. package/src/timeline-extent.js +8 -20
  313. package/src/tutorial-walk.js +17 -0
  314. package/src/ui-scrub.js +1 -1
  315. package/src/use-shot-camera-tracks.js +12 -0
  316. package/src/view-ghosts.js +97 -0
  317. package/src/view-ghosts.jsx +49 -0
  318. package/src/workflow/AgentPanel.jsx +47 -166
  319. package/src/workflow/AgentProgressCard.jsx +107 -0
  320. package/src/workflow/AgentReplyText.jsx +37 -0
  321. package/src/workflow/AgentTurnSteps.jsx +84 -0
  322. package/src/workflow/LoginPanel.jsx +140 -0
  323. package/src/workflow/WorkflowBuilder.jsx +7 -23
  324. package/src/workflow/agent-client.js +54 -51
  325. package/src/workflow/agent-panel.css +71 -1
  326. package/src/workflow/agent-progress-card.css +167 -0
  327. package/src/workflow/agent-progress-view.js +47 -0
  328. package/src/workflow/agent-turn-steps.css +113 -0
  329. package/src/workflow/agent-turn.js +191 -0
  330. package/src/workflow/login-client.js +34 -0
  331. package/src/workflow/motion-input.js +18 -31
  332. package/src/workflow/node-schema.js +1 -1
  333. package/src/workflow/reply-markdown.js +69 -0
  334. package/src/workflow/scene-asset-sync.js +8 -8
  335. package/tools/agent-session-stats.mjs +61 -0
  336. package/tools/dev-full.mjs +5 -56
  337. package/tools/eslint-baseline.json +12 -13
  338. package/tools/knip-baseline.json +0 -179
  339. package/tools/loc-baseline.json +31 -81
  340. package/tools/motion3/extract-body.mjs +103 -0
  341. package/tools/motion3/extract-rig.mjs +85 -0
  342. package/tools/process-supervisor.mjs +0 -88
  343. package/tools/run-tests.mjs +8 -124
  344. package/bin/agent/motion-runtime.mjs +0 -76
  345. package/dist/ardy/cskel27-rest.json +0 -557
  346. package/dist/assets/analytics-C9pMpW7s.js +0 -1
  347. package/dist/assets/app-B_2XB9rw.css +0 -1
  348. package/dist/assets/app-DAuvKPP8.js +0 -4920
  349. package/dist/assets/config-Bx7pYsD9.js +0 -1
  350. package/dist/assets/config-DeoFTYzb.css +0 -1
  351. package/dist/assets/demo-CvKuBgy5.js +0 -1
  352. package/dist/assets/scene-asset-sync-DKkpjKBV.js +0 -19
  353. package/dist/assets/scene-asset-sync-DjJ_cJ2R.css +0 -1
  354. package/dist/assets/ticket-BPk2Ni95.js +0 -1
  355. package/dist/assets/workflow-jJRBR94Z.js +0 -179
  356. package/dist/demo/walk-then-stop.npz +0 -0
  357. package/mcp/ardy-prompts.mjs +0 -218
  358. package/src/ardy/auto-fix-panel.css +0 -108
  359. package/src/ardy/auto-physics.js +0 -1194
  360. package/src/ardy/client.js +0 -153
  361. package/src/ardy/convert.js +0 -313
  362. package/src/ardy/cskel27-neutral.js +0 -39
  363. package/src/ardy/cskel27.js +0 -77
  364. package/src/ardy/export.js +0 -165
  365. package/src/ardy/fix-collisions.js +0 -2257
  366. package/src/ardy/foot-lock.js +0 -230
  367. package/src/ardy/motion-calibration.js +0 -118
  368. package/src/ardy/motion-edit.js +0 -213
  369. package/src/ardy/motion-url.js +0 -55
  370. package/src/ardy/mp4-duration.js +0 -102
  371. package/src/ardy/npz.js +0 -595
  372. package/src/ardy/physics-panel.css +0 -21
  373. package/src/ardy/physics-panel.jsx +0 -89
  374. package/src/ardy/physics-refine.js +0 -111
  375. package/src/ardy/physics-review.js +0 -567
  376. package/src/ardy/physics-temporal.js +0 -54
  377. package/src/ardy/platform-fit-panel.css +0 -64
  378. package/src/ardy/platform-fit-panel.jsx +0 -41
  379. package/src/ardy/platform-fit.js +0 -326
  380. package/src/ardy/playback.js +0 -711
  381. package/src/ardy/pose-pin.js +0 -97
  382. package/src/ardy/prompt-clips.js +0 -30
  383. package/src/ardy/retime.js +0 -211
  384. package/src/ardy/root-drop.js +0 -369
  385. package/src/ardy/timeline-resize.js +0 -13
  386. package/src/ardy/to-cskel27.js +0 -225
  387. package/src/ardy/trim.js +0 -33
  388. package/src/ardy/waypoints.js +0 -369
  389. package/src/ardy-bridge-contract.js +0 -281
  390. package/src/domains/motion/motion-video-capture.js +0 -664
  391. package/src/i2v-motion-client.js +0 -77
  392. package/src/i2v-motion-studio.jsx +0 -180
  393. package/src/line-edit.js +0 -1348
  394. package/src/motion/generation.js +0 -105
  395. package/src/motion-readiness-ui.jsx +0 -67
  396. package/src/motion-readiness.js +0 -40
  397. package/src/motion-resources.js +0 -177
  398. package/src/motion-store.js +0 -65
  399. package/src/motion-trail.js +0 -601
  400. package/src/multimodel-ingest.js +0 -394
  401. package/src/panels/PromptBlocksPanel.jsx +0 -444
  402. package/src/panels/VideoCapturePanel.jsx +0 -181
  403. package/src/pose-extract/browser.js +0 -42
  404. package/src/pose-extract/detector.js +0 -100
  405. package/src/pose-extract/fit.js +0 -227
  406. package/src/pose-extract/image-frame.js +0 -51
  407. package/src/pose-extract/index.js +0 -58
  408. package/src/pose-extract/keyframes.js +0 -74
  409. package/src/pose-extract/landmarks.js +0 -162
  410. package/src/pose-extract/mirror.js +0 -153
  411. package/src/pose-extract/take.js +0 -131
  412. package/src/pose-extract/video-frames.js +0 -91
  413. package/src/take-recipe.js +0 -165
  414. package/src/trail-key-conflicts.js +0 -39
  415. package/src/trail-pick.js +0 -59
  416. package/src/use-i2v-capture-modal.js +0 -15
  417. package/tools/ardy/BRIDGE.md +0 -37
  418. package/tools/ardy/GVHMR-NPZ.md +0 -68
  419. package/tools/ardy/README.md +0 -124
  420. package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
  421. package/tools/ardy/artifacts.mjs +0 -34
  422. package/tools/ardy/bridge.mjs +0 -1732
  423. package/tools/ardy/bvh-cskel27.mjs +0 -1265
  424. package/tools/ardy/cclay_constrained_generate.py +0 -561
  425. package/tools/ardy/cclay_gvhmr_worker.py +0 -264
  426. package/tools/ardy/cclay_motion_edit.py +0 -510
  427. package/tools/ardy/cclay_sequence_generate.py +0 -705
  428. package/tools/ardy/cclay_worker.py +0 -304
  429. package/tools/ardy/dump-npz.py +0 -205
  430. package/tools/ardy/extract-rest.mjs +0 -299
  431. package/tools/ardy/extract.mjs +0 -507
  432. package/tools/ardy/footage.mjs +0 -467
  433. package/tools/ardy/gvhmr-floor.mjs +0 -102
  434. package/tools/ardy/gvhmr-to-cskel27.mjs +0 -21
  435. package/tools/ardy/gvhmr_fastpath.py +0 -168
  436. package/tools/ardy/gvhmr_trajectory.py +0 -240
  437. package/tools/ardy/merge-text-encoder.py +0 -160
  438. package/tools/ardy/mocap-metrics.mjs +0 -200
  439. package/tools/ardy/mocap-quality-gate.mjs +0 -110
  440. package/tools/ardy/motion-stabilize.mjs +0 -348
  441. package/tools/ardy/npz.mjs +0 -414
  442. package/tools/ardy/pose-to-npz.mjs +0 -106
  443. package/tools/ardy/prompt-limits.mjs +0 -5
  444. package/tools/ardy/run-edit-on-box.sh +0 -73
  445. package/tools/ardy/run-local.mjs +0 -233
  446. package/tools/ardy/run-on-box.sh +0 -593
  447. package/tools/ardy/run-sequence-on-box.sh +0 -177
  448. package/tools/ardy/runners/gvhmr-worker.mjs +0 -206
  449. package/tools/ardy/runners/index.mjs +0 -18
  450. package/tools/ardy/runners/local.mjs +0 -455
  451. package/tools/ardy/runners/proc.mjs +0 -125
  452. package/tools/ardy/runners/remote.mjs +0 -252
  453. package/tools/ardy/setup-local.mjs +0 -189
  454. package/tools/ardy/setup-text-encoder-on-box.sh +0 -93
  455. package/tools/ardy/setup-text-encoder.py +0 -306
  456. package/tools/ardy/smpl-cskel27.mjs +0 -178
  457. package/tools/ardy/visual-qa.mjs +0 -196
  458. package/tools/ardy/vq-car.mjs +0 -52
  459. package/tools/bench/EXP3.md +0 -61
  460. package/tools/bench/cclay_bench_extract_incam.py +0 -125
  461. package/tools/bench/cclay_bench_extract_obs.py +0 -204
  462. package/tools/bench/cclay_bench_runner.py +0 -42
  463. package/tools/bench/cube-contact.mjs +0 -162
  464. package/tools/bench/exp3.mjs +0 -152
  465. package/tools/bench/extract-bench-lib.mjs +0 -216
  466. package/tools/bench/extract-bench.mjs +0 -302
  467. package/tools/bench/fal-generate.mjs +0 -88
  468. package/tools/bench/fit/README.md +0 -189
  469. package/tools/bench/fit/camera.mjs +0 -59
  470. package/tools/bench/fit/contact.mjs +0 -419
  471. package/tools/bench/fit/footlock.mjs +0 -234
  472. package/tools/bench/fit/motion.mjs +0 -85
  473. package/tools/bench/fit/pin.mjs +0 -27
  474. package/tools/bench/fit/remote.mjs +0 -105
  475. package/tools/bench/fit-bench.mjs +0 -126
  476. package/tools/bench/fit-sanity.mjs +0 -45
  477. package/tools/bench/metrics.mjs +0 -313
  478. package/tools/bench/obs/depth.mjs +0 -258
  479. package/tools/bench/obs/extrinsics.mjs +0 -175
  480. package/tools/bench/obs/fit_mannequin_betas.py +0 -340
  481. package/tools/bench/obs/ground.mjs +0 -294
  482. package/tools/bench/obs/heading.mjs +0 -151
  483. package/tools/bench/obs/ladder.mjs +0 -555
  484. package/tools/bench/obs/mannequin-betas.json +0 -124
  485. package/tools/bench/obs/remote.mjs +0 -83
  486. package/tools/bench/obs/rest_joints.py +0 -52
  487. package/tools/bench/obs/ybot-targets.mjs +0 -49
  488. package/tools/bench/obs-bench.mjs +0 -520
  489. package/tools/bench/score.mjs +0 -343
  490. package/tools/bench/summarize.mjs +0 -86
  491. package/tools/demo-worker/README.md +0 -137
  492. package/tools/demo-worker/api-client.mjs +0 -233
  493. package/tools/demo-worker/generate.mjs +0 -301
  494. package/tools/demo-worker/index.mjs +0 -466
  495. package/tools/demo-worker/sign-core.mjs +0 -112
  496. package/tools/demo-worker/sign.mjs +0 -161
  497. package/tools/gt-render/browser.mjs +0 -182
  498. package/tools/gt-render/camera-math.mjs +0 -276
  499. package/tools/gt-render/page.mjs +0 -267
  500. package/tools/gt-render/render.mjs +0 -514
  501. package/tools/gt-render/scene-box.mjs +0 -31
  502. package/tools/gt-render/take-transform.mjs +0 -87
  503. package/tools/kimodo/compare-seam.mjs +0 -108
  504. package/tools/kimodo/constraints.mjs +0 -244
  505. package/tools/kimodo/edit.mjs +0 -259
  506. package/tools/kimodo/effector-constraints.mjs +0 -200
  507. package/tools/kimodo/generate.mjs +0 -646
  508. package/tools/kimodo/local-output.mjs +0 -265
  509. package/tools/kimodo/measure-pose.mjs +0 -65
  510. package/tools/kimodo/measure-preserve.mjs +0 -899
  511. package/tools/kimodo/measure-waypoints.mjs +0 -55
  512. package/tools/kimodo/mlx-generate.py +0 -111
  513. package/tools/kimodo/pose-constraints.mjs +0 -248
  514. package/tools/kimodo/preserve-mask.mjs +0 -543
  515. package/tools/kimodo/read-npz.mjs +0 -189
  516. package/tools/kimodo/run-edit-on-box.mjs +0 -155
  517. package/tools/kimodo/run-sequence-on-box.mjs +0 -178
  518. package/tools/kimodo/runner.mjs +0 -284
  519. package/tools/kimodo/seam-batch.mjs +0 -77
  520. package/tools/kimodo/setup-local.mjs +0 -30
  521. package/tools/kimodo/setup-on-box.sh +0 -339
  522. package/tools/kimodo/soma77-to-cskel27.mjs +0 -305
  523. package/tools/morphgs/assets/gen_truth.py +0 -39
  524. package/tools/morphgs/assets/mesh_ori_rig.txt +0 -27
  525. package/tools/morphgs/assets/playback-check.mjs +0 -30
  526. package/tools/morphgs/demo-gate.sh +0 -26
  527. package/tools/morphgs/fbx2morphgs.mjs +0 -68
  528. package/tools/morphgs/morphgs-to-cskel27.mjs +0 -105
  529. package/tools/morphgs/patches/preprocess_src-none-mode.patch +0 -76
  530. package/tools/morphgs/setup-on-cluster.sh +0 -126
  531. package/tools/projflow/driver.py +0 -1069
  532. package/tools/projflow/generate.mjs +0 -805
  533. package/tools/projflow/hml22-to-cskel27.mjs +0 -735
  534. package/tools/projflow/line-edit-job.mjs +0 -547
  535. package/tools/projflow/replay.mjs +0 -452
  536. package/tools/projflow/runner.mjs +0 -269
  537. package/tools/projflow/service.mjs +0 -742
  538. package/tools/track/DESIGN.md +0 -513
  539. package/tools/track/backfill-provenance.mjs +0 -116
  540. package/tools/track/budget.mjs +0 -8
  541. package/tools/track/check-rig.mjs +0 -209
  542. package/tools/track/diagnostics.schema.json +0 -60
  543. package/tools/track/export-rig.mjs +0 -256
  544. package/tools/track/fallback.mjs +0 -7
  545. package/tools/track/fk-parity-fixture.mjs +0 -161
  546. package/tools/track/gate.mjs +0 -349
  547. package/tools/track/masks.mjs +0 -318
  548. package/tools/track/metrics.mjs +0 -201
  549. package/tools/track/publish-obs.mjs +0 -108
  550. package/tools/track/py/check_env.py +0 -61
  551. package/tools/track/py/eval_lr.py +0 -229
  552. package/tools/track/py/lr_viterbi.py +0 -294
  553. package/tools/track/py/masks.py +0 -405
  554. package/tools/track/py/objective.py +0 -382
  555. package/tools/track/py/rig.py +0 -508
  556. package/tools/track/py/scene.py +0 -326
  557. package/tools/track/py/test_joint_indices.py +0 -123
  558. package/tools/track/py/test_lr_viterbi.py +0 -195
  559. package/tools/track/py/test_masks.py +0 -229
  560. package/tools/track/py/test_rig.py +0 -288
  561. package/tools/track/py/test_scene.py +0 -354
  562. package/tools/track/py/test_track.py +0 -328
  563. package/tools/track/py/track.py +0 -411
  564. package/tools/track/remote.mjs +0 -140
  565. package/tools/track/rig-dump.mjs +0 -227
  566. package/tools/track/run-box-tests.mjs +0 -14
  567. package/tools/track/run-box.mjs +0 -168
  568. package/tools/track/setup-box.sh +0 -59
  569. package/tools/track/study-2d.mjs +0 -728
  570. /package/src/{ardy → motion}/collision-blockers.js +0 -0
  571. /package/src/{ardy → motion}/ground.js +0 -0
  572. /package/src/{ardy → motion}/ik-drag.js +0 -0
  573. /package/src/{ardy → motion}/ik-key-json.js +0 -0
  574. /package/src/{ardy → motion}/ik.js +0 -0
  575. /package/src/{ardy → motion}/physics-surface.js +0 -0
  576. /package/src/{ardy → motion}/playback-clock.js +0 -0
  577. /package/src/{ardy → motion}/range-pin.js +0 -0
@@ -0,0 +1,218 @@
1
+ // Style quantities: how fast a joint moves, how hard it speeds up, how far the centre of mass
2
+ // leaves the feet and how deep a knee or elbow bends. They depend on what the motion is for (a
3
+ // bat swing moves the wrists at 9 m/s), so QA measures them (output.observations) and does not
4
+ // judge them. A spec that names a style limit in checks.limits asks for that quantity to be a
5
+ // check again: speedCore/speedLimb -> SPEED, accCore/accLimb -> ACC, knee -> KNEE (bend),
6
+ // balance -> BALANCE. A pop is not style: one frame whose acceleration stands out from the
7
+ // frames around it is an artifact (a step key, a pole flip, a snapped anchor), so POP stays a
8
+ // QA error, and so is a FLOAT: the body off the ground without falling (floatChecks). A pop is a
9
+ // spike, not a large value: a hard but continuous acceleration (a swing's launch) is style.
10
+ // See docs/motion-api-v3.1.md C13.
11
+ import { add, sub, scale, len, dot, angle } from "../compiler/math.js";
12
+ import { CAPSULES, segPoints, sdBox } from "./geometry.js";
13
+ import { groundSole } from "./ground.js";
14
+
15
+ const SPEED_JOINTS = ["Hips", "Head", "LeftHand", "RightHand", "LeftFoot", "RightFoot", "LeftLeg", "RightLeg", "LeftForeArm", "RightForeArm"];
16
+ const CORE = new Set(["Hips", "Head"]);
17
+ const BENDS = [["knee_l", "LeftUpLeg", "LeftLeg", "LeftFoot"], ["knee_r", "RightUpLeg", "RightLeg", "RightFoot"],
18
+ ["elbow_l", "LeftArm", "LeftForeArm", "LeftHand"], ["elbow_r", "RightArm", "RightForeArm", "RightHand"]];
19
+ /** A pop's acceleration is more than this many times the level around it (popLevel). */
20
+ const POP_RATIO = 2;
21
+ /** FLOAT: the share of gravity an airborne run may miss (0.75: the hips fall at a quarter of g). */
22
+ const FLOAT_LIMIT = 0.75;
23
+ /** Peak speed (m/s) under which a joint counts as still and peaks leaves it out. */
24
+ const STILL = 0.05;
25
+ const r2 = x => Math.round(x * 100) / 100;
26
+
27
+ /**
28
+ * Per SPEED_JOINT: speed v[f] (m/frame, from f-1), acceleration a[f] (m/frame^2, from f-2) and
29
+ * its vector A[f]; 0 where undefined.
30
+ */
31
+ function kinematics(sk, out) {
32
+ const F = out.frames, n = F.length;
33
+ return SPEED_JOINTS.map(j => {
34
+ const X = F.map(fr => fr.s.P[sk.index[j]]), v = new Float64Array(n), a = new Float64Array(n), A = X.map(() => [0, 0, 0]);
35
+ for (let f = 1; f < n; f++) v[f] = len(sub(X[f], X[f - 1]));
36
+ for (let f = 2; f < n; f++) { A[f] = sub(sub(X[f], X[f - 1]), sub(X[f - 1], X[f - 2])); a[f] = len(A[f]); }
37
+ return { j, core: CORE.has(j), v, a, A };
38
+ });
39
+ }
40
+
41
+ /**
42
+ * The acceleration level around frame f that a pop is measured against (m/frame^2), the larger of
43
+ * far the largest acceleration 2 and 3 frames away. A one-frame event shows in up to three
44
+ * neighbouring frames (a step key in f and f+1, a one-frame glitch in f-1..f+1), so the
45
+ * frames next to f are not read here;
46
+ * burst what the frames next to f carry in f's own direction, less what the other one turns
47
+ * back. At 24 fps a hard move gathers its acceleration into 2 or 3 frames that all push
48
+ * the same way (a swing's launch, the hips turning round): a burst, not a pop, however
49
+ * hard. A glitch pushes back on both sides (+1, -2, +1), a step on one (+1, -1), and a
50
+ * jitter turns back on one side as the other follows (+, -, -, +): those carry none.
51
+ * k = { a, A } (kinematics).
52
+ */
53
+ function popLevel({ a, A }, f) {
54
+ let far = 0;
55
+ for (const q of [f - 3, f - 2, f + 2, f + 3]) if (q >= 2 && q < a.length) far = Math.max(far, a[q]);
56
+ const along = a[f] > 0 ? [f - 1, f + 1].filter(q => q >= 2 && q < a.length).map(q => dot(A[q], A[f]) / a[f]) : [];
57
+ return along.length ? Math.max(far, Math.max(...along) + Math.min(0, ...along)) : far;
58
+ }
59
+
60
+ /** True when k.a[f] (m/frame^2) is a pop: above floor and POP_RATIO times popLevel(k, f); k = { a, A } (kinematics). */
61
+ export const isPop = (k, f, floor) => k.a[f] > floor && k.a[f] > POP_RATIO * popLevel(k, f);
62
+
63
+ /** Hard POP issues (limits L: accCore, accLimb are the floor); push(kind, msg, f, value, limit). */
64
+ export function popChecks(sk, out, L, push) {
65
+ for (const k of kinematics(sk, out)) {
66
+ const floor = k.core ? L.accCore : L.accLimb;
67
+ for (let f = 2; f < k.a.length; f++) if (isPop(k, f, floor)) push("POP", `${k.j} accel ${(k.a[f] * 100).toFixed(2)} cm/f^2 in one frame`, f, k.a[f], floor);
68
+ }
69
+ }
70
+
71
+ /** True when a declared support (checks.supports) carries the body at frame f. */
72
+ function supported(out, f, supports) {
73
+ const { P } = out.frames[f].s, I = out.ctx.sk.index;
74
+ return supports.some(sp => {
75
+ const box = out.objectsAt(f)[sp.object], cap = CAPSULES.find(c => c[0] === (sp.part ?? "hips"));
76
+ return box && cap && Math.min(...segPoints(P[I[cap[1]]], P[I[cap[2]]]).map(p => sdBox(p, box))) - cap[3] < (sp.tol ?? 0.04);
77
+ });
78
+ }
79
+
80
+ /**
81
+ * Per frame: null where a declared support carries the body, "air" with no foot on the ground,
82
+ * else the balance margin in metres: how far the centre of mass (hips, spine, head) sits inside
83
+ * the box of the feet on the ground, the floor or an object's top (ground.js; < 0: outside it).
84
+ * While the hips travel every foot counts.
85
+ */
86
+ function balanceMargins(out, supports = []) {
87
+ const sk = out.ctx.sk, I = sk.index, F = out.frames;
88
+ return F.map(({ s }, f) => {
89
+ if (supported(out, f, supports)) return null;
90
+ const { P } = s;
91
+ const moving = f > 0 && f < F.length - 1 && Math.hypot(F[f + 1].s.P[I.Hips][0] - F[f - 1].s.P[I.Hips][0], F[f + 1].s.P[I.Hips][2] - F[f - 1].s.P[I.Hips][2]) / 2 > 0.006;
92
+ const feet = [];
93
+ for (const S of ["Left", "Right"]) {
94
+ const sp = groundSole(out, f, S);
95
+ if (moving || Math.min(sp.heel[1], sp.ball[1]) < 0.02) feet.push(sp.heel, sp.ball, sp.tip);
96
+ }
97
+ if (!feet.length) return "air";
98
+ const com = scale(["Hips", "Spine1", "Spine2", "Head"].reduce((acc, j) => add(acc, P[I[j]]), [0, 0, 0]), 0.25);
99
+ const xs = feet.map(p => p[0]), zs = feet.map(p => p[2]);
100
+ return Math.min(com[0] - Math.min(...xs), Math.max(...xs) - com[0], com[2] - Math.min(...zs), Math.max(...zs) - com[2]);
101
+ });
102
+ }
103
+
104
+ /** Runs [a, b] (indexes of out.frames) with both soles 2 cm or more above the ground (ground.js) and no declared support. */
105
+ function airRuns(out, supports = []) {
106
+ const n = out.frames.length, runs = [];
107
+ const air = out.frames.map((_, f) => !supported(out, f, supports)
108
+ && ["Left", "Right"].every(S => { const sp = groundSole(out, f, S); return Math.min(sp.heel[1], sp.ball[1]) >= 0.02; }));
109
+ for (let a = 0; a < n; a++) {
110
+ if (!air[a]) continue;
111
+ let b = a;
112
+ while (b + 1 < n && air[b + 1]) b++;
113
+ runs.push([a, b]);
114
+ a = b;
115
+ }
116
+ return runs;
117
+ }
118
+
119
+ /**
120
+ * Hard FLOAT issues: 4 or more frames off the ground (airRuns) in which the hips do not fall.
121
+ * A jump or a drop is ballistic: over the run the hips' vertical speed drops by gravity (the
122
+ * speed into the run minus the speed out of it, per frame, is g). A body held up by nothing
123
+ * keeps it; under a quarter of g is a float. value = the share of g missing (1 = no fall).
124
+ */
125
+ export function floatChecks(out, checks = {}) {
126
+ const F = out.frames, n = F.length, I = out.ctx.sk.index, g = 9.81 / (out.fps ?? 24) ** 2, f0 = out.f0 ?? 0, issues = [];
127
+ const y = f => F[Math.max(0, Math.min(n - 1, f))].s.P[I.Hips][1];
128
+ for (const [a, b] of airRuns(out, checks.supports)) {
129
+ const frames = b - a + 1;
130
+ if (frames < 4) continue;
131
+ const vin = a > 0 ? y(a) - y(a - 1) : y(a + 1) - y(a), vout = b < n - 1 ? y(b + 1) - y(b) : y(b) - y(b - 1);
132
+ const missing = 1 - (vin - vout) / frames / g;
133
+ if (missing <= FLOAT_LIMIT) continue;
134
+ for (let f = a; f <= b; f++) issues.push({ kind: "FLOAT", msg: `no foot on the ground f${a + f0}-f${b + f0}, hips fall at ${Math.round(Math.max(0, 1 - missing) * 100)}% of g`, frame: f, value: +missing.toFixed(4), limit: FLOAT_LIMIT });
135
+ }
136
+ return issues;
137
+ }
138
+
139
+ const bendAt = (sk, s, [, a, b, c]) => angle(sub(s.P[sk.index[b]], s.P[sk.index[a]]), sub(s.P[sk.index[c]], s.P[sk.index[b]]));
140
+
141
+ /**
142
+ * The style checks the spec opted into by naming their limit in checks.limits (own keys only);
143
+ * L is QA_LIMITS merged with them. Issues as generalChecks makes them (f indexes out.frames).
144
+ */
145
+ export function styleChecks(sk, out, checks, L) {
146
+ const own = checks?.limits ?? {}, on = k => Object.hasOwn(own, k), issues = [];
147
+ const push = (kind, msg, f, value, limit) => issues.push({ kind, msg, frame: f, value: +value.toFixed(4), limit });
148
+ for (const k of kinematics(sk, out)) {
149
+ const { j, core, v, a } = k, vk = core ? "speedCore" : "speedLimb", ak = core ? "accCore" : "accLimb";
150
+ for (let f = 1; f < v.length; f++) {
151
+ if (on(vk) && v[f] > L[vk]) push("SPEED", `${j} ${(v[f] * 100).toFixed(1)} cm/frame`, f, v[f], L[vk]);
152
+ // a pop is already a POP group
153
+ if (on(ak) && f > 1 && a[f] > L[ak] && !isPop(k, f, L[ak])) push("ACC", `${j} accel ${(a[f] * 100).toFixed(2)} cm/f^2`, f, a[f], L[ak]);
154
+ }
155
+ }
156
+ if (on("knee")) out.frames.forEach(({ s }, f) => {
157
+ for (const bend of BENDS.slice(0, 2)) {
158
+ const deg = bendAt(sk, s, bend);
159
+ if (deg > L.knee) push("KNEE", `${bend[0] === "knee_l" ? "Left" : "Right"} knee bent ${deg.toFixed(0)}deg`, f, deg, L.knee);
160
+ }
161
+ });
162
+ if (on("balance")) balanceMargins(out, checks.supports).forEach((m, f) => {
163
+ if (m === "air") push("BALANCE", "no foot on the ground", f, 0, 0);
164
+ else if (m !== null && -m > L.balance) {
165
+ const com = scale(["Hips", "Spine1", "Spine2", "Head"].reduce((acc, j) => add(acc, out.frames[f].s.P[sk.index[j]]), [0, 0, 0]), 0.25);
166
+ push("BALANCE", `centre of mass ${com[0].toFixed(2)},${com[2].toFixed(2)} outside the feet`, f, -m, L.balance);
167
+ }
168
+ });
169
+ return issues;
170
+ }
171
+
172
+ /**
173
+ * output.observations: measurements, not verdicts. Frames are absolute (out.f0 added).
174
+ * { units, peaks: { <joint>: { speed: [m/s, f], accel: [m/s^2, f] } } (joints that move: STILL),
175
+ * balance: { min: [m, f], outside?: [f0, f1], air?: [[f0, f1], ...] (at most 4 runs with both
176
+ * feet off the ground: the floor or an object's top) }, bend: { knee_l: [deg, f], ... } }
177
+ */
178
+ export function observeMotion(sk, out, checks = {}) {
179
+ const f0 = out.f0 ?? 0, fps = out.fps ?? 24, peak = (arr, k) => {
180
+ let at = 0;
181
+ for (let f = 1; f < arr.length; f++) if (arr[f] > arr[at]) at = f;
182
+ return [Math.round(arr[at] * k * 100) / 100, at + f0];
183
+ };
184
+ const peaks = Object.fromEntries(kinematics(sk, out).map(({ j, v, a }) => [j, { speed: peak(v, fps), accel: peak(a, fps * fps) }]).filter(([, p]) => p.speed[0] >= STILL));
185
+ const margins = balanceMargins(out, checks.supports), span = test => {
186
+ const fs = margins.map((m, f) => (test(m) ? f + f0 : -1)).filter(f => f >= 0);
187
+ return fs.length ? [fs[0], fs[fs.length - 1]] : null;
188
+ }, air = airRuns(out, checks.supports).slice(0, 4).map(([a, b]) => [a + f0, b + f0]);
189
+ let min = null;
190
+ margins.forEach((m, f) => { if (typeof m === "number" && (!min || m < min[0])) min = [r2(m), f + f0]; });
191
+ const outside = span(m => typeof m === "number" && m < 0);
192
+ const bend = Object.fromEntries(BENDS.map(b => {
193
+ let best = [0, f0];
194
+ out.frames.forEach(({ s }, f) => { const d = bendAt(sk, s, b); if (d > best[0]) best = [d, f + f0]; });
195
+ return [b[0], [Math.round(best[0]), best[1]]];
196
+ }));
197
+ return { units: "[value, frame]; speed m/s, accel m/s^2, balance m inside the feet (< 0 outside), bend deg (0 straight)",
198
+ peaks, balance: { min, ...(outside ? { outside } : {}), ...(air.length ? { air } : {}) }, bend };
199
+ }
200
+
201
+ /**
202
+ * Per frame, how many times too fast the motion is by what QA reports (fix/retime.js): a pop
203
+ * against its surroundings (square root: acceleration falls with the square of the stretch) and
204
+ * the speed or acceleration limits the spec opted into. L = QA_LIMITS merged with own.
205
+ */
206
+ export function speedExcess(sk, out, own, L) {
207
+ const on = k => Object.hasOwn(own ?? {}, k), e = new Float64Array(out.frames.length);
208
+ for (const k of kinematics(sk, out)) {
209
+ const { core, v, a } = k, vk = core ? "speedCore" : "speedLimb", ak = core ? "accCore" : "accLimb";
210
+ for (let f = 1; f < v.length; f++) {
211
+ if (on(vk)) e[f] = Math.max(e[f], v[f] / L[vk]);
212
+ if (f < 2) continue;
213
+ if (isPop(k, f, L[ak])) e[f] = Math.max(e[f], Math.sqrt(a[f] / Math.max(L[ak], POP_RATIO * popLevel(k, f))));
214
+ if (on(ak)) e[f] = Math.max(e[f], Math.sqrt(a[f] / L[ak]));
215
+ }
216
+ }
217
+ return e;
218
+ }
@@ -0,0 +1,69 @@
1
+ // QA WRIST (lane wristqa): a holding wrist at its anatomical limit is broken whatever the motion is for
2
+ // (C13 keeps style measured, not judged; a wrist the arm had to clamp is neither). On the frames a hand
3
+ // holds (wrist-obs.js holding: a fit, a keyed carry, a grip or a carry), the wrist's turn on its forearm
4
+ // is read where the arm asked for it: a keyed carry's need before hold.js clamped it (diag.hold_<k>),
5
+ // else the turn the solved pose shows at the joint, against the whole limits: the wtwist channel turns the
6
+ // hand alone (limbs.js wrist, the forearm bone takes no share), and a grip stays inside the comfortable twist.
7
+ // An axis more than AT deg past its limit (arm-orient.js WRIST_LIMITS: the clamp engaged, or a turn no wrist makes) is an issue:
8
+ // WRIST "hand_r flex 72deg (limit 70)", value the degrees asked, limit the limit
9
+ // Past the comfortable range only is not judged: output.observations.wrist counts it.
10
+ // The hint moves the body, not the wrist. Which move frees it depends on the elbow's path the hold plans
11
+ // over the whole carry (hold-plan.js), so no single frame predicts it (a 10 cm hip drop computed to free
12
+ // the wrist at f90 of bench S6 kept it at its limit once compiled; a 20 deg lean freed 3 of its 4
13
+ // frames): repair compiles the leans instead (repair-tune.js WRIST) and keeps the one that lowers the summed
14
+ // severity with every goal held.
15
+ import { WRIST_LIMITS, wristNeed } from "../compiler/arm-orient.js";
16
+ import { palmAxes } from "../compiler/limbs.js";
17
+ import { apply, len, mul, scale, sub, T } from "../compiler/math.js";
18
+ import { holding } from "./wrist-obs.js";
19
+
20
+ const AT = 0.5, HELD = 0.5, AXES = ["flex", "dev", "twist"], SIDES = { l: "Left", r: "Right" };
21
+ const WORDS = { flex: v => (v >= 0 ? "flex" : "extension"), dev: v => (v >= 0 ? "radial deviation" : "ulnar deviation"), twist: () => "twist" };
22
+
23
+ /** The worst axis of a wrist need against its limits: { axis, value, edge, past } (past > AT is an issue). */
24
+ function worstAxis(need) {
25
+ let best = null;
26
+ for (const n of AXES) {
27
+ const [lo, hi] = WRIST_LIMITS[n], v = need[n], edge = v >= 0 ? hi : lo, past = v >= 0 ? v - hi : lo - v;
28
+ if (!best || past > best.past) best = { axis: n, value: v, edge, past };
29
+ }
30
+ return best;
31
+ }
32
+
33
+ /** The wrist need of hand k on frame i: the hold's (before its clamp) when a keyed carry leads it, else the joint's on the solved pose. */
34
+ function needAt(sk, out, k, i) {
35
+ const { s, diag } = out.frames[i], h = diag[`hold_${k}`];
36
+ if ((h?.w ?? 0) > HELD) return h;
37
+ const S = SIDES[k], I = sk.index, hb = I[`${S}Hand`], fa = I[`${S}ForeArm`], rw = sk.restWorldRot;
38
+ const axT = scale(sub(s.P[hb], s.P[fa]), 1 / Math.max(1e-9, len(sub(s.P[hb], s.P[fa])))), neutral = mul(s.R[fa], mul(T(rw[fa]), rw[hb]));
39
+ return wristNeed(neutral, s.R[hb], axT, apply(mul(neutral, T(s.R[hb])), palmAxes(sk, s, S).l));
40
+ }
41
+
42
+ /** WRIST issues of every holding hand ({ kind, msg, frame, value, limit }, frame an out.frames index). */
43
+ export function wristIssues(sk, out) {
44
+ const issues = [], f0 = out.f0 ?? 0;
45
+ for (const k of ["l", "r"]) out.frames.forEach((_, i) => {
46
+ if (!holding(out, k, i, i + f0)) return;
47
+ const w = worstAxis(needAt(sk, out, k, i));
48
+ if (w.past <= AT) return;
49
+ const lim = Math.abs(w.edge);
50
+ issues.push({ kind: "WRIST", msg: `hand_${k} ${WORDS[w.axis](w.value)} ${Math.round(Math.abs(w.value))}deg (limit ${Math.round(lim)})`, frame: i, value: +Math.abs(w.value).toFixed(2), limit: lim });
51
+ });
52
+ return issues;
53
+ }
54
+
55
+ /** WRIST group -> { cause, hint }: what bends the wrist (fix.self's elbow turn, or where the body stands), and the body moves repair tries (repair-tune.js WRIST). */
56
+ function wristHint(X, g) {
57
+ const m = /^hand_([lr]) /.exec(g.subject), k = m?.[1];
58
+ if (!k) return null;
59
+ const f = g.worstFrame, where = `at f${f}${g.count > 1 ? ` (f${g.first}-f${g.last}, ${g.count} frames)` : ""}`;
60
+ // fix.self turned the elbow about the shoulder-wrist line and the hand kept its turn: the wrist took the turn
61
+ const turn = Math.round(X.out.ctx?.swivel?.[k]?.get(f) ?? 0);
62
+ if (Math.abs(turn) >= 1) return { cause: `fix.self elbow turn ${turn}deg at f${f}`, hint: `${g.subject} ${where}: fix.self turned the elbow ${turn} deg to keep the arm out of the body and the hand kept its turn, so the wrist took it; key elbow_${k} (the elbow pole) or the hand's target so the arm clears the body without the turn` };
63
+ const held = (X.out.frames[X.rel(f)]?.diag[`hold_${k}`]?.w ?? 0) > HELD && Object.keys(X.spec.objects ?? {}).find(id => X.spec.props?.[X.spec.objects[id]?.prop]?.hand === k);
64
+ const lean = X.fr(f).c.spine_pitch ?? 0;
65
+ return { cause: held ? `objects.${held}.keys near f${f}` : X.cause(`armw_${k}`, f),
66
+ hint: `${g.subject} ${where}: the arm cannot turn the hand that far from where the body stands; lean the upper body over the hand (spine_pitch ${Math.round(lean)} at f${f}: try ${Math.round(lean + 20)}) or bring the body to it (root), ${held ? `or key ${held} so the hand need not bend that far (its aim or pos)` : "or move what the hand reaches"}; repair: { rounds } tries leans of 10-30 deg here and keeps one that helps` };
67
+ }
68
+
69
+ export const wristHints = { WRIST: wristHint };
@@ -0,0 +1,60 @@
1
+ // Measured, not judged (output.observations.wrist, lane wrist): how far each wrist bends while its hand
2
+ // holds something. A hand holds on the frames a fit's weight is on (reachGrip, any fit), a keyed carry
3
+ // leads it (diag.hold_<k>), a grip turns it (diag.grip_<k>) or it carries a body. On those frames the
4
+ // hand's turn relative to its forearm is read the way compiler/hold.js turns it (arm-orient.js
5
+ // wristNeed): flex (+ toward the palm), deviation (+ toward the thumb, radial; - ulnar) and the twist the
6
+ // wrist joint shows (the forearm bone takes FOREARM_SHARE of a turn, so the joint's comfortable twist
7
+ // is that share of the forearm's). Frames past the comfortable ranges (arm-orient.js WRIST_COMFORT) and
8
+ // at the anatomical limits (WRIST_LIMITS) are counted.
9
+ // Why: a hand drawn by keys bent 70 deg (the limit) with 33 deg of ulnar deviation for frames while QA
10
+ // stayed clean (bench S6), and no answer said so. The shoulder too (lane elbow): a cup held at the mouth with
11
+ // the upper arm 130-160 deg up, the elbow 20 cm over the shoulder, in most bench cup takes.
12
+ import { wristNeed, WRIST_LIMITS, WRIST_COMFORT, FOREARM_SHARE } from "../compiler/arm-orient.js";
13
+ import { palmAxes } from "../compiler/limbs.js";
14
+ import { apply, len, mul, scale, sub, T } from "../compiler/math.js";
15
+ import { handOf } from "../object-pose.js";
16
+
17
+ const HOLD = 0.5, AT = 1, LEVEL = 90;
18
+ const JOINT = { twist: 1 - FOREARM_SHARE, flex: 1, dev: 1 };
19
+ const range = (k, table) => table[k].map(v => v * JOINT[k]);
20
+
21
+ export const WRIST_UNITS = "wrist: deg on the frames a hand holds (a fit, a carry or a grip on); flex + toward the palm, dev + radial / - ulnar, twist at the wrist joint; [min, f, max, f], frames past the comfortable range and at the limit; grip: the grip a keyed carry of a thin body keeps (grip-choice.js), cost [it, today's] (comfort-priced wrist, reach and body over the keys); elevation [max deg, f] of the upper arm (90 level), raised: frames the elbow is over the shoulder";
22
+
23
+ /** True when the hand of side k holds something on frame i of out (absolute frame f); qa/wrist-checks.js reads it too. */
24
+ export function holding(out, k, i, f) {
25
+ const { diag } = out.frames[i], baked = out.baked ?? {};
26
+ if ((diag[`hold_${k}`]?.w ?? 0) > HOLD || (diag[`grip_${k}`]?.w ?? 0) > HOLD) return true;
27
+ for (const fit of out.ctx.spec.fits ?? []) if (fit.side === k && (baked[`w_${fit.anchor}_${k}`]?.[f] ?? 0) > HOLD) return true;
28
+ return Object.keys(out.ctx.spec.objects ?? {}).some(id => out.ctx.spec.objects[id]?.prop && handOf(out, id, f) === k);
29
+ }
30
+
31
+ /** observeWrist(sk, out) -> { hand_l?, hand_r? } or null when no hand holds anything. Frames are absolute. */
32
+ export function observeWrist(sk, out) {
33
+ const I = sk.index, rw = sk.restWorldRot, f0 = out.f0 ?? 0, res = {};
34
+ for (const [S, k] of [["Left", "l"], ["Right", "r"]]) {
35
+ const h = I[`${S}Hand`], fa = I[`${S}ForeArm`], a = I[`${S}Arm`], row = { frames: 0, past: 0, limit: 0, raised: 0 };
36
+ out.frames.forEach(({ s }, i) => {
37
+ const f = i + f0;
38
+ if (!holding(out, k, i, f)) return;
39
+ const axT = scale(sub(s.P[h], s.P[fa]), 1 / Math.max(1e-9, len(sub(s.P[h], s.P[fa])))), neutral = mul(s.R[fa], mul(T(rw[fa]), rw[h]));
40
+ const need = wristNeed(neutral, s.R[h], axT, apply(mul(neutral, T(s.R[h])), palmAxes(sk, s, S).l));
41
+ row.frames++;
42
+ for (const n of ["flex", "dev", "twist"]) {
43
+ const v = Math.round(need[n]), cur = row[n];
44
+ row[n] = cur ? [Math.min(cur[0], v), v < cur[0] ? f : cur[1], Math.max(cur[2], v), v > cur[2] ? f : cur[3]] : [v, f, v, f];
45
+ }
46
+ // past the comfortable range by more than a degree; at a limit within a degree of it
47
+ const outside = (table, by) => ["flex", "dev", "twist"].some(n => { const [lo, hi] = range(n, table); return need[n] < lo + by || need[n] > hi - by; });
48
+ if (outside(WRIST_COMFORT, -AT)) row.past++;
49
+ if (outside(WRIST_LIMITS, AT)) row.limit++;
50
+ // the shoulder (lane elbow): the upper arm's elevation from hanging down, and the frames the elbow is over the shoulder's level
51
+ const up = sub(s.P[fa], s.P[a]), el = Math.round((Math.acos(Math.max(-1, Math.min(1, -up[1] / Math.max(1e-9, len(up))))) * 180) / Math.PI);
52
+ if (!row.elevation || el > row.elevation[0]) row.elevation = [el, f];
53
+ if (el > LEVEL) row.raised++;
54
+ });
55
+ if (row.frames) res[`hand_${k}`] = row;
56
+ }
57
+ // the grip a keyed carry chose for where it goes (grip-choice.js): what the hand of that carry holds
58
+ for (const g of out.ctx.hold?.grips?.() ?? []) if (res[`hand_${g.hand}`]) res[`hand_${g.hand}`].grip = { object: g.id, chose: g.name, cost: [g.cost, g.today] };
59
+ return Object.keys(res).length ? res : null;
60
+ }
@@ -0,0 +1,120 @@
1
+ // C2. Point references, the one grammar goals, probes and the timeline use (spec.checks keep
2
+ // their older joint/prop/anchor subset, validate-common.js refOk):
3
+ // joint:<Bone> | bone:<Bone> the bone's joint (the two are the same; names go through bones.js)
4
+ // prop:<prop>.<point> a point of a held prop (spec.props; "origin" always exists)
5
+ // anchor:<name> a spec anchor, resolved as the compiler resolves it
6
+ // landmark:mouth the measured face landmark
7
+ // object:<id>[.center|.top|.base] a body or a group (object-pose.js objectBoxesAt): the centre,
8
+ // top-face centre or bottom-face centre of the AABB of its
9
+ // part boxes' corners (default center)
10
+ // Frames are absolute; a frame outside the compiled window (out.f0 ..) answers null.
11
+ import { REF_KINDS } from "./schema/fields.js";
12
+ import { resolveBone } from "./bones.js";
13
+ import { objectBoxesAt, partsOf } from "./object-pose.js";
14
+ import { objectOk } from "./compiler/validate-common.js";
15
+ import { resolveAnchor } from "./compiler/anchors.js";
16
+ import { bonePoint } from "./compiler/skeleton.js";
17
+ import { didYouMean } from "./compiler/vocab.js";
18
+ import { add, apply, sub, len } from "./compiler/math.js";
19
+ import { sdBox } from "./qa/geometry.js";
20
+
21
+ const SYNTAX = { joint: "joint:<Bone>", bone: "bone:<Bone>", prop: "prop:<prop>.<point>", anchor: "anchor:<name>", landmark: "landmark:mouth", object: "object:<id>[.center|.top|.base]" };
22
+
23
+ function parse(ref) {
24
+ const m = /^([a-z]+):(.+)$/s.exec(String(ref));
25
+ if (!m) return null;
26
+ const [, kind, rest] = m;
27
+ if (kind === "prop") { const [name, point] = rest.split("."); return { kind, name, point: point ?? "" }; }
28
+ if (kind === "object") { const o = /^(.+)\.(center|top|base)$/s.exec(rest); return { kind, name: o ? o[1] : rest, point: o ? o[2] : null }; }
29
+ return { kind, name: rest, point: null };
30
+ }
31
+
32
+ function boxesPoint(boxes, which) {
33
+ const lo = [Infinity, Infinity, Infinity], hi = [-Infinity, -Infinity, -Infinity];
34
+ for (const b of boxes) for (const x of [-1, 1]) for (const y of [0, 2]) for (const z of [-1, 1]) {
35
+ const p = add(b.pos, apply(b.R, [x * b.half[0], y * b.half[1], z * b.half[2]]));
36
+ for (let i = 0; i < 3; i++) { lo[i] = Math.min(lo[i], p[i]); hi[i] = Math.max(hi[i], p[i]); }
37
+ }
38
+ const c = [(lo[0] + hi[0]) / 2, (lo[1] + hi[1]) / 2, (lo[2] + hi[2]) / 2];
39
+ return which === "top" ? [c[0], hi[1], c[2]] : which === "base" ? [c[0], lo[1], c[2]] : c;
40
+ }
41
+
42
+ /** World point [x, y, z] of `ref` at absolute frame f of compile result out, or null. */
43
+ export function refPoint(out, f, ref) {
44
+ const fr = out.frames?.[f - (out.f0 ?? 0)], r = parse(ref);
45
+ if (!fr || !r) return null;
46
+ const { s, diag, c } = fr, sk = out.ctx.sk;
47
+ switch (r.kind) {
48
+ case "joint": case "bone": { const { bone } = resolveBone(sk, r.name); return bone ? s.P[sk.index[bone]].slice() : null; }
49
+ case "prop": { const p = diag.props?.[r.name]?.[r.point]; return Array.isArray(p) ? p.slice() : null; }
50
+ case "anchor": return out.ctx.spec.anchors?.[r.name] ? resolveAnchor(out.ctx, r.name, s, { ...diag, frame: f }, c) : null;
51
+ case "landmark": return sk.landmarks?.[r.name] ? bonePoint(s, sk.index.Head, sk.landmarks[r.name]) : null;
52
+ case "object": { const boxes = objectBoxesAt(out, r.name, f); return boxes ? boxesPoint(boxes, r.point) : null; }
53
+ default: return null;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Metres between refs a and b at absolute frame f, or null when either is unknown there. A bare
59
+ * object:<id> (no .center/.top/.base) means "to the object's surface": the distance from the
60
+ * other point to the nearest part box (sdBox, 0 inside); two bare objects measure a's centre to
61
+ * b's surface. Any other pair is the distance between the two points.
62
+ */
63
+ export function refDistance(out, f, a, b) {
64
+ const bare = ref => { const r = parse(ref); return r?.kind === "object" && r.point === null ? r : null; };
65
+ const surf = bare(b) ? [a, bare(b)] : bare(a) ? [b, bare(a)] : null;
66
+ if (surf) {
67
+ const p = refPoint(out, f, surf[0]), boxes = objectBoxesAt(out, surf[1].name, f);
68
+ return p && boxes ? Math.max(0, Math.min(...boxes.map(box => sdBox(p, box)))) : null;
69
+ }
70
+ const p = refPoint(out, f, a), q = refPoint(out, f, b);
71
+ return p && q ? len(sub(p, q)) : null;
72
+ }
73
+
74
+ /**
75
+ * The prefixed refs a bare name could mean in this spec, rig and scene (`; did you mean
76
+ * "object:empty-2"?`), or "". A field that takes more than one ref kind stays strict.
77
+ */
78
+ function bareHint(spec, { sk, scene }, ref, kinds) {
79
+ if (typeof ref !== "string" || !ref) return "";
80
+ const id = ref.replace(/\.(center|top|base)$/, ""), [prop, point] = ref.split(".");
81
+ const bone = sk && (kinds.includes("joint") || kinds.includes("bone")) ? resolveBone(sk, ref).bone : null;
82
+ const known = [
83
+ kinds.includes("object") && (scene?.objects?.[id] || partsOf(scene, id).length) && `object:${ref}`,
84
+ bone && `${kinds.includes("joint") ? "joint" : "bone"}:${bone}`,
85
+ kinds.includes("anchor") && spec.anchors?.[ref] && `anchor:${ref}`,
86
+ kinds.includes("prop") && spec.props?.[prop] && (point === "origin" || spec.props[prop].points?.[point]) && `prop:${ref}`,
87
+ kinds.includes("landmark") && ref === "mouth" && "landmark:mouth",
88
+ ].filter(Boolean);
89
+ return known.length ? `; did you mean ${known.map(k => `"${k}"`).join(" or ")}?` : "";
90
+ }
91
+
92
+ /**
93
+ * Validates ref against the spec (props, anchors), the rig (bones) and the scene (objects and
94
+ * groups). Reports one exact message through err, prefixed `${at} "${ref}"`, and answers whether
95
+ * the ref is good. kinds limits the grammar (default all of REF_KINDS). A bare name (no kind:)
96
+ * is refused with the forms it could mean (bareHint).
97
+ */
98
+ export function checkRef(spec, err, { sk, scene }, at, ref, kinds = REF_KINDS) {
99
+ const r = parse(ref), tag = `${at} "${ref}"`;
100
+ if (!r || !kinds.includes(r.kind)) { err(`${tag} must be ${kinds.map(k => SYNTAX[k]).join(", ")}${r ? "" : bareHint(spec, { sk, scene }, ref, kinds)}`); return false; }
101
+ if (r.kind === "joint" || r.kind === "bone") {
102
+ const { bone, hint } = sk ? resolveBone(sk, r.name) : { bone: r.name };
103
+ if (!bone) err(`${tag}: unknown bone${hint}`);
104
+ return Boolean(bone);
105
+ }
106
+ if (r.kind === "prop") {
107
+ const ok = Boolean(spec.props?.[r.name] && (r.point === "origin" || spec.props[r.name].points?.[r.point]));
108
+ if (!ok) err(`${tag}: spec.props has no point ${r.name}.${r.point}`);
109
+ return ok;
110
+ }
111
+ if (r.kind === "anchor") {
112
+ if (!spec.anchors?.[r.name]) err(`${tag}: no anchor "${r.name}" in spec.anchors${didYouMean(r.name, Object.keys(spec.anchors ?? {}))}`);
113
+ return Boolean(spec.anchors?.[r.name]);
114
+ }
115
+ if (r.kind === "landmark") {
116
+ if (r.name !== "mouth") err(`${tag}: the only landmark is "mouth"`);
117
+ return r.name === "mouth";
118
+ }
119
+ return objectOk(err, scene, tag, r.name, { groups: true });
120
+ }
@@ -0,0 +1,121 @@
1
+ // C6. Repair's second move: a QA group just past its limit that carries no patch. Its cause names
2
+ // what made it, and a skill arg or a key time maps to the geometry too indirectly for a hint to
3
+ // predict (in the cup take, firstStep -1 cm cleared a thigh hit but a 5.04 cm/f^2 pop appeared;
4
+ // step did nothing). A compile is cheap, so repair tries a few small changes instead and keeps
5
+ // the best (repair.js), like an automatic { range } that stays near the agent's value:
6
+ // cause "... use[i] <skill> ..." each arg in SKILL_INFO[skill].tune, 1 then 2 steps
7
+ // either way, from the arg as sent or its default
8
+ // SPEED/ACC/POP "between keys[a] fA and keys[b] fB" (the agent's keys)
9
+ // key a 1 or 2 frames earlier, key b later
10
+ // GOAL up to 10 cm short (with or without a patch): where the body stands at the worst frame,
11
+ // 1 then 2 steps either way: the agent's root keys
12
+ // there (x, z), else the last standIdle/walkTo before
13
+ // it (SKILL_INFO[skill].place); then the lean, the
14
+ // spine_pitch of the agent's keys around it (5 deg)
15
+ // WRIST (qa/wrist-checks.js) at the worst frame: a lean there (a spine_pitch key 10, 20 or 30 deg
16
+ // forward of the lean before it, or 10 back, eased back to it 12
17
+ // frames either side when no lean key is nearer), then the lean
18
+ // keys around it 5 or 10 deg either way; which move frees
19
+ // a held wrist depends on the elbow path the hold plans, so it is
20
+ // compiled (on bench S6 the body's floor steps only made it worse)
21
+ // ; none when fix.self's elbow turn bent it (cause "fix.self ..."): a lean
22
+ // does not undo the turn (car exit: 21 compiles, 20 s, 57 -> 51 deg)
23
+ // Every candidate is a JSON Patch on the agent's spec; the order is fixed, so the search is
24
+ // deterministic. Least-change candidates come first, so a tie keeps the smaller edit.
25
+ import { excessOf } from "./qa/severity.js";
26
+ import { SKILL_INFO } from "./skills/info.js";
27
+
28
+ const NEAR = 0.5, STEPS = [-1, 1, -2, 2], PLACE_MAX = 0.1, LEAN = 5;
29
+ const isNum = x => typeof x === "number" && Number.isFinite(x);
30
+
31
+ /** A group no more than half its limit (or 5 mm, for small limits) past it. */
32
+ const nearLimit = g => excessOf(g) <= NEAR * Math.max(Math.abs(g.limit), 0.01);
33
+
34
+ /** use[i] <skill>: one patch per tune arg and step, smallest steps first. */
35
+ function skillCandidates(cause, spec) {
36
+ const m = /\buse\[(\d+)\] (\w+)/.exec(cause), i = Number(m?.[1]), u = m && spec.use?.[i];
37
+ const info = m && u?.skill === m[2] ? SKILL_INFO[u.skill] : null;
38
+ if (!info?.tune) return [];
39
+ const out = [];
40
+ for (const n of [1, 2]) for (const [arg, step] of Object.entries(info.tune)) {
41
+ const schema = info.args[arg] ?? {}, now = u.args?.[arg] ?? schema.default;
42
+ if (!isNum(now)) continue;
43
+ for (const sg of STEPS.filter(s => Math.abs(s) === n)) {
44
+ const raw = now + sg * step, value = schema.type === "integer" ? Math.round(raw) : Math.round(raw * 1e4) / 1e4;
45
+ if (!(value > 0) || value === now) continue;
46
+ out.push(u.args ? [{ op: u.args[arg] === undefined ? "add" : "replace", path: `/use/${i}/args/${arg}`, value }]
47
+ : [{ op: "add", path: `/use/${i}/args`, value: { [arg]: value } }]);
48
+ }
49
+ }
50
+ return out;
51
+ }
52
+
53
+ /** SPEED/ACC/POP between two of the agent's keys: more frames between them, 1 then 2. */
54
+ function keyCandidates(g, cause, spec) {
55
+ if (!["SPEED", "ACC", "POP"].includes(g.kind)) return [];
56
+ const m = /between keys\[(\d+)\] f(\d+) and keys\[(\d+)\] f(\d+)/.exec(cause);
57
+ if (!m) return [];
58
+ const [a, fa, b, fb] = m.slice(1).map(Number), keys = spec.keys ?? [];
59
+ if (keys[a]?.f !== fa || keys[b]?.f !== fb) return [];
60
+ const last = Number.isInteger(spec.frames) ? spec.frames - 1 : Infinity, out = [];
61
+ for (const n of [1, 2]) {
62
+ if (fa - n >= 0) out.push([{ op: "replace", path: `/keys/${a}/f`, value: fa - n }]);
63
+ if (fb + n <= last) out.push([{ op: "replace", path: `/keys/${b}/f`, value: fb + n }]);
64
+ }
65
+ return out;
66
+ }
67
+
68
+ const round4 = v => Math.round(v * 1e4) / 1e4;
69
+
70
+ /** One step (n = 1 or 2, either way) of what places the body at frame f, then of the lean. */
71
+ function placeSteps(spec, f, n) {
72
+ const keys = spec.keys ?? [], out = [];
73
+ const skill = (spec.use ?? []).map((u, i) => ({ u, i })).filter(({ u }) => (u?.skill === "standIdle" || u?.skill === "walkTo") && u.f0 <= f).sort((a, b) => a.u.f0 - b.u.f0).at(-1);
74
+ const roots = keys.map((k, j) => ({ k, j })).filter(({ k }) => Array.isArray(k?.set?.root) && k.set.root.length === 3 && isNum(k.f) && k.f > (skill?.u.f0 ?? -1));
75
+ if (roots.some(({ k }) => k.f <= f)) {
76
+ for (const axis of [0, 2]) for (const sg of [-n, n]) out.push(roots.map(({ k, j }) => ({ op: "replace", path: `/keys/${j}/set/root`, value: k.set.root.map((v, a) => (a === axis ? round4(v + sg * 0.03) : v)) })));
77
+ } else if (skill) {
78
+ const { u, i } = skill, place = SKILL_INFO[u.skill].place ?? {}, steps = u.args?.steps ?? 8;
79
+ for (const [arg, step] of Object.entries(place)) {
80
+ if (arg === "step" && steps <= 2) continue;
81
+ const now = u.args?.[arg] ?? SKILL_INFO[u.skill].args[arg]?.default;
82
+ const set = value => [u.args ? { op: u.args[arg] === undefined ? "add" : "replace", path: `/use/${i}/args/${arg}`, value } : { op: "add", path: `/use/${i}/args`, value: { [arg]: value } }];
83
+ if (Array.isArray(now)) for (const axis of [0, now.length - 1]) for (const sg of [-n, n]) out.push(set(now.map((v, a) => (a === axis ? round4(v + sg * step) : v))));
84
+ else if (isNum(now)) for (const sg of [-n, n]) if (now + sg * step > 0.05) out.push(set(round4(now + sg * step)));
85
+ }
86
+ }
87
+ const pitched = keys.map((k, j) => ({ f: k?.f, j })).filter(({ j }) => isNum(keys[j]?.set?.spine_pitch) && isNum(keys[j].f)).sort((a, b) => a.f - b.f);
88
+ const lean = [pitched.filter(p => p.f <= f).at(-1), pitched.find(p => p.f > f)].filter(Boolean).map(p => p.j);
89
+ for (const sg of lean.length ? [n, -n] : []) out.push(lean.map(j => ({ op: "replace", path: `/keys/${j}/set/spine_pitch`, value: keys[j].set.spine_pitch + sg * LEAN })));
90
+ return out;
91
+ }
92
+
93
+ const LEANS = [10, 20, 30, -10], EASE = 12;
94
+
95
+ /** WRIST: a spine_pitch key at f that leans the upper body by d from the lean before it, eased back over EASE frames where no lean key is nearer. */
96
+ function leanSteps(spec, f) {
97
+ const keys = spec.keys ?? [], at = Math.round(f), last = Number.isInteger(spec.frames) ? spec.frames - 1 : Infinity;
98
+ const pitched = keys.filter(k => isNum(k?.f) && isNum(k?.set?.spine_pitch)).sort((a, b) => a.f - b.f);
99
+ const prev = pitched.filter(k => k.f < at).at(-1), next = pitched.find(k => k.f > at), base = prev?.set.spine_pitch ?? 0;
100
+ const same = keys.findIndex(k => k?.f === at && k.set && typeof k.set === "object");
101
+ return LEANS.map(d => {
102
+ const value = base + d, ops = [same >= 0 ? { op: keys[same].set.spine_pitch === undefined ? "add" : "replace", path: `/keys/${same}/set/spine_pitch`, value } : { op: "add", path: "/keys/-", value: { f: at, set: { spine_pitch: value } } }];
103
+ if ((!prev || at - prev.f > EASE) && at - EASE >= 0) ops.push({ op: "add", path: "/keys/-", value: { f: at - EASE, set: { spine_pitch: base } } });
104
+ if ((!next || next.f - at > EASE) && at + EASE <= last) ops.push({ op: "add", path: "/keys/-", value: { f: at + EASE, set: { spine_pitch: next?.set.spine_pitch ?? base } } });
105
+ return ops;
106
+ });
107
+ }
108
+
109
+ /**
110
+ * tuneCandidates(g, spec) -> patch[] (each a JSON Patch on spec; [] when the group is not near its
111
+ * limit, already carries a patch, or its cause names nothing tunable; a GOAL group up to 10 cm
112
+ * short also gets the body placement steps)
113
+ */
114
+ export function tuneCandidates(g, spec) {
115
+ if (typeof g.cause !== "string") return [];
116
+ if (g.kind === "WRIST" && g.cause.startsWith("fix.self")) return [];
117
+ if (g.kind === "WRIST" && isNum(g.worstFrame)) return [...leanSteps(spec, g.worstFrame), ...[1, 2].flatMap(n => placeSteps(spec, g.worstFrame, n)).filter(p => p.every(op => op.path.endsWith("/spine_pitch")))];
118
+ const near = !g.patch && nearLimit(g) ? [...skillCandidates(g.cause, spec), ...keyCandidates(g, g.cause, spec)] : [];
119
+ if (g.kind !== "GOAL" || !isNum(g.worstFrame) || excessOf(g) > PLACE_MAX) return near;
120
+ return [...near, ...placeSteps(spec, g.worstFrame, 1), ...placeSteps(spec, g.worstFrame, 2)];
121
+ }