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