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,102 @@
1
+ // One compile as motion.compile runs it: frames "auto" resolves (skills/frames-auto.js), open
2
+ // parameters are searched (search.js), unnamed checks get their index as a name (on a copy,
3
+ // qa/check-names.js), skills expand (skills/), the spec is validated strictly (leniently for a
4
+ // saved take compiled again), the compiler bakes and auto-fixes the frames, QA measures them with
5
+ // a cause and a hint per issue group against the agent's own spec, the style quantities are
6
+ // measured (qa/style.js observeMotion; keyed objects: every key as drawn, qa/key-checks.js
7
+ // heldReadings; each hand and foot against the nearest object, qa/near.js observeNear; each holding
8
+ // wrist's bend, qa/wrist-obs.js observeWrist; a skill
9
+ // paced to fit a short span: observations.paced; a skill whose release or
10
+ // settle the last frame cut: observations.cut), and the timeline and the
11
+ // requested probes read the result.
12
+ // liftRequest first takes what the agent obviously meant: spec.probes are request probes, and a
13
+ // bare goals[].moved id is object:<id>; relaxSpec (lenient.js) fixes mechanical slips in the expanded
14
+ // spec before validation and the answer lists each fix in notes.
15
+ import { compileMotion } from "./compiler/compile.js";
16
+ import { validateSpec } from "./compiler/validate.js";
17
+ import { runMotionQa } from "./qa/index.js";
18
+ import { observeMotion } from "./qa/style.js";
19
+ import { nameChecks } from "./qa/check-names.js";
20
+ import { expandUse } from "./skills/index.js";
21
+ import { resolveFrames } from "./skills/frames-auto.js";
22
+ import { markFramesAuto } from "./spec-store.js";
23
+ import { findParams, searchParams } from "./search.js";
24
+ import { timelineOf } from "./timeline.js";
25
+ import { runProbes } from "./probes.js";
26
+ import { canonicalGoals } from "./compiler/validate-goals.js";
27
+ import { heldReadings, HELD_UNITS } from "./qa/key-checks.js";
28
+ import { observeNear, NEAR_UNITS } from "./qa/near.js";
29
+ import { observeWrist, WRIST_UNITS } from "./qa/wrist-obs.js";
30
+ import { planWalks, applyWalkPlans } from "./walk-plan.js";
31
+ import { relaxSpec } from "./lenient.js";
32
+
33
+ const ms = t => Math.round(performance.now() - t);
34
+
35
+ /** A spec after retime: frame-range checks follow the keys they were written against. */
36
+ function warpChecks(spec, map) {
37
+ const c = structuredClone(spec.checks ?? {}), w = r => (Array.isArray(r) ? [Math.round(map(r[0])), Math.round(map(r[1]))] : r);
38
+ for (const name of ["contacts", "orient", "below"]) for (const k of c[name] ?? []) k.when = w(k.when);
39
+ for (const k of c.clearance ?? []) k.frames = w(k.frames);
40
+ return { ...spec, checks: c };
41
+ }
42
+
43
+ /**
44
+ * liftRequest(spec, probes) -> { spec, probes }. spec.probes (sent inside the spec, or added by
45
+ * a patch) join the request's probes after them and leave the spec, so they are never stored and
46
+ * never change its specId; bare goals[].moved ids get their object: prefix (validate-goals.js).
47
+ * The spec comes back as the same object when nothing changes.
48
+ */
49
+ export function liftRequest(spec, probes) {
50
+ if (!spec || typeof spec !== "object" || Array.isArray(spec)) return { spec, probes };
51
+ const goals = canonicalGoals(spec.goals);
52
+ if (spec.probes === undefined && goals === spec.goals) return { spec, probes };
53
+ const { probes: inSpec, ...rest } = spec;
54
+ const lifted = inSpec === undefined ? probes : probes ? [...probes, ...(Array.isArray(inSpec) ? inSpec : [inSpec])] : inSpec;
55
+ return { spec: goals === spec.goals ? rest : { ...rest, goals }, probes: lifted };
56
+ }
57
+
58
+ /**
59
+ * spec -> { out, qa, observations, spec (frames and open values filled in), expanded, used,
60
+ * fixed, search, timeline, probes?, notes?, ms }. opts.checks replaces every check (the spec's and its skills') for a
61
+ * re-measure; opts.lenient ignores unknown fields (take-source.js); opts.probes are measured
62
+ * (with spec.probes, liftRequest).
63
+ */
64
+ export function compileSpec(sent, { sk, scene }, opts = {}) {
65
+ const { spec, probes: asked } = liftRequest(sent, opts.probes);
66
+ const t0 = performance.now(), ctx = { sk, scene, lenient: opts.lenient === true }, notes = [];
67
+ const checks = opts.checks && nameChecks({ checks: opts.checks }).checks;
68
+ let plans = [];
69
+ const expand = s => {
70
+ const { spec: exp, used } = expandUse(nameChecks(applyWalkPlans(s, plans)), ctx);
71
+ // plain JSON objects compile about twice as fast as structuredClone'd ones (V8 shapes)
72
+ const full = relaxSpec(JSON.parse(JSON.stringify(checks ? { ...exp, checks } : exp)), scene, notes);
73
+ validateSpec(full, ctx);
74
+ return { full, used };
75
+ };
76
+ // a walkTo with arrive gets its cadence planned on probe compiles of the walk alone (walk-plan.js); the spec keeps arrive
77
+ plans = planWalks(spec, { sk, scene, trial: s => { const full = expand(s).full; return runMotionQa(sk, compileMotion({ fps: 24, ...full }, ctx), full, { explain: false }); } });
78
+ const framed = spec?.frames === "auto" && plans.length ? { ...spec, frames: resolveFrames(applyWalkPlans(spec, plans), ctx).frames } : resolveFrames(spec, ctx), params = findParams(framed);
79
+ let resolved = framed, search = null;
80
+ if (params.length) ({ spec: resolved, search } = searchParams(framed, params, ctx, s => expand(s).full));
81
+ // the store keeps the "auto" intent beside the resolved number (spec-store.js)
82
+ if (spec?.frames === "auto") markFramesAuto(resolved);
83
+ const t1 = performance.now();
84
+ const { full: expanded, used } = expand(resolved);
85
+ const out = compileMotion({ fps: 24, ...expanded }, ctx);
86
+ const compileMs = ms(t1), t2 = performance.now();
87
+ const measured = out.timeMap ? warpChecks(expanded, out.timeMap) : expanded;
88
+ const qa = runMotionQa(sk, out, measured, { userSpec: resolved }), observations = observeMotion(sk, out, measured.checks), held = heldReadings(out);
89
+ if (held) Object.assign(observations, { units: `${observations.units}; ${HELD_UNITS}`, held });
90
+ const near = observeNear(sk, out), wrist = observeWrist(sk, out);
91
+ if (near) Object.assign(observations, { units: `${observations.units}; ${NEAR_UNITS}`, near });
92
+ if (wrist) Object.assign(observations, { units: `${observations.units}; ${WRIST_UNITS}`, wrist });
93
+ // a skill that paced its beats to fit a short span says so where every answer keeps it (C7 tier 1)
94
+ const paced = used.filter(u => u.paced).map(u => `${u.skill} paced ${u.paced.toFixed(2)} to fit f${u.f0}..f${u.end}`);
95
+ if (paced.length) observations.paced = paced;
96
+ const cut = used.filter(u => u.cut).map(u => `${u.skill}: ${u.cut}`);
97
+ if (cut.length) observations.cut = cut;
98
+ if (plans.length) observations.planned = plans.map(p => p.note);
99
+ const timeline = timelineOf(out, { used, spec: expanded }), probes = asked && runProbes(out, asked, { spec: expanded, sk, scene, notes });
100
+ return { out, qa, observations, spec: resolved, expanded, used, fixed: out.fixed, search, timeline, ...(probes ? { probes } : {}), ...(notes.length ? { notes } : {}),
101
+ ms: { compile: compileMs, qa: ms(t2), total: ms(t0) } };
102
+ }
@@ -0,0 +1,51 @@
1
+ // Play a take v3 on a live rig: local quaternions straight onto the Mixamo control bones
2
+ // (first depth-first bone of each name; the nested identity "skinned" copies are never
3
+ // written) and the hips position in rig space. The rig root carries the character's
4
+ // placement and the 0.01 centimetre scale, so metres become centimetres here.
5
+ import { normalizeBoneName } from "../poses.js";
6
+
7
+ const CM_PER_M = 100;
8
+ const resolved = new WeakMap();
9
+
10
+ function controlBones(rig, names) {
11
+ let entry = resolved.get(rig);
12
+ if (entry && entry.key === names.join()) return entry;
13
+ const first = new Map();
14
+ rig.traverse(o => {
15
+ if (!o.isBone) return;
16
+ const n = normalizeBoneName(o.name);
17
+ if (!first.has(n)) first.set(n, o);
18
+ });
19
+ const bones = names.map(name => {
20
+ const target = normalizeBoneName(name);
21
+ for (const [n, bone] of first) if (n === target || n.endsWith(target)) return bone;
22
+ return null;
23
+ });
24
+ // Bind translations: a take never moves a bone except the hips, so every other bone goes
25
+ // back to its bind position (another player may have stretched it).
26
+ const binds = rig.userData?.poseBind;
27
+ const bindPos = bones.map(b => {
28
+ const p = b && binds?.get(b)?.position;
29
+ return b ? (p ? [p.x, p.y, p.z] : b.position.toArray()) : null;
30
+ });
31
+ entry = { key: names.join(), bones, bindPos, hips: names.indexOf("Hips") };
32
+ resolved.set(rig, entry);
33
+ return entry;
34
+ }
35
+
36
+ export const isTakeV3 = motion => motion?.schema === "cozyclay.take.v3";
37
+
38
+ export function applyTakeFrame(rig, take, frame) {
39
+ if (!rig || !take) return;
40
+ const { bones, bindPos, hips } = controlBones(rig, take.bones);
41
+ const f = Math.max(0, Math.min(take.frameCount - 1, Math.round(frame) || 0)), B = take.bones.length;
42
+ for (let b = 0; b < B; b++) {
43
+ const bone = bones[b];
44
+ if (!bone) continue;
45
+ const i = (f * B + b) * 4;
46
+ bone.quaternion.set(take.rot[i], take.rot[i + 1], take.rot[i + 2], take.rot[i + 3]);
47
+ if (b === hips) bone.position.set(take.hips[f * 3] * CM_PER_M, take.hips[f * 3 + 1] * CM_PER_M, take.hips[f * 3 + 2] * CM_PER_M);
48
+ else bone.position.set(bindPos[b][0], bindPos[b][1], bindPos[b][2]);
49
+ }
50
+ rig.updateMatrixWorld(true);
51
+ }
@@ -0,0 +1,91 @@
1
+ // C5. Probes: measurements the agent asks for on this compile (motion.compile { probes }),
2
+ // whether or not it installs. At most 16 probes; each is checked first (schema/fields.js
3
+ // PROBE_FIELDS, refs.js checkRef with the full grammar) and every problem is thrown at once as
4
+ // MOTION_SPEC_INVALID, one per line, e.g.
5
+ // probes[0].distance[0] "joint:LeftHnad": unknown bone; did you mean "LeftHand"?
6
+ // Bone names may be loose ("hips", "hand_l", bones.js resolveBone); they are turned into the
7
+ // rig's names before measuring. Answers per name (a probe without a name is "probes[i]"):
8
+ // distance [a, b] { min, minAt, max, maxAt, mean, first, last } (refs.js refDistance; metres
9
+ // to 6 decimals; every `every` frames, default 1; null when no frame has a value)
10
+ // position ref { at: [{ f, p: [x,y,z] }] } (metres to 4 decimals; at most 16
11
+ // rows; every defaults to the step that fits the range in 16 rows, a smaller one grows to it)
12
+ // frames: [f0, f1] absolute and inclusive, or one frame (default the whole take); a window partly past
13
+ // the take is cut to it. Both fixes leave a note in ctx.notes (lenient.js; output.adjusted).
14
+ import { PROBE_FIELDS } from "./schema/fields.js";
15
+ import { checkRef, refPoint, refDistance } from "./refs.js";
16
+ import { resolveBone } from "./bones.js";
17
+ import { didYouMean, fmt } from "./compiler/vocab.js";
18
+ import { clampProbe, fitEvery } from "./lenient.js";
19
+
20
+ const MAX_PROBES = 16, MAX_ROWS = 16, MAX_ERRORS = 12, SHAPE = "{ name, distance: [refA, refB] | position: ref, frames: [f0, f1], every }";
21
+ const r6 = v => Math.round(v * 1e6) / 1e6, r4 = v => Math.round(v * 1e4) / 1e4;
22
+
23
+ /** joint:/bone: refs with the rig's own bone name ("joint:hips" -> "joint:Hips"). */
24
+ function exactRef(sk, ref) {
25
+ const m = /^(joint|bone):(.+)$/s.exec(ref);
26
+ return m ? `${m[1]}:${resolveBone(sk, m[2]).bone ?? m[2]}` : ref;
27
+ }
28
+
29
+ function rangeOf(p, at, last, err, notes) {
30
+ const fr = p.frames === undefined ? [0, last] : clampProbe(p.frames, last, at, notes);
31
+ if (!fr) err(`${at}.frames must be [f0, f1], integer frames within 0..${last} with f0 <= f1, got ${fmt(p.frames)}`);
32
+ return fr;
33
+ }
34
+
35
+ /** Checked probes [{ name, refs, kind, range, every }]; throws MOTION_SPEC_INVALID listing every problem. */
36
+ function checkProbes(out, probes, { spec, sk, scene, notes }) {
37
+ const errors = [], err = m => { if (errors.length < MAX_ERRORS) errors.push(m); }, last = out.frameCount - 1, list = [], names = new Map();
38
+ if (!Array.isArray(probes)) err(`probes must be an array of ${SHAPE}`);
39
+ else if (probes.length > MAX_PROBES) err(`probes has ${probes.length} entries; at most ${MAX_PROBES} per compile`);
40
+ else probes.forEach((p, i) => {
41
+ const at = `probes[${i}]`;
42
+ if (!p || typeof p !== "object" || Array.isArray(p)) return err(`${at} must be ${SHAPE}`);
43
+ for (const k of Object.keys(p)) if (!PROBE_FIELDS.includes(k)) err(`${at} has unknown field "${k}"${didYouMean(k, PROBE_FIELDS)}; a probe is ${SHAPE}`);
44
+ const name = p.name ?? at;
45
+ if (typeof name !== "string" || !name) err(`${at}.name must be a non-empty string, got ${fmt(p.name)}`);
46
+ else if (names.has(name)) err(`${at}.name "${name}" is already used by ${names.get(name)}; probe names must differ`);
47
+ else names.set(name, at);
48
+ if ((p.distance === undefined) === (p.position === undefined)) return err(`${at} needs exactly one of distance: [refA, refB] or position: ref`);
49
+ const kind = p.distance !== undefined ? "distance" : "position";
50
+ const refs = kind === "distance" ? p.distance : [p.position];
51
+ if (kind === "distance" && !(Array.isArray(refs) && refs.length === 2 && refs.every(r => typeof r === "string"))) return err(`${at}.distance must be [refA, refB], two point refs, got ${fmt(p.distance)}`);
52
+ if (kind === "position" && typeof p.position !== "string") return err(`${at}.position must be a point ref, got ${fmt(p.position)}`);
53
+ const good = refs.map((r, j) => checkRef(spec, err, { sk, scene }, kind === "distance" ? `${at}.distance[${j}]` : `${at}.position`, r)).every(Boolean);
54
+ const range = rangeOf(p, at, last, err, notes);
55
+ if (p.every !== undefined && !(Number.isInteger(p.every) && p.every >= 1)) return err(`${at}.every must be a positive integer, got ${fmt(p.every)}`);
56
+ if (!good || !range) return;
57
+ const span = range[1] - range[0] + 1, asked = p.every ?? (kind === "position" ? Math.ceil(span / MAX_ROWS) : 1);
58
+ const every = kind === "position" ? fitEvery(asked, span, MAX_ROWS, at, notes) : asked;
59
+ list.push({ name, kind, refs: refs.map(r => exactRef(sk, r)), range, every });
60
+ });
61
+ if (errors.length) throw new Error(`MOTION_SPEC_INVALID: ${errors.join("\n")}${errors.length >= MAX_ERRORS ? "\n(more problems not listed)" : ""}`);
62
+ return list;
63
+ }
64
+
65
+ function frameList([f0, f1], every) {
66
+ const out = [];
67
+ for (let f = f0; f <= f1; f += every) out.push(f);
68
+ return out;
69
+ }
70
+
71
+ function distanceOf(out, p) {
72
+ const vals = frameList(p.range, p.every).map(f => [f, refDistance(out, f, p.refs[0], p.refs[1])]).filter(([, v]) => Number.isFinite(v));
73
+ if (!vals.length) return { min: null, minAt: null, max: null, maxAt: null, mean: null, first: null, last: null };
74
+ let lo = vals[0], hi = vals[0], sum = 0;
75
+ for (const fv of vals) { if (fv[1] < lo[1]) lo = fv; if (fv[1] > hi[1]) hi = fv; sum += fv[1]; }
76
+ return { min: r6(lo[1]), minAt: lo[0], max: r6(hi[1]), maxAt: hi[0], mean: r6(sum / vals.length), first: r6(vals[0][1]), last: r6(vals[vals.length - 1][1]) };
77
+ }
78
+
79
+ function positionOf(out, p) {
80
+ return { at: frameList(p.range, p.every).map(f => { const q = refPoint(out, f, p.refs[0]); return { f, p: q ? q.map(r4) : null }; }) };
81
+ }
82
+
83
+ /**
84
+ * runProbes(out, probes, { spec, sk, scene }) -> { [name]: result }
85
+ * @param {object} out compileMotion result
86
+ * @param {object[]} probes the request's probes
87
+ * @param {{ spec: object, sk: object, scene: object, notes?: string[] }} ctx spec = the expanded spec; notes gets the windows cut and every grown
88
+ */
89
+ export function runProbes(out, probes, ctx) {
90
+ return Object.fromEntries(checkProbes(out, probes, ctx).map(p => [p.name, p.kind === "distance" ? distanceOf(out, p) : positionOf(out, p)]));
91
+ }
@@ -0,0 +1,34 @@
1
+ // BODY on the fitted skin (src/motion3/body.js): an upper arm, forearm or hand going into any other
2
+ // part of the body (torso, hips, head, legs, the other arm) more than L.skin. Each limb probes as
3
+ // spheres on its bone; the body answers from its signed distance grids. The parts are named as the
4
+ // capsules were (qa/geometry.js CAPSULES), so "hand_r into torso_hi" reads the same as before; a
5
+ // spec's checks.allow { part, object: <part>, depth } lets a declared contact touch deeper.
6
+ import { bodyFor, limbDepth } from "../body.js";
7
+
8
+ const REGIONS = [
9
+ [/^Hips$/, "hips"], [/^(Spine|Spine1)$/, "torso_lo"], [/^Spine2$/, "torso_hi"], [/^Neck$/, "neck"], [/^Head/, "head"],
10
+ [/Shoulder$/, "shoulder"], [/UpLeg$/, "thigh"], [/Leg$/, "shin"], [/(Foot|Toe)/, "foot"], [/ForeArm$/, "farm"], [/Arm$/, "uarm"], [/Hand$/, "hand"],
11
+ ];
12
+ /** The QA part name of bone b ("thigh_l", "torso_hi", ...). */
13
+ export function regionOf(sk, b) {
14
+ const name = sk.names[b] ?? "", side = /^Left/.test(name) ? "_l" : /^Right/.test(name) ? "_r" : "";
15
+ const hit = REGIONS.find(([re]) => re.test(name));
16
+ return hit ? `${hit[1]}${side}` : name;
17
+ }
18
+
19
+ export const ARM_BONES = ["Arm", "ForeArm", "Hand"].flatMap(n => [`Left${n}`, `Right${n}`]);
20
+
21
+ /** L: limits { skin } (m); push(kind, msg, frame, value, limit); allowed(part, into): a declared depth or null. */
22
+ export function bodyChecks(sk, out, L, push, allowed) {
23
+ const body = bodyFor(sk);
24
+ if (!body) return;
25
+ const limbs = ARM_BONES.map(n => sk.index[n]);
26
+ out.frames.forEach(({ s }, f) => {
27
+ for (const b of limbs) {
28
+ const hit = limbDepth(body, sk, s, b);
29
+ if (hit.into < 0) continue;
30
+ const part = regionOf(sk, b), into = regionOf(sk, hit.into), lim = Math.max(allowed(part, into) ?? 0, L.skin);
31
+ if (hit.depth > lim) push("BODY", `${part} into ${into}`, f, hit.depth, lim);
32
+ }
33
+ });
34
+ }
@@ -0,0 +1,14 @@
1
+ // Unnamed checks get a name from their place in the agent's own spec ("contacts[0]"), so their
2
+ // QA groups say which check failed and two unnamed checks stay two groups (severity.js groupKey
3
+ // keeps a bracketed index). Runs on a copy before the skills expand, so skill checks keep theirs.
4
+
5
+ const NAMED = ["contacts", "orient", "below", "clearance", "appear"];
6
+
7
+ /** spec -> a copy whose unnamed spec.checks entries are named "<list>[<index>]" (spec when none are). */
8
+ export function nameChecks(spec) {
9
+ const c = spec?.checks;
10
+ if (!c || typeof c !== "object" || !NAMED.some(k => Array.isArray(c[k]) && c[k].some(e => e && typeof e === "object" && e.name === undefined))) return spec;
11
+ const checks = { ...c };
12
+ for (const k of NAMED) if (Array.isArray(c[k])) checks[k] = c[k].map((e, i) => (e && typeof e === "object" && e.name === undefined ? { ...e, name: `${k}[${i}]` } : e));
13
+ return { ...spec, checks };
14
+ }
@@ -0,0 +1,121 @@
1
+ // General motion checks (every take): scene collisions, ground, IK reach, planted-foot slide,
2
+ // bone length, one-frame pops (style.js), knee and elbow flips, knees bending backward, knees and
3
+ // elbows folded past their stop (KNEE: the shin in the thigh; ELBOW: the forearm in the upper arm,
4
+ // compiler/limbs.js), self contact, hands and held props entering the head (and the torso,
5
+ // contact-checks.js; arms into any other body part on the fitted skin, body-checks.js). Speed, acceleration, knee bend and balance are style: style.js measures them
6
+ // and checks them only when the spec names their limit (checks.limits).
7
+ import { add, sub, scale, apply, len, dist, cross, norm, dot, angle } from "../compiler/math.js";
8
+ import { CAPSULES, segPoints, sdBox, segDist, headDepth, solePoints } from "./geometry.js";
9
+ import { contactChecks } from "./contact-checks.js";
10
+ import { bodyChecks } from "./body-checks.js";
11
+ import { popChecks } from "./style.js";
12
+ import { groundSole } from "./ground.js";
13
+ import { footChecks } from "./foot-checks.js";
14
+ import { ELBOW_STOP, KNEE_STOP } from "../compiler/limbs.js";
15
+
16
+ export const QA_LIMITS = { allow: 0.005, groundAllow: 0.01, reach: 0.01, slide: 0.0015, plantH: 0.012,
17
+ speedCore: 0.04, speedLimb: 0.16, accCore: 0.015, accLimb: 0.05, flip: 60, knee: 150, headAllow: 0.003, self: 0,
18
+ body: 0.005, skin: 0.015, hands: 0.01, object: 0.005, grab: 0.03, balance: 0.08 };
19
+ /** FLIP reads a bend plane only where the limb is bent this far (deg) from straight and from folded flat. */
20
+ const FLIP_BEND = 20;
21
+ /** KNEE (folds) and ELBOW: degrees past KNEE_STOP or ELBOW_STOP that count (the IK sits on the stop to rounding). */
22
+ const STOP_TOL = 0.01;
23
+
24
+ export const allowance = (allow, part, obj) => {
25
+ for (const a of allow ?? []) if (part.startsWith(a.part) && (a.object === obj || (a.object.endsWith("*") && obj.startsWith(a.object.slice(0, -1))))) return a.depth;
26
+ return null;
27
+ };
28
+
29
+ export function generalChecks(sk, out, opts = {}) {
30
+ const L = { ...QA_LIMITS, ...opts.limits }, issues = [], I = sk.index, F = out.frames, n = F.length;
31
+ const push = (kind, msg, f, value, limit) => issues.push({ kind, msg, frame: f, value: +value.toFixed(4), limit });
32
+ for (let f = 0; f < n; f++) {
33
+ const { s, diag } = F[f], objs = out.objectsAt(f);
34
+ // bounding spheres of the boxes: a capsule farther away than its own reach cannot touch one
35
+ const spheres = Object.entries(objs).filter(([, box]) => box.half && !box.ignore)
36
+ .map(([id, box]) => [id, box, add(box.pos, apply(box.R, [0, box.half[1], 0])), len(box.half)]);
37
+ for (const [part, a, b, r] of CAPSULES) {
38
+ const A = s.P[I[a]], B = s.P[I[b]], mid = scale(add(A, B), 0.5), reach = r + dist(A, B) / 2;
39
+ let pts = null;
40
+ for (const [id, box, c, rb] of spheres) {
41
+ const lim = allowance(opts.allow, part, id) ?? L.allow;
42
+ if (reach - (dist(mid, c) - rb) <= lim) continue;
43
+ pts ??= segPoints(A, B);
44
+ const pen = r - Math.min(...pts.map(p => sdBox(p, box)));
45
+ if (pen > lim) push("COLLIDE", `${part} x ${id}`, f, pen, lim);
46
+ }
47
+ }
48
+ footChecks(sk, s, f, spheres, L, (part, id) => allowance(opts.allow, part, id), push);
49
+ for (const side of ["Left", "Right"]) {
50
+ const sp = solePoints(sk, s, side), low = Math.min(sp.heel[1], sp.ball[1], sp.tip[1]);
51
+ if (low < -L.groundAllow) push("GROUND", `${side} sole below floor`, f, -low, L.groundAllow);
52
+ }
53
+ for (const [k, v] of Object.entries(diag)) if (/miss_[lr]$/.test(k) && v > L.reach) push("REACH", k, f, v, L.reach);
54
+ // hands, forearms and held props must stay out of the head
55
+ const probes = [];
56
+ for (const S of ["Left", "Right"]) probes.push(...segPoints(s.P[I[`${S}Hand`]], s.P[I[`${S}HandMiddle3`]], 5).map(p => [`hand_${S[0].toLowerCase()}`, p, 0.022]));
57
+ for (const [pn, pts] of Object.entries(diag.props ?? {})) for (const [k, p] of Object.entries(pts)) if (Array.isArray(p) && k !== "origin" && !(opts.mouthPoints ?? []).includes(`${pn}.${k}`)) probes.push([`prop_${pn}`, p, 0.004]);
58
+ for (const [nm, p, r] of probes) { const d = headDepth(sk, s, p) + r; if (d > L.headAllow) push("FACE", `${nm} into head`, f, d, L.headAllow); }
59
+ if (segDist(s.P[I.LeftLeg], s.P[I.LeftFoot], s.P[I.RightLeg], s.P[I.RightFoot]) < 0.1 + L.self) push("SELF", "shins touch", f, 0.1, 0.1);
60
+ if (segDist(s.P[I.LeftUpLeg], s.P[I.LeftLeg], s.P[I.RightUpLeg], s.P[I.RightLeg]) < 0.135 + L.self) push("SELF", "thighs overlap", f, 0.135, 0.135);
61
+ for (const S of ["Left", "Right"]) {
62
+ const th = s.P[I[`${S}UpLeg`]], kn = s.P[I[`${S}Leg`]], an = s.P[I[`${S}Foot`]];
63
+ const bend = angle(sub(kn, th), sub(an, kn));
64
+ // the foot's heading on the ground; the toe sits below the ankle, which must not count
65
+ const toeDir = sub(s.P[I[`${S}ToeBase`]], an); toeDir[1] = 0;
66
+ const mid = sub(kn, [(th[0] + an[0]) / 2, (th[1] + an[1]) / 2, (th[2] + an[2]) / 2]);
67
+ if (bend > 3 && dot(mid, toeDir) < -0.002) push("KNEE", `${S} knee bends backward`, f, bend, 0);
68
+ // a knee folded past its stop puts the shin in the thigh (the solver stops at KNEE_STOP)
69
+ if (bend > KNEE_STOP + STOP_TOL) push("KNEE", `${S} knee folds ${bend.toFixed(0)}deg`, f, bend, KNEE_STOP);
70
+ // an elbow folded past its stop puts the forearm in the upper arm (the solver stops at ELBOW_STOP)
71
+ const fold = angle(sub(s.P[I[`${S}ForeArm`]], s.P[I[`${S}Arm`]]), sub(s.P[I[`${S}Hand`]], s.P[I[`${S}ForeArm`]]));
72
+ if (fold > ELBOW_STOP + STOP_TOL) push("ELBOW", `${S} elbow folds ${fold.toFixed(0)}deg`, f, fold, ELBOW_STOP);
73
+ }
74
+ }
75
+ const allowed = (part, into) => allowance(opts.allow, part, into);
76
+ contactChecks(sk, out, L, push, allowed);
77
+ bodyChecks(sk, out, L, push, allowed);
78
+ // temporal checks; a planted heel or ball is low over the ground under it, the floor or an object's top (ground.js)
79
+ for (const S of ["Left", "Right"]) {
80
+ const sole = F.map((_, f) => groundSole(out, f, S));
81
+ for (let f = 1; f < n; f++) {
82
+ const a = sole[f - 1], b = sole[f];
83
+ for (const key of ["heel", "ball"]) {
84
+ const planted = b[key][1] < L.plantH && a[key][1] < L.plantH && Math.abs(b[key][1] - a[key][1]) < 0.0005;
85
+ const hv = Math.hypot(b[key][0] - a[key][0], b[key][2] - a[key][2]);
86
+ if (planted && hv > L.slide) push("SLIDE", `${S} ${key} slides`, f, hv, L.slide);
87
+ }
88
+ }
89
+ }
90
+ for (let j = 0; j < sk.names.length; j++) {
91
+ const p = sk.parent[j]; if (p < 0) continue;
92
+ const L0 = dist(sk.restWorldPos[j], sk.restWorldPos[p]);
93
+ const drift = Math.max(...F.map(fr => Math.abs(dist(fr.s.P[j], fr.s.P[p]) - L0)));
94
+ if (drift > 0.001) push("LENGTH", `${sk.names[p]}-${sk.names[j]}`, 0, drift, 0.001);
95
+ }
96
+ popChecks(sk, out, L, push);
97
+ flipChecks(sk, F, L, push);
98
+ return issues;
99
+ }
100
+
101
+ /**
102
+ * FLIP: a knee's or elbow's bend plane turns more than L.flip degrees a frame. The plane is read
103
+ * only on frames the limb is bent FLIP_BEND or more from straight and from folded flat: near
104
+ * either end a few millimetres set it, so the hand or foot moving a few centimetres turns it any
105
+ * way (session df6acdcc: the right elbow folded to 171 deg as the wrist passed the shoulder, and
106
+ * its plane turned 71 deg in a frame while the elbow moved 3 cm). Across the frames between, the
107
+ * turn is shared out per frame, so a pole that flips while the limb is straight still shows.
108
+ */
109
+ function flipChecks(sk, F, L, push) {
110
+ const I = sk.index, bent = Math.sin(FLIP_BEND * Math.PI / 180);
111
+ for (const S of ["Left", "Right"]) for (const [a, b, c] of [[`${S}UpLeg`, `${S}Leg`, `${S}Foot`], [`${S}Arm`, `${S}ForeArm`, `${S}Hand`]]) {
112
+ let prev = null, at = 0;
113
+ for (let f = 0; f < F.length; f++) {
114
+ const P = F[f].s.P, up = sub(P[I[b]], P[I[a]]), down = sub(P[I[c]], P[I[b]]), nv = cross(up, down);
115
+ if (len(nv) < bent * len(up) * len(down)) continue;
116
+ const u = norm(nv), turn = prev ? angle(u, prev) : 0, gap = f - at;
117
+ if (turn / gap > L.flip) push("FLIP", `${b} bend plane jumps ${turn.toFixed(0)}deg${gap > 1 ? ` over ${gap} frames` : ""}`, f, turn / gap, L.flip);
118
+ prev = u; at = f;
119
+ }
120
+ }
121
+ }
@@ -0,0 +1,51 @@
1
+ // Contact checks on top of the general set (ported from the car-exit prototype QA): hands and
2
+ // held prop points entering the torso, the two hands overlapping, and hinged scene objects
3
+ // (spec.objects, e.g. a door) passing through the rest of the scene. Corners only for objects.
4
+ import { add, sub, scale, dot, len, apply, clamp } from "../compiler/math.js";
5
+ import { CAPSULES, sdBox, segDist } from "./geometry.js";
6
+ import { movesRecord } from "../carried.js";
7
+
8
+ const TORSO = CAPSULES.filter(c => c[0].startsWith("torso"));
9
+ const HAND_R = CAPSULES.find(c => c[0] === "hand_l")[3];
10
+ const PROP_R = 0.004;
11
+
12
+ function pointSegDist(p, a, b) {
13
+ const ab = sub(b, a), t = clamp(dot(sub(p, a), ab) / Math.max(dot(ab, ab), 1e-12), 0, 1);
14
+ return len(sub(p, add(a, scale(ab, t))));
15
+ }
16
+
17
+ function corners(o) {
18
+ const out = [];
19
+ for (const x of [-1, 1]) for (const y of [0, 2]) for (const z of [-1, 1]) out.push(add(o.pos, apply(o.R, [x * o.half[0], y * o.half[1], z * o.half[2]])));
20
+ return out;
21
+ }
22
+
23
+ /** L: limits { body, hands, object } (m); push(kind, msg, frame, value, limit); allowed(part, into): a declared depth or null. */
24
+ export function contactChecks(sk, out, L, push, allowed = () => null) {
25
+ const I = sk.index, hinged = Object.entries(out.ctx.spec.objects ?? {}).filter(([, o]) => movesRecord(o)).map(([id]) => id);
26
+ for (let f = 0; f < out.frames.length; f++) {
27
+ const { s, diag } = out.frames[f], P = s.P;
28
+ // hands into the torso: body-checks.js, on the fitted skin; held prop points stay on the capsules
29
+ const probes = [];
30
+ for (const [pn, pts] of Object.entries(diag.props ?? {})) for (const [k, p] of Object.entries(pts)) if (Array.isArray(p) && k !== "origin") probes.push([`prop_${pn}`, p, PROP_R]);
31
+ for (const [part, a, b, r] of TORSO) {
32
+ const worst = new Map();
33
+ for (const [nm, p, pr] of probes) worst.set(nm, Math.max(worst.get(nm) ?? -Infinity, r + pr - pointSegDist(p, P[I[a]], P[I[b]])));
34
+ for (const [nm, d] of worst) if (d > L.body) push("BODY", `${nm} into ${part}`, f, d, L.body);
35
+ }
36
+ const overlap = 2 * HAND_R - segDist(P[I.LeftHand], P[I.LeftHandMiddle3], P[I.RightHand], P[I.RightHandMiddle3]);
37
+ const handsLim = Math.max(allowed("hand_l", "hand_r") ?? 0, L.hands);
38
+ if (overlap > handsLim) push("SELF", "hands overlap", f, overlap, handsLim);
39
+ if (!hinged.length) continue;
40
+ const objs = out.objectsAt(f);
41
+ for (const id of hinged) {
42
+ if (!objs[id]?.half) continue;
43
+ const cs = corners(objs[id]);
44
+ for (const [oid, box] of Object.entries(objs)) {
45
+ if (hinged.includes(oid) || !box.half || box.ignore) continue;
46
+ const depth = -Math.min(...cs.map(p => sdBox(p, box)));
47
+ if (depth > L.object) push("OBJECT", `${id} into ${oid}`, f, depth, L.object);
48
+ }
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,80 @@
1
+ // Actionable QA: every issue group gets a `cause` (the spec element most responsible: a key and
2
+ // channel, an anchor, a fit, a generator step, a skill) and a `hint` (what to change, with a
3
+ // direction and an amount in world metres or degrees, at a frame). Hints are derived from the
4
+ // geometry of the worst frame and from the keys that bracket it. A hint function returns
5
+ // { cause, hint, fix? }; fix rides on the group unseen (non-enumerable) for qa/patch.js.
6
+ import { contactHints } from "./hints-contact.js";
7
+ import { motionHints } from "./hints-motion.js";
8
+ import { objectHints } from "./hints-object.js";
9
+ import { goalHints } from "./hints-goal.js";
10
+ import { wristHints } from "./wrist-checks.js";
11
+ import { attachPatches } from "./patch.js";
12
+
13
+ const HINTS = { ...contactHints, ...motionHints, ...objectHints, ...goalHints, ...wristHints };
14
+
15
+ function keyTable(out) {
16
+ const byCh = new Map();
17
+ for (const k of out.keys ?? []) for (const ch of Object.keys(k.set ?? {})) {
18
+ if (!byCh.has(ch)) byCh.set(ch, []);
19
+ byCh.get(ch).push({ f: k.f, src: k.src ?? "keys", step: k.step?.includes(ch) });
20
+ }
21
+ for (const list of byCh.values()) list.sort((a, b) => a.f - b.f);
22
+ return byCh;
23
+ }
24
+
25
+ /** The context every hint function reads: frames by absolute index, keys, drivers. */
26
+ function context(sk, out, spec) {
27
+ const table = keyTable(out);
28
+ const X = {
29
+ sk, out, spec, I: sk.index,
30
+ fr: f => out.frames[Math.max(0, Math.min(out.frames.length - 1, f - (out.f0 ?? 0)))],
31
+ rel: f => Math.max(0, Math.min(out.frames.length - 1, f - (out.f0 ?? 0))),
32
+ keys: ch => table.get(ch) ?? [],
33
+ bracket(ch, f) {
34
+ const list = X.keys(ch);
35
+ let prev = null, next = null;
36
+ for (const k of list) { if (k.f <= f) prev = k; else { next = k; break; } }
37
+ return { prev, next };
38
+ },
39
+ /** "keys[12] f200 hand_r" when a key sits within 2 frames, else the two keys around f. */
40
+ cause(ch, f) {
41
+ const { prev, next } = X.bracket(ch, f);
42
+ if (!prev && !next) return `${ch} (not keyed)`;
43
+ const near = prev && f - prev.f <= 2 ? prev : next && next.f - f <= 2 ? next : null;
44
+ if (near) return `${near.src} f${Math.round(near.f)} ${ch}`;
45
+ if (!next) return `${ch} held since ${prev.src} f${Math.round(prev.f)}`;
46
+ if (!prev) return `${ch} before ${next.src} f${Math.round(next.f)}`;
47
+ return `${ch} between ${prev.src} f${Math.round(prev.f)} and ${next.src === prev.src ? "" : `${next.src} `}f${Math.round(next.f)}`;
48
+ },
49
+ /** What drives hand k at frame f: an anchor weight, the hand target, or the FK arm. */
50
+ hand(k, f) {
51
+ const c = X.fr(f).c;
52
+ if (!(c[`armw_${k}`] > 0.5)) return { label: `arm ${k} (FK: aswing_${k}/abend_${k}, or armw_${k} 1 and hand_${k})`, ch: `aswing_${k}` };
53
+ let best = null;
54
+ for (const [ch, w] of Object.entries(c)) {
55
+ const mm = /^w_(.+)_([lr])$/.exec(ch);
56
+ if (mm && mm[2] === k && w > 0.5 && (!best || w > best.w)) best = { w, anchor: mm[1], ch };
57
+ }
58
+ if (!best) return { label: `hand_${k}`, ch: `hand_${k}` };
59
+ const fit = (spec.fits ?? []).findIndex(ft => ft.anchor === best.anchor && ft.side === k);
60
+ return { label: `anchor ${best.anchor}`, ch: best.ch, anchor: best.anchor, fit: fit >= 0 ? fit : null };
61
+ },
62
+ };
63
+ return X;
64
+ }
65
+
66
+ /** Adds cause + hint (+ patch) to the first `limit` groups (mutates and returns them); userSpec is what patches address. */
67
+ export function explainGroups(sk, out, spec, groups, { limit = 16, userSpec } = {}) {
68
+ if (!groups.length) return groups;
69
+ const X = context(sk, out, spec);
70
+ for (const g of groups.slice(0, limit)) {
71
+ try {
72
+ const r = HINTS[g.kind]?.(X, g);
73
+ if (r) { g.cause = r.cause; g.hint = r.hint; }
74
+ if (r?.fix) Object.defineProperty(g, "fix", { value: r.fix, enumerable: false, configurable: true });
75
+ } catch (error) {
76
+ g.cause = "unknown"; g.hint = `no hint (${error.message})`;
77
+ }
78
+ }
79
+ return attachPatches(groups, { X, userSpec });
80
+ }
@@ -0,0 +1,5 @@
1
+ // Compact number formatting for QA hints (millimetre precision).
2
+ export const m3 = x => { const r = Math.round(x * 1000) / 1000; return Object.is(r, -0) ? 0 : r; };
3
+
4
+ /** Compact vector: [+0.021, 0, -0.004]. */
5
+ export const vec = v => `[${v.map(x => { const r = m3(x); return r > 0 ? `+${r}` : `${r}`; }).join(", ")}]`;
@@ -0,0 +1,72 @@
1
+ // COLLIDE for the feet: the foot's skin against the scene bodies, as "foot_<k> x <id>". The shin capsule ends at
2
+ // the ankle and GROUND reads the floor only, so a swinging toe went 9.6 cm into a stair riser with QA clean
3
+ // (session e7edb718). The probes are points on the fitted skin of the foot and toe bones (body.js grids: the
4
+ // cells within half a step of the skin, one per SPACING m cube, carried by their bone); a rig without a fitted
5
+ // body uses the sole points. A foot on a body's top is a step, not a collision: a probe the top face is nearest
6
+ // to may sit up to the ground's allowance in it (QA_LIMITS.groundAllow, as GROUND gives the floor); through a
7
+ // side face the usual allowance holds. A part-wise allow entry ({ part: "foot", object }) sets both.
8
+ import { add, apply, dist, dot, scale } from "../compiler/math.js";
9
+ import { bodyFor } from "../body.js";
10
+ import { sdBox, boxNormal, solePoints } from "./geometry.js";
11
+
12
+ const UP = [0, 1, 0], TOP = 0.7, SPACING = 0.025;
13
+ const skins = new Map();
14
+
15
+ /** Skin points of a foot in its bones' bind frames: [[bone index, local point], ...] (null for a rig without a fitted body). */
16
+ function footSkin(sk, side) {
17
+ const key = `${sk.model}:${side}`;
18
+ if (skins.has(key)) return skins.get(key);
19
+ const body = bodyFor(sk), out = body ? [] : null;
20
+ for (const g of body?.grids ?? []) {
21
+ if (g.b !== sk.index[`${side}Foot`] && g.b !== sk.index[`${side}ToeBase`]) continue;
22
+ const [nx, ny, nz] = g.dims, seen = new Set();
23
+ for (let i = 0; i < nx; i++) for (let j = 0; j < ny; j++) for (let k = 0; k < nz; k++) {
24
+ if (Math.abs(g.data[(i * ny + j) * nz + k] / 1000) > body.step / 2) continue;
25
+ const q = [g.min[0] + i * body.step, g.min[1] + j * body.step, g.min[2] + k * body.step], cell = q.map(v => Math.floor(v / SPACING)).join();
26
+ if (!seen.has(cell)) { seen.add(cell); out.push([g.b, q]); }
27
+ }
28
+ }
29
+ skins.set(key, out);
30
+ return out;
31
+ }
32
+
33
+ /** The foot's collision probes at state s: world points on its skin (the sole points without a fitted body). */
34
+ export function footProbes(sk, s, side) {
35
+ const skin = footSkin(sk, side);
36
+ if (!skin?.length) {
37
+ const sp = solePoints(sk, s, side), mid = (a, b) => scale(add(a, b), 0.5);
38
+ return [sp.heel, mid(sp.heel, sp.ball), sp.ball, mid(sp.ball, sp.tip), sp.tip];
39
+ }
40
+ return skin.map(([b, q]) => add(s.P[b], apply(s.R[b], q)));
41
+ }
42
+
43
+ /**
44
+ * How deep a foot's probes go into a box, through its top and through its sides: { top, side } in metres
45
+ * (-Infinity: not in), each with the probe that goes deepest that way ({ topAt, sideAt }).
46
+ */
47
+ export function footDepths(probes, box) {
48
+ const out = { top: -Infinity, side: -Infinity, topAt: null, sideAt: null };
49
+ for (const p of probes) {
50
+ const d = -sdBox(p, box);
51
+ if (d <= 0) continue;
52
+ const way = dot(boxNormal(p, box), UP) > TOP ? "top" : "side";
53
+ if (d > out[way]) { out[way] = d; out[`${way}At`] = p; }
54
+ }
55
+ return out;
56
+ }
57
+
58
+ /**
59
+ * Push COLLIDE "foot_<k> x <id>" for each foot in each body at frame f. spheres: [id, box, centre, radius] of
60
+ * the bodies (checks.js); allowed(part, id) the spec's allow depth or null.
61
+ */
62
+ export function footChecks(sk, s, f, spheres, L, allowed, push) {
63
+ for (const [side, k] of [["Left", "l"], ["Right", "r"]]) {
64
+ const probes = footProbes(sk, s, side), part = `foot_${k}`;
65
+ for (const [id, box, c, rb] of spheres) {
66
+ if (probes.every(p => dist(p, c) > rb)) continue;
67
+ const d = footDepths(probes, box), own = allowed(part, id), sideLim = own ?? L.allow, topLim = own ?? L.groundAllow;
68
+ if (d.side > sideLim) push("COLLIDE", `${part} x ${id}`, f, d.side, sideLim);
69
+ else if (d.top > topLim) push("COLLIDE", `${part} x ${id}`, f, d.top, topLim);
70
+ }
71
+ }
72
+ }