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,143 @@
1
+ // Validation of spec.objects (C3): a hinge { pivot, axis?, channel } or a carry { prop, grab?,
2
+ // release?, at?, along?, visible?, level?, keys? }. Both take a body or a group (Empty) id; at/along
3
+ // pin the palm offset (a group: its root; without them a group keeps its grab-frame offset); keys
4
+ // (C14) pose a carried object while it is in hand (rot, or aim + roll: C16). A switch { visible?,
5
+ // level? } (C15: no prop, no hinge field) takes any record, a light or an effect too, and moves
6
+ // nothing; so does a follow { follow, offset?, aim?, visible?, level? } (C16), which rides a point
7
+ // of the body. A free entry { keys?, fall?, visible?, level? } (C17: no prop, no follow, no hinge field) moves a body or a
8
+ // group on its own: world pose keys over the whole take, and gravity falls; a carry takes fall too, before its grab
9
+ // or from its release on. Fields: schema/fields.js OBJECT_FIELDS, OBJECT_KEY_FIELDS, FALL_FIELDS. See validate.js.
10
+ import { isVec3, fmt, didYouMean } from "./vocab.js";
11
+ import { objectOk } from "./validate-common.js";
12
+ import { OBJECT_FIELDS, OBJECT_KEY_FIELDS, FALL_FIELDS } from "../schema/fields.js";
13
+ import { isSwitch, isFollow, movesRecord } from "../carried.js";
14
+ import { isFree } from "../free-motion.js";
15
+ import { checkRef } from "../refs.js";
16
+ import { partsOf } from "../object-pose.js";
17
+
18
+ const KEY = "{ f, pos: [x, y, z], rot: [x, y, z, w] } or { f, pos, aim: [x, y, z], roll? }", FALL = "{ at, onto?, bounce? }";
19
+ const isQuat = q => Array.isArray(q) && q.length === 4 && q.every(Number.isFinite) && Math.abs(Math.hypot(...q) - 1) <= 0.01;
20
+ const isDir = v => isVec3(v) && Math.hypot(...v) > 1e-9;
21
+
22
+ /** A key's rotation: rot (a unit quaternion) or aim (a world direction for its +Y) with an optional roll (degrees about it), not both. */
23
+ function keyTurnOk(err, kat, k) {
24
+ if (k.rot !== undefined && k.aim !== undefined) return err(`${kat} gives both rot and aim; give one: rot [x, y, z, w], or aim [x, y, z] (the world direction its +Y points) with roll`);
25
+ if (k.aim === undefined) {
26
+ if (k.roll !== undefined) err(`${kat}.roll turns the object about aim; give aim [x, y, z] with it, or leave roll out (rot fixes the roll)`);
27
+ if (k.lean !== undefined) err(`${kat}.lean lets the compiler lean aim; give aim [x, y, z] with it (rot is used exactly)`);
28
+ if (!isQuat(k.rot)) err(`${kat}.rot must be a unit quaternion [x, y, z, w], got ${fmt(k.rot)}${k.rot === undefined ? "; or give aim [x, y, z], the world direction its +Y points" : ""}`);
29
+ return;
30
+ }
31
+ if (!isDir(k.aim)) err(`${kat}.aim must be a non-zero world direction [x, y, z] for the object's +Y (a bat: knob to barrel), got ${fmt(k.aim)}`);
32
+ if (k.roll !== undefined && !Number.isFinite(k.roll)) err(`${kat}.roll must be degrees about aim (a number), got ${fmt(k.roll)}`);
33
+ if (k.lean !== undefined && !(Number.isFinite(k.lean) && k.lean >= 0 && k.lean <= 45)) err(`${kat}.lean must be the most degrees the compiler may lean aim (0..45), got ${fmt(k.lean)}`);
34
+ }
35
+
36
+ /** objects.<id>.keys (C14): whole frames inside grab..release (free, C17: the whole take), one per frame, a world position and a unit quaternion. */
37
+ function validateKeys(err, unknown, at, o, frames, free = false) {
38
+ if (!Array.isArray(o.keys)) return err(`${at}.keys must be an array of ${KEY}`);
39
+ const last = Number.isInteger(frames) ? frames - 1 : Infinity, lo = free ? 0 : o.grab ?? 0, hi = free ? last : o.release ?? last, seen = new Map();
40
+ o.keys.forEach((k, i) => {
41
+ const kat = `${at}.keys[${i}]`;
42
+ if (!k || typeof k !== "object" || Array.isArray(k)) return err(`${kat} must be ${KEY}, got ${fmt(k)}`);
43
+ for (const f of Object.keys(k)) if (!OBJECT_KEY_FIELDS.includes(f)) unknown(`${kat} has unknown field "${f}"${didYouMean(f, OBJECT_KEY_FIELDS)}; a key is ${KEY}`);
44
+ if (!Number.isInteger(k.f)) err(`${kat}.f must be a whole frame, got ${fmt(k.f)}`);
45
+ else if (k.f < lo || k.f > hi) err(free ? `${kat}.f = ${k.f} is outside the take (0..${hi})` : `${kat}.f = ${k.f} is outside grab..release (${lo}..${hi}); keys pose a carried object only while it is in hand (drop its prop to move it on its own, C17)`);
46
+ else if (seen.has(k.f)) err(`${kat}.f = ${k.f}: keys[${seen.get(k.f)}] is at that frame already; one key per frame`);
47
+ else seen.set(k.f, i);
48
+ if (!isVec3(k.pos)) err(`${kat}.pos must be [x, y, z], the world position of the object's base (a group: its root), got ${fmt(k.pos)}`);
49
+ keyTurnOk(err, kat, k);
50
+ });
51
+ }
52
+
53
+ /** An object: ref a follow entry may use: one no follow entry moves (else it would follow itself). */
54
+ function stillRef(spec, err, scene, at, ref) {
55
+ const id = /^object:(.+?)(?:\.(?:center|top|base))?$/s.exec(ref)?.[1];
56
+ if (id === undefined) return;
57
+ const movers = partsOf(scene, id).flatMap(p => [p, ...(scene.objects[p].ancestors ?? [])].find(k => movesRecord(spec.objects?.[k])) ?? []);
58
+ const by = movers.find(k => isFollow(spec.objects[k]));
59
+ if (by) err(`${at} "${ref}": "${id}" moves with the follow entry objects.${by}; a follow entry rides a joint, landmark, prop or anchor point, or an object no follow entry moves`);
60
+ }
61
+
62
+ /** follow (C16): a point ref, offset along the record's own axes, aim "face" | "up" | [x, y, z] | a point ref. */
63
+ function validateFollow(spec, err, ctx, at, o) {
64
+ if (checkRef(spec, err, ctx, `${at}.follow`, o.follow)) stillRef(spec, err, ctx.scene, `${at}.follow`, o.follow);
65
+ if (o.offset !== undefined && !isVec3(o.offset)) err(`${at}.offset must be [x, y, z], metres along the record's own axes after aim (+Y along aim), got ${fmt(o.offset)}`);
66
+ const aim = o.aim;
67
+ if (aim === undefined || aim === "face" || aim === "up") return;
68
+ if (Array.isArray(aim)) { if (!isDir(aim)) err(`${at}.aim must be a non-zero world direction [x, y, z], got ${fmt(aim)}`); return; }
69
+ if (typeof aim !== "string" || !/^[a-z]+:/.test(aim)) return err(`${at}.aim must be "face", "up", a world direction [x, y, z] or a point ref (toward it), where the record's +Y points; got ${fmt(aim)}`);
70
+ if (checkRef(spec, err, ctx, `${at}.aim`, aim)) stillRef(spec, err, ctx.scene, `${at}.aim`, aim);
71
+ }
72
+
73
+ function frameOk(err, at, v, frames) {
74
+ if (v === undefined) return true;
75
+ if (Number.isInteger(v) && v >= 0 && (!Number.isInteger(frames) || v < frames)) return true;
76
+ err(`${at} must be a whole frame 0..${Number.isInteger(frames) ? frames - 1 : "frames-1"}, got ${fmt(v)}`);
77
+ return false;
78
+ }
79
+
80
+ /** objects.<id>.fall (C17): { at, onto?, bounce? } or a list, each on its own frame; a carry falls only out of hand. */
81
+ function validateFalls(err, unknown, at, o, { frames, scene, carry }) {
82
+ const list = Array.isArray(o.fall) ? o.fall : [o.fall], seen = new Set();
83
+ if (!list.length) err(`${at}.fall is an empty list; give ${FALL} or a list of them`);
84
+ list.forEach((fl, i) => {
85
+ const fat = Array.isArray(o.fall) ? `${at}.fall[${i}]` : `${at}.fall`;
86
+ if (!fl || typeof fl !== "object" || Array.isArray(fl)) return err(`${fat} must be ${FALL} (at: the frame it starts to fall), got ${fmt(fl)}`);
87
+ for (const k of Object.keys(fl)) if (!FALL_FIELDS.includes(k)) unknown(`${fat} has unknown field "${k}"${didYouMean(k, FALL_FIELDS)}; a fall is ${FALL}`);
88
+ if (fl.at === undefined) err(`${fat}.at must be the whole frame it starts to fall`);
89
+ else if (frameOk(err, `${fat}.at`, fl.at, frames)) {
90
+ if (seen.has(fl.at)) err(`${fat}.at = ${fl.at}: another fall starts on that frame`);
91
+ seen.add(fl.at);
92
+ const g = o.grab ?? 0, r = o.release;
93
+ if (carry && fl.at >= g && (r === undefined || fl.at < r)) err(`${fat}.at = ${fl.at} is while it is in hand (grab ${g}..release ${r ?? "end"}); a carried object falls before its grab or from its release on`);
94
+ }
95
+ if (fl.onto !== undefined) objectOk(err, scene, `${fat}.onto`, fl.onto, { groups: true });
96
+ if (fl.bounce !== undefined && !(Number.isFinite(fl.bounce) && fl.bounce >= 0 && fl.bounce <= 0.6)) err(`${fat}.bounce must be 0..0.6 (the share of its landing speed it bounces back with; 0 lands dead), got ${fmt(fl.bounce)}`);
97
+ });
98
+ }
99
+
100
+ /** visible and level name channels (C15): visible shows the record above 0.5, level scales its light, glow and effect rate. */
101
+ function channelsOk(err, at, o) {
102
+ if (o.visible !== undefined && (typeof o.visible !== "string" || !o.visible)) err(`${at}.visible must name a channel (shown above 0.5)`);
103
+ if (o.level !== undefined && (typeof o.level !== "string" || !o.level)) err(`${at}.level must name a channel (the record's level 0..4: 0 dark, 1 as authored; it scales its light, glow and effect rate)`);
104
+ }
105
+
106
+ export function validateObjects(spec, err, { sk, scene, frames = spec.frames, unknown = err }) {
107
+ for (const [id, o] of Object.entries(spec.objects ?? {})) {
108
+ const at = `objects.${id}`, kind = o?.prop !== undefined ? "carry" : isFollow(o) ? "follow" : isFree(o) ? "free" : isSwitch(o) ? "switch" : "hinge", fields = OBJECT_FIELDS[kind];
109
+ // a switch and a follow take any record (a light, an effect); the others a body or a group
110
+ if ((kind !== "switch" && kind !== "follow") || !scene?.others?.[id]) objectOk(err, scene, at, id, { groups: true });
111
+ for (const f of Object.keys(o ?? {})) if (!fields.includes(f) && !(kind === "hinge" && (f === "keys" || f === "fall"))) unknown(`${at} (${kind}) has unknown field "${f}"${didYouMean(f, fields)}; fields: ${fields.join(", ")}`);
112
+ if (kind === "follow") {
113
+ validateFollow(spec, err, { sk, scene }, at, o);
114
+ channelsOk(err, at, o);
115
+ continue;
116
+ }
117
+ if (kind === "switch") {
118
+ if (o.visible === undefined && o.level === undefined) err(`${at} needs visible or level (a channel): a switch shows, hides or dims "${id}" without moving it; a carry needs prop, a hinge pivot and channel`);
119
+ channelsOk(err, at, o);
120
+ continue;
121
+ }
122
+ if (kind === "free") {
123
+ if (o.keys !== undefined) validateKeys(err, unknown, at, o, frames, true);
124
+ if (o.fall !== undefined) validateFalls(err, unknown, at, o, { frames, scene, carry: false });
125
+ channelsOk(err, at, o);
126
+ continue;
127
+ }
128
+ if (kind === "carry") {
129
+ if (!spec.props?.[o.prop]) err(`${at}.prop "${o.prop}" is not in spec.props`);
130
+ for (const f of ["at", "along"]) if (o[f] !== undefined && !isVec3(o[f])) err(`${at}.${f} must be [d, l, n] along the palm axes, got ${fmt(o[f])}`);
131
+ channelsOk(err, at, o);
132
+ if (frameOk(err, `${at}.grab`, o.grab, frames) && frameOk(err, `${at}.release`, o.release, frames)
133
+ && o.grab !== undefined && o.release !== undefined && !(o.grab < o.release)) err(`${at}: grab (${o.grab}) must come before release (${o.release})`);
134
+ if (o.keys !== undefined) validateKeys(err, unknown, at, o, frames);
135
+ if (o.fall !== undefined) validateFalls(err, unknown, at, o, { frames, scene, carry: true });
136
+ continue;
137
+ }
138
+ if (o?.keys !== undefined || o?.fall !== undefined) err(`${at}: a hinge turns by its channel "${o.channel}", keyed in spec.keys; keys and fall move an object with no pivot, axis or channel (C17), or a carried one (prop)`);
139
+ if (!isVec3(o?.pivot)) err(`${at}.pivot must be [x, y, z] (world hinge point), got ${fmt(o?.pivot)}`);
140
+ if (o?.axis !== undefined && (!isVec3(o.axis) || Math.hypot(...o.axis) < 1e-9)) err(`${at}.axis must be a non-zero [x, y, z], got ${fmt(o.axis)}`);
141
+ if (typeof o?.channel !== "string" || !o.channel) err(`${at}.channel must name the angle channel (degrees)`);
142
+ }
143
+ }
@@ -0,0 +1,116 @@
1
+ // Validation of the spec parts other than keys: anchors, props, fits, generators, checks and
2
+ // fix; objects (validate-objects.js) and goals (validate-goals.js) run from here too. Unknown
3
+ // fields go through opts.unknown, which ignores them on a lenient reload. See validate.js.
4
+ import { didYouMean, suggestField, CONVENTIONS, isVec3, fmt } from "./vocab.js";
5
+ import { FIX_KEYS } from "../fix/index.js";
6
+ import { QA_LIMITS } from "../qa/checks.js";
7
+ import { ANCHOR_FIELDS, PROP_FIELDS, FIT_FIELDS, WALK_FIELDS, CHECK_FIELDS, CHECK_ENTRY_FIELDS } from "../schema/fields.js";
8
+ import { boneOk, objectOk, refOk } from "./validate-common.js";
9
+ import { validateObjects } from "./validate-objects.js";
10
+ import { validateGoals } from "./validate-goals.js";
11
+
12
+ const SIDE = new Set(["l", "r"]);
13
+ const isObject = v => Boolean(v) && typeof v === "object" && !Array.isArray(v);
14
+
15
+ /** Every field of obj not in fields goes to unknown, with a suggestion and the list. */
16
+ function fieldsOk(unknown, at, obj, fields) {
17
+ if (!isObject(obj)) return;
18
+ for (const f of Object.keys(obj)) if (!fields.includes(f)) unknown(`${at} has unknown field "${f}"${suggestField(f, fields)} (fields: ${fields.join(", ")})`);
19
+ }
20
+
21
+ function validateAnchors(spec, err, { sk, scene, unknown = err }) {
22
+ for (const [name, a] of Object.entries(spec.anchors ?? {})) {
23
+ const at = `anchors.${name}`;
24
+ if (/_[lr]$/.test(name)) err(`${at}: an anchor name may not end in _l or _r (the weight channel w_${name}_l would read as anchor "${name.slice(0, -2)}"); the side goes on the weight channel, so name it "${name.slice(0, -2)}"`);
25
+ if (!a || !ANCHOR_FIELDS[a.kind]) { err(`${at}.kind must be one of ${Object.keys(ANCHOR_FIELDS).join(", ")}, got ${fmt(a?.kind)}`); continue; }
26
+ fieldsOk(unknown, `${at} (${a.kind})`, a, ["kind", ...ANCHOR_FIELDS[a.kind]]);
27
+ for (const f of ["p", "offset", "local"]) if (a[f] !== undefined && !isVec3(a[f])) err(`${at}.${f} must be [x, y, z], got ${fmt(a[f])}`);
28
+ if ((a.kind === "world" || a.kind === "body") && a.p === undefined) err(`${at} (${a.kind}) needs p: [x, y, z]${a.kind === "body" ? `, ${CONVENTIONS.pole}` : ""}`);
29
+ if (a.kind === "bone") boneOk(err, sk, at, a.bone);
30
+ if (a.kind === "segment") { boneOk(err, sk, `${at}.from`, a.from); boneOk(err, sk, `${at}.to`, a.to); if (a.reach !== undefined && !SIDE.has(a.reach)) err(`${at}.reach must be "l" or "r"`); }
31
+ if (a.kind === "object") objectOk(err, scene, at, a.object, { groups: true });
32
+ if (a.kind === "landmark" && a.name !== "mouth") err(`${at}: the only landmark is "mouth", got ${fmt(a.name)}`);
33
+ if (a.kind === "prop" && !spec.props?.[a.prop]?.points?.[a.point] && a.point !== "origin") err(`${at} needs prop point ${a.prop}.${a.point}, which spec.props does not define`);
34
+ }
35
+ }
36
+
37
+ function validateProps(spec, err, { sk, unknown = err }) {
38
+ for (const [name, p] of Object.entries(spec.props ?? {})) {
39
+ const at = `props.${name}`;
40
+ fieldsOk(unknown, at, p, PROP_FIELDS);
41
+ if (!SIDE.has(p?.hand)) err(`${at}.hand must be "l" or "r", got ${fmt(p?.hand)}`);
42
+ if (!Array.isArray(p?.origin) || !p.origin.length) err(`${at}.origin must list one or more bones`);
43
+ else p.origin.forEach(b => boneOk(err, sk, `${at}.origin`, b));
44
+ for (const [pt, v] of Object.entries(p?.points ?? {})) if (!isVec3(v)) err(`${at}.points.${pt} must be [d, l, n] along the palm axes, got ${fmt(v)}`);
45
+ }
46
+ (spec.fits ?? []).forEach((fit, i) => {
47
+ const at = `fits[${i}]`;
48
+ fieldsOk(unknown, at, fit, FIT_FIELDS);
49
+ if (!SIDE.has(fit?.side)) err(`${at}.side must be "l" or "r", got ${fmt(fit?.side)}`);
50
+ if (!spec.anchors?.[fit?.anchor]) err(`${at}.anchor "${fit?.anchor}" is not in spec.anchors${didYouMean(String(fit?.anchor), Object.keys(spec.anchors ?? {}))}`);
51
+ const e = fit?.effector;
52
+ if (e?.prop !== undefined) { if (!spec.props?.[e.prop]?.points?.[e.point]) err(`${at}.effector ${e.prop}.${e.point} is not a point of spec.props`); }
53
+ else if (Array.isArray(e?.joints)) e.joints.forEach(j => boneOk(err, sk, `${at}.effector`, j));
54
+ else err(`${at}.effector must be { prop, point } or { joints: [bone, ...] }`);
55
+ if (fit?.offset !== undefined && !isVec3(fit.offset)) err(`${at}.offset must be [x, y, z]`);
56
+ if (fit?.orient !== undefined && typeof fit.orient !== "boolean") err(`${at}.orient must be true or false (false: the hand takes no grip turn), got ${fmt(fit.orient)}`);
57
+ });
58
+ }
59
+
60
+ function validateGenerators(spec, err, { frames, unknown = err, past = "" }) {
61
+ (spec.generators ?? []).forEach((g, i) => {
62
+ const at = `generators[${i}]`;
63
+ if (g?.type !== "walk") return err(`${at}.type must be "walk", got ${fmt(g?.type)}`);
64
+ fieldsOk(unknown, at, g, WALK_FIELDS);
65
+ if (!Number.isInteger(g.f0) || g.f0 < 0 || g.f0 > frames - 1) err(`${at}.f0 must be an integer frame within 0..${frames - 1}, got ${fmt(g.f0)}${g.f0 > frames - 1 ? past : ""}`);
66
+ if (g.heading !== undefined && !(Array.isArray(g.heading) && g.heading.length === 2 && g.heading.every(Number.isFinite))) err(`${at}.heading must be [h0, h1] in degrees (${CONVENTIONS.yaw}), got ${fmt(g.heading)}`);
67
+ for (const f of WALK_FIELDS.slice(2)) if (f !== "heading" && g[f] !== undefined && !Number.isFinite(g[f])) err(`${at}.${f} must be a finite number, got ${fmt(g[f])}`);
68
+ if (g.steps !== undefined && !(Number.isInteger(g.steps) && g.steps >= 1)) err(`${at}.steps must be an integer >= 1, got ${fmt(g.steps)}`);
69
+ if (g.turnDistance !== undefined && !(g.turnDistance > 0)) err(`${at}.turnDistance must be > 0 m, got ${fmt(g.turnDistance)}`);
70
+ });
71
+ }
72
+
73
+ function validateChecks(spec, err, { sk, scene, frames, unknown = err, past = "" }) {
74
+ const c = spec.checks ?? {};
75
+ for (const k of Object.keys(c)) if (!CHECK_FIELDS.includes(k)) unknown(`checks.${k} is not a check${didYouMean(k, CHECK_FIELDS)} (checks: ${CHECK_FIELDS.join(", ")})`);
76
+ // each list entry is an object with known fields; a bad entry is named once and skipped
77
+ const each = (name, check = () => {}) => {
78
+ const v = c[name], shape = `{ ${CHECK_ENTRY_FIELDS[name].join(", ")} }`;
79
+ if (v === undefined) return;
80
+ if (!Array.isArray(v)) return err(`checks.${name} must be an array of ${shape}, got ${fmt(v)}`);
81
+ v.forEach((k, i) => {
82
+ if (!isObject(k)) return err(`checks.${name}[${i}] must be ${shape}, got ${fmt(k)}`);
83
+ fieldsOk(unknown, `checks.${name}[${i}]`, k, CHECK_ENTRY_FIELDS[name]);
84
+ check(k, i);
85
+ });
86
+ };
87
+ const when = (at, w) => {
88
+ if (w === undefined || typeof w === "string") return;
89
+ if (!(Array.isArray(w) && w.length === 2 && w.every(Number.isInteger) && w[0] <= w[1] && w[0] >= 0 && w[1] <= frames - 1)) err(`${at}.when must be a weight channel or [f0, f1] within 0..${frames - 1}, got ${fmt(w)}${Array.isArray(w) && w[1] > frames - 1 ? past : ""}`);
90
+ };
91
+ each("contacts", (k, i) => { refOk(spec, err, sk, `checks.contacts[${i}].point`, k.point); refOk(spec, err, sk, `checks.contacts[${i}].target`, k.target); when(`checks.contacts[${i}]`, k.when); });
92
+ each("orient", (k, i) => {
93
+ refOk(spec, err, sk, `checks.orient[${i}].from`, k.from); refOk(spec, err, sk, `checks.orient[${i}].to`, k.to); when(`checks.orient[${i}]`, k.when);
94
+ if (!(k.along === "face" || k.along === "up" || isVec3(k.along))) err(`checks.orient[${i}].along must be "face", "up" or [x, y, z]`);
95
+ });
96
+ each("below", (k, i) => { boneOk(err, sk, `checks.below[${i}].joint`, k.joint); boneOk(err, sk, `checks.below[${i}].ref`, k.ref); when(`checks.below[${i}]`, k.when); });
97
+ each("clearance", (k, i) => { objectOk(err, scene, `checks.clearance[${i}]`, k.object); when(`checks.clearance[${i}]`, k.frames); });
98
+ each("appear", (k, i) => { boneOk(err, sk, `checks.appear[${i}].joint`, k.joint); if (!spec.anchors?.[k.anchor]) err(`checks.appear[${i}].anchor "${k.anchor}" is not in spec.anchors`); });
99
+ each("supports", (k, i) => objectOk(err, scene, `checks.supports[${i}]`, k.object));
100
+ each("allow");
101
+ for (const name of Object.keys(c.limits ?? {})) if (!(name in QA_LIMITS)) unknown(`checks.limits.${name} is not a QA limit${didYouMean(name, Object.keys(QA_LIMITS))}`);
102
+ const fix = spec.fix;
103
+ if (fix !== undefined && fix !== false && (typeof fix !== "object" || fix === null)) err("spec.fix must be false or { reach, slide, penetration, self, retime }");
104
+ else for (const k of Object.keys(fix || {})) if (!FIX_KEYS.includes(k)) unknown(`spec.fix.${k} is not a fix${didYouMean(k, FIX_KEYS)} (fixes: ${FIX_KEYS.join(", ")})`);
105
+ }
106
+
107
+ /** opts = { sk, scene, frames, lenient, unknown: err for an unknown field (a no-op when lenient), past: the
108
+ * advice a frame past the end gets (validate.js) }. */
109
+ export function validateParts(spec, err, opts) {
110
+ validateAnchors(spec, err, opts);
111
+ validateProps(spec, err, opts);
112
+ validateObjects(spec, err, opts);
113
+ validateGenerators(spec, err, opts);
114
+ validateChecks(spec, err, opts);
115
+ validateGoals(spec, err, opts);
116
+ }
@@ -0,0 +1,122 @@
1
+ // Strict motion spec validation with exact messages, run before every compile through the
2
+ // pipeline (after skills expand, with open parameters at a trial value). Every problem found is
3
+ // listed at once, one per line, so the agent fixes them in one edit.
4
+ import { VEC_CHANNELS, POLE_CHANNELS, NUM_CHANNELS, MAP_CHANNELS, FINGER_PRESETS, rangeOf, CONVENTIONS, didYouMean, suggestField, isVec3, fmt } from "./vocab.js";
5
+ import { validateParts } from "./validate-parts.js";
6
+ import { SPEC_FIELDS, KEY_FIELDS } from "../schema/fields.js";
7
+ const MAX_ERRORS = 12;
8
+
9
+
10
+ /** Channels other than the body's own: hinge channels, visible and level channels (C15), channels a check names, *_vis switches, spec.channels. */
11
+ function customChannels(spec) {
12
+ const out = new Set(spec.channels ?? []);
13
+ for (const o of Object.values(spec.objects ?? {})) for (const ch of [o?.channel, o?.visible, o?.level]) if (typeof ch === "string" && ch) out.add(ch);
14
+ const c = spec.checks ?? {};
15
+ const list = v => (Array.isArray(v) ? v.filter(k => k && typeof k === "object") : []);
16
+ for (const k of [c.contacts, c.orient, c.below, c.clearance].flatMap(list)) if (typeof k.when === "string") out.add(k.when);
17
+ for (const k of list(c.appear)) if (typeof k.channel === "string") out.add(k.channel);
18
+ return out;
19
+ }
20
+
21
+ function checkValue(err, at, ch, v) {
22
+ if (VEC_CHANNELS.has(ch)) {
23
+ if (!isVec3(v)) return err(`${at} ${ch} must be [x, y, z] (3 finite numbers), got ${fmt(v)}`);
24
+ if (POLE_CHANNELS.has(ch)) {
25
+ const l = Math.hypot(...v);
26
+ if (l < 1e-6) return err(`${at} ${ch} = ${fmt(v)} has no direction; a pole is ${CONVENTIONS.pole}`);
27
+ if (ch.startsWith("knee") && v[2] < -0.7 * l) err(`${at} ${ch} = ${fmt(v)} points backward; knees bend toward +z. A pole is ${CONVENTIONS.pole}`);
28
+ }
29
+ if (ch.startsWith("foot") && v[1] < 0.04) err(`${at} ${ch} y = ${v[1]} puts the ankle into the floor; foot_* is ${CONVENTIONS.foot}`);
30
+ return;
31
+ }
32
+ if (MAP_CHANNELS.has(ch)) {
33
+ if (!v || typeof v !== "object" || Array.isArray(v)) return err(`${at} ${ch} must be { preset: weight }, presets ${FINGER_PRESETS.join(", ")}; got ${fmt(v)}`);
34
+ for (const [p, w] of Object.entries(v)) {
35
+ if (!FINGER_PRESETS.includes(p)) err(`${at} ${ch} preset "${p}" is unknown${didYouMean(p, FINGER_PRESETS)} (presets: ${FINGER_PRESETS.join(", ")})`);
36
+ else if (!(Number.isFinite(w) && w >= 0 && w <= 1)) err(`${at} ${ch}.${p} must be a weight within 0..1, got ${fmt(w)}`);
37
+ }
38
+ return;
39
+ }
40
+ if (!Number.isFinite(v)) {
41
+ const conv = ch === "yaw" ? ` (degrees; ${CONVENTIONS.yaw})` : ch.startsWith("fyaw") ? ` (${CONVENTIONS.fyaw})` : "";
42
+ return err(`${at} ${ch} must be a finite number${conv}, got ${fmt(v)}`);
43
+ }
44
+ const r = rangeOf(ch);
45
+ if (r && (v < r[1] || v > r[2])) err(`${at} ${ch} = ${v} is outside ${r[1]}..${r[2]} (${r[3]})`);
46
+ }
47
+
48
+ const list = v => (Array.isArray(v) ? v : []);
49
+ const lastOf = r => (Array.isArray(r) ? r[1] : r);
50
+
51
+ /**
52
+ * The advice an "outside 0..frames-1" message ends with: the length every frame the spec names
53
+ * needs (keys, walk starts, check windows, goals), and frames: "auto" when that covers it ("auto"
54
+ * counts keys and walks, not check windows or goals; skills/frames-auto.js).
55
+ */
56
+ function pastEndAdvice(spec, frames) {
57
+ const counted = [...list(spec.keys).map(k => k?.f), ...list(spec.generators).map(g => g?.f0)].filter(Number.isInteger);
58
+ const c = spec.checks ?? {};
59
+ const windows = [...["contacts", "orient", "below"].flatMap(n => list(c[n]).map(k => lastOf(k?.when))), ...list(c.clearance).map(k => lastOf(k?.frames)),
60
+ ...list(spec.goals).map(g => lastOf(g?.at))].filter(Number.isInteger);
61
+ const need = Math.max(frames - 1, ...counted, ...windows) + 1;
62
+ return `; set frames >= ${need}${Math.max(0, ...counted) + 1 >= need ? " or frames: \"auto\"" : ""}`;
63
+ }
64
+
65
+ function validateKeys(spec, err, frames, unknown, past = "") {
66
+ if (spec.keys !== undefined && !Array.isArray(spec.keys)) return err("spec.keys must be an array of { f, set, step? }");
67
+ const anchors = Object.keys(spec.anchors ?? {}), custom = customChannels(spec);
68
+ const known = [...VEC_CHANNELS, ...NUM_CHANNELS, ...MAP_CHANNELS, ...custom, ...anchors.flatMap(a => [`w_${a}_l`, `w_${a}_r`])];
69
+ (spec.keys ?? []).forEach((key, i) => {
70
+ const name = key?.src ? (key.src.startsWith("keys[") ? key.src : `${key.src} key`) : `keys[${i}]`;
71
+ if (!key || typeof key !== "object") return err(`${name} must be { f, set, step? }`);
72
+ for (const field of Object.keys(key)) if (!KEY_FIELDS.includes(field)) unknown(`${name} has unknown field "${field}"${suggestField(field, KEY_FIELDS)}; a key is { f, set, step? }`);
73
+ if (!Number.isInteger(key.f)) return err(`${name}.f must be an integer frame, got ${fmt(key.f)}`);
74
+ if (key.f < 0 || key.f > frames - 1) err(`${name}.f = ${key.f} is outside 0..${frames - 1} (spec.frames ${frames})${key.f > 0 ? past : ""}`);
75
+ const at = `${name} (f${key.f})`;
76
+ if (!key.set || typeof key.set !== "object" || Array.isArray(key.set)) return err(`${at} needs set: { channel: value }`);
77
+ for (const [ch, v] of Object.entries(key.set)) {
78
+ const w = /^w_(.+)_([lr])$/.exec(ch);
79
+ if (w && !anchors.includes(w[1])) err(`${at} sets ${ch}, but there is no anchor "${w[1]}"${didYouMean(ch, known)}`);
80
+ else if (w) checkValue(err, at, ch, v);
81
+ else if (VEC_CHANNELS.has(ch) || NUM_CHANNELS.has(ch) || MAP_CHANNELS.has(ch)) checkValue(err, at, ch, v);
82
+ else if (custom.has(ch) || ch.endsWith("_vis")) { if (!Number.isFinite(v)) err(`${at} ${ch} must be a finite number, got ${fmt(v)}`); }
83
+ else err(`${at} sets unknown channel "${ch}"${didYouMean(ch, known)}`);
84
+ }
85
+ for (const ch of key.step ?? []) if (!(ch in key.set)) err(`${at} step names "${ch}", which this key does not set`);
86
+ if (key.step !== undefined && !Array.isArray(key.step)) err(`${at} step must be an array of channel names`);
87
+ });
88
+ }
89
+
90
+ /**
91
+ * Throws MOTION_SPEC_INVALID listing every problem; scene objects are checked when given.
92
+ * lenient (a saved take compiled again, take-source.js) ignores unknown fields.
93
+ */
94
+ /** spec.flow: false, or { smooth?: boolean, lag?: false | 0..6 frames | { spine?, head?, arms? } } (channels.js). */
95
+ function validateFlow(flow, err, unknown) {
96
+ if (flow === undefined || flow === false) return;
97
+ if (!flow || typeof flow !== "object" || Array.isArray(flow)) return err(`spec.flow must be false or { smooth?, lag? }, got ${fmt(flow)}`);
98
+ for (const k of Object.keys(flow)) if (k !== "smooth" && k !== "lag") unknown(`spec.flow has unknown field "${k}" (fields: smooth, lag)`);
99
+ if (flow.smooth !== undefined && typeof flow.smooth !== "boolean") err(`spec.flow.smooth must be true or false, got ${fmt(flow.smooth)}`);
100
+ const frames = v => Number.isFinite(v) && v >= 0 && v <= 6, lag = flow.lag, LAG = "false, 0..6 frames or { spine?, head?, arms? }";
101
+ if (lag === undefined || lag === false || frames(lag)) return;
102
+ if (!lag || typeof lag !== "object" || Array.isArray(lag)) return err(`spec.flow.lag must be ${LAG}, got ${fmt(lag)}`);
103
+ for (const [k, v] of Object.entries(lag)) {
104
+ if (!["spine", "head", "arms"].includes(k)) unknown(`spec.flow.lag has unknown field "${k}" (fields: spine, head, arms)`);
105
+ else if (!frames(v)) err(`spec.flow.lag.${k} must be 0..6 frames, got ${fmt(v)}`);
106
+ }
107
+ }
108
+
109
+ export function validateSpec(spec, { sk, scene, lenient = false } = {}) {
110
+ const errors = [], err = m => { if (errors.length < MAX_ERRORS) errors.push(m); }, unknown = lenient ? () => {} : err;
111
+ if (!spec || typeof spec !== "object") throw new Error("MOTION_SPEC_INVALID: spec must be an object");
112
+ if (spec.schema !== "cozyclay.motion-spec.v1") err(`spec.schema must be "cozyclay.motion-spec.v1", got ${fmt(spec.schema)}`);
113
+ for (const k of Object.keys(spec)) if (!SPEC_FIELDS.includes(k)) unknown(`spec.${k} is not a spec field${didYouMean(k, SPEC_FIELDS)} (fields: ${SPEC_FIELDS.join(", ")})`);
114
+ if (spec.fps !== undefined && spec.fps !== 24) err(`spec.fps must be 24, the timeline rate, got ${fmt(spec.fps)}`);
115
+ validateFlow(spec.flow, err, unknown);
116
+ const frames = spec.frames;
117
+ if (!Number.isInteger(frames) || frames < 1 || frames > 20000) err(`spec.frames must be an integer 1..20000 or "auto", got ${fmt(frames)}`);
118
+ const past = Number.isInteger(frames) ? pastEndAdvice(spec, frames) : "";
119
+ validateKeys(spec, err, Number.isInteger(frames) ? frames : 20000, unknown, past);
120
+ validateParts(spec, err, { sk, scene, frames: Number.isInteger(frames) ? frames : 20000, lenient, unknown, past });
121
+ if (errors.length) throw new Error(`MOTION_SPEC_INVALID: ${errors.join("\n")}${errors.length >= MAX_ERRORS ? "\n(more problems not listed)" : ""}`);
122
+ }
@@ -0,0 +1,73 @@
1
+ // The motion spec's channel vocabulary and sign conventions, shared by the validator (exact
2
+ // error messages) and QA hints. Degrees for angles, metres for points.
3
+
4
+ const sided = names => names.flatMap(n => [`${n}_l`, `${n}_r`]);
5
+
6
+ export const VEC_CHANNELS = new Set(["root", ...sided(["foot", "knee", "hand", "elbow"])]);
7
+ export const POLE_CHANNELS = new Set(sided(["knee", "elbow"]));
8
+ export const NUM_CHANNELS = new Set(["yaw", "pitch", "roll", "spine_yaw", "spine_pitch", "spine_roll", "head_yaw", "head_pitch", "head_roll", "arm_rest", "arm_hang",
9
+ ...sided(["fyaw", "fpitch", "armw", "wtwist", "wflex", "aswing", "abend"])]);
10
+ export const MAP_CHANNELS = new Set(sided(["fingers"]));
11
+ // Channels the skills key (standIdle, walks): valid in a spec, left out of motion.describe so a first spec need not read them.
12
+ export const SKILL_CHANNELS = new Set(["arm_hang"]);
13
+ export const FINGER_PRESETS = ["relaxed", "fist", "grip", "cigarette", "point"];
14
+
15
+ // [min, max, convention] for numeric channels whose values have a hard range.
16
+ const RANGES = [
17
+ [/^(armw)_[lr]$/, 0, 1, "0 = FK arm, 1 = IK to the hand target"],
18
+ [/^arm_hang$/, 0, 1, "1 = a free FK forearm hangs plumb"],
19
+ [/^w_.+_[lr]$/, 0, 1, "a hand-target weight; the weights of one hand add up to at most 1"],
20
+ [/^wtwist_[lr]$/, -85, 85, "wrist twist about the forearm in degrees"],
21
+ [/^wflex_[lr]$/, -70, 70, "wrist flex about the knuckle line in degrees"],
22
+ [/^fpitch_[lr]$/, -60, 90, "toe-down pitch in degrees: + lifts the heel (the toes stay flat), - lifts the toes"],
23
+ [/^(pitch|spine_pitch|head_pitch)$/, -120, 120, "degrees, + bends forward"],
24
+ [/^(roll|spine_roll|head_roll)$/, -90, 90, "degrees"],
25
+ ];
26
+ export const rangeOf = ch => RANGES.find(([re]) => re.test(ch)) ?? null;
27
+
28
+ export const CONVENTIONS = {
29
+ yaw: "yaw 0 faces +Z and + turns toward +X",
30
+ fyaw: "foot heading in degrees, as yaw",
31
+ pole: "a body-frame direction (turned by the root yaw only): +z forward, +x the character's left, +y up",
32
+ foot: "the ANKLE target in world metres (standing: 0.087 X Bot, 0.105 Y Bot)",
33
+ hand: "the WRIST target in world metres",
34
+ root: "the hips joint in world metres (standing: 1.03 X Bot, 0.99 Y Bot)",
35
+ };
36
+
37
+ /** What each channel family means (a family drops the _l/_r side), for motion.describe "channels". */
38
+ export const CHANNEL_DOCS = {
39
+ root: CONVENTIONS.root, yaw: `degrees; ${CONVENTIONS.yaw}`, pitch: "whole-body pitch, degrees", roll: "whole-body roll, degrees",
40
+ spine_yaw: "degrees", spine_pitch: "degrees, + bends forward", spine_roll: "degrees", head_yaw: "degrees", head_pitch: "degrees, + looks down", head_roll: "degrees",
41
+ arm_rest: "FK arms down from the T-pose (default 72)", arm_hang: "1 = a free FK forearm hangs plumb (standIdle and walks key it)", foot: CONVENTIONS.foot, fyaw: CONVENTIONS.fyaw, fpitch: "toe-down pitch, degrees, + heel up",
42
+ knee: `knee pole, ${CONVENTIONS.pole}`, armw: "0 = FK arm, 1 = IK to hand_* / the anchors", hand: CONVENTIONS.hand, elbow: `elbow pole, ${CONVENTIONS.pole}`,
43
+ wtwist: "wrist twist, degrees", wflex: "wrist flex, degrees", aswing: "FK arm swing, degrees, + forward", abend: "FK elbow bend, degrees",
44
+ fingers: "{ preset: weight 0..1 }",
45
+ };
46
+ export const channelFamily = ch => ch.replace(/_[lr]$/, "");
47
+
48
+ /** "; did you mean ..." for an unknown field: frame words point at the field that takes frames, distance words at the tolerance. */
49
+ const FIELD_ALIASES = [[["frames", "when", "range", "f", "from", "within", "during", "at"], ["when", "frames", "at", "f"]], [["max", "maxDist", "distance", "tolerance", "limit"], ["tol", "min"]]];
50
+ export function suggestField(name, fields) {
51
+ for (const [words, targets] of FIELD_ALIASES) {
52
+ const to = words.includes(name) && targets.find(t => fields.includes(t) && t !== name);
53
+ if (to) return `; did you mean "${to}"?`;
54
+ }
55
+ return didYouMean(name, fields, Math.max(2, Math.floor(name.length / 3)));
56
+ }
57
+
58
+ export const isVec3 = v => Array.isArray(v) && v.length === 3 && v.every(Number.isFinite);
59
+ export const fmt = v => JSON.stringify(v)?.slice(0, 60);
60
+
61
+ /** Levenshtein distance, for "did you mean" suggestions. */
62
+ function editDistance(a, b) {
63
+ const d = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
64
+ for (let j = 1; j <= b.length; j++) d[0][j] = j;
65
+ for (let i = 1; i <= a.length; i++) for (let j = 1; j <= b.length; j++) d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
66
+ return d[a.length][b.length];
67
+ }
68
+
69
+ export function didYouMean(name, options, maxD = Math.max(3, Math.floor(name.length / 3))) {
70
+ let best = null, bestD = Infinity;
71
+ for (const o of options) { const d = editDistance(name, o); if (d < bestD) { best = o; bestD = d; } }
72
+ return best && bestD <= maxD ? `; did you mean "${best}"?` : "";
73
+ }