cozyclay 2.1.1 → 3.0.0

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