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