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,72 @@
1
+ // Small dependency-free vector / 3x3 / quaternion helpers for the motion3 compiler.
2
+ // Vectors are [x, y, z]; matrices are row-major [[...],[...],[...]] acting on column
3
+ // vectors (R v); quaternions are three.js order [x, y, z, w].
4
+
5
+ export const add = (a, b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
6
+ export const sub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
7
+ export const scale = (a, s) => [a[0] * s, a[1] * s, a[2] * s];
8
+ export const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
9
+ export const cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
10
+ export const len = a => Math.hypot(a[0], a[1], a[2]);
11
+ export const dist = (a, b) => len(sub(a, b));
12
+ export function norm(a) {
13
+ const l = len(a);
14
+ return l > 1e-12 ? [a[0] / l, a[1] / l, a[2] / l] : [0, 0, 0];
15
+ }
16
+ export const clamp = (x, lo, hi) => Math.min(hi, Math.max(lo, x));
17
+ const rad = d => d * Math.PI / 180;
18
+ const deg = r => r * 180 / Math.PI;
19
+
20
+ export const I3 = () => [[1, 0, 0], [0, 1, 0], [0, 0, 1]];
21
+ export function mul(A, B) {
22
+ const C = [[0, 0, 0], [0, 0, 0], [0, 0, 0]];
23
+ for (let i = 0; i < 3; i++) for (let j = 0; j < 3; j++) C[i][j] = A[i][0] * B[0][j] + A[i][1] * B[1][j] + A[i][2] * B[2][j];
24
+ return C;
25
+ }
26
+ export const apply = (R, v) => [R[0][0] * v[0] + R[0][1] * v[1] + R[0][2] * v[2], R[1][0] * v[0] + R[1][1] * v[1] + R[1][2] * v[2], R[2][0] * v[0] + R[2][1] * v[1] + R[2][2] * v[2]];
27
+ export const T = R => [[R[0][0], R[1][0], R[2][0]], [R[0][1], R[1][1], R[2][1]], [R[0][2], R[1][2], R[2][2]]];
28
+ const cols = (a, b, c) => [[a[0], b[0], c[0]], [a[1], b[1], c[1]], [a[2], b[2], c[2]]];
29
+
30
+ export function rx(d) { const t = rad(d), c = Math.cos(t), s = Math.sin(t); return [[1, 0, 0], [0, c, -s], [0, s, c]]; }
31
+ export function ry(d) { const t = rad(d), c = Math.cos(t), s = Math.sin(t); return [[c, 0, s], [0, 1, 0], [-s, 0, c]]; }
32
+ export function rz(d) { const t = rad(d), c = Math.cos(t), s = Math.sin(t); return [[c, -s, 0], [s, c, 0], [0, 0, 1]]; }
33
+
34
+ /** Rotation by `d` degrees about unit-normalised axis `ax` (Rodrigues). */
35
+ export function axisRot(ax, d) {
36
+ const [x, y, z] = norm(ax), t = rad(d), c = Math.cos(t), s = Math.sin(t), k = 1 - c;
37
+ return [[c + x * x * k, x * y * k - z * s, x * z * k + y * s], [y * x * k + z * s, c + y * y * k, y * z * k - x * s], [z * x * k - y * s, z * y * k + x * s, c + z * z * k]];
38
+ }
39
+
40
+ /** Orthonormal frame whose first column is d and second lies toward p. */
41
+ function frame(d, p) {
42
+ const a = norm(d);
43
+ let b = sub(p, scale(a, dot(p, a)));
44
+ if (len(b) < 1e-9) b = sub([0, 1, 0], scale(a, a[1])), len(b) < 1e-9 && (b = sub([1, 0, 0], scale(a, a[0])));
45
+ b = norm(b);
46
+ return cols(a, b, cross(a, b));
47
+ }
48
+
49
+ /** Rotation taking frame (d0, p0) onto frame (d1, p1). */
50
+ export const align = (d0, p0, d1, p1) => mul(frame(d1, p1), T(frame(d0, p0)));
51
+
52
+ export function quatFromMat(R) {
53
+ const m00 = R[0][0], m11 = R[1][1], m22 = R[2][2], tr = m00 + m11 + m22;
54
+ let x, y, z, w;
55
+ if (tr > 0) { const s = 0.5 / Math.sqrt(tr + 1); w = 0.25 / s; x = (R[2][1] - R[1][2]) * s; y = (R[0][2] - R[2][0]) * s; z = (R[1][0] - R[0][1]) * s; }
56
+ else if (m00 > m11 && m00 > m22) { const s = 2 * Math.sqrt(1 + m00 - m11 - m22); w = (R[2][1] - R[1][2]) / s; x = 0.25 * s; y = (R[0][1] + R[1][0]) / s; z = (R[0][2] + R[2][0]) / s; }
57
+ else if (m11 > m22) { const s = 2 * Math.sqrt(1 + m11 - m00 - m22); w = (R[0][2] - R[2][0]) / s; x = (R[0][1] + R[1][0]) / s; y = 0.25 * s; z = (R[1][2] + R[2][1]) / s; }
58
+ else { const s = 2 * Math.sqrt(1 + m22 - m00 - m11); w = (R[1][0] - R[0][1]) / s; x = (R[0][2] + R[2][0]) / s; y = (R[1][2] + R[2][1]) / s; z = 0.25 * s; }
59
+ const l = Math.hypot(x, y, z, w);
60
+ return [x / l, y / l, z / l, w / l];
61
+ }
62
+
63
+ export function matFromQuat([x, y, z, w]) {
64
+ return [
65
+ [1 - 2 * (y * y + z * z), 2 * (x * y - z * w), 2 * (x * z + y * w)],
66
+ [2 * (x * y + z * w), 1 - 2 * (x * x + z * z), 2 * (y * z - x * w)],
67
+ [2 * (x * z - y * w), 2 * (y * z + x * w), 1 - 2 * (x * x + y * y)],
68
+ ];
69
+ }
70
+
71
+ /** Angle in degrees between two directions. */
72
+ export const angle = (a, b) => deg(Math.acos(clamp(dot(norm(a), norm(b)), -1, 1)));
@@ -0,0 +1,162 @@
1
+ // One frame of the body: channel values -> world-space bone state.
2
+ // Conventions (CozyClay): metres, +Y up, yaw 0 faces +Z, positive yaw turns toward +X,
3
+ // the character's left is +X at yaw 0. Body-frame vectors (poles, body-local targets) are
4
+ // rotated by the root yaw only.
5
+ import { add, sub, scale, norm, len, apply, mul, rx, ry, rz } from "./math.js";
6
+ import { restState, rotateSubtree } from "./skeleton.js";
7
+ import { twoBone, orientFoot, wrist, fingers, limitFold, askedFold, swivelElbow, ELBOW_FOLD, KNEE_FOLD } from "./limbs.js";
8
+ import { holdHand } from "./hold.js";
9
+ import { clampWrist } from "./wrist-clamp.js";
10
+ import { ELBOW_REST } from "./arm-orient.js";
11
+ import { wristBody, bodyDistance } from "../body.js";
12
+
13
+ export const SIDES = { l: "Left", r: "Right" };
14
+ /** How much of an auto-fix elbow turn an arm at IK weight w takes (fix/self.js plans with the same). */
15
+ export const turnWeight = w => { const x = Math.min(1, Math.max(0, w)); return x * x * (3 - 2 * x); };
16
+ const KNEE_REST = [0, 0, 1], LEG = ["UpLeg", "Leg", "Foot"];
17
+
18
+ const v3 = (x, d = [0, 0, 0]) => (Array.isArray(x) || ArrayBuffer.isView(x) ? [x[0], x[1], x[2]] : d);
19
+ const num = (x, d = 0) => (typeof x === "number" && Number.isFinite(x) ? x : d);
20
+
21
+ /** Split a body-frame rotation over a chain of bones, each turning about its own joint. */
22
+ function spread(sk, s, bones, yaw, pitch, roll) {
23
+ const k = bones.length;
24
+ for (const name of bones) {
25
+ const b = sk.index[name];
26
+ rotateSubtree(sk, s, b, s.P[b].slice(), mul(ry(yaw / k), mul(rx(pitch / k), rz(roll / k))));
27
+ }
28
+ }
29
+
30
+ // Soft reach (spec.fix.reach): a wrist target up to 5 cm beyond the arm's reach is pulled onto
31
+ // the reach sphere through a C1 soft knee, so the arm never snaps straight; the overshoot is
32
+ // recorded. Farther targets are staging errors and stay for QA (REACH).
33
+ const SOFT_KNEE = 0.99, SOFT_MAX = 0.9995;
34
+ export const REACH_FIX_MAX = 0.05;
35
+ function softReach(sk, s, a, fa, h, target, diag, k) {
36
+ const R = len(sub(s.P[fa], s.P[a])) + len(sub(s.P[h], s.P[fa])), v = sub(target, s.P[a]), d = len(v);
37
+ const k0 = SOFT_KNEE * R, w = (SOFT_MAX - SOFT_KNEE) * R;
38
+ if (d <= k0 || d - SOFT_MAX * R > REACH_FIX_MAX) return target;
39
+ const dc = k0 + w * (1 - Math.exp(-(d - k0) / w));
40
+ diag[`reach_over_${k}`] = Math.max(0, d - SOFT_MAX * R);
41
+ return add(s.P[a], scale(v, dc / d));
42
+ }
43
+
44
+ /**
45
+ * hooks.handTarget(side, s) -> { target:[x,y,z] } world wrist target before the FK/IK blend
46
+ * hooks.afterArm(side, s, diag) lets the caller measure props carried by that hand.
47
+ * hooks.push(side) -> [x,y,z] | undefined, a world offset added to the IK target (auto-fix); hooks.skinPush(side) the same
48
+ * for the self fix (fix/self.js), taken as far as the arm is on its IK.
49
+ * hooks.softReach clamps unreachable wrist targets smoothly (auto-fix).
50
+ * hooks.foldFree lets the arm IK fold past the elbow's stop (a fit's measuring passes, compile.js).
51
+ * hooks.hold(side, s) -> { w, palm, measure, pole?, second? } | null: a held object's keys lead the hand (hold.js).
52
+ * hooks.keepOut(side, s, target) -> target: the blended wrist target kept out of the body (auto-fix, compile.js).
53
+ * hooks.swivel(side) -> deg | undefined, an elbow turn about the shoulder-wrist line, the hand kept (auto-fix, fix/self.js).
54
+ * diag.arm_fold_<k>: the flexion (deg) the wrist target asked when past ELBOW_FOLD (arm_miss_<k> is then how far inside).
55
+ * diag.leg_fold_<k>: the knee flexion (deg) the ankle target asked when past KNEE_FOLD (leg_miss_<k> is then how far inside).
56
+ */
57
+ // arm_hang (keyed by standIdle and the walk): a free FK forearm hangs plumb from the elbow, as a person's does,
58
+ // instead of carrying on the upper arm's slant out from the body (the hands sat about 9 cm wider than a person's);
59
+ // a working arm (on IK, or a hand that holds something: compile.js hangoff, idle.js busyHands) takes none of it. A rig whose thigh comes near (the X Bot) stops HANG_BACK
60
+ // degrees short of the first angle that brings its forearm or hand within HANG_CLEAR of the skin standing, so the
61
+ // walk's sway does not carry it into the thigh. Faded out as arm_rest raises the arms (none at 45); once per rig and rest.
62
+ const HANG_CLEAR = 0.095, HANG_BACK = 2, hangs = new Map();
63
+ function hangOf(sk, down) {
64
+ const key = `${sk.model}|${down}`;
65
+ if (hangs.has(key)) return hangs.get(key);
66
+ const full = (90 - down) * Math.min(1, Math.max(0, (down - 45) / 27)), wb = wristBody(sk);
67
+ const clearAt = h => {
68
+ const s = restState(sk);
69
+ return [["Left", -1], ["Right", 1]].every(([side, sg]) => {
70
+ const a = sk.index[`${side}Arm`], fa = sk.index[`${side}ForeArm`], hd = sk.index[`${side}Hand`], tip = sk.index[`${side}HandMiddle1`];
71
+ rotateSubtree(sk, s, a, s.P[a].slice(), rz(sg * down));
72
+ rotateSubtree(sk, s, fa, s.P[fa].slice(), rz(sg * h));
73
+ const pts = [0.5, 0.75, 1].map(t => add(s.P[fa], scale(sub(s.P[hd], s.P[fa]), t))).concat([s.P[tip]]);
74
+ return pts.every(p => bodyDistance(wb.body, s, p, wb.skip).d >= HANG_CLEAR);
75
+ });
76
+ };
77
+ let hang = full;
78
+ if (wb && full > 0) {
79
+ for (let h = 0; h <= full; h += 0.5) if (!clearAt(h)) { hang = Math.max(0, h - HANG_BACK); break; }
80
+ }
81
+ hangs.set(key, hang);
82
+ return hang;
83
+ }
84
+
85
+ export function pose(sk, c, hooks = {}) {
86
+ const s = restState(sk);
87
+ const diag = {};
88
+ // Base: arms down from the bind T-pose (body frame, before everything else).
89
+ const down = num(c.arm_rest, 72), hang = num(c.arm_hang) && num(c.arm_hang) * hangOf(sk, down);
90
+ for (const [side, sg, k] of [["Left", -1, "l"], ["Right", 1, "r"]]) {
91
+ const a = sk.index[`${side}Arm`], fa = sk.index[`${side}ForeArm`], h = hang * (1 - Math.min(1, Math.max(0, num(c[`armw_${k}`]) + num(c[`hangoff_${k}`]))));
92
+ rotateSubtree(sk, s, a, s.P[a].slice(), rz(sg * down));
93
+ if (h) rotateSubtree(sk, s, fa, s.P[fa].slice(), rz(sg * h));
94
+ }
95
+ // FK arm swing (forward +) and elbow bend, body frame.
96
+ for (const [k, side] of Object.entries(SIDES)) {
97
+ const bd = num(c[`abend_${k}`]), sw = num(c[`aswing_${k}`]);
98
+ const fa = sk.index[`${side}ForeArm`], a = sk.index[`${side}Arm`];
99
+ // an FK bend past the fold eases to its stop like the IK (limbs.js elbowFold)
100
+ if (bd) { rotateSubtree(sk, s, fa, s.P[fa].slice(), rx(-bd)); limitFold(sk, s, side); }
101
+ if (sw) rotateSubtree(sk, s, a, s.P[a].slice(), rx(-sw));
102
+ }
103
+ spread(sk, s, ["Neck", "Head"], num(c.head_yaw), num(c.head_pitch), num(c.head_roll));
104
+ spread(sk, s, ["Spine", "Spine1", "Spine2"], num(c.spine_yaw), num(c.spine_pitch), num(c.spine_roll));
105
+ // Root: turn about the hips joint, then put the hips at `root`.
106
+ const hips = sk.index.Hips, yaw = num(c.yaw);
107
+ rotateSubtree(sk, s, hips, s.P[hips].slice(), mul(ry(yaw), mul(rx(num(c.pitch)), rz(num(c.roll)))));
108
+ const shift = sub(v3(c.root, s.P[hips]), s.P[hips]);
109
+ for (let j = 0; j < s.P.length; j++) s.P[j] = add(s.P[j], shift);
110
+ const body = ry(yaw);
111
+ // Legs: two-bone IK to the ankle targets, the knee no further folded than its stop (limbs.js KNEE_STOP), then aim the feet.
112
+ for (const [k, side] of Object.entries(SIDES)) {
113
+ if (!c[`foot_${k}`]) continue;
114
+ const target = v3(c[`foot_${k}`]);
115
+ diag[`leg_miss_${k}`] = twoBone(sk, s, sk.index[`${side}UpLeg`], sk.index[`${side}Leg`], sk.index[`${side}Foot`],
116
+ target, apply(body, v3(c[`knee_${k}`], KNEE_REST)), KNEE_REST, "knee");
117
+ // a target nearer the hip than the knee folds leaves a residual (leg_miss); leg_fold says what it asked
118
+ const fold = askedFold(sk, s, side, target, LEG);
119
+ if (fold > KNEE_FOLD) diag[`leg_fold_${k}`] = fold;
120
+ orientFoot(sk, s, side, num(c[`fyaw_${k}`], yaw), num(c[`fpitch_${k}`]));
121
+ }
122
+ // Arms: blend the FK hand toward the hook's target, poles blended the same way.
123
+ for (const k of hooks.armOrder ?? ["r", "l"]) {
124
+ const side = SIDES[k], w = num(c[`armw_${k}`]);
125
+ const a = sk.index[`${side}Arm`], fa = sk.index[`${side}ForeArm`], h = sk.index[`${side}Hand`];
126
+ const poleIk = norm(apply(body, v3(c[`elbow_${k}`], [0, -1, -0.3])));
127
+ diag[`arm_miss_${k}`] = 0;
128
+ if (w > 1e-4 && hooks.handTarget) {
129
+ const fkHand = s.P[h].slice();
130
+ const goal = hooks.handTarget(k, s, diag) ?? fkHand;
131
+ let target = add(fkHand, scale(sub(goal, fkHand), w));
132
+ if (hooks.keepOut) target = hooks.keepOut(k, s, target);
133
+ const push = hooks.push?.(k);
134
+ if (push) target = add(target, push);
135
+ // the self fix's push rides the IK as far as the arm is on it (an arm coming in from FK would jump)
136
+ const skinPush = hooks.skinPush?.(k);
137
+ if (skinPush) target = add(target, scale(skinPush, turnWeight(w)));
138
+ if (hooks.softReach) target = softReach(sk, s, a, fa, h, target, diag, k);
139
+ const poleFk = norm(sub(s.P[fa], scale(add(s.P[a], fkHand), 0.5)));
140
+ const pole = add(scale(poleFk, 1 - w), scale(poleIk, w));
141
+ // a target nearer the shoulder than the elbow folds leaves a residual too; arm_fold says what it asked
142
+ diag[`arm_miss_${k}`] = twoBone(sk, s, a, fa, h, target, pole, ELBOW_REST, hooks.foldFree ? null : "elbow");
143
+ const fold = askedFold(sk, s, side, target);
144
+ if (fold > ELBOW_FOLD && !hooks.foldFree) diag[`arm_fold_${k}`] = fold;
145
+ }
146
+ wrist(sk, s, side, num(c[`wtwist_${k}`]), num(c[`wflex_${k}`]));
147
+ fingers(sk, s, side, c[`fingers_${k}`]);
148
+ const held = hooks.hold?.(k, s);
149
+ // a keyed carry's hand follows its object; else a fit's hand takes a grip on the body under its anchor (grip.js),
150
+ // and while the keys only ease in (before the first key) the arm they ease from takes it too
151
+ if (!held || held.w < 1) hooks.grip?.(k, s, diag);
152
+ if (held) holdHand(sk, s, side, held, ELBOW_REST, diag, k);
153
+ // after a held object's grip, so its hand keeps the grip the hold solved; an arm coming in from FK turns
154
+ // no more than its IK weight (the IK elbow already lands somewhere else than the FK one)
155
+ const turn = hooks.swivel?.(k);
156
+ // the turn keeps the hand's world turn; a wrist it takes past its limits stops there and the hand turns with the elbow
157
+ // (wrist-clamp.js), unless keys lead what the hand holds: that hand keeps the object's grip (hold.js limits its wrist)
158
+ if (turn) { swivelElbow(sk, s, side, turn * turnWeight(w)); if (!held) clampWrist(sk, s, side); }
159
+ hooks.afterArm?.(k, s, diag);
160
+ }
161
+ return { s, diag };
162
+ }
@@ -0,0 +1,49 @@
1
+ // Quaternion helpers for the held-object keys (../object-keys.js) and the hand-from-object solve
2
+ // (hold.js). Quaternions are three.js order [x, y, z, w], as math.js quatFromMat / matFromQuat;
3
+ // a rotation vector is axis * radians.
4
+
5
+ export const qmul = (a, b) => [
6
+ a[3] * b[0] + a[0] * b[3] + a[1] * b[2] - a[2] * b[1],
7
+ a[3] * b[1] - a[0] * b[2] + a[1] * b[3] + a[2] * b[0],
8
+ a[3] * b[2] + a[0] * b[1] - a[1] * b[0] + a[2] * b[3],
9
+ a[3] * b[3] - a[0] * b[0] - a[1] * b[1] - a[2] * b[2],
10
+ ];
11
+ export const qconj = q => [-q[0], -q[1], -q[2], q[3]];
12
+ export const qdot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3];
13
+ export function qnorm(q) {
14
+ const l = Math.hypot(q[0], q[1], q[2], q[3]);
15
+ return l > 1e-12 ? [q[0] / l, q[1] / l, q[2] / l, q[3] / l] : [0, 0, 0, 1];
16
+ }
17
+
18
+ /** Rotation vector of unit q, the short way round (|v| <= pi). */
19
+ export function qlog(q) {
20
+ const sg = q[3] < 0 ? -1 : 1, x = q[0] * sg, y = q[1] * sg, z = q[2] * sg, w = q[3] * sg, s = Math.hypot(x, y, z);
21
+ if (s < 1e-12) return [2 * x, 2 * y, 2 * z];
22
+ const a = 2 * Math.atan2(s, w) / s;
23
+ return [x * a, y * a, z * a];
24
+ }
25
+
26
+ /** Unit quaternion of rotation vector v. */
27
+ export function qexp(v) {
28
+ const a = Math.hypot(v[0], v[1], v[2]);
29
+ if (a < 1e-12) return qnorm([v[0] / 2, v[1] / 2, v[2] / 2, 1]);
30
+ const s = Math.sin(a / 2) / a;
31
+ return [v[0] * s, v[1] * s, v[2] * s, Math.cos(a / 2)];
32
+ }
33
+
34
+ /** Rotate vector v by unit q. */
35
+ export const qrot = (q, v) => qmul(qmul(q, [v[0], v[1], v[2], 0]), qconj(q)).slice(0, 3);
36
+
37
+ /** The shortest turn taking unit direction a onto unit direction b (half a turn about a normal of a when they are opposite). */
38
+ export function arcQuat(a, b) {
39
+ const w = 1 + a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
40
+ if (w > 1e-9) return qnorm([a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0], w]);
41
+ const n = Math.abs(a[0]) < 0.9 ? [0, a[2], -a[1]] : [-a[2], 0, a[0]], l = Math.hypot(...n);
42
+ return [n[0] / l, n[1] / l, n[2] / l, 0];
43
+ }
44
+
45
+ /** The turn by `deg` degrees about unit axis `u` (right-handed). */
46
+ export const axisQuat = (u, deg) => { const h = deg * Math.PI / 360, s = Math.sin(h); return [u[0] * s, u[1] * s, u[2] * s, Math.cos(h)]; };
47
+
48
+ /** a -> b by t along the short arc (t = 0: a, t = 1: b). */
49
+ export const slerp = (a, b, t) => qmul(a, qexp(qlog(qmul(qconj(a), b)).map(c => c * t)));
@@ -0,0 +1,74 @@
1
+ // Rig skeleton for the motion3 compiler: bind data from src/motion3/rigs/<model>.json
2
+ // (extracted by tools/motion3/extract-rig.mjs), in METRES, rig root at the origin
3
+ // facing +Z with +X on the character's left.
4
+ import { apply, add, sub, mul, T, matFromQuat, quatFromMat } from "./math.js";
5
+
6
+ const CM = 0.01;
7
+
8
+ export function createSkeleton(rigJson) {
9
+ const bones = rigJson.bones;
10
+ const names = bones.map(b => b.name);
11
+ const index = Object.fromEntries(names.map((n, i) => [n, i]));
12
+ const parent = bones.map(b => b.parent);
13
+ const children = names.map(() => []);
14
+ parent.forEach((p, i) => { if (p >= 0) children[p].push(i); });
15
+ // subtree[i] = i and every descendant, parents before children
16
+ const subtree = names.map((_, i) => {
17
+ const out = [], stack = [i];
18
+ while (stack.length) { const j = stack.shift(); out.push(j); stack.unshift(...children[j]); }
19
+ return out;
20
+ });
21
+ const restLocalPos = bones.map(b => b.localPos.map(v => v * CM));
22
+ const restLocalRot = bones.map(b => matFromQuat(b.localQuat));
23
+ const restWorldPos = bones.map(b => b.worldPos.map(v => v * CM));
24
+ const restWorldRot = bones.map(b => matFromQuat(b.worldQuat));
25
+ const L = rigJson.landmarks;
26
+ const head = index.Head;
27
+ // Landmarks re-expressed in the Head bone's rest frame, so they ride the head.
28
+ const toHead = p => apply(T(restWorldRot[head]), sub(p.map(v => v * CM), restWorldPos[head]));
29
+ const landmarks = {
30
+ mouth: toHead(L.mouth),
31
+ headCentre: toHead(L.headEllipsoid.centre),
32
+ headRadii: L.headEllipsoid.radii.map(v => v * CM),
33
+ // how far the skin's sole lies below the ToeBase / Toe_End joints at bind (m; qa/geometry.js solePoints)
34
+ sole: { ball: (L.sole?.ball ?? 0) * CM, tip: (L.sole?.tip ?? 0) * CM },
35
+ };
36
+ return { model: rigJson.model, names, index, parent, children, subtree, restLocalPos, restLocalRot, restWorldPos, restWorldRot, landmarks };
37
+ }
38
+
39
+ /** Rest pose as a mutable world-space state { P, R }. */
40
+ export function restState(sk) {
41
+ return { P: sk.restWorldPos.map(p => p.slice()), R: sk.restWorldRot.map(r => r.map(row => row.slice())) };
42
+ }
43
+
44
+ /** Rigidly rotate bone `b`'s subtree by world rotation R about world point `pivot`. */
45
+ export function rotateSubtree(sk, s, b, pivot, R) {
46
+ for (const j of sk.subtree[b]) {
47
+ s.P[j] = add(pivot, apply(R, sub(s.P[j], pivot)));
48
+ s.R[j] = mul(R, s.R[j]);
49
+ }
50
+ }
51
+
52
+ /** World state -> per-bone LOCAL quaternions [x,y,z,w] plus the hips local position (m). */
53
+ export function toLocal(sk, s) {
54
+ const quats = sk.names.map((_, j) => {
55
+ const p = sk.parent[j];
56
+ return quatFromMat(p < 0 ? s.R[j] : mul(T(s.R[p]), s.R[j]));
57
+ });
58
+ return { quats, hips: s.P[sk.index.Hips].slice() };
59
+ }
60
+
61
+ /** Forward kinematics from local quaternions + hips position back to a world state. */
62
+ export function fk(sk, quats, hips) {
63
+ const P = [], R = [];
64
+ for (const j of sk.subtree[sk.index.Hips]) {
65
+ const p = sk.parent[j], Lr = matFromQuat(quats[j]);
66
+ if (p < 0) { R[j] = Lr; P[j] = hips.slice(); continue; }
67
+ R[j] = mul(R[p], Lr);
68
+ P[j] = add(P[p], apply(R[p], sk.restLocalPos[j]));
69
+ }
70
+ return { P, R };
71
+ }
72
+
73
+ /** Point fixed to a bone, given in that bone's own frame (e.g. a landmark). */
74
+ export const bonePoint = (s, b, local) => add(s.P[b], apply(s.R[b], local));
@@ -0,0 +1,32 @@
1
+ // The checks every part of the validator shares: a bone, a scene object, a check point.
2
+ // Messages are exact (the agent fixes them in one edit). See validate.js.
3
+ import { didYouMean } from "./vocab.js";
4
+ import { partsOf } from "../object-pose.js";
5
+
6
+ export function boneOk(err, sk, at, bone) {
7
+ if (!sk || sk.index[bone] !== undefined) return true;
8
+ err(`${at} names unknown bone "${bone}"${didYouMean(String(bone), sk.names)}`);
9
+ return false;
10
+ }
11
+
12
+ /**
13
+ * A scene object id. A group (an Empty: no box of its own) passes only with { groups: true }
14
+ * (carry, hinge, object: refs and object anchors take groups, see C3); elsewhere the message lists its parts.
15
+ */
16
+ export function objectOk(err, scene, at, id, { groups = false } = {}) {
17
+ const ids = Object.keys(scene?.objects ?? {});
18
+ if (!ids.length || ids.includes(id)) return true;
19
+ const parts = partsOf(scene, id);
20
+ if (parts.length && groups) return true;
21
+ if (parts.length) err(`${at} names "${id}", a group (Empty) with no body of its own; its parts are ${parts.join(", ")}. Name one part`);
22
+ else err(`${at} names object "${id}", which is not in the scene${didYouMean(String(id), [...new Set([...ids, ...ids.flatMap(k => scene.objects[k].ancestors ?? []), ...Object.keys(scene.others ?? {})])])}`);
23
+ return false;
24
+ }
25
+
26
+ export function refOk(spec, err, sk, at, ref) {
27
+ const [kind, rest] = String(ref).split(/:(.*)/s);
28
+ if (kind === "joint") return boneOk(err, sk, at, rest);
29
+ if (kind === "anchor") return spec.anchors?.[rest] ? true : err(`${at} "${ref}" names no anchor`);
30
+ if (kind === "prop") { const [p, pt] = (rest ?? "").split("."); return spec.props?.[p]?.points?.[pt] || pt === "origin" ? true : err(`${at} "${ref}" names no prop point`); }
31
+ err(`${at} "${ref}" must be joint:<Bone>, prop:<prop>.<point> or anchor:<name>`);
32
+ }
@@ -0,0 +1,118 @@
1
+ // Validation of spec.goals (C8), exact messages through err(...):
2
+ // - each goal names exactly one predicate of near | above | moved | still | speed
3
+ // (schema/fields.js GOAL_FIELDS)
4
+ // - near/above take two refs, moved/still/speed one ref (refs.js checkRef, the full ref grammar);
5
+ // a bare "<id>[.center|.top|.base]" in moved, still or speed is taken as object:<id>
6
+ // (canonicalGoals, before validation; the spec a specId names holds the object: form); moved
7
+ // and still on any other point measure from at[0], so they need at: [f0, f1] with f0 < f1
8
+ // - tol (near, still) and by (above, moved, speed) are numbers; speed needs by (m/s, > 0);
9
+ // mode "all" | "any", at an absolute frame or [f0, f1] within 0..frames-1; goal names differ
10
+ // - after / before name another goal (its name, or goals[i] for an unnamed one); a goal that
11
+ // waits on itself through after/before is refused, listing the loop
12
+ // - unknown fields go through opts.unknown (ignored when lenient)
13
+ import { GOAL_FIELDS } from "../schema/fields.js";
14
+ import { checkRef } from "../refs.js";
15
+ import { GOAL_KINDS, ONE_REF, goalKinds, goalName } from "../qa/goals.js";
16
+ import { didYouMean, fmt } from "./vocab.js";
17
+
18
+ const SHAPE = "{ name?, near | above | moved | still | speed, tol?, by?, at, mode?, after?, before? }";
19
+ const TOL = new Set(["near", "still"]), BY = new Set(["above", "moved", "speed"]);
20
+ // tol on above/moved is refused, not mapped: a tolerance reads as by: -tol (slack) or by: tol (clearance),
21
+ // and agents meant both, so the message names each by that could be meant with the number sent.
22
+ function tolOnBy(kind, tol) {
23
+ const v = Number.isFinite(tol) && tol !== 0 ? Math.abs(tol) : 0.05;
24
+ if (kind === "speed") return `speed takes by (the point reaches by m/s): use by: ${v} (m/s), not tol`;
25
+ return kind === "above" ? `above takes by (a.y - b.y >= by, default 0): use by: ${v} for a at least ${v} m above b, or by: -${v} to let a sit up to ${v} m below b, not tol`
26
+ : `moved takes by (the object moves >= by, default 0.05): use by: ${v} (metres), not tol`;
27
+ }
28
+ const bareRef = v => typeof v === "string" && v !== "" && !/^[a-z]+:/.test(v);
29
+ const bareOf = g => ["moved", "still", "speed"].find(k => bareRef(g?.[k]));
30
+
31
+ /** goals with every bare moved, still or speed id written as "object:<id>"; goals itself when none is bare. */
32
+ export function canonicalGoals(goals) {
33
+ return Array.isArray(goals) && goals.some(bareOf) ? goals.map(g => { const k = bareOf(g); return k ? { ...g, [k]: `object:${g[k]}` } : g; }) : goals;
34
+ }
35
+
36
+ function validateAt(err, at, v, frames, past = "") {
37
+ const ok = f => Number.isInteger(f);
38
+ if (v === undefined) return err(`${at} needs at: a frame or [f0, f1] (absolute frames within 0..${frames - 1})`);
39
+ const range = Array.isArray(v) ? v : [v, v];
40
+ if (!(range.length === 2 && range.every(ok) && range[0] <= range[1])) return err(`${at}.at must be an integer frame or [f0, f1] with f0 <= f1, got ${fmt(v)}`);
41
+ if (range[0] < 0 || range[1] > frames - 1) err(`${at}.at = ${fmt(v)} is outside 0..${frames - 1} (spec.frames ${frames})${range[1] > frames - 1 ? past : ""}`);
42
+ }
43
+
44
+ /** The one-ref predicates: an object id or a point ref; moved and still on a point need a span. */
45
+ function validateOneRef(spec, err, ctx, at, g, kind) {
46
+ const ref = g[kind];
47
+ if (typeof ref !== "string") return err(`${at}.${kind} must be an object id or a point ref (object:<id>[.center|.top|.base], prop:<prop>.<point>, joint:<Bone>, landmark:mouth, anchor:<name>), got ${fmt(ref)}`);
48
+ if (checkRef(spec, err, ctx, `${at}.${kind}`, ref) && kind !== "speed" && !ref.startsWith("object:") && (Number.isInteger(g.at) || (Array.isArray(g.at) && g.at[0] === g.at[1])))
49
+ err(`${at}.${kind} "${ref}" measures ${kind === "moved" ? "how far the point gets" : "how far the point strays"} from where it is at at[0], so at must be [f0, f1] with f0 < f1, got ${fmt(g.at)}`);
50
+ }
51
+
52
+ function validateGoal(spec, err, opts, g, i, names) {
53
+ const at = `goals[${i}]`, ctx = { sk: opts.sk, scene: opts.scene }, unknown = opts.unknown ?? err;
54
+ if (!g || typeof g !== "object" || Array.isArray(g)) return err(`${at} must be ${SHAPE}, got ${fmt(g)}`);
55
+ for (const f of Object.keys(g)) if (!GOAL_FIELDS.includes(f)) unknown(`${at} has unknown field "${f}"${didYouMean(f, GOAL_FIELDS)} (fields: ${GOAL_FIELDS.join(", ")})`);
56
+ if (g.name !== undefined) {
57
+ if (typeof g.name !== "string" || !g.name.trim()) err(`${at}.name must be a non-empty string, got ${fmt(g.name)}`);
58
+ else if (names.has(g.name)) err(`${at}.name "${g.name}" is already used by goals[${names.get(g.name)}]; goal names must differ`);
59
+ else names.set(g.name, i);
60
+ }
61
+ const kinds = goalKinds(g), kind = kinds[0];
62
+ if (!kinds.length) err(`${at} needs one predicate: ${GOAL_KINDS.join(", ")}`);
63
+ if (kinds.length > 1) err(`${at} names ${kinds.join(" and ")}; a goal takes exactly one of ${GOAL_KINDS.join(", ")} (split it into ${kinds.length} goals)`);
64
+ if (kinds.length === 1 && ONE_REF.has(kind)) validateOneRef(spec, err, ctx, at, g, kind);
65
+ else if (kinds.length === 1) {
66
+ const pair = g[kind];
67
+ if (!(Array.isArray(pair) && pair.length === 2 && pair.every(r => typeof r === "string"))) err(`${at}.${kind} must be [ref, ref] (two point refs), got ${fmt(pair)}`);
68
+ else pair.forEach((ref, j) => checkRef(spec, err, ctx, `${at}.${kind}[${j}]`, ref));
69
+ }
70
+ if (g.tol !== undefined) {
71
+ if (kinds.length === 1 && !TOL.has(kind)) err(`${at}.tol applies to near and still; ${tolOnBy(kind, g.tol)}`);
72
+ else if (!(Number.isFinite(g.tol) && g.tol >= 0)) err(`${at}.tol must be a number >= 0 (metres), got ${fmt(g.tol)}`);
73
+ }
74
+ if (g.by !== undefined) {
75
+ if (kinds.length === 1 && !BY.has(kind)) err(`${at}.by applies to above, moved and speed; ${kind} takes tol`);
76
+ else if (!Number.isFinite(g.by) || (kind === "moved" && g.by < 0) || (kind === "speed" && g.by <= 0)) err(`${at}.by must be a ${kind === "moved" ? "number >= 0 (metres)" : kind === "speed" ? "number > 0 (m/s)" : "finite number (metres)"}, got ${fmt(g.by)}`);
77
+ } else if (kind === "speed" && kinds.length === 1) err(`${at}.speed needs by: the speed the point must reach, in m/s (e.g. by: 2)`);
78
+ if (g.mode !== undefined && g.mode !== "all" && g.mode !== "any") err(`${at}.mode must be "all" or "any", got ${fmt(g.mode)}`);
79
+ validateAt(err, at, g.at, opts.frames, opts.past);
80
+ }
81
+
82
+ /** after/before: each names another goal, and no goal waits on itself; one message per loop. */
83
+ function validateOrder(goals, err) {
84
+ const names = new Map(goals.map((g, i) => [g && typeof g === "object" ? goalName(g, i) : `goals[${i}]`, i]));
85
+ const waits = goals.map((g, i) => ["after", "before"].flatMap(side => {
86
+ if (!g || typeof g !== "object" || g[side] === undefined) return [];
87
+ const j = names.get(g[side]);
88
+ if (typeof g[side] !== "string" || j === undefined) { err(`goals[${i}].${side} must name another goal (its name, or goals[<i>] when it has none), got ${fmt(g[side])}${typeof g[side] === "string" ? didYouMean(g[side], [...names.keys()]) : ""}`); return []; }
89
+ if (j === i) { err(`goals[${i}].${side} names the goal itself; name the goal whose first met frame this one waits for`); return []; }
90
+ return [j];
91
+ }));
92
+ const state = goals.map(() => 0), path = [];
93
+ const visit = i => {
94
+ if (state[i] === 2) return;
95
+ if (state[i] === 1) {
96
+ const loop = path.slice(path.indexOf(i)).map(j => `"${goalName(goals[j], j)}"`);
97
+ return err(`goals wait on each other through after/before: ${[...loop, loop[0]].join(" -> ")}; one of them must not wait`);
98
+ }
99
+ state[i] = 1; path.push(i);
100
+ for (const j of waits[i]) visit(j);
101
+ path.pop(); state[i] = 2;
102
+ };
103
+ goals.forEach((_, i) => visit(i));
104
+ }
105
+
106
+ /**
107
+ * validateGoals(spec, err, opts)
108
+ * @param {object} spec the expanded spec
109
+ * @param {(msg: string) => void} err collects one problem (validate.js)
110
+ * @param {{ sk?: object, scene?: object, frames: number, lenient?: boolean, unknown?: (msg: string) => void, past?: string }} opts
111
+ */
112
+ export function validateGoals(spec, err, opts) {
113
+ if (spec.goals === undefined) return;
114
+ if (!Array.isArray(spec.goals)) return err(`spec.goals must be an array of goals ${SHAPE}, got ${fmt(spec.goals)}`);
115
+ const names = new Map();
116
+ spec.goals.forEach((g, i) => validateGoal(spec, err, opts, g, i, names));
117
+ validateOrder(spec.goals, err);
118
+ }