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,171 @@
1
+ // The hand a fit puts on a scene body takes a grip on it (lane wrist). A fit only places a point of the
2
+ // hand (reachGrip's fingers on the anchor); with nothing asking for the hand's turn, the wrist stayed
3
+ // straight on the forearm and kept whatever roll the arm left it, so the palm did not face what it held
4
+ // (a box side, a guitar neck: 0 deg of wrist on every holding frame, bench S10, session df388a9b), or
5
+ // the agent keyed wflex/wtwist to fix it at the wrist alone. Now, while a fit's weight is on and its
6
+ // anchor is on a body (an object anchor's body, or any body within NEAR m of the anchor):
7
+ // fingers stay on the forearm's line: no flex or deviation is asked (a power grip turns the
8
+ // forearm, not the wrist: the wrist stays in its comfortable middle)
9
+ // roll the hand turns about the forearm: on a thin body (two half sizes under THIN m: a can, a
10
+ // cup, a bar, a handle, a neck) the knuckle line takes the body's long axis, a cylindrical
11
+ // grasp with the body across the palm, at the end the forearm turns to least; on anything
12
+ // else the end-state-comfort thumb (people take the grip that ends comfortable, Rosenbaum):
13
+ // up when the forearm is level, toward the midline when it points down or up. The turn
14
+ // stops where the forearm's whole twist leaves its comfortable range (arm-orient.js
15
+ // WRIST_COMFORT, +-50 deg: Morrey's daily-task range); the forearm bone takes its share and
16
+ // the wrist the rest (hold.js orient), by the fit's weight
17
+ // point the fingers' point stays where the fit put it, the elbow in the plane the arm had
18
+ // The spec wins: a hand whose wtwist_<k> or wflex_<k> is turned on the frame keeps it, a fit with
19
+ // orient: false is left alone, and a prop anchor is the second-hand grip's (second-hand.js). A hand
20
+ // that carries something keeps the carry's own grip (its grab offset) once the carry leads it. A keyed
21
+ // carry's grab offset is read off the plain solve of its grab frame (object-keys.js attach), so when it
22
+ // carries a body that is not thin (a box, a crate) the carrying hand takes the grip on the reach, on
23
+ // that frame and up to its first key (ownGrip): the offset the carry keeps is then a grip, not the
24
+ // forearm's neutral roll. A thin body (a cup, a can) keeps the neutral offset: its aim keys pick the
25
+ // roll the arm holds most easily from it, and the cylindrical grip measured worse on every bench cup.
26
+ // Those frames read the carried body where it rested before the grab, never a pose the hand moves.
27
+ import { add, sub, scale, norm, cross, dot, len, apply, ry, mul, T, axisRot } from "./math.js";
28
+ import { palmAxes, twoBone } from "./limbs.js";
29
+ import { aimArm, orient } from "./hold.js";
30
+ import { ELBOW_REST, WRIST_COMFORT, wristNeed } from "./arm-orient.js";
31
+ import { resolveAnchor } from "./anchors.js";
32
+ import { sdBox } from "../qa/geometry.js";
33
+ import { partsOf } from "../object-pose.js";
34
+
35
+ const NEAR = 0.06, THIN = 0.045, ALIGN = 0.3, PREFER = 30, DEG = 180 / Math.PI, ITER = 2, UP = [0, 1, 0];
36
+ const cols = (a, b, c) => [[a[0], b[0], c[0]], [a[1], b[1], c[1]], [a[2], b[2], c[2]]];
37
+ const flat = (v, n) => sub(v, scale(n, dot(v, n)));
38
+
39
+ /** The fit of side k whose weight is on most at channels c (null when none), its weight times the arm's IK weight. */
40
+ function activeFit(spec, c, k) {
41
+ let best = null;
42
+ for (const fit of spec.fits ?? []) {
43
+ const w = c[`w_${fit.anchor}_${fit.side}`] ?? 0;
44
+ if (fit.side === k && fit.orient !== false && w > 1e-4 && (!best || w > best.w)) best = { fit, w };
45
+ }
46
+ return best && { ...best, w: best.w * Math.min(1, Math.max(0, c[`armw_${k}`] ?? 0)) };
47
+ }
48
+
49
+ /** The fit's effector point on pose s (anchors.js measureProps' rule for a prop point; the joints' mean otherwise). */
50
+ function effectorAt(sk, spec, s, side, eff) {
51
+ if (eff.prop) {
52
+ const prop = spec.props?.[eff.prop];
53
+ if (!prop) return null;
54
+ const ax = palmAxes(sk, s, side), o = scale(prop.origin.map(b => s.P[sk.index[b]]).reduce(add, [0, 0, 0]), 1 / prop.origin.length);
55
+ const [d, l, n] = eff.point === "origin" ? [0, 0, 0] : prop.points?.[eff.point] ?? [0, 0, 0];
56
+ return add(o, add(scale(ax.d, d), add(scale(ax.l, l), scale(ax.n, n))));
57
+ }
58
+ const js = eff.joints ?? [`${side}HandIndex2`, `${side}HandMiddle2`];
59
+ return scale(js.map(j => s.P[sk.index[j]]).reduce(add, [0, 0, 0]), 1 / js.length);
60
+ }
61
+
62
+ /** The carry entry over body id (its own or a group's) and the side whose prop carries it, else null. */
63
+ function carryOf(ctx, id) {
64
+ const { spec } = ctx, o = ctx.scene?.objects?.[id];
65
+ for (const k of [id, ...(o?.ancestors ?? [])]) { const e = spec.objects?.[k]; if (e?.prop) return { key: k, entry: e, side: spec.props?.[e.prop]?.hand ?? null }; }
66
+ return null;
67
+ }
68
+
69
+ /**
70
+ * Whether hand k takes the grip on keyed carry `key` at frame f although it carries it (see above): a keyed
71
+ * carry that keeps its grab offset, from its reach to its grab frame and up to its first key. Answers the
72
+ * frame to read the carried body at (before the grab), or null.
73
+ */
74
+ function ownGrip(ctx, key, k, f) {
75
+ const o = ctx.spec.objects?.[key];
76
+ const half = ctx.scene?.objects?.[key]?.half;
77
+ if (!ctx.hold?.span || !o?.prop || ctx.spec.props?.[o.prop]?.hand !== k || !o.keys?.length || o.grab === undefined || o.at !== undefined || o.along !== undefined || (half && thinAxis(half) >= 0)) return null;
78
+ const [grab] = ctx.hold.span(key);
79
+ return grab >= 1 && f <= Math.max(grab, ctx.hold.firstKey(key) - 1) ? Math.min(f, grab - 1) : null;
80
+ }
81
+
82
+ /** The body box the grip is on: the anchor's object (its nearest part) or the nearest body within NEAR m; null when none. */
83
+ function bodyAt(ctx, f, k, A, anchor, now) {
84
+ const scene = ctx.scene?.objects ?? {}, ids = anchor?.kind === "object" ? partsOf(ctx.scene, anchor.object) : Object.keys(scene);
85
+ let best = null;
86
+ for (const id of ids) {
87
+ const half = scene[id]?.half;
88
+ if (!half) continue;
89
+ const own = ownGrip(ctx, carryOf(ctx, id)?.key, k, f), pose = ctx.objectPose?.(id, own ?? f, own === null ? now : null) ?? scene[id];
90
+ if (!pose?.R) continue;
91
+ const box = { pos: pose.pos, R: pose.R, half }, d = sdBox(A, box);
92
+ if (!best || d < best.d) best = { box, d, id };
93
+ }
94
+ // a body this hand carries keeps the carry's own grip (its grab offset, which its keys are written on:
95
+ // the same with keys or without), and no other body takes its place
96
+ const carry = best && carryOf(ctx, best.id);
97
+ if (!best || (carry?.side === k && ownGrip(ctx, carry.key, k, f) === null)) return null;
98
+ return anchor?.kind === "object" || best.d <= NEAR ? best.box : null;
99
+ }
100
+
101
+ const AXES = [0, 1, 2];
102
+ /** The long axis of a thin body (two half sizes under THIN m: a can, a cup, a bar, a handle, a neck), else -1. */
103
+ function thinAxis(half) {
104
+ const long = AXES.reduce((a, i) => (half[i] > half[a] ? i : a), 0);
105
+ return AXES.filter(i => i !== long).every(i => half[i] < THIN) ? long : -1;
106
+ }
107
+
108
+ /**
109
+ * The hand's palm frame (columns d, l, n of limbs.js palmAxes) for the grip: the fingers stay on the
110
+ * forearm's line (no flex or deviation added: a power grip turns the forearm, not the wrist) and the
111
+ * hand turns about it. On a thin body the knuckle line takes the body's long axis (a cylindrical grasp:
112
+ * the can, the bar, the neck across the palm) with the thumb at its comfortable end; on anything else,
113
+ * or a thin body along the forearm, the end-state-comfort thumb: up when the forearm is level, toward
114
+ * the midline when it points down or up.
115
+ */
116
+ function gripFrame(side, d, now, yawDeg, axis) {
117
+ const right = side === "Right", medial = apply(ry(yawDeg ?? 0), [right ? 1 : -1, 0, 0]), vy = Math.abs(d[1]);
118
+ const thumb = flat(add(scale(UP, 1 - vy), scale(medial, vy)), d), want = norm(len(thumb) > 1e-3 ? thumb : flat(medial, d));
119
+ // a thin body lies across the palm, the knuckle line along its long axis: of its two ends the thumb takes the
120
+ // one the forearm turns to least (the end-state-comfort thumb counts PREFER deg in its favour)
121
+ const across = axis ? flat(axis, d) : null;
122
+ if (!across || len(across) <= ALIGN) return cols(d, want, cross(d, want));
123
+ const turn = l => Math.acos(Math.max(-1, Math.min(1, dot(l, now)))) * DEG - (dot(l, want) > 0 ? PREFER : 0);
124
+ const a = norm(across), l = turn(a) <= turn(scale(a, -1)) ? a : scale(a, -1);
125
+ return cols(d, l, cross(d, l));
126
+ }
127
+
128
+ /** Frame F (columns d, l, n) turned about its own d so the forearm's whole twist (from `twist` deg now) stays within the comfortable range: the roll stops where the forearm is comfortable. */
129
+ function comfortable(F, now, twist) {
130
+ const d = [F[0][0], F[1][0], F[2][0]], l = [F[0][1], F[1][1], F[2][1]];
131
+ const want = Math.atan2(dot(cross(now, l), d), dot(now, l)) * DEG, [lo, hi] = WRIST_COMFORT.twist;
132
+ const got = Math.min(hi, Math.max(lo, twist + want)) - twist;
133
+ if (Math.abs(got - want) < 1e-6) return F;
134
+ const r = apply(axisRot(d, got), now);
135
+ return cols(d, r, cross(d, r));
136
+ }
137
+
138
+ /**
139
+ * gripHand(ctx, sk, s, side, k, c, f, diag): turn the hand of a fit onto the body under its anchor
140
+ * (see above); a no-op when nothing asks for it. diag.grip_<k> = { w, twist, flex, dev, body }.
141
+ */
142
+ export function gripHand(ctx, sk, s, side, k, c, f, diag) {
143
+ // a wrist the spec turns on this frame is the agent's (or a skill's) own; a neutral 0 asks for nothing
144
+ if (Math.abs(c[`wtwist_${k}`] ?? 0) > 1e-3 || Math.abs(c[`wflex_${k}`] ?? 0) > 1e-3) return;
145
+ const on = activeFit(ctx.spec, c, k);
146
+ if (!on || on.w < 1e-3) return;
147
+ // a hand that carries something on this frame holds that (its carry's grip), whatever its fit's anchor
148
+ if (Object.keys(ctx.spec.objects ?? {}).some(id => ctx.spec.objects[id]?.prop && ctx.objectHand?.(id, f) === k && ownGrip(ctx, id, k, f) === null)) return;
149
+ const now = { s, diag, c }, anchor = ctx.spec.anchors?.[on.fit.anchor];
150
+ // a point of a prop another hand holds is the second-hand grip's (second-hand.js)
151
+ if (anchor?.kind === "prop") return;
152
+ const A = add(resolveAnchor(ctx, on.fit.anchor, s, diag, c, k), on.fit.offset ?? [0, 0, 0]);
153
+ const box = bodyAt(ctx, f, k, A, anchor, now);
154
+ const eff = box && effectorAt(sk, ctx.spec, s, side, on.fit.effector ?? {});
155
+ if (!eff) return;
156
+ const measure = st => ({ pos: effectorAt(sk, ctx.spec, st, side, on.fit.effector ?? {}), R: (({ d, l, n }) => cols(d, l, n))(palmAxes(sk, st, side)) });
157
+ const I = sk.index, a = I[`${side}Arm`], fa = I[`${side}ForeArm`], h = I[`${side}Hand`];
158
+ const long = thinAxis(box.half), axis = long >= 0 ? apply(box.R, AXES.map(i => (i === long ? 1 : 0))) : null;
159
+ const ax = palmAxes(sk, s, side), roll = norm(flat(ax.l, ax.d));
160
+ const axT = norm(sub(s.P[h], s.P[fa])), neutral = mul(s.R[fa], mul(T(sk.restWorldRot[fa]), sk.restWorldRot[h]));
161
+ const twist = wristNeed(neutral, s.R[h], axT, apply(mul(neutral, T(s.R[h])), ax.l)).twist;
162
+ const palm = { pos: eff, R: comfortable(gripFrame(side, ax.d, roll, c.yaw, axis), roll, twist) };
163
+ const { goal, plane, wristAt } = aimArm(sk, s, side, palm, measure(s), on.w, "palm");
164
+ let need = { twist: 0, flex: 0, dev: 0 };
165
+ for (let i = 0; i < ITER; i++) {
166
+ // the elbow keeps the plane the arm had (bendPlane), the fingers' point goes back where the fit put it
167
+ twoBone(sk, s, a, fa, h, wristAt(i ? s.R[h] : goal), plane, ELBOW_REST, "elbow");
168
+ need = orient(sk, s, side, h, fa, goal);
169
+ }
170
+ diag[`grip_${k}`] = { w: on.w, ...need };
171
+ }
@@ -0,0 +1,48 @@
1
+ // While a walk runs the head keeps the gaze steadier than the trunk: the trunk's quick sway (the root's and the
2
+ // spine's yaw, pitch and roll less their moving average over about a stride, so a turn, a lean or a bow the keys
3
+ // hold is not swayed against) is taken back by the head, a part of it (STEADY) and no more than LIMIT degrees; little
4
+ // of the pitch: a person's head nods with the trunk's dip at each step.
5
+ // Only from a walk's start to its last step (walk.js tags them "<tag> start", "<tag> step <n>"; the settle after
6
+ // holds still, and a take cut in it ends the same), faded in and out over FADE frames, and never on a head axis the
7
+ // spec's own keys move during that walk: a keyed look wins. The average holds the take's end value past its end.
8
+ const STEADY = { yaw: 0.5, pitch: 0.2, roll: 0.7 }, LIMIT = { yaw: 4, pitch: 6, roll: 10 }, WINDOW = 33, FADE = 8;
9
+ const AXES = ["yaw", "pitch", "roll"];
10
+
11
+ /** The walk spans in the keys, start to last step: [[f0, f1], ...]. */
12
+ function walkSpans(keys) {
13
+ const spans = new Map();
14
+ for (const k of keys) {
15
+ const m = / (start|step \d+)$/.exec(k.src ?? ""), tag = m && k.src.slice(0, -m[0].length);
16
+ if (!m || !/\bwalk(To)?$/.test(tag)) continue;
17
+ const span = spans.get(tag) ?? [Infinity, -Infinity];
18
+ spans.set(tag, [Math.min(span[0], k.f), Math.max(span[1], k.f)]);
19
+ }
20
+ return [...spans.values()].filter(([a, b]) => b > a);
21
+ }
22
+
23
+ const movingAverage = (xs, w) => {
24
+ const half = Math.floor(w / 2), at = i => xs[Math.max(0, Math.min(xs.length - 1, i))];
25
+ return xs.map((_, f) => { let s = 0; for (let i = f - half; i <= f + half; i++) s += at(i); return s / (2 * half + 1); });
26
+ };
27
+
28
+ /** Adds the head's share of the trunk's sway to the baked head channels inside every walk (in place). */
29
+ export function steadyHead(baked, keys, frameCount) {
30
+ const spans = walkSpans(keys);
31
+ if (!spans.length) return;
32
+ const series = name => baked[name] ?? new Array(frameCount).fill(0);
33
+ for (const axis of AXES) {
34
+ const spine = series(`spine_${axis}`), trunk = series(axis).map((v, f) => v + spine[f]);
35
+ const mean = movingAverage(trunk, WINDOW);
36
+ let head = null;
37
+ for (const [f0, f1] of spans) {
38
+ const keyed = keys.some(k => k.f >= f0 && k.f <= f1 && k.set?.[`head_${axis}`] !== undefined && !/\bwalk(To)? (start|step \d+|end)$/.test(k.src ?? ""));
39
+ if (keyed) continue;
40
+ head ??= series(`head_${axis}`).slice();
41
+ for (let f = Math.max(0, f0); f <= Math.min(frameCount - 1, f1); f++) {
42
+ const fade = Math.min(1, (f - f0) / FADE, (f1 - f) / FADE), sway = Math.max(-LIMIT[axis], Math.min(LIMIT[axis], trunk[f] - mean[f]));
43
+ head[f] -= STEADY[axis] * sway * fade * fade * (3 - 2 * fade);
44
+ }
45
+ }
46
+ if (head) baked[`head_${axis}`] = head;
47
+ }
48
+ }
@@ -0,0 +1,134 @@
1
+ // The hand follows a held object (docs/motion-api-v3.1.md C14). On frames where
2
+ // spec.objects[id].keys lead (../object-keys.js), pose.js calls holdHand after the plain arm
3
+ // (IK or FK, wrist channels, fingers): the wrist target and the hand's orientation are solved so
4
+ // the grip (the prop's palm frame) lands on the pose the object keys ask for, hand = object o
5
+ // attach^-1. The whole arm turns the object (arm-orient.js, lane K8): the elbow swings about the
6
+ // shoulder-wrist axis to the pole that leaves the wrist least strained, the forearm takes its
7
+ // share of the turn about its own axis, and the wrist adds the rest within anatomical limits
8
+ // relative to the forearm: twist about the forearm, flex about the knuckle line, deviation toward
9
+ // the thumb (+, radial) or the little finger (-, ulnar). What the arm or the wrist cannot do is
10
+ // left as a residual: QA measures it on the object (qa/key-checks.js) and reads the reasons from
11
+ // diag.hold_<side>.
12
+ import { add, sub, scale, norm, dot, len, mul, T, apply, axisRot, angle, quatFromMat, matFromQuat } from "./math.js";
13
+ import { twoBone, palmAxes, carried, askedFold, reachAt, ELBOW_FOLD } from "./limbs.js";
14
+ import { rotateSubtree } from "./skeleton.js";
15
+ import { slerp } from "./quat.js";
16
+ import { bodyFor, segmentDepth } from "../body.js";
17
+ import { WRIST_LIMITS, FOREARM_SHARE, ELBOW_REST, wristNeed, swivel, landscape, shoulderPrice, comfortPrice } from "./arm-orient.js";
18
+
19
+ // STRAIGHT: a plain arm whose elbow sits less than this (m) off its shoulder-wrist midpoint is
20
+ // straight and has no bend plane of its own. A straight FK arm keeps 1 to 3 mm there from its bone
21
+ // lengths (Y Bot: the upper arm is 2 mm shorter than the forearm, so the elbow sits 1 mm up the
22
+ // arm), and that direction is no bend: as the swivel's reference it put the elbow "up", so the
23
+ // limit 100 deg from it ruled out an elbow pointing down (session d5c110a7's batting stance).
24
+ // ROLL_MARGIN: the share of each wrist half range by which holdCost tightens the limits, so the
25
+ // roll picked for a key leaves the frames between keys (they turn between the picks) some room.
26
+ // The fold price (holdCost): an arm folded near flat turns the hand any way at all, so without a price
27
+ // the roll of a stance key at the shoulder folded it (session d5c110a7: wrist 6-9 cm from the shoulder,
28
+ // 162 deg), and the frames between keys, where the wrist moves a few cm, broke away from it. twoBone
29
+ // now stops the fold (limbs.js ELBOW_FOLD, ELBOW_STOP); the price still steers the roll off it.
30
+ const ITERATIONS = 3, REACH_PRICE = 1, STRAIGHT = 0.005, ROLL_MARGIN = 0.15, BODY_PRICE = 4;
31
+ const clampTo = (v, [lo, hi]) => Math.min(hi, Math.max(lo, v));
32
+
33
+ /**
34
+ * Turn the hand (and its fingers) about the wrist so its world rotation is `goal`, as far as the
35
+ * limits let it: the forearm bone turns FOREARM_SHARE of the twist about its own axis, the hand
36
+ * the rest plus flex and deviation. Returns what it needed, degrees { twist, flex, dev }.
37
+ */
38
+ export function orient(sk, s, side, h, fa, goal) {
39
+ const axT = scale(sub(s.P[h], s.P[fa]), 1 / len(sub(s.P[h], s.P[fa]))), neutral = mul(s.R[fa], mul(T(sk.restWorldRot[fa]), sk.restWorldRot[h]));
40
+ const { axF, axD, ...need } = wristNeed(neutral, goal, axT, apply(mul(neutral, T(s.R[h])), palmAxes(sk, s, side).l));
41
+ const got = Object.fromEntries(Object.entries(need).map(([k, v]) => [k, clampTo(v, WRIST_LIMITS[k])]));
42
+ const want = mul(mul(axisRot(add(scale(axF, got.flex), scale(axD, got.dev)), Math.hypot(got.flex, got.dev)), axisRot(axT, got.twist)), neutral);
43
+ rotateSubtree(sk, s, fa, s.P[fa].slice(), axisRot(axT, FOREARM_SHARE * got.twist));
44
+ rotateSubtree(sk, s, h, s.P[h].slice(), mul(want, T(s.R[h])));
45
+ return need;
46
+ }
47
+
48
+ /**
49
+ * The bend direction the channels gave the arm of `side` on pose s, what its swivel turns from: the
50
+ * elbow off the shoulder-wrist midpoint, or for a straight arm (STRAIGHT) where its elbow would
51
+ * point if it bent, the upper arm's rest bend carried by the bone, so the swivel's limits sit on
52
+ * the upper arm's own roll.
53
+ */
54
+ export function bendPlane(sk, s, side) {
55
+ const I = sk.index, a = I[`${side}Arm`], bend = sub(s.P[I[`${side}ForeArm`]], scale(add(s.P[a], s.P[I[`${side}Hand`]]), 0.5));
56
+ return len(bend) > STRAIGHT ? bend : carried(sk, s, a, ELBOW_REST);
57
+ }
58
+
59
+ /**
60
+ * What holding `palm` with weight w needs on pose s: the hand's goal rotation, the wrist target for
61
+ * a hand rotation (pin "wrist": where it is; "palm": where the palm stays put), the bend direction
62
+ * (bendPlane).
63
+ */
64
+ export function aimArm(sk, s, side, palm, now, w, pin) {
65
+ const I = sk.index, h = I[`${side}Hand`];
66
+ // the palm frame in the hand bone's frame: fixed for this frame's finger curl
67
+ const localR = mul(T(s.R[h]), now.R), localP = apply(T(s.R[h]), sub(now.pos, s.P[h]));
68
+ const goal = matFromQuat(slerp(quatFromMat(s.R[h]), quatFromMat(mul(palm.R, T(localR))), w));
69
+ const from = s.P[h].slice();
70
+ // the wrist goes where the grip lands with the orientation the wrist reached (the goal at first)
71
+ const grip = R => sub(palm.pos, apply(R, localP));
72
+ return { goal, plane: bendPlane(sk, s, side), wristAt: pin === "wrist" ? () => from : pin === "palm" ? grip : R => add(from, scale(sub(grip(R), from), w)) };
73
+ }
74
+
75
+ /**
76
+ * held = { w, palm: { pos, R }, measure(s) -> { pos, R }, pole?, second?, pin? }: blend weight
77
+ * (1 inside the keyed span), the palm frame the keys ask for, the function that measures it on a
78
+ * pose, the planned elbow pole (../hold-plan.js; searched here when absent) and, for a second hand
79
+ * (../second-hand.js), the id of the object it grips and what stays put (pin "wrist" or "palm").
80
+ */
81
+ export function holdHand(sk, s, side, held, restBend, diag, k) {
82
+ const I = sk.index, a = I[`${side}Arm`], fa = I[`${side}ForeArm`], h = I[`${side}Hand`], { w } = held;
83
+ const { goal, plane, wristAt } = aimArm(sk, s, side, held.palm, held.measure(s), w, held.pin);
84
+ const arm = swivel(sk, s, side, goal, wristAt(goal), plane, restBend, { toward: held.pole, w, comfort: held.comfort });
85
+ let miss = 0, fold = 0, need = { twist: 0, flex: 0, dev: 0 };
86
+ for (let i = 0; i < ITERATIONS; i++) {
87
+ const W = wristAt(i ? s.R[h] : goal);
88
+ fold = askedFold(sk, s, side, W);
89
+ miss = twoBone(sk, s, a, fa, h, W, arm.pole, restBend, "elbow");
90
+ need = orient(sk, s, side, h, fa, goal);
91
+ }
92
+ // fold: the flexion (deg) the wrist asked when past ELBOW_FOLD; miss is then how far inside the fold it is
93
+ diag[`hold_${k}`] = { w, miss: Math.max(0, miss), ...need, swivel: swungFrom(s, a, fa, h, plane), ...(fold > ELBOW_FOLD ? { fold } : {}), ...(held.second ? { second: held.second } : {}) };
94
+ diag[`arm_miss_${k}`] *= 1 - w;
95
+ }
96
+
97
+ /**
98
+ * The swivel landscape holdHand searches for `held` on pose s (the plain arm of `side`): what
99
+ * ../hold-plan.js plans the elbow's path on.
100
+ */
101
+ export function holdLandscape(sk, s, side, held) {
102
+ const { goal, plane, wristAt } = aimArm(sk, s, side, held.palm, held.measure(s), held.w);
103
+ return landscape(sk, s, side, goal, wristAt(goal), plane, ELBOW_REST, 0, held.comfort);
104
+ }
105
+
106
+ /**
107
+ * The arm's cost of holding the palm frame `palm` on pose s (the plain arm of `side`, `now` its
108
+ * palm frame there): the wrist strain at the pole holdHand would take, its limits ROLL_MARGIN
109
+ * tighter, plus REACH_PRICE per squared centimetre the arm is short or the wrist nearer the shoulder
110
+ * than the elbow folds (ELBOW_FOLD), plus the shoulder's price with opts.shoulder. An aim key without roll takes the cheapest roll (key-aim.js).
111
+ */
112
+ export function holdCost(sk, s, side, palm, now, { shoulder = false } = {}) {
113
+ const { goal, plane, wristAt } = aimArm(sk, s, side, palm, now, 1), W = wristAt(goal);
114
+ const arm = swivel(sk, s, side, goal, W, plane, ELBOW_REST, { margin: ROLL_MARGIN });
115
+ // the wrist no nearer the shoulder than the elbow folds (ELBOW_FOLD): an arm folded flat holds anything, and nothing between keys
116
+ const I = sk.index, S = s.P[I[`${side}Arm`]], A = len(sub(s.P[I[`${side}ForeArm`]], S)), B = len(sub(s.P[I[`${side}Hand`]], s.P[I[`${side}ForeArm`]]));
117
+ const fold = Math.max(0, reachAt(A, B, ELBOW_FOLD) - len(sub(W, S)));
118
+ // opts.shoulder adds the shoulder's price at that elbow (arm-orient.js shoulderPrice) and the arm's depth into the body
119
+ // (body.js, priced as grip-choice.js prices it): a leaning aim key's pick (key-aim.js) keeps the elbow down, not in the chest
120
+ const arm2 = shoulder ? swivel(sk, s, side, goal, W, plane, ELBOW_REST, { comfort: true }) : arm;
121
+ return (shoulder ? comfortPrice(arm2.need) + shoulderPrice(S, arm2.elbow) + intoBody(sk, s, side, S, arm2.elbow, W) : arm.cost) + REACH_PRICE * ((arm.miss * 100) ** 2 + (fold * 100) ** 2);
122
+ }
123
+
124
+ /** BODY_PRICE per squared centimetre the upper arm or the forearm at elbow E and wrist W goes into the body (body.js). */
125
+ function intoBody(sk, s, side, S, E, W) {
126
+ const body = bodyFor(sk), I = sk.index;
127
+ return body ? BODY_PRICE * (Math.max(segmentDepth(body, sk, s, I[`${side}Arm`], S, E), segmentDepth(body, sk, s, I[`${side}ForeArm`], E, W)) * 100) ** 2 : 0;
128
+ }
129
+
130
+ /** Degrees the elbow swung about the shoulder-wrist axis from the bend direction `plane` the channels gave. */
131
+ function swungFrom(s, a, fa, h, plane) {
132
+ const u = norm(sub(s.P[h], s.P[a])), off = v => sub(v, scale(u, dot(v, u)));
133
+ return angle(off(sub(s.P[fa], s.P[a])), off(plane));
134
+ }
@@ -0,0 +1,113 @@
1
+ // The idle layer: a body that holds its place is never a statue. Wherever the take holds a pose (no
2
+ // keyed channel changes: not a hand, a door or a finger) with its hands free (no arm on IK, no hand
3
+ // target or fit weight, nothing carried), it breathes (the chest lifts and settles:
4
+ // spine_pitch), lets its head drift a little (head_yaw, head_pitch) and, standing, shifts its weight
5
+ // from foot to foot (the hips sway over the planted feet, the pelvis lists toward the unloaded side and
6
+ // the spine rights it: root, roll, spine_roll). The layer only adds to channels that carry no contact:
7
+ // the feet, hand targets, anchors and objects are left as keyed, so a planted foot stays put and a hand
8
+ // on a target stays on it. It fades in RAMP s after the take starts or the body stops, out before it
9
+ // moves again, and stays off across the span of a `still` goal. Deterministic: its phases come from where the character
10
+ // first stands, so a recompile bakes the same take. spec.idle: false turns it off. The offsets ride on
11
+ // the solved pose only (compile.js solveFrame): out.baked keeps the keyed values, so QA's causes and
12
+ // patches still name the keys.
13
+ const BREATH = { period: 3.8, deg: 0.8 }, HEAD = { yaw: 0.9, pitch: 0.6 }, SWAY = { period: 5.6, m: 0.012, list: 1.4 };
14
+ const RAMP = 0.5, MOVE = 1e-4, TAU = 2 * Math.PI;
15
+ const smoothstep = x => x * x * (3 - 2 * x);
16
+ const differs = (a, b) => (Array.isArray(a) ? a.some((v, i) => Math.abs(v - b[i]) > MOVE)
17
+ : a && typeof a === "object" ? Object.keys({ ...a, ...b }).some(k => Math.abs((a[k] ?? 0) - (b?.[k] ?? 0)) > 1e-3) : Math.abs((a ?? 0) - (b ?? 0)) > 0.01);
18
+
19
+ /** Phases in [0, TAU) from a text (FNV-1a hash, then mulberry32). */
20
+ function phases(text, n) {
21
+ let h = 2166136261;
22
+ for (let i = 0; i < text.length; i++) h = Math.imul(h ^ text.charCodeAt(i), 16777619);
23
+ let a = h >>> 0;
24
+ return Array.from({ length: n }, () => {
25
+ a = (a + 0x6d2b79f5) >>> 0;
26
+ let t = Math.imul(a ^ (a >>> 15), a | 1);
27
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
28
+ return (((t ^ (t >>> 14)) >>> 0) / 4294967296) * TAU;
29
+ });
30
+ }
31
+
32
+ /** The weight of the layer per frame: 0 while anything keyed moves, a hand works or a still goal holds the body, easing to 1 RAMP s into a hold. */
33
+ function holdWeights(baked, spec, frameCount, fps) {
34
+ // the take's first frame is its keyed pose: the layer fades in from there
35
+ const moving = new Array(frameCount).fill(false), watch = Object.keys(baked);
36
+ moving[0] = true;
37
+ for (let f = 1; f < frameCount; f++) if (watch.some(ch => differs(baked[ch][f], baked[ch][f - 1]))) moving[f] = moving[f - 1] = true;
38
+ // a working hand: an arm on IK, a hand target or fit weight, or an object carried from its grab on
39
+ const hands = watch.filter(ch => /^(armw_[lr]|w_.+_[lr])$/.test(ch));
40
+ for (let f = 0; f < frameCount; f++) if (hands.some(ch => baked[ch][f] > 1e-3)) moving[f] = true;
41
+ for (const o of Object.values(spec.objects ?? {})) {
42
+ if (!Number.isFinite(o?.grab)) continue;
43
+ for (let f = Math.max(0, o.grab); f < Math.min(frameCount, Number.isFinite(o.release) ? o.release : frameCount); f++) moving[f] = true;
44
+ }
45
+ for (const g of spec.goals ?? []) {
46
+ const at = Array.isArray(g?.at) ? g.at : null;
47
+ if (g?.still === undefined || !at?.length) continue;
48
+ for (let f = Math.max(0, Math.round(at[0])); f <= Math.min(frameCount - 1, Math.round(at[at.length - 1])); f++) moving[f] = true;
49
+ }
50
+ const ramp = Math.max(1, Math.round(RAMP * fps)), d = new Array(frameCount).fill(Infinity);
51
+ for (let f = 0, last = -Infinity; f < frameCount; f++) { if (moving[f]) last = f; d[f] = f - last; }
52
+ for (let f = frameCount - 1, next = Infinity; f >= 0; f--) { if (moving[f]) next = f; d[f] = Math.min(d[f], next - f); }
53
+ return d.map(x => smoothstep(Math.min(1, x / ramp)));
54
+ }
55
+
56
+ /**
57
+ * Per hand, how much it works at each frame: 1 while its arm is on IK, a hand target or fit weight of that hand is
58
+ * on, or it carries an object from its grab on; easing to 0 over RAMP s away from that. pose.js keeps a working
59
+ * hand's forearm on its slant (arm_hang), eased so the arm does not jump as it goes onto IK. null for an idle hand.
60
+ */
61
+ export function busyHands(baked, spec, frameCount, fps) {
62
+ const out = {}, ramp = Math.max(1, Math.round(RAMP * fps));
63
+ for (const k of ["l", "r"]) {
64
+ const own = Object.keys(baked).filter(ch => ch === `armw_${k}` || new RegExp(`^w_.+_${k}$`).test(ch));
65
+ const busy = Array.from({ length: frameCount }, (_, f) => own.some(ch => baked[ch][f] > 1e-3));
66
+ for (const o of Object.values(spec.objects ?? {})) {
67
+ if (!Number.isFinite(o?.grab) || spec.props?.[o.prop]?.hand !== k) continue;
68
+ for (let f = Math.max(0, o.grab); f < Math.min(frameCount, Number.isFinite(o.release) ? o.release : frameCount); f++) busy[f] = true;
69
+ }
70
+ if (!busy.some(Boolean)) { out[k] = null; continue; }
71
+ const d = new Array(frameCount).fill(Infinity);
72
+ for (let f = 0, last = -Infinity; f < frameCount; f++) { if (busy[f]) last = f; d[f] = f - last; }
73
+ for (let f = frameCount - 1, next = Infinity; f >= 0; f--) { if (busy[f]) next = f; d[f] = Math.min(d[f], next - f); }
74
+ out[k] = d.map(x => 1 - smoothstep(Math.min(1, x / ramp)));
75
+ }
76
+ return out;
77
+ }
78
+
79
+ /** The idle offsets per frame ({ channel: delta }, root a world [dx, 0, dz]) for the baked channels, or null (compile.js rebake). */
80
+ export function idleLayer(baked, { spec, sk, frameCount }) {
81
+ if (spec.idle === false || !baked.root || !baked.foot_l || !baked.foot_r) return null;
82
+ const fps = spec.fps ?? 30, w = holdWeights(baked, spec, frameCount, fps);
83
+ if (!w.some(x => x > 0)) return null;
84
+ const P = sk.restWorldPos, standY = P[sk.index.Hips][1], ankY = P[sk.index.LeftFoot][1];
85
+ const [pb, py, pp, ps, py2] = phases(JSON.stringify([baked.root[0].map(v => v.toFixed(3)), (baked.yaw?.[0] ?? 0).toFixed(1)]), 5);
86
+ const out = new Array(frameCount).fill(null);
87
+ for (let f = 0; f < frameCount; f++) {
88
+ if (!(w[f] > 0)) continue;
89
+ const t = f / fps, k = w[f];
90
+ // breathing in lifts the chest (spine_pitch + bends forward)
91
+ const d = out[f] = { spine_pitch: -k * BREATH.deg * Math.sin((TAU * t) / BREATH.period + pb),
92
+ head_yaw: k * HEAD.yaw * (0.6 * Math.sin((TAU * t) / 7.3 + py) + 0.4 * Math.sin((TAU * t) / 11.9 + py2)),
93
+ head_pitch: k * HEAD.pitch * Math.sin((TAU * t) / 9.1 + pp) };
94
+ // the weight shift: standing only (both feet flat on the floor, the hips at standing height)
95
+ const feet = ["l", "r"].every(s => baked[`foot_${s}`][f][1] < ankY + 0.03 && Math.abs(baked[`fpitch_${s}`]?.[f] ?? 0) < 2);
96
+ if (!feet || baked.root[f][1] < standY - 0.15) continue;
97
+ const s = k * Math.sin((TAU * t) / SWAY.period + ps), yaw = ((baked.yaw?.[f] ?? 0) * Math.PI) / 180;
98
+ d.root = [Math.cos(yaw) * SWAY.m * s, 0, -Math.sin(yaw) * SWAY.m * s];
99
+ // the hips over the left foot drop the right side (roll +), the spine stays upright
100
+ d.roll = SWAY.list * s;
101
+ d.spine_roll = -SWAY.list * s;
102
+ }
103
+ return out;
104
+ }
105
+
106
+ /** Frame f's channels with the idle offsets added (a new object; c itself is left alone). */
107
+ export function withIdle(c, layer, f) {
108
+ const d = layer?.[f];
109
+ if (!d) return c;
110
+ const out = { ...c };
111
+ for (const [ch, v] of Object.entries(d)) out[ch] = Array.isArray(v) ? (Array.isArray(c[ch]) ? c[ch].map((x, i) => x + v[i]) : c[ch]) : (Number(c[ch]) || 0) + v;
112
+ return out;
113
+ }
@@ -0,0 +1,170 @@
1
+ // Two-bone limb IK, foot orientation, wrist and finger posing on a world-space state.
2
+ // Every edit rotates a bone's subtree about that bone's own joint, so the result maps
3
+ // exactly back to local quaternions (skeleton.js toLocal).
4
+ import { add, sub, scale, dot, len, norm, cross, clamp, apply, T, align, axisRot, ry, rx } from "./math.js";
5
+ import { rotateSubtree } from "./skeleton.js";
6
+
7
+ /** Direction fixed to bone b that points along `restDir` in the rest pose. */
8
+ export const carried = (sk, s, b, restDir) => apply(s.R[b], apply(T(sk.restWorldRot[b]), restDir));
9
+
10
+ // Elbow flexion: degrees between the upper arm and the forearm (0 straight), read on the joints as QA
11
+ // reads it (Arm->ForeArm against ForeArm->Hand). Active flexion ends near 145 deg (clinical norms
12
+ // 140-150: AMA 140, Kendall 145, AAOS 150), where the forearm's muscle meets the upper arm's; the
13
+ // passive fold stops a few degrees on. An arm IK follows its wrist target freely to ELBOW_FOLD, eases
14
+ // onto ELBOW_STOP (armReach) and leaves a target nearer the shoulder as a residual (REACH), like one
15
+ // out of reach. Before, twoBone folded an arm flat: the forearm lay in the upper arm and the bend plane
16
+ // was any plane at all (session df6acdcc, 171 deg).
17
+ export const ELBOW_FOLD = 145, ELBOW_STOP = 150;
18
+ // Knee flexion, read on the joints as QA reads it (UpLeg->Leg against Leg->Foot, 0 straight). Active
19
+ // flexion ends near 135-140 deg (AAOS 135); past it the knee folds only passively, under the body's
20
+ // weight, until the thigh rests on the calf: a deep squat with the heels down reaches 150-160 deg, and
21
+ // passive flexion stops near 160 (the heel at the buttock). A leg IK follows its ankle target freely to
22
+ // KNEE_FOLD (the squats the sessions keyed bend 125-138 deg), eases onto KNEE_STOP as the thigh presses
23
+ // on the calf, and leaves a target nearer the hip as a residual (REACH leg_miss). Before, twoBone folded
24
+ // a knee flat: the shin went into the thigh.
25
+ export const KNEE_FOLD = 150, KNEE_STOP = 160;
26
+ /** The flexion (deg) that puts the wrist d metres from the shoulder of an arm with bones a, b (180 at |a - b| or nearer). */
27
+ export const flexionAt = (a, b, d) => (Math.acos(clamp((d * d - a * a - b * b) / (2 * a * b), -1, 1)) * 180) / Math.PI;
28
+ /** The shoulder-wrist distance of an arm with bones a, b folded `deg` degrees. */
29
+ export const reachAt = (a, b, deg) => Math.sqrt(a * a + b * b + 2 * a * b * Math.cos((deg * Math.PI) / 180));
30
+ /**
31
+ * The shoulder-wrist distance an arm with bones a, b takes for a wrist target d metres away: d while
32
+ * the elbow folds ELBOW_FOLD or less (d >= k), then d + (k - d)^2 / 4w (w = k - the ELBOW_STOP distance)
33
+ * down to d = k - 2w, and the stop beyond. The wrist slows at a constant rate (C1, no kink as it
34
+ * engages: half the peak deceleration of an exponential ease as wide) and sits on the stop once the
35
+ * target is 2w inside. A leg passes KNEE_FOLD and KNEE_STOP (the hip-ankle distance).
36
+ */
37
+ export function armReach(a, b, d, fold = ELBOW_FOLD, stop = ELBOW_STOP) {
38
+ const k = reachAt(a, b, fold);
39
+ if (d >= k) return d;
40
+ const w = k - reachAt(a, b, stop);
41
+ return d > k - 2 * w ? d + ((k - d) * (k - d)) / (4 * w) : k - w;
42
+ }
43
+ /**
44
+ * The wrist point an arm (shoulder S, bones a, b) reaches for a wrist target T: T while the elbow
45
+ * folds ELBOW_FOLD or less, else the point armReach(|T - S|) from the shoulder on the ray from the fold
46
+ * centre C through T. No point can stand for every target inside the stop and still move smoothly (a
47
+ * ball has no continuous retraction onto its boundary): the nearest point (the ray from the shoulder)
48
+ * whips round the shoulder as a target passes near it (session df6acdcc: 120 deg in 2 frames). So C
49
+ * sits on the stop's sphere straight behind the shoulder (`back`, the chest's -Z), in the back, where
50
+ * no hand reaches, and a target passing in front of, above, below or beside the shoulder glides.
51
+ * A leg passes the knee's fold and stop, and `back` is the pelvis's up (upOf): no foot goes into the
52
+ * pelvis, and a squat's ankle target under the hip goes on down from it.
53
+ */
54
+ export function foldPoint(S, T, a, b, back, fold = ELBOW_FOLD, stop = ELBOW_STOP) {
55
+ const k = reachAt(a, b, fold), d = len(sub(T, S));
56
+ if (d >= k) return T;
57
+ const r = armReach(a, b, d, fold, stop), m = scale(back, reachAt(a, b, stop)), C = add(S, m), e = sub(T, C);
58
+ const u = len(e) > 1e-9 ? norm(e) : scale(back, -1), em = dot(u, m);
59
+ return add(C, scale(u, -em + Math.sqrt(Math.max(0, em * em - dot(m, m) + r * r))));
60
+ }
61
+ /** The direction behind the shoulders on pose s (the chest bone's rest -Z), where foldPoint's rays start. */
62
+ export const backOf = (sk, s) => carried(sk, s, sk.index.Spine2 ?? sk.index.Hips, [0, 0, -1]);
63
+ /** The direction up from the hips on pose s (the pelvis's rest +Y), where a knee's foldPoint rays start. */
64
+ const upOf = (sk, s) => carried(sk, s, sk.index.Hips, [0, 1, 0]);
65
+ /** The flexion (deg) an arm with bones a, b takes when `deg` is asked (armReach as an angle). */
66
+ export const elbowFold = (a, b, deg) => flexionAt(a, b, armReach(a, b, reachAt(a, b, deg)));
67
+ /** The flexion (deg) a wrist target asks of the arm of `side` on pose s (a leg's ankle target: bones UpLeg, Leg, Foot). */
68
+ export function askedFold(sk, s, side, target, [u, m, e] = ["Arm", "ForeArm", "Hand"]) {
69
+ const S = s.P[sk.index[`${side}${u}`]], E = s.P[sk.index[`${side}${m}`]], W = s.P[sk.index[`${side}${e}`]];
70
+ return flexionAt(len(sub(E, S)), len(sub(W, E)), len(sub(target, S)));
71
+ }
72
+
73
+ /** Ease an elbow folded past ELBOW_FOLD (an FK abend) back as the IK would (elbowFold), the forearm turning in its bend plane. */
74
+ export function limitFold(sk, s, side) {
75
+ const a = sk.index[`${side}Arm`], fa = sk.index[`${side}ForeArm`], up = sub(s.P[fa], s.P[a]), down = sub(s.P[sk.index[`${side}Hand`]], s.P[fa]);
76
+ const deg = (Math.acos(clamp(dot(norm(up), norm(down)), -1, 1)) * 180) / Math.PI, n = cross(up, down);
77
+ if (deg > ELBOW_FOLD && len(n) > 1e-12) rotateSubtree(sk, s, fa, s.P[fa].slice(), axisRot(n, elbowFold(len(up), len(down), deg) - deg));
78
+ }
79
+
80
+ /** The stops twoBone keeps to: the free fold and the stop (deg), and the fold centre's direction on pose s. */
81
+ const STOPS = { elbow: [ELBOW_FOLD, ELBOW_STOP, backOf], knee: [KNEE_FOLD, KNEE_STOP, upOf] };
82
+
83
+ /** Solve root-mid-end so `end` lands on `target` (clamped to reach), bending toward `pole`.
84
+ * `pole0` is the limb's current bend reference carried by the upper bone. Returns the miss (m).
85
+ * `stop`: "elbow" or "knee", the mid joint folds no further than armReach allows with that joint's
86
+ * fold and stop (foldPoint), and the miss is the residual either way (out of reach, or nearer the
87
+ * shoulder or hip than the fold). */
88
+ export function twoBone(sk, s, root, mid, end, target, pole, restBend, stop = null) {
89
+ const Rt = s.P[root], M = s.P[mid], E = s.P[end];
90
+ const a = len(sub(M, Rt)), b = len(sub(E, M)), lim = STOPS[stop];
91
+ // a joint folds no further than armReach lets it: the end goes to foldPoint, and misses the target by the rest
92
+ const W = lim ? foldPoint(Rt, target, a, b, lim[2](sk, s), lim[0], lim[1]) : target, inside = W === target ? 0 : len(sub(W, target));
93
+ const v = sub(W, Rt), d = len(v), u = scale(v, 1 / Math.max(d, 1e-9));
94
+ const dc = clamp(d, Math.abs(a - b) + 1e-4, (a + b) * 0.9995);
95
+ const l = (a * a - b * b + dc * dc) / (2 * dc), h = Math.sqrt(Math.max(a * a - l * l, 0));
96
+ const pv = norm(sub(pole, scale(u, dot(pole, u))));
97
+ const Mn = add(Rt, add(scale(u, l), scale(pv, h)));
98
+ const p0 = carried(sk, s, root, restBend);
99
+ rotateSubtree(sk, s, root, Rt.slice(), align(sub(M, Rt), p0, sub(Mn, Rt), pv));
100
+ const p1 = carried(sk, s, mid, restBend);
101
+ const Tn = add(Rt, scale(u, dc));
102
+ rotateSubtree(sk, s, mid, s.P[mid].slice(), align(sub(s.P[end], s.P[mid]), p1, sub(Tn, s.P[mid]), pv));
103
+ return inside || d - dc;
104
+ }
105
+
106
+ /** Aim a foot: Foot->ToeBase along yaw/pitch of its rest direction, sole kept level. */
107
+ export function orientFoot(sk, s, side, yawDeg, pitchDeg) {
108
+ const foot = sk.index[`${side}Foot`], toe = sk.index[`${side}ToeBase`];
109
+ const rest = sub(sk.restWorldPos[toe], sk.restWorldPos[foot]);
110
+ const want = apply(ry(yawDeg), apply(rx(pitchDeg), rest));
111
+ // level the sole: the foot's own carried up vector goes to world up (no roll from the leg IK)
112
+ rotateSubtree(sk, s, foot, s.P[foot].slice(), align(sub(s.P[toe], s.P[foot]), carried(sk, s, foot, [0, 1, 0]), want, [0, 1, 0]));
113
+ // Toe-off: bend the toe joint so the toes stay flat on the ground while the heel rises.
114
+ const tip = sk.index[`${side}Toe_End`];
115
+ if (pitchDeg > 0 && tip !== undefined) {
116
+ const seg = sub(s.P[tip], s.P[toe]), flat = [seg[0], 0, seg[2]];
117
+ if (len(flat) > 1e-6) rotateSubtree(sk, s, toe, s.P[toe].slice(), align(seg, [0, 1, 0], flat, [0, 1, 0]));
118
+ }
119
+ }
120
+
121
+ export function palmAxes(sk, s, side) {
122
+ const h = sk.index[`${side}Hand`], m1 = sk.index[`${side}HandMiddle1`];
123
+ const d = norm(sub(s.P[m1], s.P[h]));
124
+ let l = sub(s.P[sk.index[`${side}HandIndex1`]], s.P[sk.index[`${side}HandPinky1`]]);
125
+ l = norm(sub(l, scale(d, dot(l, d))));
126
+ return { d, l, n: cross(d, l) };
127
+ }
128
+
129
+ /** Turn the elbow `deg` about the shoulder-wrist line: the hand keeps its place and its world turn. */
130
+ export function swivelElbow(sk, s, side, deg) {
131
+ const a = sk.index[`${side}Arm`], h = sk.index[`${side}Hand`], R = axisRot(sub(s.P[h], s.P[a]), deg);
132
+ rotateSubtree(sk, s, a, s.P[a].slice(), R);
133
+ rotateSubtree(sk, s, h, s.P[h].slice(), T(R));
134
+ }
135
+
136
+ /** Wrist twist (about the forearm) and flex (about the knuckle line), degrees, clamped. */
137
+ export function wrist(sk, s, side, twistDeg, flexDeg) {
138
+ if (!twistDeg && !flexDeg) return;
139
+ const h = sk.index[`${side}Hand`], fa = sk.index[`${side}ForeArm`];
140
+ const axT = norm(sub(s.P[h], s.P[fa]));
141
+ const { l } = palmAxes(sk, s, side);
142
+ const axF = norm(sub(l, scale(axT, dot(l, axT))));
143
+ rotateSubtree(sk, s, h, s.P[h].slice(), axisRot(axF, clamp(flexDeg, -70, 70)));
144
+ rotateSubtree(sk, s, h, s.P[h].slice(), axisRot(axT, clamp(twistDeg, -85, 85)));
145
+ }
146
+
147
+ // Finger curl presets, degrees per phalanx (1, 2, 3); thumb is [opposition, curl, curl].
148
+ const FINGER_PRESETS = {
149
+ relaxed: { Index: [12, 18, 10], Middle: [15, 20, 12], Ring: [18, 22, 12], Pinky: [20, 24, 14], Thumb: [15, 10, 8] },
150
+ fist: { Index: [80, 95, 60], Middle: [85, 95, 60], Ring: [85, 95, 60], Pinky: [85, 95, 60], Thumb: [45, 30, 35] },
151
+ grip: { Index: [55, 60, 35], Middle: [60, 62, 35], Ring: [62, 62, 35], Pinky: [65, 62, 35], Thumb: [40, 20, 20] },
152
+ cigarette: { Index: [8, 10, 6], Middle: [10, 12, 8], Ring: [60, 75, 45], Pinky: [65, 80, 45], Thumb: [35, 25, 20] },
153
+ point: { Index: [0, 0, 0], Middle: [80, 95, 60], Ring: [85, 95, 60], Pinky: [85, 95, 60], Thumb: [40, 30, 30] },
154
+ };
155
+
156
+ /** Blend presets by weight over `relaxed` and curl every finger toward the palm. */
157
+ export function fingers(sk, s, side, weights) {
158
+ const base = FINGER_PRESETS.relaxed, sign = side === "Left" ? -1 : 1;
159
+ const curl = (f, k) => base[f][k] + Object.entries(weights ?? {}).reduce((acc, [p, w]) => acc + (w || 0) * ((FINGER_PRESETS[p]?.[f]?.[k] ?? base[f][k]) - base[f][k]), 0);
160
+ for (const f of ["Index", "Middle", "Ring", "Pinky", "Thumb"]) {
161
+ for (let k = 0; k < 3; k++) {
162
+ const j = sk.index[`${side}Hand${f}${k + 1}`];
163
+ if (j === undefined) continue;
164
+ const { l, d } = palmAxes(sk, s, side);
165
+ const ax = f === "Thumb" && k === 0 ? d : l;
166
+ rotateSubtree(sk, s, j, s.P[j].slice(), axisRot(ax, sign * curl(f, k)));
167
+ }
168
+ }
169
+ }
170
+