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,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
+ }