cozyclay 2.1.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (583) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +12 -30
  3. package/bin/agent/agent-routes.mjs +40 -113
  4. package/bin/agent/agent-runner.mjs +9 -10
  5. package/bin/agent/cliproxy-provider.mjs +135 -0
  6. package/bin/agent/credential-store.mjs +30 -7
  7. package/bin/agent/pi-tools.mjs +4 -1
  8. package/bin/agent/provider-keys.mjs +32 -0
  9. package/bin/agent/provider-login.mjs +110 -0
  10. package/bin/agent/provider-routes.mjs +99 -0
  11. package/bin/agent/providers.mjs +19 -97
  12. package/bin/agent/script-tool.mjs +17 -4
  13. package/bin/agent/studio-motion-reference.mjs +23 -0
  14. package/bin/agent/studio-prompt.mjs +1 -1
  15. package/bin/agent/studio-rebase-text.mjs +32 -0
  16. package/bin/agent/studio-rebase.mjs +26 -39
  17. package/bin/agent/studio-tools.mjs +9 -28
  18. package/bin/agent/tool-arguments.mjs +49 -0
  19. package/bin/agent/turn-frames.mjs +66 -0
  20. package/bin/codex-auth.mjs +9 -2
  21. package/bin/cozyclay.mjs +10 -116
  22. package/bin/live/cli.mjs +22 -6
  23. package/bin/mcp-runtime.mjs +2 -12
  24. package/bin/package-signature.mjs +0 -4
  25. package/dist/app/index.html +7 -6
  26. package/dist/assets/EXRLoader-CvVIvGVE.js +1 -0
  27. package/dist/assets/analytics-C9sUnndd.js +1 -0
  28. package/dist/assets/app-CCh--1Hc.js +5087 -0
  29. package/dist/assets/app-f-fkV5uP.css +1 -0
  30. package/dist/assets/compile-worker-CkxiD6lH.js +5 -0
  31. package/dist/assets/fflate.module-KbGj5-H6.js +5 -0
  32. package/dist/assets/{first-shot-handoff-BlT33CGf.js → first-shot-handoff-CC4xe4HM.js} +1 -1
  33. package/dist/assets/{landing-BSesrCpL.js → landing-DjHD8u7k.js} +1 -1
  34. package/dist/assets/scene-asset-sync-Hx_FzdOH.js +20 -0
  35. package/dist/assets/scene-asset-sync-_S8NQl5B.css +1 -0
  36. package/dist/assets/workflow-C4yKH1p4.js +179 -0
  37. package/dist/cozyclay-package.json +1 -1
  38. package/dist/index.html +3 -3
  39. package/dist/llms.txt +14 -0
  40. package/dist/sw.js +3 -6
  41. package/dist/workflow/index.html +5 -4
  42. package/mcp/LIVE-PROTOCOL.md +7 -47
  43. package/mcp/README.md +16 -26
  44. package/mcp/live-hub.mjs +6 -10
  45. package/mcp/package.json +1 -3
  46. package/mcp/tool-handlers.mjs +4 -168
  47. package/package.json +3 -22
  48. package/src/App.jsx +174 -2639
  49. package/src/agent-progress.js +79 -0
  50. package/src/agent-turn-mark.js +30 -0
  51. package/src/analytics.js +1 -19
  52. package/src/app-context.js +5 -0
  53. package/src/app-stage.jsx +2 -34
  54. package/src/asset-pane.jsx +6 -5
  55. package/src/camera-block.js +21 -0
  56. package/src/camera-direction.js +120 -0
  57. package/src/character-kind.js +3 -9
  58. package/src/command-bus.js +8 -6
  59. package/src/commands/cast.js +5 -34
  60. package/src/commands/elements/character.js +1 -1
  61. package/src/commands/elements/motion.js +1 -6
  62. package/src/commands/elements/object.js +6 -2
  63. package/src/commands/elements/shot.js +4 -1
  64. package/src/commands/elements.js +38 -14
  65. package/src/commands/index.js +3 -1
  66. package/src/commands/kits.js +37 -0
  67. package/src/commands/motion-timing.js +57 -0
  68. package/src/commands/motion.js +9 -152
  69. package/src/commands/motion3-compile.js +65 -0
  70. package/src/commands/motion3.js +207 -0
  71. package/src/commands/objects.js +6 -4
  72. package/src/commands/shot.js +49 -11
  73. package/src/commands/stage.js +14 -1
  74. package/src/commands/view.js +4 -1
  75. package/src/document-mirrors.js +42 -0
  76. package/src/document-store.js +4 -0
  77. package/src/domains/cast.js +19 -194
  78. package/src/domains/motion/motion-range-pin-rules.js +1 -1
  79. package/src/domains/motion/take-source.js +51 -0
  80. package/src/domains/motion.js +60 -1838
  81. package/src/domains/scenes.js +12 -55
  82. package/src/domains/shots.js +2 -2
  83. package/src/domains/stage.js +4 -2
  84. package/src/draft-ghost.js +46 -0
  85. package/src/draft-ghost.jsx +129 -0
  86. package/src/dualview.jsx +4 -3
  87. package/src/effects/EffectsLayer.jsx +112 -0
  88. package/src/effects/effect-meshes.js +232 -0
  89. package/src/effects/effect-record.js +103 -0
  90. package/src/effects/emitter.js +59 -0
  91. package/src/effects/simulate.js +261 -0
  92. package/src/effects/state.js +59 -0
  93. package/src/execution-telemetry.js +2 -3
  94. package/src/first-success-guide.jsx +5 -5
  95. package/src/ghost-set.js +257 -0
  96. package/src/held-object-edit.jsx +113 -0
  97. package/src/held-object-pose.js +175 -0
  98. package/src/hierarchy-model.js +1 -1
  99. package/src/hierarchy-panel.jsx +1 -1
  100. package/src/imported-mesh.jsx +103 -0
  101. package/src/kits/held.js +84 -0
  102. package/src/kits/index.js +70 -0
  103. package/src/kits/smoking.js +57 -0
  104. package/src/light-blockers.js +93 -0
  105. package/src/motion/beat-board-model.js +84 -0
  106. package/src/motion/beat-board.css +124 -0
  107. package/src/motion/beat-board.jsx +59 -0
  108. package/src/motion/center-of-mass.js +66 -0
  109. package/src/motion/fix-collisions.js +850 -0
  110. package/src/motion/physics-review.js +118 -0
  111. package/src/{ardy → motion}/physics-support.js +1 -76
  112. package/src/motion/rig-snapshot.js +22 -0
  113. package/src/motion/take-segments.jsx +140 -0
  114. package/src/{ardy → motion}/timeline-coordinates.js +0 -21
  115. package/src/{ardy → motion}/timeline.css +1 -2
  116. package/src/{ardy → motion}/timeline.jsx +48 -419
  117. package/src/motion/waypoints.js +76 -0
  118. package/src/motion3/beats.js +83 -0
  119. package/src/motion3/body.js +158 -0
  120. package/src/motion3/bones.js +47 -0
  121. package/src/motion3/carried.js +47 -0
  122. package/src/motion3/compile-job.js +75 -0
  123. package/src/motion3/compile-worker.js +10 -0
  124. package/src/motion3/compiler/anchors.js +105 -0
  125. package/src/motion3/compiler/arm-orient.js +128 -0
  126. package/src/motion3/compiler/channels.js +125 -0
  127. package/src/motion3/compiler/compile.js +205 -0
  128. package/src/motion3/compiler/gait-rhythm.js +97 -0
  129. package/src/motion3/compiler/grip.js +171 -0
  130. package/src/motion3/compiler/head-steady.js +48 -0
  131. package/src/motion3/compiler/hold.js +134 -0
  132. package/src/motion3/compiler/idle.js +113 -0
  133. package/src/motion3/compiler/limbs.js +170 -0
  134. package/src/motion3/compiler/math.js +72 -0
  135. package/src/motion3/compiler/pose.js +162 -0
  136. package/src/motion3/compiler/quat.js +49 -0
  137. package/src/motion3/compiler/skeleton.js +74 -0
  138. package/src/motion3/compiler/validate-common.js +32 -0
  139. package/src/motion3/compiler/validate-goals.js +118 -0
  140. package/src/motion3/compiler/validate-objects.js +143 -0
  141. package/src/motion3/compiler/validate-parts.js +116 -0
  142. package/src/motion3/compiler/validate.js +122 -0
  143. package/src/motion3/compiler/vocab.js +73 -0
  144. package/src/motion3/compiler/walk-pelvis.js +275 -0
  145. package/src/motion3/compiler/walk.js +207 -0
  146. package/src/motion3/compiler/wrist-clamp.js +32 -0
  147. package/src/motion3/delta.js +47 -0
  148. package/src/motion3/describe.js +174 -0
  149. package/src/motion3/docs.js +27 -0
  150. package/src/motion3/edit-carry.js +21 -0
  151. package/src/motion3/fix/ease.js +16 -0
  152. package/src/motion3/fix/ground.js +37 -0
  153. package/src/motion3/fix/index.js +52 -0
  154. package/src/motion3/fix/push.js +71 -0
  155. package/src/motion3/fix/reach.js +46 -0
  156. package/src/motion3/fix/retime.js +79 -0
  157. package/src/motion3/fix/self.js +211 -0
  158. package/src/motion3/fix/slide.js +70 -0
  159. package/src/motion3/fix/turn.js +51 -0
  160. package/src/motion3/follow.js +39 -0
  161. package/src/motion3/free-motion.js +173 -0
  162. package/src/motion3/grip-choice.js +130 -0
  163. package/src/motion3/hold-plan.js +157 -0
  164. package/src/motion3/json-patch.js +77 -0
  165. package/src/motion3/key-aim.js +105 -0
  166. package/src/motion3/key-edit.js +82 -0
  167. package/src/motion3/lenient.js +127 -0
  168. package/src/motion3/object-keys.js +236 -0
  169. package/src/motion3/object-pose.js +182 -0
  170. package/src/motion3/object-tracks.js +12 -0
  171. package/src/motion3/pipeline.js +102 -0
  172. package/src/motion3/playback.js +51 -0
  173. package/src/motion3/probes.js +91 -0
  174. package/src/motion3/qa/body-checks.js +34 -0
  175. package/src/motion3/qa/check-names.js +14 -0
  176. package/src/motion3/qa/checks.js +121 -0
  177. package/src/motion3/qa/contact-checks.js +51 -0
  178. package/src/motion3/qa/explain.js +80 -0
  179. package/src/motion3/qa/fmt.js +5 -0
  180. package/src/motion3/qa/foot-checks.js +72 -0
  181. package/src/motion3/qa/geometry.js +77 -0
  182. package/src/motion3/qa/goals.js +191 -0
  183. package/src/motion3/qa/ground.js +60 -0
  184. package/src/motion3/qa/hints-contact.js +179 -0
  185. package/src/motion3/qa/hints-goal.js +160 -0
  186. package/src/motion3/qa/hints-knee.js +24 -0
  187. package/src/motion3/qa/hints-motion.js +209 -0
  188. package/src/motion3/qa/hints-object.js +91 -0
  189. package/src/motion3/qa/index.js +42 -0
  190. package/src/motion3/qa/key-checks.js +82 -0
  191. package/src/motion3/qa/near.js +96 -0
  192. package/src/motion3/qa/object-checks.js +135 -0
  193. package/src/motion3/qa/patch.js +161 -0
  194. package/src/motion3/qa/severity.js +38 -0
  195. package/src/motion3/qa/spec-checks.js +65 -0
  196. package/src/motion3/qa/style.js +218 -0
  197. package/src/motion3/qa/wrist-checks.js +69 -0
  198. package/src/motion3/qa/wrist-obs.js +60 -0
  199. package/src/motion3/refs.js +120 -0
  200. package/src/motion3/repair-tune.js +121 -0
  201. package/src/motion3/repair.js +83 -0
  202. package/src/motion3/report.js +67 -0
  203. package/src/motion3/rigs/index.js +14 -0
  204. package/src/motion3/rigs/x-bot-tpose-body.json +1 -0
  205. package/src/motion3/rigs/x-bot-tpose.json +1 -0
  206. package/src/motion3/rigs/y-bot-tpose-body.json +1 -0
  207. package/src/motion3/rigs/y-bot-tpose.json +1 -0
  208. package/src/motion3/scene.js +60 -0
  209. package/src/motion3/schema/brief.js +89 -0
  210. package/src/motion3/schema/fields.js +44 -0
  211. package/src/motion3/schema/spec-examples.js +46 -0
  212. package/src/motion3/schema/topics.js +111 -0
  213. package/src/motion3/schema/types.js +147 -0
  214. package/src/motion3/search.js +117 -0
  215. package/src/motion3/second-hand.js +112 -0
  216. package/src/motion3/skills/basic.js +82 -0
  217. package/src/motion3/skills/book.js +76 -0
  218. package/src/motion3/skills/car.js +145 -0
  219. package/src/motion3/skills/frames-auto.js +29 -0
  220. package/src/motion3/skills/index.js +83 -0
  221. package/src/motion3/skills/info.js +65 -0
  222. package/src/motion3/skills/smoke-kit.js +109 -0
  223. package/src/motion3/skills/smoke.js +160 -0
  224. package/src/motion3/spec-store.js +59 -0
  225. package/src/motion3/take-edit.js +155 -0
  226. package/src/motion3/take.js +63 -0
  227. package/src/motion3/timeline.js +127 -0
  228. package/src/motion3/views.js +121 -0
  229. package/src/motion3/walk-plan.js +119 -0
  230. package/src/object-catalog.jsx +7 -1
  231. package/src/object-colors.js +98 -0
  232. package/src/object-gizmo-parts.js +115 -0
  233. package/src/object-gizmo.jsx +37 -122
  234. package/src/object-glow.js +139 -0
  235. package/src/object-hidden.js +33 -0
  236. package/src/object-look.js +196 -0
  237. package/src/object-size.js +10 -0
  238. package/src/object-travel.js +71 -6
  239. package/src/panels/CameraPanel.jsx +7 -7
  240. package/src/panels/HdrEnvironmentControls.jsx +59 -0
  241. package/src/panels/LightPanel.jsx +25 -4
  242. package/src/panels/ObjectLookPanel.jsx +88 -0
  243. package/src/panels/PoseImagePanel.jsx +1 -1
  244. package/src/panels/PosePanel.jsx +2 -17
  245. package/src/panels/RigControlPanel.jsx +5 -53
  246. package/src/panels/SubjectBox.jsx +4 -1
  247. package/src/panels/details.css +14 -0
  248. package/src/posestudio.jsx +4 -27
  249. package/src/project-browser.jsx +3 -4
  250. package/src/project-resources.js +18 -31
  251. package/src/project.js +13 -104
  252. package/src/props.jsx +25 -280
  253. package/src/rail-dot-drag.js +23 -0
  254. package/src/render-passes.js +10 -0
  255. package/src/resource-status.jsx +1 -0
  256. package/src/result-modal.jsx +4 -19
  257. package/src/room.jsx +20 -12
  258. package/src/scene-assets.js +8 -6
  259. package/src/scene-light-place.js +72 -0
  260. package/src/scene-lights.jsx +148 -0
  261. package/src/scene-objects.js +31 -139
  262. package/src/scenes.js +15 -105
  263. package/src/semantic-edit.js +2 -5
  264. package/src/set-pieces.jsx +183 -0
  265. package/src/settings-menu.jsx +3 -11
  266. package/src/shell/BottomDock.jsx +11 -53
  267. package/src/shell/CompositionGuideButton.jsx +20 -0
  268. package/src/shell/DetailsSlot.jsx +16 -200
  269. package/src/shell/GhostsMenuGroup.jsx +74 -0
  270. package/src/shell/LibrarySlot.jsx +1 -3
  271. package/src/shell/MenuBar.jsx +3 -3
  272. package/src/shell/PreferencesDialog.jsx +15 -264
  273. package/src/shell/PreferencesSlot.jsx +2 -8
  274. package/src/shell/ShotPreviewHeader.jsx +17 -0
  275. package/src/shell/TopBar.jsx +5 -83
  276. package/src/shell/ViewportToolbar.jsx +23 -93
  277. package/src/shell/agent-glass.css +19 -6
  278. package/src/shell/details-selection.js +42 -0
  279. package/src/shell/ghosts-menu.css +22 -0
  280. package/src/shell/glass-regions.css +29 -3
  281. package/src/shell/preferences.css +6 -1
  282. package/src/shell/viewport.css +12 -0
  283. package/src/shot-authoring.js +4 -7
  284. package/src/soft-shadows.js +37 -0
  285. package/src/stage-attach-frames.js +12 -12
  286. package/src/stage-canvas-rigs.jsx +3 -3
  287. package/src/stage-capture.jsx +42 -8
  288. package/src/stage-characters.jsx +22 -216
  289. package/src/stage-editor-gizmos.jsx +6 -6
  290. package/src/stage-environment.js +93 -0
  291. package/src/stage-hdr-builtins.js +107 -0
  292. package/src/stage-hdr-image.js +116 -0
  293. package/src/stage-hdr-library.js +70 -0
  294. package/src/stage-hdr-map.js +94 -0
  295. package/src/stage-hdr.js +130 -0
  296. package/src/stage-lighting.js +149 -0
  297. package/src/stage-occlusion.js +210 -0
  298. package/src/stage-sky.jsx +159 -0
  299. package/src/stage-startup-defaults.js +5 -9
  300. package/src/studio-actions.js +12 -14
  301. package/src/studio-agent-commands.js +46 -16
  302. package/src/studio-agent-motion.js +9 -9
  303. package/src/studio-agent-protocol.js +46 -73
  304. package/src/studio-app-binding.js +29 -45
  305. package/src/studio-asset-list.js +17 -0
  306. package/src/studio-contact-sheet.js +37 -5
  307. package/src/studio-elements.js +27 -7
  308. package/src/studio-entity-bounds.js +18 -0
  309. package/src/studio-error-codes.js +8 -0
  310. package/src/studio-export-framing.js +152 -0
  311. package/src/studio-lighting-check.js +103 -0
  312. package/src/studio-motion-visual.js +150 -0
  313. package/src/studio-schema-errors.js +80 -0
  314. package/src/studio-support-gap.js +74 -0
  315. package/src/styles.css +1 -19
  316. package/src/timeline-defaults.js +5 -0
  317. package/src/timeline-extent.js +8 -20
  318. package/src/tutorial-walk.js +17 -0
  319. package/src/ui-scrub.js +1 -1
  320. package/src/use-shot-camera-tracks.js +12 -0
  321. package/src/view-ghosts.js +97 -0
  322. package/src/view-ghosts.jsx +49 -0
  323. package/src/workflow/AgentPanel.jsx +47 -166
  324. package/src/workflow/AgentProgressCard.jsx +107 -0
  325. package/src/workflow/AgentReplyText.jsx +37 -0
  326. package/src/workflow/AgentTurnSteps.jsx +84 -0
  327. package/src/workflow/LoginPanel.jsx +140 -0
  328. package/src/workflow/WorkflowBuilder.jsx +7 -23
  329. package/src/workflow/agent-client.js +54 -51
  330. package/src/workflow/agent-panel.css +71 -1
  331. package/src/workflow/agent-progress-card.css +167 -0
  332. package/src/workflow/agent-progress-view.js +47 -0
  333. package/src/workflow/agent-turn-steps.css +113 -0
  334. package/src/workflow/agent-turn.js +191 -0
  335. package/src/workflow/login-client.js +34 -0
  336. package/src/workflow/motion-input.js +18 -31
  337. package/src/workflow/node-schema.js +1 -1
  338. package/src/workflow/reply-markdown.js +69 -0
  339. package/src/workflow/scene-asset-sync.js +8 -8
  340. package/tools/agent-session-stats.mjs +61 -0
  341. package/tools/dev-full.mjs +5 -56
  342. package/tools/eslint-baseline.json +12 -13
  343. package/tools/headless-studio.mjs +85 -0
  344. package/tools/knip-baseline.json +0 -179
  345. package/tools/loc-baseline.json +33 -83
  346. package/tools/motion3/extract-body.mjs +103 -0
  347. package/tools/motion3/extract-rig.mjs +85 -0
  348. package/tools/process-supervisor.mjs +0 -88
  349. package/tools/run-tests.mjs +9 -124
  350. package/bin/agent/motion-runtime.mjs +0 -76
  351. package/dist/ardy/cskel27-rest.json +0 -557
  352. package/dist/assets/analytics-B41FmPYI.js +0 -1
  353. package/dist/assets/app-Do-Og5Pu.css +0 -1
  354. package/dist/assets/app-DwP-tl2x.js +0 -4920
  355. package/dist/assets/config-Bx7pYsD9.js +0 -1
  356. package/dist/assets/config-DeoFTYzb.css +0 -1
  357. package/dist/assets/demo-Dt0qZfS7.js +0 -1
  358. package/dist/assets/scene-asset-sync-BCWDJB1d.js +0 -19
  359. package/dist/assets/scene-asset-sync-DjJ_cJ2R.css +0 -1
  360. package/dist/assets/ticket-jqMhqL88.js +0 -1
  361. package/dist/assets/workflow-Br-K7Srs.js +0 -179
  362. package/dist/demo/walk-then-stop.npz +0 -0
  363. package/mcp/ardy-prompts.mjs +0 -218
  364. package/src/ardy/auto-fix-panel.css +0 -108
  365. package/src/ardy/auto-physics.js +0 -1194
  366. package/src/ardy/client.js +0 -153
  367. package/src/ardy/convert.js +0 -313
  368. package/src/ardy/cskel27-neutral.js +0 -39
  369. package/src/ardy/cskel27.js +0 -77
  370. package/src/ardy/export.js +0 -165
  371. package/src/ardy/fix-collisions.js +0 -2257
  372. package/src/ardy/foot-lock.js +0 -230
  373. package/src/ardy/motion-calibration.js +0 -118
  374. package/src/ardy/motion-edit.js +0 -213
  375. package/src/ardy/motion-url.js +0 -55
  376. package/src/ardy/mp4-duration.js +0 -102
  377. package/src/ardy/npz.js +0 -595
  378. package/src/ardy/physics-panel.css +0 -21
  379. package/src/ardy/physics-panel.jsx +0 -89
  380. package/src/ardy/physics-refine.js +0 -111
  381. package/src/ardy/physics-review.js +0 -567
  382. package/src/ardy/physics-temporal.js +0 -54
  383. package/src/ardy/platform-fit-panel.css +0 -64
  384. package/src/ardy/platform-fit-panel.jsx +0 -41
  385. package/src/ardy/platform-fit.js +0 -326
  386. package/src/ardy/playback.js +0 -711
  387. package/src/ardy/pose-pin.js +0 -97
  388. package/src/ardy/prompt-clips.js +0 -30
  389. package/src/ardy/retime.js +0 -211
  390. package/src/ardy/root-drop.js +0 -369
  391. package/src/ardy/timeline-resize.js +0 -13
  392. package/src/ardy/to-cskel27.js +0 -225
  393. package/src/ardy/trim.js +0 -33
  394. package/src/ardy/waypoints.js +0 -369
  395. package/src/ardy-bridge-contract.js +0 -281
  396. package/src/domains/motion/motion-video-capture.js +0 -664
  397. package/src/i2v-motion-client.js +0 -77
  398. package/src/i2v-motion-studio.jsx +0 -180
  399. package/src/line-edit.js +0 -1348
  400. package/src/motion/generation.js +0 -105
  401. package/src/motion-readiness-ui.jsx +0 -67
  402. package/src/motion-readiness.js +0 -40
  403. package/src/motion-resources.js +0 -177
  404. package/src/motion-store.js +0 -65
  405. package/src/motion-trail.js +0 -601
  406. package/src/multimodel-ingest.js +0 -394
  407. package/src/panels/PromptBlocksPanel.jsx +0 -444
  408. package/src/panels/VideoCapturePanel.jsx +0 -181
  409. package/src/pose-extract/browser.js +0 -42
  410. package/src/pose-extract/detector.js +0 -100
  411. package/src/pose-extract/fit.js +0 -227
  412. package/src/pose-extract/image-frame.js +0 -51
  413. package/src/pose-extract/index.js +0 -58
  414. package/src/pose-extract/keyframes.js +0 -74
  415. package/src/pose-extract/landmarks.js +0 -162
  416. package/src/pose-extract/mirror.js +0 -153
  417. package/src/pose-extract/take.js +0 -131
  418. package/src/pose-extract/video-frames.js +0 -91
  419. package/src/take-recipe.js +0 -165
  420. package/src/trail-key-conflicts.js +0 -39
  421. package/src/trail-pick.js +0 -59
  422. package/src/use-i2v-capture-modal.js +0 -15
  423. package/tools/ardy/BRIDGE.md +0 -37
  424. package/tools/ardy/GVHMR-NPZ.md +0 -68
  425. package/tools/ardy/README.md +0 -124
  426. package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
  427. package/tools/ardy/artifacts.mjs +0 -34
  428. package/tools/ardy/bridge.mjs +0 -1732
  429. package/tools/ardy/bvh-cskel27.mjs +0 -1265
  430. package/tools/ardy/cclay_constrained_generate.py +0 -561
  431. package/tools/ardy/cclay_gvhmr_worker.py +0 -264
  432. package/tools/ardy/cclay_motion_edit.py +0 -510
  433. package/tools/ardy/cclay_sequence_generate.py +0 -705
  434. package/tools/ardy/cclay_worker.py +0 -304
  435. package/tools/ardy/dump-npz.py +0 -205
  436. package/tools/ardy/extract-rest.mjs +0 -299
  437. package/tools/ardy/extract.mjs +0 -507
  438. package/tools/ardy/footage.mjs +0 -467
  439. package/tools/ardy/gvhmr-floor.mjs +0 -102
  440. package/tools/ardy/gvhmr-to-cskel27.mjs +0 -21
  441. package/tools/ardy/gvhmr_fastpath.py +0 -168
  442. package/tools/ardy/gvhmr_trajectory.py +0 -240
  443. package/tools/ardy/merge-text-encoder.py +0 -160
  444. package/tools/ardy/mocap-metrics.mjs +0 -200
  445. package/tools/ardy/mocap-quality-gate.mjs +0 -110
  446. package/tools/ardy/motion-stabilize.mjs +0 -348
  447. package/tools/ardy/npz.mjs +0 -414
  448. package/tools/ardy/pose-to-npz.mjs +0 -106
  449. package/tools/ardy/prompt-limits.mjs +0 -5
  450. package/tools/ardy/run-edit-on-box.sh +0 -73
  451. package/tools/ardy/run-local.mjs +0 -233
  452. package/tools/ardy/run-on-box.sh +0 -593
  453. package/tools/ardy/run-sequence-on-box.sh +0 -177
  454. package/tools/ardy/runners/gvhmr-worker.mjs +0 -206
  455. package/tools/ardy/runners/index.mjs +0 -18
  456. package/tools/ardy/runners/local.mjs +0 -455
  457. package/tools/ardy/runners/proc.mjs +0 -125
  458. package/tools/ardy/runners/remote.mjs +0 -252
  459. package/tools/ardy/setup-local.mjs +0 -189
  460. package/tools/ardy/setup-text-encoder-on-box.sh +0 -93
  461. package/tools/ardy/setup-text-encoder.py +0 -306
  462. package/tools/ardy/smpl-cskel27.mjs +0 -178
  463. package/tools/ardy/visual-qa.mjs +0 -196
  464. package/tools/ardy/vq-car.mjs +0 -52
  465. package/tools/bench/EXP3.md +0 -61
  466. package/tools/bench/cclay_bench_extract_incam.py +0 -125
  467. package/tools/bench/cclay_bench_extract_obs.py +0 -204
  468. package/tools/bench/cclay_bench_runner.py +0 -42
  469. package/tools/bench/cube-contact.mjs +0 -162
  470. package/tools/bench/exp3.mjs +0 -152
  471. package/tools/bench/extract-bench-lib.mjs +0 -216
  472. package/tools/bench/extract-bench.mjs +0 -302
  473. package/tools/bench/fal-generate.mjs +0 -88
  474. package/tools/bench/fit/README.md +0 -189
  475. package/tools/bench/fit/camera.mjs +0 -59
  476. package/tools/bench/fit/contact.mjs +0 -419
  477. package/tools/bench/fit/footlock.mjs +0 -234
  478. package/tools/bench/fit/motion.mjs +0 -85
  479. package/tools/bench/fit/pin.mjs +0 -27
  480. package/tools/bench/fit/remote.mjs +0 -105
  481. package/tools/bench/fit-bench.mjs +0 -126
  482. package/tools/bench/fit-sanity.mjs +0 -45
  483. package/tools/bench/metrics.mjs +0 -313
  484. package/tools/bench/obs/depth.mjs +0 -258
  485. package/tools/bench/obs/extrinsics.mjs +0 -175
  486. package/tools/bench/obs/fit_mannequin_betas.py +0 -340
  487. package/tools/bench/obs/ground.mjs +0 -294
  488. package/tools/bench/obs/heading.mjs +0 -151
  489. package/tools/bench/obs/ladder.mjs +0 -555
  490. package/tools/bench/obs/mannequin-betas.json +0 -124
  491. package/tools/bench/obs/remote.mjs +0 -83
  492. package/tools/bench/obs/rest_joints.py +0 -52
  493. package/tools/bench/obs/ybot-targets.mjs +0 -49
  494. package/tools/bench/obs-bench.mjs +0 -520
  495. package/tools/bench/score.mjs +0 -343
  496. package/tools/bench/summarize.mjs +0 -86
  497. package/tools/demo-worker/README.md +0 -137
  498. package/tools/demo-worker/api-client.mjs +0 -233
  499. package/tools/demo-worker/generate.mjs +0 -301
  500. package/tools/demo-worker/index.mjs +0 -466
  501. package/tools/demo-worker/sign-core.mjs +0 -112
  502. package/tools/demo-worker/sign.mjs +0 -161
  503. package/tools/gt-render/browser.mjs +0 -159
  504. package/tools/gt-render/camera-math.mjs +0 -276
  505. package/tools/gt-render/page.mjs +0 -267
  506. package/tools/gt-render/render.mjs +0 -514
  507. package/tools/gt-render/scene-box.mjs +0 -31
  508. package/tools/gt-render/take-transform.mjs +0 -87
  509. package/tools/kimodo/compare-seam.mjs +0 -108
  510. package/tools/kimodo/constraints.mjs +0 -244
  511. package/tools/kimodo/edit.mjs +0 -259
  512. package/tools/kimodo/effector-constraints.mjs +0 -200
  513. package/tools/kimodo/generate.mjs +0 -646
  514. package/tools/kimodo/local-output.mjs +0 -265
  515. package/tools/kimodo/measure-pose.mjs +0 -65
  516. package/tools/kimodo/measure-preserve.mjs +0 -899
  517. package/tools/kimodo/measure-waypoints.mjs +0 -55
  518. package/tools/kimodo/mlx-generate.py +0 -111
  519. package/tools/kimodo/pose-constraints.mjs +0 -248
  520. package/tools/kimodo/preserve-mask.mjs +0 -543
  521. package/tools/kimodo/read-npz.mjs +0 -189
  522. package/tools/kimodo/run-edit-on-box.mjs +0 -155
  523. package/tools/kimodo/run-sequence-on-box.mjs +0 -178
  524. package/tools/kimodo/runner.mjs +0 -284
  525. package/tools/kimodo/seam-batch.mjs +0 -77
  526. package/tools/kimodo/setup-local.mjs +0 -30
  527. package/tools/kimodo/setup-on-box.sh +0 -339
  528. package/tools/kimodo/soma77-to-cskel27.mjs +0 -305
  529. package/tools/morphgs/assets/gen_truth.py +0 -39
  530. package/tools/morphgs/assets/mesh_ori_rig.txt +0 -27
  531. package/tools/morphgs/assets/playback-check.mjs +0 -30
  532. package/tools/morphgs/demo-gate.sh +0 -26
  533. package/tools/morphgs/fbx2morphgs.mjs +0 -68
  534. package/tools/morphgs/morphgs-to-cskel27.mjs +0 -105
  535. package/tools/morphgs/patches/preprocess_src-none-mode.patch +0 -76
  536. package/tools/morphgs/setup-on-cluster.sh +0 -126
  537. package/tools/projflow/driver.py +0 -1069
  538. package/tools/projflow/generate.mjs +0 -805
  539. package/tools/projflow/hml22-to-cskel27.mjs +0 -735
  540. package/tools/projflow/line-edit-job.mjs +0 -547
  541. package/tools/projflow/replay.mjs +0 -452
  542. package/tools/projflow/runner.mjs +0 -269
  543. package/tools/projflow/service.mjs +0 -742
  544. package/tools/track/DESIGN.md +0 -513
  545. package/tools/track/backfill-provenance.mjs +0 -116
  546. package/tools/track/budget.mjs +0 -8
  547. package/tools/track/check-rig.mjs +0 -209
  548. package/tools/track/diagnostics.schema.json +0 -60
  549. package/tools/track/export-rig.mjs +0 -256
  550. package/tools/track/fallback.mjs +0 -7
  551. package/tools/track/fk-parity-fixture.mjs +0 -161
  552. package/tools/track/gate.mjs +0 -349
  553. package/tools/track/masks.mjs +0 -318
  554. package/tools/track/metrics.mjs +0 -201
  555. package/tools/track/publish-obs.mjs +0 -108
  556. package/tools/track/py/check_env.py +0 -61
  557. package/tools/track/py/eval_lr.py +0 -229
  558. package/tools/track/py/lr_viterbi.py +0 -294
  559. package/tools/track/py/masks.py +0 -405
  560. package/tools/track/py/objective.py +0 -382
  561. package/tools/track/py/rig.py +0 -508
  562. package/tools/track/py/scene.py +0 -326
  563. package/tools/track/py/test_joint_indices.py +0 -123
  564. package/tools/track/py/test_lr_viterbi.py +0 -195
  565. package/tools/track/py/test_masks.py +0 -229
  566. package/tools/track/py/test_rig.py +0 -288
  567. package/tools/track/py/test_scene.py +0 -354
  568. package/tools/track/py/test_track.py +0 -328
  569. package/tools/track/py/track.py +0 -411
  570. package/tools/track/remote.mjs +0 -140
  571. package/tools/track/rig-dump.mjs +0 -227
  572. package/tools/track/run-box-tests.mjs +0 -14
  573. package/tools/track/run-box.mjs +0 -168
  574. package/tools/track/setup-box.sh +0 -59
  575. package/tools/track/study-2d.mjs +0 -728
  576. /package/src/{ardy → motion}/collision-blockers.js +0 -0
  577. /package/src/{ardy → motion}/ground.js +0 -0
  578. /package/src/{ardy → motion}/ik-drag.js +0 -0
  579. /package/src/{ardy → motion}/ik-key-json.js +0 -0
  580. /package/src/{ardy → motion}/ik.js +0 -0
  581. /package/src/{ardy → motion}/physics-surface.js +0 -0
  582. /package/src/{ardy → motion}/playback-clock.js +0 -0
  583. /package/src/{ardy → motion}/range-pin.js +0 -0
@@ -0,0 +1,160 @@
1
+ // Hints for GOAL groups (C8, C6). The hint moves whatever drives the goal's moving side at the
2
+ // worst frame by the shortfall (+3 mm): a hand (a hand joint, a prop point, an object in hand),
3
+ // else a foot or the root, through moveHint (hints-contact.js, read only). The fix is
4
+ // { channel: "hand_l", f, delta } when the hand target drives that hand (qa/patch.js turns it into
5
+ // a patch), { path: "/anchors/<a>/p", delta } when a world anchor does, and none for an FK arm.
6
+ // still moves the point back toward where it should stay; speed and a goal that waits on another
7
+ // (after/before) get a hint without a fix. Two causes come first, because moving the hand target
8
+ // cannot cure them:
9
+ // - another anchor weight of that hand is above 0 at the frame (it ramps between two keys
10
+ // across the goal), so the hand goes part of the way to that anchor: key it 0 there;
11
+ // - the arm is at full reach (the wrist target is clamped or missed): move the body (bodyMove).
12
+ import { resolveBone } from "../bones.js";
13
+ import { inHandAt } from "../object-pose.js";
14
+ import { refPoint } from "../refs.js";
15
+ import { scale, norm, sub, len } from "../compiler/math.js";
16
+ import { moveHint } from "./hints-contact.js";
17
+ import { goalKinds, goalName, goalNeed, goalWindow, movedHome, movedRef } from "./goals.js";
18
+ import { m3, vec } from "./fmt.js";
19
+
20
+ const MARGIN = 0.003, LEAK = 0.02, BODY_MAX = 0.2;
21
+
22
+ /**
23
+ * The body move that brings shoulder k `need` m nearer its hand at f (the arm points at its
24
+ * target when at full reach): { delta, hint, fix } with fix { channel: "root", f, delta, hold:
25
+ * true } (qa/patch.js moves what places the body: a standIdle at, a walkTo's steps or the agent's
26
+ * root keys). null when the arm points mostly up or down, where walking closer does not help.
27
+ */
28
+ export function bodyMove(X, k, f, need, why = "") {
29
+ const S = k === "l" ? "Left" : "Right", { s } = X.fr(f), v = sub(s.P[X.I[`${S}Hand`]], s.P[X.I[`${S}Arm`]]);
30
+ const flat = Math.hypot(v[0], v[2]) / Math.max(len(v), 1e-9);
31
+ if (flat < 0.5) return null;
32
+ const delta = scale(norm([v[0], 0, v[2]]), Math.min(BODY_MAX, need / flat));
33
+ return { delta, hint: `body: move ${vec(delta)} m toward the target at f${f}, ${X.cause("root", f)} (arm ${k} is at full reach)${why}`, fix: { channel: "root", f, delta, hold: true } };
34
+ }
35
+
36
+ /** Arm k cannot reach its wrist target at f (soft reach clamped it, or the IK missed; not a target inside the elbow's fold, arm_fold). */
37
+ const atReach = (X, k, f) => (X.fr(f).diag[`reach_over_${k}`] ?? 0) > 1e-3 || ((X.fr(f).diag[`arm_miss_${k}`] ?? 0) > 2e-3 && !X.fr(f).diag[`arm_fold_${k}`]);
38
+
39
+ /** The largest other anchor weight of hand k above LEAK at f while `drive` leads it, or null. */
40
+ function leakOf(X, k, f, drive) {
41
+ let best = null;
42
+ for (const [ch, w] of Object.entries(X.fr(f).c)) {
43
+ if (ch === drive || !new RegExp(`^w_.+_${k}$`).test(ch) || !(w > LEAK) || (best && w <= best.w)) continue;
44
+ best = { ch, w };
45
+ }
46
+ return best;
47
+ }
48
+
49
+ /** Leak hint: key the stray weight 0 at the goal's last frame when it rises from 0 there, else at f. */
50
+ function leakHint(X, goal, part, f, why) {
51
+ const k = part.slice(-1), d = X.hand(k, f), leak = d.anchor ? leakOf(X, k, f, d.ch) : null;
52
+ if (!leak) return null;
53
+ const end = Array.isArray(goal.at) ? goal.at[1] : goal.at, prev = X.bracket(leak.ch, f).prev;
54
+ const at = prev && Math.abs(X.out.baked[leak.ch]?.[Math.round(prev.f)] ?? 1) <= LEAK && end >= f && !(X.bracket(leak.ch, f).next?.f <= end) ? end : f;
55
+ const share = leak.w / Math.max(1, (X.fr(f).c[d.ch] ?? 1) + leak.w), other = leak.ch.replace(/^w_/, "").replace(/_[lr]$/, "");
56
+ return { cause: X.cause(leak.ch, f), hint: `${leak.ch} is ${m3(leak.w)} at f${f}, so anchor ${other} takes ${Math.round(share * 100)}% of the hand target from ${d.label}: key ${leak.ch} 0 at f${at}${why}`,
57
+ fix: { channel: leak.ch, f: at, value: 0 } };
58
+ }
59
+
60
+ /** The goal a GOAL group reports: the longest name its subject starts with. */
61
+ function goalOf(X, g) {
62
+ let best = null;
63
+ (X.spec?.goals ?? []).forEach((goal, i) => {
64
+ const name = goalName(goal, i);
65
+ if (g.subject.startsWith(`${name}:`) && (!best || name.length > best.name.length)) best = { goal, name, i };
66
+ });
67
+ return best;
68
+ }
69
+
70
+ /** The record that carries object `id` at frame f (the id itself or a group above it), or null. */
71
+ function carrierOf(X, id, f) {
72
+ const ids = [id, ...(X.out.ctx.scene?.objects?.[id]?.ancestors ?? [])];
73
+ const key = ids.find(k => inHandAt(X.spec.objects?.[k], f));
74
+ return key ? X.spec.objects[key] : null;
75
+ }
76
+
77
+ /** The body part (moveHint name) that moves ref at frame f: hand_<k>, foot_<k>, root, or null for a still point. */
78
+ function partOf(X, ref, f) {
79
+ const [, kind, rest] = /^([a-z]+):(.+)$/s.exec(ref) ?? [];
80
+ if (kind === "prop") return X.spec.props?.[rest.split(".")[0]]?.hand ? `hand_${X.spec.props[rest.split(".")[0]].hand}` : null;
81
+ if (kind === "object") {
82
+ const o = carrierOf(X, rest.replace(/\.(center|top|base)$/, ""), f), k = o && X.spec.props?.[o.prop]?.hand;
83
+ return k ? `hand_${k}` : null;
84
+ }
85
+ if (kind === "landmark") return "root";
86
+ if (kind !== "joint" && kind !== "bone") return null;
87
+ const bone = resolveBone(X.sk, rest).bone ?? "", side = /^Left/.test(bone) ? "l" : /^Right/.test(bone) ? "r" : null;
88
+ if (side && /^(Left|Right)(Hand|ForeArm)/.test(bone)) return `hand_${side}`;
89
+ if (side && /^(Left|Right)(Foot|Toe|Leg)/.test(bone)) return `foot_${side}`;
90
+ return "root";
91
+ }
92
+
93
+ /** fix for moving `part` by delta at f, when it applies to a channel or a world anchor. */
94
+ function fixFor(X, part, f, delta) {
95
+ const k = part.slice(-1);
96
+ if (!part.startsWith("hand_")) return { channel: part, f, delta };
97
+ const d = X.hand(k, f);
98
+ if (d.ch === `hand_${k}`) return { channel: d.ch, f, delta };
99
+ if (d.anchor && X.spec.anchors?.[d.anchor]?.kind === "world") return { path: `/anchors/${d.anchor}/p`, delta };
100
+ return null;
101
+ }
102
+
103
+ /** The side to move and its delta: [part, delta], hands first, then a body part, a to b (still: back toward its home, from `from`). */
104
+ function plan(X, goal, kind, f, amount, from) {
105
+ if (kind === "moved" || kind === "still") {
106
+ const ref = goal[kind], part = partOf(X, ref, f), home = movedHome(X.out, goal, from), p = refPoint(X.out, f, movedRef(ref));
107
+ const away = p && home && len(sub(p, home)) > 1e-3 ? norm(sub(p, home)) : [0, 1, 0];
108
+ return part ? [part, scale(away, kind === "still" ? -amount : amount)] : null;
109
+ }
110
+ const [a, b] = goal[kind], pa = partOf(X, a, f), pb = partOf(X, b, f);
111
+ const rank = p => (p?.startsWith("hand_") ? 2 : p ? 1 : 0), moveA = rank(pa) >= rank(pb);
112
+ const part = moveA ? pa : pb, sign = moveA ? 1 : -1;
113
+ if (!part) return null;
114
+ if (kind === "above") return [part, [0, sign * amount, 0]];
115
+ const qa = refPoint(X.out, f, a), qb = refPoint(X.out, f, b);
116
+ return qa && qb ? [part, scale(norm(sub(qb, qa)), sign * amount)] : null;
117
+ }
118
+
119
+ export const goalHints = {
120
+ /** GOAL(X, g) -> { cause, hint, fix? } | null */
121
+ GOAL(X, g) {
122
+ const found = X.spec && goalOf(X, g), f = g.worstFrame;
123
+ if (!found) return { cause: `goal ${g.subject.split(":")[0]}`, hint: `change the keys around f${f} until the goal holds` };
124
+ const { goal, name, i } = found, kind = goalKinds(goal)[0], amount = g.worst + MARGIN, w = goalWindow(X.out, X.spec.goals, i);
125
+ if (w.wait) {
126
+ const other = goal.after ?? goal.before;
127
+ return { cause: `goal ${name}`, hint: `${name} ${w.wait}: make goal "${other}" hold${goal.after !== undefined && goal.before !== undefined ? ` and "${goal.before}"` : ""} where this one needs it, or move at` };
128
+ }
129
+ const unit = kind === "speed" ? "m/s" : "m", at = kind === "near" || kind === "still" ? "<=" : ">=";
130
+ const why = ` (goal "${name}" is ${m3(g.worst)} ${unit} ${kind === "still" ? "off" : "short"}; needs ${at} ${goalNeed(goal, kind)})`;
131
+ if (kind === "speed") {
132
+ const part = partOf(X, goal.speed, f);
133
+ return { cause: part ? X.cause(part, f) : `goal ${name}`,
134
+ hint: `${goal.speed} is too slow at f${f}: put its keys around f${f} closer in time, or farther apart in space${why}` };
135
+ }
136
+ const move = plan(X, goal, kind, f, amount, w.a);
137
+ if (!move) {
138
+ if (kind === "still" && goal.still.startsWith("object:")) {
139
+ const id = goal.still.slice(7).replace(/\.(center|top|base)$/, "");
140
+ return { cause: `objects.${id}`, hint: `${id} moves at f${f}, where it should stay: hold its hinge channel or keys until then, or start them later${why}` };
141
+ }
142
+ if (kind === "still") return { cause: `goal ${name}`, hint: `${goal.still} moves at f${f} and nothing that follows the body moves it; name a joint, prop point or held object${why}` };
143
+ if (kind === "moved" && !goal.moved.startsWith("object:")) return { cause: `goal ${name}`, hint: `${goal.moved} does not move with the body at f${f}; name a joint, prop point or held object` };
144
+ if (kind === "moved") {
145
+ const id = goal.moved.slice(7).replace(/\.(center|top|base)$/, "");
146
+ return { cause: `objects.${id}`, hint: `${id} is not in a hand at f${f}, so nothing moves it: carry it (objects.${id} = { prop, grab }) or hinge it` };
147
+ }
148
+ return { cause: `goal ${name}`, hint: `neither ${goal[kind][0]} nor ${goal[kind][1]} moves with the body at f${f}; name a joint, prop point or held object` };
149
+ }
150
+ const [part, delta] = move;
151
+ if (part.startsWith("hand_")) {
152
+ const leak = leakHint(X, goal, part, f, why);
153
+ if (leak) return leak;
154
+ const body = atReach(X, part.slice(-1), f) && bodyMove(X, part.slice(-1), f, amount, why);
155
+ if (body) return { cause: X.cause("root", f), hint: body.hint, fix: body.fix };
156
+ }
157
+ const r = moveHint(X, part, f, delta, why), fix = fixFor(X, part, f, delta);
158
+ return { cause: r.cause, hint: r.hint, ...(fix ? { fix } : {}) };
159
+ },
160
+ };
@@ -0,0 +1,24 @@
1
+ // The knee's stop in QA hints (compiler/limbs.js KNEE_FOLD, KNEE_STOP). An ankle target nearer the hip
2
+ // than the knee folds leaves the ankle farther out: under the hip in a squat, down into the floor. So
3
+ // REACH leg_miss, GROUND on that foot and KNEE (folds past the stop) say the same thing: the hips are
4
+ // lower than the legs fold, bring the root up off the feet (a target above the hip: the foot out from it).
5
+ import { sub, scale, norm, len, dist, dot } from "../compiler/math.js";
6
+ import { reachAt, KNEE_FOLD, KNEE_STOP } from "../compiler/limbs.js";
7
+ import { vec, m3 } from "./fmt.js";
8
+
9
+ /** What the solver says of a knee on its stop: ` (the knee would fold 168deg; it bends to 150deg and stops at 160deg)`. */
10
+ export const kneeWhy = (lead, asked) => ` (${lead}the knee would fold ${Math.round(asked)}deg; it bends to ${KNEE_FOLD}deg and stops at ${KNEE_STOP}deg)`;
11
+
12
+ /**
13
+ * Hint for leg k at frame f whose ankle point T (the foot_k target, or the drawn ankle) is nearer the
14
+ * hip than the knee folds KNEE_FOLD: the root up by what puts T at that fold's reach when T is under the
15
+ * hip, else foot_k out from the hip by that much. `why` says why (kneeWhy).
16
+ */
17
+ export function kneeFoldHint(X, k, f, why, T) {
18
+ const S = k === "l" ? "Left" : "Right", P = X.fr(f).s.P, H = P[X.I[`${S}UpLeg`]], K = P[X.I[`${S}Leg`]], A = P[X.I[`${S}Foot`]];
19
+ const v = sub(T, H), R = reachAt(dist(H, K), dist(K, A), KNEE_FOLD) + 0.002, out = scale(norm(v), Math.max(0, R - len(v)));
20
+ // the hips up by d leave the target R from the hip: |v - d up| = R, the root above the target
21
+ const d = v[1] + Math.sqrt(Math.max(0, v[1] * v[1] - dot(v, v) + R * R));
22
+ if (v[1] < 0) return { cause: X.cause("root", f), hint: `root: raise ${m3(d)} m at f${f}${why}, or key foot_${k} farther from the hip (wider or more forward)`, fix: { channel: "root", f, delta: [0, d, 0] } };
23
+ return { cause: X.cause(`foot_${k}`, f), hint: `foot_${k}: move ${vec(out)} m out from the hip at f${f}${why}, or raise root ${m3(d)} m`, fix: { channel: `foot_${k}`, f, delta: out } };
24
+ }
@@ -0,0 +1,209 @@
1
+ // Hints for motion-type issues (reach, knees and elbows past their stop, pole flips, sliding, pops,
2
+ // floating, opted-in speed and acceleration) and for the spec's own checks (contacts, orient,
3
+ // below, appear). Orient and below try small changes of the wrist or elbow channels on the worst
4
+ // frame and scale the best one to the needed amount.
5
+ import { add, sub, scale, norm, len, dist, angle, apply, ry, clamp, dot } from "../compiler/math.js";
6
+ import { solveFrame } from "../compiler/compile.js";
7
+ import { channelsAt } from "../compiler/channels.js";
8
+ import { resolveAnchor } from "../compiler/anchors.js";
9
+ import { carried, reachAt, ELBOW_FOLD, ELBOW_STOP, KNEE_FOLD } from "../compiler/limbs.js";
10
+ import { pointOf } from "./spec-checks.js";
11
+ import { solePoints, ankleY } from "./geometry.js";
12
+ import { vec, m3 } from "./fmt.js";
13
+ import { moveHint } from "./hints-contact.js";
14
+ import { kneeFoldHint, kneeWhy } from "./hints-knee.js";
15
+ import { bodyMove } from "./hints-goal.js";
16
+
17
+ const LIMB = { Leg: ["UpLeg", "Leg", "Foot", "knee"], ForeArm: ["Arm", "ForeArm", "Hand", "elbow"] };
18
+ const sideOf = s => (s.startsWith("Left") ? "l" : "r");
19
+ const weightOf = ch => (/^(w_|armw)/.test(ch) ? 0.5 : /^(root|foot|hand)/.test(ch) ? 1 : /^(knee|elbow)/.test(ch) ? 0.2 : 0.01);
20
+
21
+ /** Channels that move joint j, ranked by how much each changed into frame f (metres-equivalent). */
22
+ function movers(X, j, f) {
23
+ const k = sideOf(j), b = X.out.baked;
24
+ let list = ["root", "yaw", "pitch"];
25
+ if (j === "Head") list = ["head_yaw", "head_pitch", "spine_pitch", "spine_yaw", "root", "yaw"];
26
+ else if (/Hand|ForeArm/.test(j)) list = [`hand_${k}`, `armw_${k}`, `elbow_${k}`, `aswing_${k}`, `abend_${k}`, ...Object.keys(b).filter(ch => new RegExp(`^w_.+_${k}$`).test(ch)), "spine_yaw", "spine_pitch", "root", "yaw"];
27
+ else if (/Foot|Leg/.test(j)) list = [`foot_${k}`, `knee_${k}`, `fpitch_${k}`, "root"];
28
+ const change = ch => {
29
+ const a = b[ch]?.[f - 1], c = b[ch]?.[f];
30
+ if (a === undefined || c === undefined || typeof a === "object" && !Array.isArray(a)) return 0;
31
+ return (Array.isArray(a) ? dist(a, c) : Math.abs(c - a)) * weightOf(ch);
32
+ };
33
+ return list.filter(ch => b[ch]).sort((p, q) => change(q) - change(p));
34
+ }
35
+
36
+ function speedHint(X, g, power) {
37
+ const j = g.subject.split(" ")[0], f = g.worstFrame, ch = movers(X, j, f)[0] ?? "root";
38
+ const { prev, next } = X.bracket(ch, f), stepped = X.keys(ch).find(k => k.step && Math.abs(k.f - f) <= 1);
39
+ if (stepped) return { cause: X.cause(ch, f), hint: `${ch}: a step key jumps at f${Math.round(stepped.f)}; ease it over 4+ frames (key the old value 4 frames earlier, drop the step)` };
40
+ const factor = Math.ceil(Math.pow(g.worst / g.limit, power) * 1.1 * 10) / 10;
41
+ const a = Math.round(prev?.f ?? f - 4), b = Math.round(next?.f ?? f + 4), extra = Math.ceil((b - a) * (factor - 1));
42
+ const anchor = /^w_(.+)_[lr]$/.exec(ch);
43
+ return { cause: X.cause(ch, f), hint: `retime: stretch f${a}-f${b} by ${factor}x (+${extra} frames, move the keys from f${b} on later)${anchor ? ` - the hand switches to anchor ${anchor[1]} too fast` : ""}; or fix.retime: true`,
44
+ fix: X.spec.fix === false ? undefined : { path: "/fix/retime", value: true } };
45
+ }
46
+
47
+ /** A check point (joint:/prop:/anchor:) on a freshly solved frame. */
48
+ function pointOn(X, fr, ref) {
49
+ const [kind, rest] = ref.split(/:(.*)/s);
50
+ if (kind === "joint") return fr.s.P[X.I[rest]];
51
+ if (kind === "prop") { const [p, pt] = rest.split("."); return fr.diag.props?.[p]?.[pt]; }
52
+ return resolveAnchor(X.out.ctx, rest, fr.s, fr.diag, fr.c);
53
+ }
54
+
55
+ /** Try channel changes on frame f; returns the best { ch, value, gain } for measure() (higher is better). */
56
+ function probe(X, f, tries, measure) {
57
+ const c0 = channelsAt(X.out.baked, f), base = measure(X.fr(f));
58
+ let best = null;
59
+ for (const [ch, value, unit] of tries) {
60
+ const gain = measure(solveFrame(X.out.ctx, { ...c0, [ch]: value }, f)) - base;
61
+ if (gain > 1e-6 && (!best || gain / unit > best.gain / best.unit)) best = { ch, value, gain, unit };
62
+ }
63
+ return { best, base, c0 };
64
+ }
65
+
66
+ const hipOrShoulder = (X, k, f, limb) => X.fr(f).s.P[X.I[`${k === "l" ? "Left" : "Right"}${LIMB[limb][0]}`]];
67
+
68
+ export const motionHints = {
69
+ REACH(X, g) {
70
+ const [, limb, k] = /^(leg|arm)_miss_([lr])$/.exec(g.subject) ?? [], f = g.worstFrame, need = g.worst + 0.002;
71
+ if (limb === "leg") {
72
+ // an ankle target nearer the hip than the knee folds (compiler/limbs.js): the hips up off it
73
+ const fold = X.fr(f).diag[`leg_fold_${k}`];
74
+ if (fold) return kneeFoldHint(X, k, f, kneeWhy(`the ankle target is ${m3(g.worst)} m inside the shin's reach: `, fold), X.fr(f).c[`foot_${k}`]);
75
+ const T = X.fr(f).c[`foot_${k}`] ?? [0, 0, 0], H = hipOrShoulder(X, k, f, "Leg"), v = sub(T, H), R = dist(H, T) - g.worst;
76
+ const disc = v[1] * v[1] - dot(v, v) + R * R, side = [v[0], 0, v[2]];
77
+ const alt = disc >= 0 ? `or root down ${m3(-v[1] - Math.sqrt(disc) + 0.002)} m${g.worst <= 0.05 ? " (fix.reach does this when on)" : ""}`
78
+ : `or root ${vec(scale(norm(side), len(side) - R * 0.95))} m toward the foot (the foot is ${m3(len(side))} m sideways of the hip: the root and foot keys disagree)`;
79
+ return { cause: X.cause(`foot_${k}`, f), hint: `foot_${k}: move ${vec(scale(norm(sub(H, T)), need))} m toward the hip at f${f}, ${alt}`, fix: { channel: `foot_${k}`, f, delta: scale(norm(sub(H, T)), need) } };
80
+ }
81
+ const S = k === "l" ? "Left" : "Right", { s, diag } = X.fr(f), dir = norm(sub(s.P[X.I[`${S}Hand`]], s.P[X.I[`${S}Arm`]]));
82
+ // a target nearer the shoulder than the elbow folds (compiler/limbs.js): out from the shoulder, or the body back from it
83
+ const fold = diag[`arm_fold_${k}`];
84
+ if (fold) {
85
+ const r = moveHint(X, `hand_${k}`, f, scale(dir, need), ` (the hand target is ${m3(g.worst)} m inside the forearm's reach: the elbow would fold ${Math.round(fold)}deg; it bends to ${ELBOW_FOLD}deg and stops at ${ELBOW_STOP}deg)`);
86
+ return { cause: r.cause, hint: `${r.hint}, or bring root ${vec(scale([-dir[0], 0, -dir[2]], need))} m back from it`, fix: r.fix };
87
+ }
88
+ const r = moveHint(X, `hand_${k}`, f, scale(dir, -need), "");
89
+ // a hand on a point the scene or the spec pins (an object anchor, one a goal or contact check names) stays: the body comes closer
90
+ const a = X.hand(k, f).anchor, refs = JSON.stringify([X.spec.goals ?? [], X.spec.checks?.contacts ?? []]);
91
+ const pinned = a && (X.spec.anchors?.[a]?.kind === "object" || refs.includes(`"anchor:${a}"`)), fit = X.spec.fits?.[X.hand(k, f).fit ?? -1];
92
+ // a fit pushes the wrist target far past reach; the body comes as far as the effector is short
93
+ const eff = fit?.effector?.prop && X.fr(f).diag.props?.[fit.effector.prop]?.[fit.effector.point];
94
+ const gap = eff ? dist(eff, add(pointOn(X, X.fr(f), `anchor:${a}`), fit.offset ?? [0, 0, 0])) + 0.01 : need;
95
+ const body = pinned && bodyMove(X, k, f, Math.min(need, gap));
96
+ if (body) return { cause: r.cause, hint: `${body.hint} (out of reach by ${m3(g.worst)} m), or ${r.hint}`, fix: body.fix };
97
+ return { cause: r.cause, hint: `${r.hint} (out of reach by ${m3(g.worst)} m), or bring root ${vec(scale([dir[0], 0, dir[2]], need))} m closer`, fix: r.fix };
98
+ },
99
+ KNEE(X, g) {
100
+ const k = sideOf(g.subject), S = k === "l" ? "Left" : "Right", f = g.worstFrame, { s, c } = X.fr(f), P = s.P, I = X.I;
101
+ if (/backward/.test(g.subject)) {
102
+ const toe = sub(P[I[`${S}ToeBase`]], P[I[`${S}Foot`]]); toe[1] = 0;
103
+ const pole = add(norm(apply(ry(-(c.yaw ?? 0)), toe)), [0, 0.3, 0]);
104
+ return { cause: X.cause(`knee_${k}`, f), hint: `knee_${k}: key ${vec(norm(pole))} (body frame, along the foot) at f${f}; a knee pole is +z forward, +x the character's left`, fix: { channel: `knee_${k}`, f, value: norm(pole) } };
105
+ }
106
+ // past the stop (the solver stops every knee at KNEE_STOP, so this pose came from outside it): the hips up off the foot
107
+ if (/ folds /.test(g.subject)) return kneeFoldHint(X, k, f, ` (the knee folds ${m3(g.worst)}deg, past its ${g.limit}deg stop; it bends freely to ${KNEE_FOLD}deg)`, P[I[`${S}Foot`]]);
108
+ const H = P[I[`${S}UpLeg`]], K = P[I[`${S}Leg`]], A = P[I[`${S}Foot`]], a = dist(H, K), b = dist(K, A);
109
+ const inner = (180 - (g.limit - 5)) * Math.PI / 180, want = Math.sqrt(a * a + b * b - 2 * a * b * Math.cos(inner));
110
+ return { cause: X.cause(`foot_${k}`, f), hint: `foot_${k}: move ${vec(scale(norm(sub(A, H)), want - dist(H, A)))} m away from the hip at f${f} (the knee folds ${m3(g.worst)} deg, limit ${g.limit})`,
111
+ fix: { channel: `foot_${k}`, f, delta: scale(norm(sub(A, H)), want - dist(H, A)) } };
112
+ },
113
+ // the solver stops every elbow at ELBOW_STOP, so a fold past it comes from outside the solver: the hand out from the shoulder
114
+ ELBOW(X, g) {
115
+ const k = sideOf(g.subject), S = k === "l" ? "Left" : "Right", f = g.worstFrame, P = X.fr(f).s.P, I = X.I;
116
+ const A = P[I[`${S}Arm`]], E = P[I[`${S}ForeArm`]], W = P[I[`${S}Hand`]], delta = scale(norm(sub(W, A)), reachAt(dist(A, E), dist(E, W), ELBOW_FOLD) - dist(A, W));
117
+ return moveHint(X, `hand_${k}`, f, delta, ` (the elbow folds ${m3(g.worst)}deg, past its ${g.limit}deg stop; it bends freely to ${ELBOW_FOLD}deg)`);
118
+ },
119
+ FLIP(X, g) {
120
+ const [, S, limb] = /^(Left|Right)(Leg|ForeArm) /.exec(g.subject) ?? [], f = g.worstFrame;
121
+ if (!limb) return null;
122
+ const k = sideOf(S), [r, m, e, pole] = LIMB[limb], I = X.I, gap = +(/ over (\d+) frames$/.exec(g.subject)?.[1] ?? 1);
123
+ // an elbow on its stop: the target passes near the shoulder and the stopped hand swings round it, whatever the pole
124
+ const fd = X.fr(f).diag, asked = limb === "ForeArm" && (fd[`arm_fold_${k}`] ?? fd[`hold_${k}`]?.fold);
125
+ if (asked) {
126
+ const held = fd[`hold_${k}`]?.fold && Object.entries(X.spec.objects ?? {}).find(([, o]) => o.keys?.length && X.spec.props?.[o.prop]?.hand === k)?.[0];
127
+ return { cause: held ? `objects.${held}.keys` : X.cause(X.hand(k, f).ch, f), hint: `the ${held ? `keys of ${held} ask` : "hand target asks"} the elbow to fold ${Math.round(asked)}deg at f${f}, past its ${ELBOW_STOP}deg stop, so the hand swings round the shoulder on the stop: move ${held ? "the keyed pose" : "the target"} out from the shoulder (REACH says how far)` };
128
+ }
129
+ const bendAt = q => { const P = X.fr(q).s.P; return angle(sub(P[I[`${S}${m}`]], P[I[`${S}${r}`]]), sub(P[I[`${S}${e}`]], P[I[`${S}${m}`]])); };
130
+ // a flip over several frames passed through the straight (or folded) frames between: the straightest names it
131
+ const fb = Array.from({ length: Math.max(1, gap - 1) }, (_, i) => (gap > 1 ? f - 1 - i : f)).reduce((p, q) => (bendAt(q) < bendAt(p) ? q : p)), bend = bendAt(fb);
132
+ const ch = `${pole}_${k}`, prevPole = X.out.baked[ch]?.[Math.max(0, f - 1)];
133
+ if (prevPole && len(prevPole) < 0.5) return { cause: X.cause(ch, f), hint: `${ch}: the pole passes near zero (length ${m3(len(prevPole))}) at f${f} while it turns between opposite keys; add an in-between pole key (e.g. the average direction, normalised) so it turns, not shrinks` };
134
+ const straight = bend < 12 ? `the limb is almost straight at f${fb} (bend ${m3(bend)} deg), so the pole cannot hold it: bring the ${limb === "Leg" ? "foot" : "hand"} target ~0.02 m toward the ${limb === "Leg" ? "hip" : "shoulder"}, or ` : "";
135
+ return { cause: X.cause(ch, f), hint: `${ch}: ${straight}keep the pole on one side through f${f}${prevPole ? ` (key ${vec(prevPole)} there)` : ""}`, fix: prevPole && { channel: ch, f, value: prevPole } };
136
+ },
137
+ SLIDE(X, g) {
138
+ const k = sideOf(g.subject), S = k === "l" ? "Left" : "Right", key = g.subject.includes("heel") ? "heel" : "ball";
139
+ const f0 = Math.max(0, g.first - 1), at = f => solePoints(X.sk, X.fr(f).s, S)[key];
140
+ const slid = Math.hypot(at(g.last)[0] - at(f0)[0], at(g.last)[2] - at(f0)[2]), hold = X.out.baked[`foot_${k}`]?.[f0];
141
+ // the ankle stays and the foot turns flat on the floor (fyaw): the turn is the slide
142
+ const turn = X.out.baked[`fyaw_${k}`], end = X.out.baked[`foot_${k}`]?.[g.last];
143
+ if (turn && hold && end && Math.hypot(end[0] - hold[0], end[2] - hold[2]) < slid / 2 && Math.abs(turn[g.last] - turn[f0]) > 1) return { cause: X.cause(`fyaw_${k}`, g.worstFrame),
144
+ hint: `fyaw_${k}: the flat foot turns ${m3(turn[f0])} -> ${m3(turn[g.last])} deg over f${f0}-f${g.last} and the ${key} slides ${m3(slid)} m; key fyaw_${k} ${m3(turn[f0])} at f${g.last} so it turns once the foot is up, or lift the heel first (fpitch_${k} +) so it pivots on the ball`,
145
+ fix: { channel: `fyaw_${k}`, f: g.last, value: turn[f0] } };
146
+ return { cause: X.cause(`foot_${k}`, g.worstFrame), hint: `foot_${k}: hold it still over f${f0}-f${g.last}${hold ? ` (key ${vec(hold)} at both ends)` : ""}; the ${key} slides ${m3(slid)} m${slid < 0.01 ? "; fix.slide locks this when on" : ""}`,
147
+ fix: hold && { channel: `foot_${k}`, f: g.last, value: hold } };
148
+ },
149
+ SPEED: (X, g) => speedHint(X, g, 1),
150
+ POP: (X, g) => speedHint(X, g, 0.5),
151
+ ACC: (X, g) => speedHint(X, g, 0.5),
152
+ FLOAT: (X, g) => ({ cause: X.cause("root", g.worstFrame), hint: `over f${g.first}-f${g.last} lower the root or key foot_l/foot_r back onto the ground, the floor or the top of an object under the foot (the ankle stands ${ankleY(X.sk)} m above it); a jump needs the hips to rise and fall; a seat or ledge the hips rest on goes in checks.supports (a foot on an object's top needs none)` }),
153
+ LENGTH: () => ({ cause: "compiler", hint: "a bone changed length: report the spec, this is a compiler fault" }),
154
+ CONTACT(X, g) {
155
+ const ck = (X.spec.checks?.contacts ?? []).find(c => g.subject.startsWith(`${c.name}:`)), f = g.worstFrame, i = X.rel(f);
156
+ if (!ck) return null;
157
+ const a = pointOf(X.out, i, ck.point), b = pointOf(X.out, i, ck.target), delta = sub(b, a);
158
+ const prop = /^prop:([^.]+)/.exec(ck.point), k = prop ? X.spec.props?.[prop[1]]?.hand : sideOf(ck.point.slice(6));
159
+ const { diag } = X.fr(f), over = Math.max(diag[`reach_over_${k}`] ?? 0, diag[`arm_miss_${k}`] ?? 0);
160
+ const anchor = /^anchor:(.+)$/.exec(ck.target)?.[1], fit = (X.spec.fits ?? []).findIndex(ft => ft.side === k && ft.anchor === anchor);
161
+ const cause = fit >= 0 ? `fits[${fit}] (${k} -> anchor ${anchor})` : moveHint(X, `hand_${k}`, f, delta).cause;
162
+ const toRoot = scale(norm([delta[0], 0, delta[2]]), over + 0.01), fold = diag[`arm_fold_${k}`];
163
+ if (over > 0.002 && fold) {
164
+ const S = X.fr(f).s.P[X.I[`${k === "l" ? "Left" : "Right"}Arm`]], back = scale(norm([S[0] - b[0], 0, S[2] - b[2]]), over + 0.01);
165
+ return { cause, hint: `inside the forearm's reach by ${m3(over)} m at f${f} (the elbow would fold ${Math.round(fold)}deg; it stops at ${ELBOW_STOP}deg): bring root ${vec(back)} m back from ${anchor ?? "the target"}, or move the target out from the shoulder`, fix: { channel: "root", f, delta: back } };
166
+ }
167
+ if (over > 0.002) return { cause, hint: `out of reach by ${m3(over)} m at f${f}: bring root ${vec(toRoot)} m toward ${anchor ?? "the target"}, or move the target`, fix: { channel: "root", f, delta: toRoot } };
168
+ const r = moveHint(X, `hand_${k}`, f, delta);
169
+ return { cause, hint: `${r.hint}${fit < 0 && anchor ? `; or add fits: [{ side: "${k}", anchor: "${anchor}", effector: ${prop ? `{ prop: "${prop[1]}", point: "${ck.point.split(".")[1]}" }` : "{ joints: [...] }"} }]` : ""}`, fix: r.fix };
170
+ },
171
+ ORIENT(X, g) {
172
+ const ck = (X.spec.checks?.orient ?? []).find(c => g.subject.startsWith(`${c.name}:`)), f = g.worstFrame;
173
+ if (!ck) return null;
174
+ const prop = /^prop:([^.]+)/.exec(ck.from), k = prop ? X.spec.props?.[prop[1]]?.hand : sideOf(ck.from.slice(6)), minCos = ck.minCos ?? 0.64;
175
+ const along = fr => (ck.along === "up" ? [0, 1, 0] : ck.along === "face" ? carried(X.sk, fr.s, X.I.Head, [0, 0, 1]) : norm(ck.along));
176
+ const cos = fr => dot(norm(sub(pointOn(X, fr, ck.to), pointOn(X, fr, ck.from))), along(fr));
177
+ const c = channelsAt(X.out.baked, f), tw = c[`wtwist_${k}`] ?? 0, fx = c[`wflex_${k}`] ?? 0;
178
+ const { best, base } = probe(X, f, [[`wtwist_${k}`, clamp(tw + 15, -85, 85), 15], [`wtwist_${k}`, clamp(tw - 15, -85, 85), 15], [`wflex_${k}`, clamp(fx + 15, -70, 70), 15], [`wflex_${k}`, clamp(fx - 15, -70, 70), 15]], cos);
179
+ if (!best) return { cause: X.cause(`wtwist_${k}`, f), hint: `turn the prop another way: wrist twist/flex do not help at f${f} (cos ${m3(base)}, needs ${minCos}); change elbow_${k} or the prop points` };
180
+ const from = best.ch.startsWith("wtwist") ? tw : fx, lim = best.ch.startsWith("wtwist") ? 85 : 70;
181
+ const want = clamp(from + (best.value - from) * Math.min(4, (minCos - base + 0.03) / best.gain), -lim, lim);
182
+ return { cause: X.cause(best.ch, f), hint: `${best.ch}: key ${m3(want)} deg at f${f} (now ${m3(from)}; cos ${m3(base)}, needs ${minCos})`, fix: { channel: best.ch, f, value: want } };
183
+ },
184
+ BELOW(X, g) {
185
+ const ck = (X.spec.checks?.below ?? []).find(c => g.subject.startsWith(`${c.name}:`)), f = g.worstFrame;
186
+ if (!ck) return null;
187
+ const k = sideOf(ck.joint), ch = `${/Leg/.test(ck.joint) ? "knee" : "elbow"}_${k}`, pole = channelsAt(X.out.baked, f)[ch] ?? [0, -1, -0.3];
188
+ const drop = fr => fr.s.P[X.I[ck.ref]][1] - fr.s.P[X.I[ck.joint]][1];
189
+ // candidate poles (body frame), nearest to the current one first among those that are enough
190
+ const need = (ck.min ?? 0.08) + 0.01, c0 = channelsAt(X.out.baked, f), base = drop(X.fr(f));
191
+ const tries = [[0, -1, 0], [0.5, -1, 0], [-0.5, -1, 0], [0, -1, -0.4], [0, -1, 0.4], [0.5, -1, 0.5], [-0.5, -1, 0.5], [0.5, -1, -0.4], [-0.5, -1, -0.4]]
192
+ .map(p => ({ p, d: drop(solveFrame(X.out.ctx, { ...c0, [ch]: p }, f)), turn: angle(p, pole) }));
193
+ const ok = tries.filter(t => t.d >= need).sort((p, q) => p.turn - q.turn)[0], best = ok ?? tries.sort((p, q) => q.d - p.d)[0];
194
+ if (!(best.d > base)) return { cause: X.cause(ch, f), hint: `${ch}: no pole change lowers ${ck.joint} at f${f}; lower the hand target instead` };
195
+ return { cause: X.cause(ch, f), hint: `${ch}: key ${vec(best.p)} (body frame: +y up, +z forward) at f${f} (${ck.joint} ${m3(base)} m below ${ck.ref} -> ${m3(best.d)}, needs ${ck.min ?? 0.08})`, fix: { channel: ch, f, value: best.p } };
196
+ },
197
+ APPEAR(X, g) {
198
+ const ck = (X.spec.checks?.appear ?? []).find(c => g.subject.startsWith(`${c.name}:`)), f = g.worstFrame;
199
+ if (!ck) return null;
200
+ let best = null;
201
+ for (let q = Math.max(X.out.f0 ?? 0, f - 24); q <= Math.min((X.out.f0 ?? 0) + X.out.frames.length - 1, f + 24); q++) {
202
+ const d = len(sub(pointOf(X.out, X.rel(q), `joint:${ck.joint}`), pointOf(X.out, X.rel(q), `anchor:${ck.anchor}`)));
203
+ if (!best || d < best.d) best = { q, d };
204
+ }
205
+ const tol = ck.tol ?? 0.035;
206
+ return { cause: X.cause(ck.channel, f), hint: best.d <= tol ? `${ck.channel}: move the step from f${f} to f${best.q} (${ck.joint} ${m3(best.d)} m from ${ck.anchor} there)`
207
+ : `${ck.joint} never gets within ${tol} m of ${ck.anchor} near f${f} (closest ${m3(best.d)} m at f${best.q}): hold its weight w_${ck.anchor}_${sideOf(ck.joint)} at 1 around the switch` };
208
+ },
209
+ };
@@ -0,0 +1,91 @@
1
+ // Hints for object issues (C3, C6, 7.1). A hint function takes the explain context X
2
+ // (qa/explain.js) and the group g and returns { cause, hint, fix? } (fix: see qa/patch.js).
3
+ // GRAB looks within +-24 frames for the frame nearest the grab where the hand is within the
4
+ // limit and proposes fix { path: "/objects/<id>/grab", value: f }; failing that it
5
+ // moves the hand to the object (moveHint) with a fix on what drives the hand
6
+ // COLLIDE for a carried object ("<part> x <id>", "<id> x <other>") moves the holding hand,
7
+ // so the object comes out; the other hand's arm in it while that hand is on an object
8
+ // anchor on it moves the anchor's local point out instead; any other COLLIDE is the body hint (hints-contact.js)
9
+ // REACH for a keyed object ("<id>: 6.2 cm off its keys", key-checks.js); else the arm and leg hint (hints-motion.js)
10
+ import { contactHints, moveHint } from "./hints-contact.js";
11
+ import { motionHints } from "./hints-motion.js";
12
+ import { keyHint } from "./key-checks.js";
13
+ import { CAPSULES, segPoints, sdBox, boxNormal } from "./geometry.js";
14
+ import { grabGap, boxOverlap } from "./object-checks.js";
15
+ import { isCarried, propOrigin } from "../carried.js";
16
+ import { objectBoxesAt, partsOf, spanOf } from "../object-pose.js";
17
+ import { groupsOf } from "../scene.js";
18
+ import { sub, scale, len, apply, T, add, clamp } from "../compiler/math.js";
19
+
20
+ const MARGIN = 0.003, SEARCH = 24;
21
+
22
+ /** A fix that moves hand k by delta at f, on whatever drives it there (hand channel, fit or world anchor). */
23
+ function handFix(X, k, f, delta) {
24
+ const d = X.hand(k, f);
25
+ if (d.ch === `hand_${k}`) return { channel: d.ch, f, delta };
26
+ if (d.fit !== null && d.fit !== undefined) return X.spec.fits[d.fit].offset ? { path: `/fits/${d.fit}/offset`, delta } : { path: `/fits/${d.fit}/offset`, value: delta };
27
+ if (d.anchor && X.spec.anchors?.[d.anchor]?.kind === "world") return { path: `/anchors/${d.anchor}/p`, delta };
28
+ return undefined;
29
+ }
30
+
31
+ function moveHand(X, k, f, delta, why) {
32
+ const r = moveHint(X, `hand_${k}`, f, delta, why), fix = handFix(X, k, f, delta);
33
+ return fix ? { ...r, fix } : r;
34
+ }
35
+
36
+ /** True when hand k is on an object anchor (weight > 0.5) on record `id` or a body or group under it at f. */
37
+ function anchoredOn(X, k, f, id) {
38
+ const a = X.spec.anchors?.[X.hand(k, f).anchor], scene = X.out.ctx.scene;
39
+ return a?.kind === "object" && [scene.objects?.[a.object]?.ancestors, groupsOf(scene)[a.object]?.ancestors, [a.object]].some(ids => ids?.includes(id));
40
+ }
41
+
42
+ /** Closest point of a box (base pivot) to p. */
43
+ function closest(box, p) {
44
+ const q = apply(T(box.R), sub(p, box.pos)), c = [0, box.half[1], 0];
45
+ return add(box.pos, apply(box.R, q.map((v, i) => clamp(v, c[i] - box.half[i], c[i] + box.half[i]))));
46
+ }
47
+
48
+ export const objectHints = {
49
+ REACH: (X, g) => keyHint(X, g) ?? motionHints.REACH(X, g),
50
+ /** GRAB(X, g) -> { cause, hint, fix? } */
51
+ GRAB(X, g) {
52
+ const id = g.subject.split(":")[0], grab = g.worstFrame, o = X.spec?.objects?.[id];
53
+ const plain = { cause: `objects.${id}.grab`, hint: `${id}: move the hand to it by f${grab}, or set objects.${id}.grab to the frame the hand arrives` };
54
+ if (!X.out || !isCarried(o)) return plain;
55
+ let best = null;
56
+ for (let f = grab - SEARCH; f <= grab + SEARCH; f++) {
57
+ const gap = grabGap(X.out, id, f);
58
+ if (gap !== null && gap <= g.limit && (!best || Math.abs(f - grab) < Math.abs(best[0] - grab))) best = [f, gap];
59
+ }
60
+ if (best) return { cause: plain.cause, hint: `${id}: set objects.${id}.grab to ${best[0]} (the hand is ${(best[1] * 100).toFixed(1)} cm from it there, ${(g.worst * 100).toFixed(1)} cm at f${grab})`, fix: { path: `/objects/${id}/grab`, value: best[0] } };
61
+ const { sk, spec, scene } = X.out.ctx, p = propOrigin(sk, spec, X.fr(grab).s, o);
62
+ const target = partsOf(scene, id).map(part => closest(scene.objects[part], p)).reduce((a, b) => (len(sub(b, p)) < len(sub(a, p)) ? b : a));
63
+ const gap = len(sub(target, p)), delta = scale(sub(target, p), gap > 1e-9 ? (gap - g.limit / 2) / gap : 0);
64
+ const r = moveHand(X, spec.props[o.prop].hand, grab, delta, ` (to ${id} at grab)`);
65
+ return { ...r, hint: `${r.hint}; or set objects.${id}.grab to the frame the hand arrives` };
66
+ },
67
+ /** COLLIDE(X, g) for a carried object, else the body hint. */
68
+ COLLIDE(X, g) {
69
+ const [, a, b] = /^(\S+) x (.+)$/.exec(g.subject) ?? [], objects = X.spec?.objects ?? {};
70
+ const id = isCarried(objects[b]) ? b : isCarried(objects[a]) ? a : null;
71
+ if (!id || !X.out) return contactHints.COLLIDE(X, g);
72
+ const o = objects[id], k = X.spec.props[o.prop].hand, [grab, release] = spanOf(X.out, o), need = g.worst - g.limit + MARGIN;
73
+ const f = g.worstFrame, held = f >= grab && f <= release, at = Math.min(f, release), boxes = objectBoxesAt(X.out, id, f) ?? [];
74
+ if (id === b) {
75
+ const cap = CAPSULES.find(c => c[0] === a), { s } = X.fr(f);
76
+ if (!cap || !boxes.length) return null;
77
+ const pts = segPoints(s.P[X.I[cap[1]]], s.P[X.I[cap[2]]]);
78
+ const [box, p] = boxes.flatMap(bx => pts.map(q => [bx, q])).reduce((m, x) => (sdBox(x[1], x[0]) < sdBox(m[1], m[0]) ? x : m));
79
+ const n = boxNormal(p, box), j = /^(hand|farm|uarm)_([lr])$/.exec(a)?.[2];
80
+ // the other hand on an object anchor on what hand k carries rides with it: moving the carry cannot part them, its point moves out
81
+ if (held && j && j !== k && anchoredOn(X, j, f, id)) return moveHint(X, `hand_${j}`, f, scale(n, need), ` (out of ${id})`);
82
+ return held ? moveHand(X, k, f, scale(n, -need), ` (${id} off ${a})`) : moveHint(X, a, f, scale(n, need), ` (out of ${id})`);
83
+ }
84
+ const ob = X.out.objectsAt(X.rel(f))[b];
85
+ if (!ob || !boxes.length) return null;
86
+ // out on the side it came from: where it was the frame before the hit (or at grab)
87
+ const was = objectBoxesAt(X.out, id, Math.max(grab, g.first - 1)) ?? boxes;
88
+ const hit = boxes.map((bx, i) => boxOverlap(bx, ob, sub(was[i].pos, ob.pos))).reduce((m, x) => (x.depth > m.depth ? x : m));
89
+ return moveHand(X, k, at, scale(hit.axis, hit.depth - g.limit + MARGIN), held ? ` (${id} out of ${b})` : ` (${id} is put down in ${b}; it stays where the hand lets go at f${release})`);
90
+ },
91
+ };
@@ -0,0 +1,42 @@
1
+ // Motion QA entry: general checks on every take, the checks the spec declares, the objects it
2
+ // moves (object-checks.js), its goals (goals.js) and the style checks it opted into by naming
3
+ // their limit (style.js; otherwise style is measured, not judged: observeMotion). Groups are
4
+ // ranked by severity.js.
5
+ import { generalChecks, QA_LIMITS } from "./checks.js";
6
+ import { styleChecks, floatChecks } from "./style.js";
7
+ import { specChecks } from "./spec-checks.js";
8
+ import { objectChecks } from "./object-checks.js";
9
+ import { goalChecks } from "./goals.js";
10
+ import { wristIssues } from "./wrist-checks.js";
11
+ import { explainGroups } from "./explain.js";
12
+ import { LOW_IS_BAD, groupKey, rankGroups } from "./severity.js";
13
+
14
+ /** Group issues by kind + subject (severity.js groupKey); each group reports count, worst value and frame range. */
15
+ function summarizeIssues(issues) {
16
+ const groups = new Map();
17
+ for (const i of issues) {
18
+ const key = groupKey({ kind: i.kind, subject: i.msg });
19
+ const g = groups.get(key) ?? { kind: i.kind, subject: i.msg, count: 0, worst: i.value, worstFrame: i.frame, first: i.frame, last: i.frame, limit: i.limit };
20
+ g.count++; g.first = Math.min(g.first, i.frame); g.last = Math.max(g.last, i.frame);
21
+ const worse = LOW_IS_BAD.has(i.kind) ? i.value < g.worst : i.value > g.worst;
22
+ if (worse) { g.worst = i.value; g.worstFrame = i.frame; g.subject = i.msg; }
23
+ groups.set(key, g);
24
+ }
25
+ return rankGroups([...groups.values()]);
26
+ }
27
+
28
+ /**
29
+ * opts.explain (default true) adds cause + hint (and patch, qa/patch.js) to the top groups
30
+ * (explain.js); opts.userSpec is the spec as the agent sent it, which patches address. Frames
31
+ * are absolute even when the compile was windowed (out.f0). qa.goals is there when spec.goals is.
32
+ */
33
+ export function runMotionQa(sk, out, spec, opts = {}) {
34
+ const checks = spec.checks ?? {}, goals = goalChecks(out, spec.goals ?? []);
35
+ const issues = [...generalChecks(sk, out, { allow: checks.allow, limits: checks.limits, mouthPoints: checks.mouthPoints }), ...specChecks(out, checks),
36
+ ...objectChecks(sk, out, spec), ...floatChecks(out, checks),
37
+ ...styleChecks(sk, out, checks, { ...QA_LIMITS, ...checks.limits }), ...wristIssues(sk, out), ...goals.issues];
38
+ if (out.f0) for (const i of issues) i.frame += out.f0;
39
+ const groups = summarizeIssues(issues);
40
+ if (opts.explain !== false) explainGroups(sk, out, spec, groups, { userSpec: opts.userSpec });
41
+ return { ok: issues.length === 0, total: issues.length, ...(spec.goals ? { goals: goals.results } : {}), groups };
42
+ }
@@ -0,0 +1,82 @@
1
+ // QA of held-object keys (C14, ../object-keys.js). From the first to the last key the object
2
+ // leads the hand; the object is drawn in the hand, so what the arm or the wrist could not reach
3
+ // leaves the object off the pose its keys ask for. That residual is measured on the object, as the
4
+ // farthest corner of its part boxes from where the keys put it:
5
+ // REACH "<id>: 6.2 cm off its keys", value metres, limit checks.limits.reach (default 0.01)
6
+ // The hint names the key and why the hand fell short (diag.hold_<side>, compiler/hold.js): the
7
+ // arm out of reach, the grip nearer the shoulder than the elbow folds (compiler/limbs.js), or a
8
+ // wrist turn past its limit after the elbow swung and the forearm turned (lane K8: the whole arm
9
+ // follows the object, compiler/arm-orient.js).
10
+ // Measured, not judged (C16, output.observations.held): every key as drawn, so the agent sees how
11
+ // far each one is from what the hand can hold without a probe.
12
+ import { keyedEntries, curveIn, keyedPose } from "../object-keys.js";
13
+ import { rootPoseAt, partsOf } from "../object-pose.js";
14
+ import { relative } from "../carried.js";
15
+ import { groupsOf } from "../scene.js";
16
+ import { WRIST_LIMITS } from "../compiler/arm-orient.js";
17
+ import { ELBOW_FOLD, ELBOW_STOP } from "../compiler/limbs.js";
18
+ import { add, apply, dist, norm, angle, matFromQuat } from "../compiler/math.js";
19
+
20
+ const SUBJECT = /^(.+): [\d.]+ cm off its keys$/;
21
+ const WRIST_WORDS = { twist: "twist about the forearm", flex: "flex", dev: "deviation (+ toward the thumb)" };
22
+
23
+ /** The corners of id's part boxes in its root frame (the box spans y 0..2 half on its base pivot). */
24
+ function cornersOf(scene, id) {
25
+ const root = scene.objects[id] ?? groupsOf(scene)[id];
26
+ return partsOf(scene, id).flatMap(p => {
27
+ const box = scene.objects[p], rel = relative(root, box), [x, y, z] = box.half;
28
+ return [-x, x].flatMap(a => [0, 2 * y].flatMap(b => [-z, z].map(c => add(rel.pos, apply(rel.R, [a, b, c])))));
29
+ });
30
+ }
31
+
32
+ /** REACH issues of every keyed carry, frames as out.frames indexes. */
33
+ export function keyIssues(out, L, push) {
34
+ const { scene } = out.ctx, f0 = out.f0 ?? 0;
35
+ for (const [id] of keyedEntries(out.ctx.spec)) {
36
+ const curve = curveIn(out, id), corners = cornersOf(scene, id);
37
+ if (!corners.length) continue;
38
+ for (let f = Math.max(f0, Math.ceil(curve.t[0])); f <= Math.min(f0 + out.frames.length - 1, Math.floor(curve.t[curve.t.length - 1])); f++) {
39
+ const want = keyedPose(curve, f), got = rootPoseAt(out, id, f);
40
+ const off = Math.max(...corners.map(c => dist(add(want.pos, apply(want.R, c)), add(got.pos, apply(got.R, c)))));
41
+ if (off > L.reach) push("REACH", `${id}: ${(off * 100).toFixed(1)} cm off its keys`, f - f0, off, L.reach);
42
+ }
43
+ }
44
+ }
45
+
46
+ /** REACH for a keyed object ("<id>: 6.2 cm off its keys") -> { cause, hint }, else null. */
47
+ export function keyHint(X, g) {
48
+ const id = SUBJECT.exec(g.subject)?.[1], o = X.spec?.objects?.[id];
49
+ if (!o?.keys?.length) return null;
50
+ const f = g.worstFrame, k = X.spec.props?.[o.prop]?.hand, h = X.fr(f).diag[`hold_${k}`] ?? {};
51
+ const near = o.keys.reduce((best, key, i) => (Math.abs(key.f - f) < Math.abs(o.keys[best].f - f) ? i : best), 0);
52
+ const wrist = Object.entries(WRIST_LIMITS).filter(([n, [lo, hi]]) => h[n] < lo || h[n] > hi).map(([n, [lo, hi]]) => `the wrist ${WRIST_WORDS[n]} needs ${Math.round(h[n])}deg (limit ${lo}..${hi})`);
53
+ // the arm already swung the elbow and turned the forearm (compiler/arm-orient.js): say so with what the wrist still needs
54
+ if (wrist.length) wrist[wrist.length - 1] += ` after the elbow swung ${Math.round(h.swivel ?? 0)}deg and the forearm turned`;
55
+ // the grip nearer the shoulder than the elbow folds (h.fold, the flexion it asked): out from the shoulder, not nearer the hand
56
+ const fold = h.miss > 0.005 && h.fold, short = fold ? `the grip is ${(h.miss * 100).toFixed(1)} cm inside the forearm's reach (the elbow would fold ${Math.round(h.fold)}deg; it bends to ${ELBOW_FOLD}deg and stops at ${ELBOW_STOP}deg)` : `the arm is ${(h.miss * 100).toFixed(1)} cm short`;
57
+ const why = [...(h.miss > 0.005 ? [short] : []), ...wrist];
58
+ const roll = o.keys[near].aim && o.keys[near].roll === undefined ? "" : "; an aim key without roll takes the roll the arm holds most easily";
59
+ return { cause: `objects.${id}.keys[${near}] f${o.keys[near].f}`,
60
+ hint: `${id} at f${f}: ${why.length ? why.join(", ") : "the hand cannot hold it there"}; ${fold ? "key a pose farther from the shoulder, or turn or move the body away from it" : "key a pose nearer the hand, or bring the body to it"} (root, yaw, spine_yaw)${roll}` };
61
+ }
62
+
63
+ const r1 = v => Math.round(v * 10) / 10, r2 = v => Math.round(v * 100) / 100;
64
+ /** observations.held units: what a row of a keyed object says. */
65
+ export const HELD_UNITS = "held: per key, deg = the drawn +Y off the asked one (aim, or rot's +Y), cm = the drawn base off pos, got = the drawn +Y (when 1 deg off or more), rot = what an aim key used";
66
+
67
+ /**
68
+ * Every key of every keyed carry as drawn: { <id>: [{ f, deg, cm, got?, rot? }] } in key order
69
+ * (frames as the spec writes them), or null when nothing is keyed.
70
+ */
71
+ export function heldReadings(out) {
72
+ const held = keyedEntries(out.ctx.spec);
73
+ if (!held.length) return null;
74
+ return Object.fromEntries(held.map(([id, o]) => {
75
+ const curve = curveIn(out, id), keys = [...o.keys].sort((a, b) => a.f - b.f);
76
+ return [id, keys.map((k, i) => {
77
+ const got = rootPoseAt(out, id, Math.round(curve.t[i])), y = [got.R[0][1], got.R[1][1], got.R[2][1]];
78
+ const want = k.aim ? norm(k.aim) : apply(matFromQuat(curve.q[i]), [0, 1, 0]), deg = r1(angle(want, y));
79
+ return { f: k.f, deg, cm: r1(dist(k.pos, got.pos) * 100), ...(deg >= 1 ? { got: y.map(r2) } : {}), ...(k.aim ? { rot: curve.rots[i] } : {}) };
80
+ })];
81
+ }));
82
+ }