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
@@ -1,419 +0,0 @@
1
- import { CSKEL27_PARENTS } from "../../../src/ardy/cskel27.js";
2
- import { deriveBoneOffsets, forwardKinematics, globalRotations, matMul, matTranspose, quatToMat } from "../../../src/ardy/convert.js";
3
- import { add, bodyOffsets, cloneMotion, jointsAt, localsAt, matVec, shiftFrame, smoothstep, sub, vec } from "./motion.mjs";
4
-
5
- const FEET = [21, 22, 25, 26], HANDS = [10, 16];
6
- const EPS = 1e-5;
7
-
8
- export function validateBoxes(boxes) {
9
- if (!Array.isArray(boxes) || boxes.some(b => ![b?.min, b?.max].every(v => Array.isArray(v) && v.length === 3 && v.every(Number.isFinite)) || b.min.some((v, k) => v >= b.max[k]))) {
10
- throw new Error("scene: boxes must be {min:[x,y,z],max:[x,y,z]} with min < max");
11
- }
12
- return boxes;
13
- }
14
-
15
- /** Smallest translation of an interior POINT to a box face. Boundary is not
16
- * penetration. Used for hand contact proximity; no skin radius is implied. */
17
- export function boxPenetration(point, box) {
18
- if (point.some((v, k) => v <= box.min[k] || v >= box.max[k])) return [0, 0, 0];
19
- const candidates = box.min.flatMap((lo, k) => [[k, lo - point[k]], [k, box.max[k] - point[k]]]);
20
- candidates.sort((a, b) => Math.abs(a[1]) - Math.abs(b[1]));
21
- const delta = [0, 0, 0]; delta[candidates[0][0]] = candidates[0][1];
22
- return delta;
23
- }
24
-
25
- /** Open-interior segment/AABB slab intersection catches a bone crossing a
26
- * box even when both of its joints lie outside (point-only tests miss it). */
27
- export function segmentPenetratesBox(a, b, box) {
28
- let near = 0, far = 1;
29
- for (let k = 0; k < 3; k++) {
30
- const d = b[k] - a[k];
31
- if (Math.abs(d) < 1e-12) {
32
- if (a[k] <= box.min[k] || a[k] >= box.max[k]) return false;
33
- } else {
34
- const t0 = (box.min[k] - a[k]) / d, t1 = (box.max[k] - a[k]) / d;
35
- near = Math.max(near, Math.min(t0, t1)); far = Math.min(far, Math.max(t0, t1));
36
- if (near >= far) return false;
37
- }
38
- }
39
- return near < far;
40
- }
41
-
42
- function intersects(points, box) {
43
- return CSKEL27_PARENTS.some((p, j) => p !== null && segmentPenetratesBox(points[p], points[j], box));
44
- }
45
-
46
- /** Conservative rigid skeleton projection. On collision, clear the whole
47
- * skeleton along its nearest feasible separating axis, not independent
48
- * joint pushes (which would stretch bones). Floor wins over downward pushes.
49
- * This tests joint/bone segments, NOT mesh/capsule thickness or self-collision.
50
- * Multiple mutually obstructing boxes fail explicitly instead of silently
51
- * emitting penetration. Cube/mesh refinement is a separate experiment. */
52
- export function resolveScene(points, boxes, { floorY = 0 } = {}) {
53
- let placed = points.map(p => p.slice()), total = [0, 0, 0];
54
- for (let pass = 0; pass < 32; pass++) {
55
- const floorLift = Math.max(0, floorY + EPS - Math.min(...FEET.map(j => placed[j][1])));
56
- if (floorLift) { const d = [0, floorLift, 0]; placed = placed.map(p => add(p, d)); total = add(total, d); }
57
- const box = boxes.find(b => intersects(placed, b));
58
- if (!box) return { delta: total, positions: placed };
59
- const candidates = [];
60
- for (let k = 0; k < 3; k++) {
61
- const lo = Math.min(...placed.map(p => p[k])), hi = Math.max(...placed.map(p => p[k]));
62
- for (const amount of [box.min[k] - hi - EPS, box.max[k] - lo + EPS]) {
63
- if (k === 1 && Math.min(...FEET.map(j => placed[j][1])) + amount < floorY) continue;
64
- const delta = [0, 0, 0]; delta[k] = amount; candidates.push(delta);
65
- }
66
- }
67
- candidates.sort((a, b) => Math.hypot(...a) - Math.hypot(...b));
68
- placed = placed.map(p => add(p, candidates[0])); total = add(total, candidates[0]);
69
- }
70
- throw new Error("scene: no collision-free rigid placement after 32 projections; constraints conflict");
71
- }
72
-
73
- export function penetrates(points, boxes, floorY = 0) {
74
- return Math.min(...FEET.map(j => points[j][1])) < floorY - EPS || boxes.some(b => intersects(points, b));
75
- }
76
-
77
- /** Scene corrections that are smooth in time. Per frame: the smallest push
78
- * that clears the skeleton (its nearest face may change between frames). Then
79
- * each signed axis component
80
- * is dilated by `radius` frames and Gaussian-smoothed (sigma = radius / 3):
81
- * the result is at least the needed push at every frame, so it stays
82
- * collision-free, and it ramps in and out instead of snapping. The per-frame
83
- * solver picks each frame's nearest face from scratch and teleported the body
84
- * whenever that face changed (fal stepup: 0.76 m in one frame). */
85
- export function continuousSceneDeltas(motion, boxes, radius) {
86
- const n = motion.frames, shifted = (f, delta) => jointsAt(motion, f).map(p => add(p, delta));
87
- const blocked = (f, delta) => penetrates(shifted(f, delta), boxes);
88
- const raw = Array.from({ length: n }, (_, f) => (blocked(f, [0, 0, 0]) ? resolveScene(shifted(f, [0, 0, 0]), boxes).delta : [0, 0, 0]));
89
- const sigma = Math.max(1, radius / 3), kernel = Array.from({ length: 2 * radius + 1 }, (_, k) => Math.exp(-0.5 * ((k - radius) / sigma) ** 2));
90
- const at = (x, f) => x[Math.min(n - 1, Math.max(0, f))];
91
- const ramp = (x) => {
92
- const dilated = x.map((_, f) => { let m = 0; for (let k = -radius; k <= radius; k++) m = Math.max(m, at(x, f + k)); return m; });
93
- return dilated.map((_, f) => { let s = 0, t = 0; for (let k = -radius; k <= radius; k++) { s += kernel[k + radius] * at(dilated, f + k); t += kernel[k + radius]; } return s / t; });
94
- };
95
- const axes = [0, 1, 2].map(k => {
96
- const up = ramp(raw.map(d => Math.max(0, d[k]))), down = ramp(raw.map(d => Math.max(0, -d[k])));
97
- return up.map((v, f) => v - down[f]);
98
- });
99
- // Where opposite pushes on one axis overlap, the ramp can fall short: top it
100
- // up by the smallest extra push (small, since the ramp already carries most).
101
- return raw.map((_, f) => { const smooth = [axes[0][f], axes[1][f], axes[2][f]]; return blocked(f, smooth) ? add(smooth, resolveScene(shifted(f, smooth), boxes).delta) : smooth; });
102
- }
103
-
104
- // Only these descendants can be moved without moving the torso/attachment.
105
- // [hip/shoulder, knee/elbow, ankle/wrist, ...toe/finger joints]
106
- const LIMBS = [[19, 20, 21, 22], [23, 24, 25, 26], [8, 9, 10, 11, 12], [14, 15, 16, 17, 18]];
107
- const LIMB_BONES = new Set(LIMBS.flatMap(chain => chain.slice(1)));
108
- const dot = (a, b) => a.reduce((s, v, k) => s + v * b[k], 0);
109
- const scale = (v, s) => v.map(x => x * s);
110
- const unit = v => scale(v, 1 / Math.hypot(...v));
111
- const cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
112
- const clamp = (x, lo, hi) => Math.max(lo, Math.min(hi, x));
113
-
114
- /** Forbidden ray offsets for a whole BONE, not just its endpoints. Clip the
115
- * convex polygon (t,s) with a + t*(b-a) + s*ray inside the six box planes,
116
- * then project onto s. A small margin survives Float32 FK/writeback. */
117
- function boneRayInterval(a, b, ray, box) {
118
- const corners = Array.from({ length: 8 }, (_, i) => box.min.map((v, k) => i & (1 << k) ? box.max[k] : v));
119
- const extent = 1 + Math.max(...corners.flatMap(p => [Math.hypot(...sub(p, a)), Math.hypot(...sub(p, b))]));
120
- let polygon = [[0, -extent], [1, -extent], [1, extent], [0, extent]];
121
- for (let k = 0; k < 3; k++) for (const sign of [-1, 1]) {
122
- const face = sign > 0 ? box.max[k] + EPS : box.min[k] - EPS;
123
- const distance = p => sign * (a[k] + p[0] * (b[k] - a[k]) + p[1] * ray[k] - face);
124
- const clipped = [];
125
- for (let i = 0; i < polygon.length; i++) {
126
- const p = polygon[i], q = polygon[(i + 1) % polygon.length], dp = distance(p), dq = distance(q);
127
- if (dp <= 0) clipped.push(p);
128
- if ((dp < 0 && dq > 0) || (dp > 0 && dq < 0)) clipped.push(p.map((v, j) => v + (q[j] - v) * dp / (dp - dq)));
129
- }
130
- polygon = clipped;
131
- if (!polygon.length) return null;
132
- }
133
- return [Math.min(...polygon.map(p => p[1])), Math.max(...polygon.map(p => p[1]))];
134
- }
135
-
136
- /** Ray offsets s at which one frame's TORSO (non-limb bones) crosses a box.
137
- * The torso MUST be clear: limb IK cannot repair its penetration. Swept
138
- * interval endpoints avoid a depth grid missing a narrow feasible corridor. */
139
- function torsoRayIntervals(points, ray, boxes) {
140
- const intervals = [];
141
- for (let j = 1; j < 27; j++) if (!LIMB_BONES.has(j)) for (const box of boxes) {
142
- const interval = boneRayInterval(points[CSKEL27_PARENTS[j]], points[j], ray, box);
143
- if (interval) intervals.push(interval);
144
- }
145
- return intervals;
146
- }
147
-
148
- /** Smallest t >= t0 (t = sign * s) outside every forbidden torso interval. */
149
- function clearFrom(intervals, sign, t0) {
150
- let t = t0;
151
- for (let moved = true; moved;) {
152
- moved = false;
153
- for (const [lo, hi] of intervals) {
154
- const a = sign > 0 ? lo : -hi, b = sign > 0 ? hi : -lo;
155
- if (t >= a - EPS && t <= b + EPS) { t = b + 2 * EPS; moved = true; }
156
- }
157
- }
158
- return t;
159
- }
160
-
161
- /** Exact upper envelope max_g (required[g] - path budget g..f): the smallest
162
- * curve at or above `required` whose change on edge f-1..f is <= budget[f]. */
163
- function slopeEnvelope(required, budget) {
164
- const t = required.slice();
165
- for (let f = 1; f < t.length; f++) t[f] = Math.max(t[f], t[f - 1] - budget[f]);
166
- for (let f = t.length - 2; f >= 0; f--) t[f] = Math.max(t[f], t[f + 1] - budget[f + 1]);
167
- return t;
168
- }
169
-
170
- /** Time-varying offset for one sign: per frame the minimal torso-clear
171
- * t_req(f) >= 0 (0 when the frame needs none), then its slope-limited upper
172
- * envelope, so t is 0 far from any collision and ramps in/out. The vector
173
- * correction step |s_f r_f - s_{f-1} r_{f-1}| <= |ds| + |s| |dr|, so each
174
- * edge's slope budget is rayMaxStepM minus what the turning ray already uses
175
- * at the peak offset. An envelope value may land inside another torso
176
- * interval; raise that frame's requirement past it and rebuild. Requirements
177
- * only increase through a finite set of interval ends, so this terminates.
178
- * Returns null when the sign cannot stay within the caps/budget. */
179
- function signedRayOffsets(intervals, sign, caps, turns, rayMaxStepM) {
180
- const required = intervals.map(i => clearFrom(i, sign, 0));
181
- for (;;) {
182
- const peak = Math.max(...required), budget = turns.map(d => rayMaxStepM - peak * d - 1e-9);
183
- if (budget.some((b, f) => f && !(b > 0))) return null;
184
- const t = slopeEnvelope(required, budget);
185
- if (t.some((v, f) => v > caps[f])) return null;
186
- let raised = false;
187
- for (let f = 0; f < t.length; f++) {
188
- const clear = clearFrom(intervals[f], sign, t[f]);
189
- if (clear > t[f]) { required[f] = clear; raised = true; }
190
- }
191
- if (!raised) return t.map(v => sign * v);
192
- }
193
- }
194
-
195
- function perpendicular(v) {
196
- const axis = Math.abs(v[0]) < 0.8 ? [1, 0, 0] : [0, 1, 0];
197
- return unit(sub(axis, scale(v, dot(axis, v))));
198
- }
199
-
200
- function swingRotation(from, to) {
201
- const a = unit(from), b = unit(to), cosine = clamp(dot(a, b), -1, 1);
202
- return cosine < -1 + 1e-10 ? quatToMat([0, ...perpendicular(a)]) : quatToMat([1 + cosine, ...cross(a, b)]);
203
- }
204
-
205
- /** Analytic two-bone IK; only the chain changes. Preserve the effector's
206
- * world orientation so a corrected ankle does not tip its toe into the floor
207
- * (and a wrist does not swing its fingers back through the box). */
208
- function limbPose(locals, points, globals, offsets, chain, target, poleAngle) {
209
- const [a, b, c] = chain, origin = points[a], toward = sub(target, origin);
210
- const l0 = Math.hypot(...offsets[b]), l1 = Math.hypot(...offsets[c]);
211
- const distance = Math.hypot(...toward), direction = distance > EPS ? scale(toward, 1 / distance) : unit(sub(points[c], origin));
212
- const reach = clamp(distance, Math.abs(l0 - l1) + EPS, l0 + l1 - EPS);
213
- const along = (l0 * l0 + reach * reach - l1 * l1) / (2 * reach);
214
- const height = Math.sqrt(Math.max(0, l0 * l0 - along * along));
215
- const oldBend = sub(points[b], origin), projected = sub(oldBend, scale(direction, dot(oldBend, direction)));
216
- const bend = Math.hypot(...projected) > EPS ? unit(projected) : perpendicular(direction);
217
- const pole = add(scale(bend, Math.cos(poleAngle)), scale(cross(direction, bend), Math.sin(poleAngle)));
218
- const elbow = add(origin, add(scale(direction, along), scale(pole, height)));
219
- const end = add(origin, scale(direction, reach));
220
- const upper = matMul(swingRotation(sub(points[b], origin), sub(elbow, origin)), globals[a]);
221
- const movedLower = matMul(upper, locals[b]);
222
- const lower = matMul(swingRotation(matVec(movedLower, offsets[c]), sub(end, elbow)), movedLower);
223
- const solved = locals.slice();
224
- solved[a] = matMul(matTranspose(globals[CSKEL27_PARENTS[a]]), upper);
225
- solved[b] = matMul(matTranspose(upper), lower);
226
- solved[c] = matMul(matTranspose(lower), globals[c]);
227
- return solved;
228
- }
229
-
230
- function limbBlocked(points, chain, boxes) {
231
- return chain.slice(1).some(j => (FEET.includes(j) && points[j][1] < -EPS) || boxes.some(box => segmentPenetratesBox(points[CSKEL27_PARENTS[j]], points[j], box)));
232
- }
233
-
234
- function clearLimb(locals, points, offsets, chain, boxes) {
235
- const end = points[chain[2]], tips = chain.slice(2), globals = globalRotations(locals);
236
- // Each face contributes an effector coordinate which clears ALL distal
237
- // joints. Cartesian combinations include edges/corners and multiple boxes.
238
- const coordinates = end.map((v, k) => {
239
- const relative = tips.map(j => points[j][k] - end[k]);
240
- return [v, ...boxes.flatMap(box => [box.min[k] - Math.max(...relative) - 4 * EPS, box.max[k] - Math.min(...relative) + 4 * EPS])];
241
- });
242
- const floor = FEET.includes(chain[2]) ? EPS - Math.min(...tips.map(j => points[j][1] - end[1])) : -Infinity;
243
- coordinates[1] = [...new Set(coordinates[1].map(y => Math.max(y, floor)))];
244
- const targets = coordinates[0].flatMap(x => coordinates[1].flatMap(y => coordinates[2].map(z => [x, y, z])));
245
- targets.sort((a, b) => Math.hypot(...sub(a, end)) - Math.hypot(...sub(b, end)));
246
- let best = null;
247
- for (const target of targets) for (let i = 0; i < 16; i++) {
248
- const solved = limbPose(locals, points, globals, offsets, chain, target, i * Math.PI / 8);
249
- const positions = forwardKinematics(solved, offsets, points[0]);
250
- if (limbBlocked(positions, chain, boxes)) continue;
251
- const cost = chain.slice(1).reduce((s, j) => s + dot(sub(positions[j], points[j]), sub(positions[j], points[j])), 0);
252
- if (!best || cost < best.cost) best = { locals: solved, positions, cost };
253
- }
254
- return best;
255
- }
256
-
257
- /** cameraOrigin opts into ray + limb correction instead of rigid stance
258
- * locks. Those locks (including floor lifts) would violate the root's camera
259
- * ray and reintroduce the glide this path is meant to remove. s(f) is the
260
- * slope-limited envelope of each frame's minimal torso-clear offset: 0 where
261
- * no correction is needed (so known A/B ends stay put when collisions are
262
- * elsewhere), one sign for the clip (either may be chosen; the smaller peak
263
- * wins). A single clip-level s contradicted those ends. Bound BOTH scalar
264
- * motion and vector correction as rays turn. Residual limb penetration is
265
- * fixed by two-bone IK; an impossible torso placement or limb solve fails
266
- * explicitly. */
267
- function fitRayContacts(motion, boxes, cameraOrigin, rayMaxOffsetM, rayMaxStepM) {
268
- if (!Array.isArray(cameraOrigin) || cameraOrigin.length !== 3 || !cameraOrigin.every(Number.isFinite)) throw new Error("cameraOrigin must be a finite world [x,y,z]");
269
- if (!(Number.isFinite(rayMaxOffsetM) && rayMaxOffsetM >= 0 && Number.isFinite(rayMaxStepM) && rayMaxStepM > 0)) throw new Error("rayMaxOffsetM must be nonnegative and rayMaxStepM positive");
270
- const out = cloneMotion(motion), n = motion.frames;
271
- // G5 independently filters joints and rotations; its posed joints may
272
- // already disagree with FK. Solve against the actual playback skeleton.
273
- const offsets = motion.boneScale ? bodyOffsets(motion.boneScale) : deriveBoneOffsets(jointsAt(motion, 0), localsAt(motion, 0));
274
- const locals = Array.from({ length: n }, (_, f) => localsAt(motion, f));
275
- const points = locals.map((r, f) => forwardKinematics(r, offsets, vec(motion.rootPos, f * 3)));
276
- const rays = points.map(p => {
277
- const v = sub(p[0], cameraOrigin);
278
- if (Math.hypot(...v) < EPS) throw new Error("scene ray: root coincides with cameraOrigin");
279
- return unit(v);
280
- });
281
- let inputFkResidualM = 0;
282
- for (let f = 0; f < n; f++) {
283
- const input = jointsAt(motion, f);
284
- for (let j = 0; j < 27; j++) inputFkResidualM = Math.max(inputFkResidualM, Math.hypot(...sub(points[f][j], input[j])));
285
- }
286
- // Do not translate through the camera, even if a large limit is supplied.
287
- const caps = points.map(p => Math.min(rayMaxOffsetM, Math.hypot(...sub(p[0], cameraOrigin)) - EPS));
288
- const turns = rays.map((r, f) => (f ? Math.hypot(...sub(r, rays[f - 1])) : 0));
289
- const intervals = points.map((p, f) => torsoRayIntervals(p, rays[f], boxes));
290
- const peak = x => Math.max(...x.map(Math.abs)), total = x => x.reduce((a, v) => a + Math.abs(v), 0);
291
- const options = [1, -1].map(sign => signedRayOffsets(intervals, sign, caps, turns, rayMaxStepM)).filter(Boolean);
292
- options.sort((a, b) => peak(a) - peak(b) || total(a) - total(b));
293
- if (!options.length) throw new Error(`scene ray: no torso-clear ray offset within ${rayMaxOffsetM.toFixed(3)} m and ${rayMaxStepM.toFixed(3)} m/frame; ray limits/scene conflict`);
294
- const s = options[0], limbFixFrames = [];
295
- let sceneOverrides = 0, limbFixes = 0, sceneMaxStepM = 0, maxRootStepM = 0, rayMaxStep = 0;
296
- for (let f = 0; f < n; f++) {
297
- const delta = scale(rays[f], s[f]);
298
- out.rootPos.set(add(points[f][0], delta), f * 3);
299
- let positions = points[f].map(p => add(p, delta)), rotations = locals[f], fixed = false;
300
- for (const chain of LIMBS) {
301
- if (!limbBlocked(positions, chain, boxes)) continue;
302
- const solved = clearLimb(rotations, positions, offsets, chain, boxes);
303
- if (!solved) throw new Error(`scene ray: no collision-free limb IK at frame ${f}, joint ${chain[0]}; root was not lifted`);
304
- rotations = solved.locals; positions = solved.positions; fixed = true; limbFixes++;
305
- }
306
- if (fixed) limbFixFrames.push(f);
307
- if (fixed || Math.abs(s[f]) > EPS) sceneOverrides++;
308
- for (let j = 0; j < 27; j++) out.rotMats.set(rotations[j].flat(), (f * 27 + j) * 9);
309
- // Recompute from the stored Float32 channels, not unquantized IK points.
310
- positions = forwardKinematics(localsAt(out, f), offsets, vec(out.rootPos, f * 3));
311
- if (penetrates(positions, boxes)) throw new Error(`scene ray: residual penetration after FK at frame ${f}`);
312
- for (let j = 0; j < 27; j++) out.posedJoints.set(positions[j], (f * 27 + j) * 3);
313
- if (f) {
314
- sceneMaxStepM = Math.max(sceneMaxStepM, Math.hypot(...sub(delta, scale(rays[f - 1], s[f - 1]))));
315
- rayMaxStep = Math.max(rayMaxStep, Math.abs(s[f] - s[f - 1]));
316
- maxRootStepM = Math.max(maxRootStepM, Math.hypot(...sub(vec(out.rootPos, f * 3), vec(out.rootPos, (f - 1) * 3))));
317
- }
318
- }
319
- return { motion: out, diagnostics: {
320
- runs: 0, lockedFrames: 0, support: Array(n).fill(-1), maxLockResidualM: 0,
321
- sceneOverrides, maxCorrectionM: peak(s), sceneMaxStepM, maxRootStepM,
322
- rayOffset: { minM: Math.min(...s), maxM: Math.max(...s), meanM: s.reduce((a, v) => a + v, 0) / n, maxAbsM: peak(s), maxStepM: rayMaxStep, framesNonZero: s.filter(v => Math.abs(v) > EPS).length },
323
- rayMaxOffsetM, rayMaxStepM, limbFixFrames, limbFixes, inputFkResidualM,
324
- geometry: "cskel27 joint/bone segments; no skin radius", sceneSolver: "camera-ray",
325
- solver: "slope-limited per-frame torso-clear camera-ray offset + two-bone limb IK; no rigid contact locks",
326
- } };
327
- }
328
-
329
- function contactTarget(point, joint, boxes, height) {
330
- if (FEET.includes(joint)) {
331
- let y = 0;
332
- for (const box of boxes) if (point[0] >= box.min[0] && point[0] <= box.max[0] && point[2] >= box.min[2] && point[2] <= box.max[2] && Math.abs(point[1] - box.max[1]) <= height) y = Math.max(y, box.max[1]);
333
- return Math.abs(point[1] - y) <= height ? [point[0], y + EPS, point[2]] : null;
334
- }
335
- // A hand may support on any known box face; never invent a hand contact
336
- // from a low percentile of its own trajectory when there is no surface.
337
- for (const box of boxes) {
338
- const clamped = point.map((x, k) => Math.max(box.min[k], Math.min(box.max[k], x)));
339
- const target = add(clamped, boxPenetration(clamped, box));
340
- if (Math.hypot(...sub(point, target)) <= height) return target;
341
- }
342
- return null;
343
- }
344
-
345
- /** F5 uses F4 motion + known floor/boxes ONLY. Low, slow runs nominate one
346
- * support point (feet first, then a box-contact hand); rigid root offsets
347
- * lock it without changing pose or bone lengths. Other simultaneous contacts
348
- * are not an IK solve. Scene feasibility overrides pinning/locks; diagnostics
349
- * expose every such override instead of claiming incompatible constraints.
350
- */
351
- export function fitContacts(motion, { boxes = [], contactHeight = 0.04, maxSpeed = 0.18, minStanceSeconds = 0.08, sceneSmoothSeconds = null, cameraOrigin = null, rayMaxOffsetM = 0.75, rayMaxStepM = 0.02 } = {}) {
352
- validateBoxes(boxes);
353
- if (![contactHeight, maxSpeed, minStanceSeconds].every(x => Number.isFinite(x) && x > 0)) throw new Error("contact thresholds must be positive");
354
- if (cameraOrigin !== null) return fitRayContacts(motion, boxes, cameraOrigin, rayMaxOffsetM, rayMaxStepM);
355
- const out = cloneMotion(motion), n = motion.frames, sites = [...FEET, ...HANDS];
356
- const minimum = Math.max(2, Math.ceil(minStanceSeconds * motion.fps));
357
- const targets = new Map(), candidates = new Map();
358
- for (const j of sites) {
359
- const track = Array.from({ length: n }, (_, f) => vec(motion.posedJoints, (f * 27 + j) * 3));
360
- const target = track.map(p => contactTarget(p, j, boxes, contactHeight));
361
- const eligible = target.map((p, f) => {
362
- const lo = Math.max(0, f - 1), hi = Math.min(n - 1, f + 1);
363
- const speed = Math.max(...[lo, hi].map(t => Math.hypot(...sub(track[f], track[t])) * motion.fps));
364
- return !!p && speed <= maxSpeed;
365
- });
366
- const good = new Uint8Array(n);
367
- for (let f = 0; f < n;) {
368
- if (!eligible[f]) { f++; continue; }
369
- const start = f; while (f < n && eligible[f]) f++;
370
- if (f - start >= minimum) good.fill(1, start, f);
371
- }
372
- targets.set(j, target); candidates.set(j, good);
373
- }
374
- const support = new Int8Array(n).fill(-1);
375
- for (let f = 0; f < n; f++) {
376
- const previous = f ? support[f - 1] : -1;
377
- support[f] = previous >= 0 && candidates.get(previous)[f] ? previous : (sites.find(j => candidates.get(j)[f]) ?? -1);
378
- }
379
- const offsets = new Array(n).fill(null), anchors = new Array(n).fill(null);
380
- let carry = [0, 0, 0], runs = 0, lockedFrames = 0;
381
- for (let f = 0; f < n;) {
382
- const j = support[f];
383
- if (j < 0) { f++; continue; }
384
- const first = f, target = targets.get(j)[f];
385
- // Keep accumulated horizontal travel across stance changes, but the
386
- // surface owns contact height. A hand's authored face owns all axes.
387
- const anchor = FEET.includes(j) ? [target[0] + carry[0], target[1], target[2] + carry[2]] : target;
388
- while (f < n && support[f] === j) {
389
- offsets[f] = sub(anchor, vec(motion.posedJoints, (f * 27 + j) * 3)); anchors[f] = anchor; f++;
390
- }
391
- carry = offsets[f - 1]; runs++; lockedFrames += f - first;
392
- }
393
- // Smooth offset bridges through flight/swing; no reset snap on release.
394
- for (let f = 0; f < n;) {
395
- if (offsets[f]) { f++; continue; }
396
- const start = f; while (f < n && !offsets[f]) f++;
397
- const a = start ? offsets[start - 1] : [0, 0, 0], b = f < n ? offsets[f] : a;
398
- for (let t = start; t < f; t++) { const w = smoothstep((t - start + 1) / (f - start + 1)); offsets[t] = a.map((v, k) => v + (b[k] - v) * w); }
399
- }
400
- let sceneOverrides = 0, maxLockResidualM = 0, maxCorrectionM = 0, sceneMaxStepM = 0;
401
- // sceneSmoothSeconds selects the time-smooth scene solver (its ramp half
402
- // width); null keeps the original per-frame projection (F5 and earlier).
403
- let sceneDeltas = null;
404
- if (sceneSmoothSeconds !== null) {
405
- if (!(Number.isFinite(sceneSmoothSeconds) && sceneSmoothSeconds > 0)) throw new Error("sceneSmoothSeconds must be a positive duration");
406
- for (let f = 0; f < n; f++) shiftFrame(out, f, offsets[f]);
407
- sceneDeltas = continuousSceneDeltas(out, boxes, Math.max(1, Math.round(sceneSmoothSeconds * motion.fps)));
408
- }
409
- for (let f = 0; f < n; f++) {
410
- if (!sceneDeltas) shiftFrame(out, f, offsets[f]);
411
- const { delta } = sceneDeltas ? { delta: sceneDeltas[f] } : resolveScene(jointsAt(out, f), boxes);
412
- if (f && sceneDeltas) sceneMaxStepM = Math.max(sceneMaxStepM, Math.hypot(...sub(sceneDeltas[f], sceneDeltas[f - 1])));
413
- if (Math.hypot(...delta) > 1e-6) sceneOverrides++;
414
- shiftFrame(out, f, delta);
415
- maxCorrectionM = Math.max(maxCorrectionM, Math.hypot(...add(offsets[f], delta)));
416
- if (support[f] >= 0) maxLockResidualM = Math.max(maxLockResidualM, Math.hypot(...sub(vec(out.posedJoints, (f * 27 + support[f]) * 3), anchors[f])));
417
- }
418
- return { motion: out, diagnostics: { runs, lockedFrames, sceneOverrides, maxLockResidualM, maxCorrectionM, support: Array.from(support), geometry: "cskel27 joint/bone segments; no skin radius", solver: "one rigid support; scene constraints override locks and A/B pins", sceneSolver: sceneDeltas ? "continuous" : "per-frame", ...(sceneDeltas ? { sceneSmoothSeconds, sceneMaxStepM } : {}) } };
419
- }
@@ -1,234 +0,0 @@
1
- /** Foot lock: keep a planted foot fixed in the world during stance by solving
2
- * analytic two-bone IK on the leg (UpLeg-Leg-Foot). Root-only corrections
3
- * (depth, scene shifts) translate the body while local rotations stay put, so
4
- * the planted ankle slides with the pelvis; this pass moves only the leg's
5
- * local rotations back under the anchor. Root, spine and arms are untouched;
6
- * the foot keeps its input world orientation. */
7
- import { forwardKinematics, globalRotations, matMul, matTranspose } from "../../../src/ardy/convert.js";
8
- import { bodyOffsets, cloneMotion, localsAt, smoothstep, vec } from "./motion.mjs";
9
-
10
- const LEGS = [
11
- { foot: "left", hip: 23, knee: 24, ankle: 25, toe: 26 },
12
- { foot: "right", hip: 19, knee: 20, ankle: 21, toe: 22 },
13
- ];
14
- const HIPS = 0;
15
-
16
- const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
17
- const cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
18
- const sub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
19
- const scale = (a, s) => [a[0] * s, a[1] * s, a[2] * s];
20
- const norm = (a) => Math.hypot(a[0], a[1], a[2]);
21
- const unit = (a) => scale(a, 1 / norm(a));
22
- const perp = (a, axis) => sub(a, scale(axis, dot(a, axis)));
23
- const mulVec = (m, v) => m.map((row) => dot(row, v));
24
- const columns = (x, y, z) => [[x[0], y[0], z[0]], [x[1], y[1], z[1]], [x[2], y[2], z[2]]];
25
- const angleDeg = (a, b) => {
26
- const r = matMul(matTranspose(a), b);
27
- return (Math.acos(Math.max(-1, Math.min(1, (r[0][0] + r[1][1] + r[2][2] - 1) / 2))) * 180) / Math.PI;
28
- };
29
- const median = (values) => {
30
- const s = [...values].sort((a, b) => a - b), m = (s.length - 1) / 2;
31
- return (s[Math.floor(m)] + s[Math.ceil(m)]) / 2;
32
- };
33
-
34
- /** Minimal rotation taking unit vector a onto unit vector b (Rodrigues). */
35
- function rotationBetween(a, b) {
36
- const axis = cross(a, b), s = norm(axis), c = dot(a, b);
37
- if (s < 1e-12) return [[1, 0, 0], [0, 1, 0], [0, 0, 1]];
38
- const [x, y, z] = scale(axis, 1 / s), t = 1 - c;
39
- return [
40
- [t * x * x + c, t * x * y - s * z, t * x * z + s * y],
41
- [t * x * y + s * z, t * y * y + c, t * y * z - s * x],
42
- [t * x * z - s * y, t * y * z + s * x, t * z * z + c],
43
- ];
44
- }
45
-
46
- /** Knee pole: the knee's offset from the hip-ankle line; for a straight leg the
47
- * toe direction (knees bend over the toes); last resort any perpendicular. */
48
- function poleDirection(H, K, A, toe, axis) {
49
- for (const candidate of [perp(sub(K, H), axis), perp(sub(toe, A), axis), perp([0, 0, 1], axis), perp([1, 0, 0], axis)]) {
50
- if (norm(candidate) > 1e-3) return unit(candidate);
51
- }
52
- throw new Error("footlock: no knee pole direction");
53
- }
54
-
55
- /** Soft IK reach: near full extension the knee angle is hypersensitive to the
56
- * ankle distance (a straightening knee sweeps centimetres per millimetre), so
57
- * the reach saturates exponentially inside the last `soft` metres instead of
58
- * snapping straight. Applied relative to the input distance d0 so a zero
59
- * request reproduces the input pose exactly. */
60
- function softReach(d0, distance, length, soft) {
61
- if (!(soft > 0)) return Math.min(distance, length);
62
- const knee = length - soft, cap = length - 1e-9 * length;
63
- const squash = (x) => (x <= knee ? x : knee + soft * (1 - Math.exp(-(x - knee) / soft)));
64
- const unsquash = (y) => (y <= knee ? y : knee - soft * Math.log(1 - (Math.min(y, cap) - knee) / soft));
65
- return squash(unsquash(d0) + distance - d0);
66
- }
67
-
68
- /** Two-bone IK on one leg in place: updates locals of hip/knee/ankle so the
69
- * ankle reaches T (clamped to the reachable shell), the knee stays in the
70
- * plane through hip, target and the original pole, and the foot keeps its
71
- * world orientation. */
72
- function solveLeg(locals, G, P, leg, T, softness) {
73
- const H = P[leg.hip], K = P[leg.knee], A = P[leg.ankle];
74
- const a = norm(sub(K, H)), b = norm(sub(A, K));
75
- const axis0 = unit(sub(A, H)), pole = poleDirection(H, K, A, P[leg.toe], axis0);
76
- const toTarget = sub(T, H), distance = norm(toTarget);
77
- const axis1 = distance > 1e-9 ? scale(toTarget, 1 / distance) : axis0;
78
- const reach = Math.max(softReach(norm(sub(A, H)), distance, a + b, softness * (a + b)), Math.abs(a - b) + 1e-6);
79
- const cosA = Math.max(-1, Math.min(1, (a * a + reach * reach - b * b) / (2 * a * reach)));
80
- const sinA = Math.sqrt(1 - cosA * cosA);
81
- const pole1 = norm(perp(pole, axis1)) > 1e-6 ? unit(perp(pole, axis1)) : poleDirection(H, K, A, P[leg.toe], axis1);
82
- const K1 = [0, 1, 2].map((k) => H[k] + axis1[k] * a * cosA + pole1[k] * a * sinA);
83
- const A1 = [0, 1, 2].map((k) => H[k] + axis1[k] * reach);
84
- // Thigh: align the (thigh, bend-normal) frame; knee: hinge about the normal.
85
- const u0 = unit(sub(K, H)), n0 = unit(perp(cross(axis0, pole), u0));
86
- const u1 = unit(sub(K1, H)), n1 = unit(perp(cross(axis1, pole1), u1));
87
- const thigh = matMul(columns(u1, n1, cross(u1, n1)), matTranspose(columns(u0, n0, cross(u0, n0))));
88
- const knee = rotationBetween(unit(mulVec(thigh, sub(A, K))), unit(sub(A1, K1)));
89
- const gUp = matMul(thigh, G[leg.hip]), gLeg = matMul(knee, matMul(thigh, G[leg.knee]));
90
- locals[leg.hip] = matMul(matTranspose(G[HIPS]), gUp);
91
- locals[leg.knee] = matMul(matTranspose(gUp), gLeg);
92
- locals[leg.ankle] = matMul(matTranspose(gLeg), G[leg.ankle]);
93
- }
94
-
95
- /** Highest support (floor or box top under the foot) the foot may stand on. */
96
- function supportUnder(points, lowY, floorY, boxes, maxHeight) {
97
- let best = floorY;
98
- for (const box of boxes) {
99
- const inside = points.some((p) => p[0] >= box.min[0] && p[0] <= box.max[0] && p[2] >= box.min[2] && p[2] <= box.max[2]);
100
- if (inside && box.max[1] > best && lowY >= box.max[1] - maxHeight) best = box.max[1];
101
- }
102
- return best;
103
- }
104
-
105
- /** Per frame: { goal } (absolute ankle target) inside a stance, { offset } in
106
- * the ramps around it, null where the leg is left alone. The ramps fade the
107
- * stance-edge correction (not a pull toward the anchor), so the swing keeps its
108
- * shape; each ramp lasts at least blendFrames and long enough that the added
109
- * per-frame displacement stays near maxBlendStepM. Two stances whose ramps meet
110
- * share the gap: the correction interpolates from one edge to the next. */
111
- function planCorrections(motion, leg, stances, blendFrames, maxBlendStepM) {
112
- const T = motion.frames, plan = new Array(T).fill(null);
113
- const ankle = (f) => vec(motion.posedJoints, (f * 27 + leg.ankle) * 3);
114
- const toeY = (f) => motion.posedJoints[(f * 27 + leg.toe) * 3 + 1];
115
- // A ramp carries a stance-edge correction onto frames whose foot pitch
116
- // differs; never let it push the foot's lowest point under the support
117
- // (or deeper than the input already was). The foot keeps its world
118
- // orientation, so the toe moves with the ankle.
119
- const fade = (f, offset, level) => {
120
- const lowest = Math.min(ankle(f)[1], toeY(f));
121
- return { offset: [offset[0], Math.max(offset[1], -Math.max(0, lowest - level)), offset[2]] };
122
- };
123
- const goal = (stance, f) => [stance.anchor[0], stance.height(f), stance.anchor[2]];
124
- // smoothstep's peak slope is 1.5 / (n + 1) per frame over an n-frame ramp.
125
- const ramp = (d) => Math.max(blendFrames, Math.ceil((1.5 * norm(d)) / maxBlendStepM) - 1);
126
- const edges = stances.map((s) => {
127
- const first = sub(goal(s, s.start), ankle(s.start)), last = sub(goal(s, s.end), ankle(s.end));
128
- return { first, last, before: ramp(first), after: ramp(last) };
129
- });
130
- stances.forEach((s, i) => {
131
- for (let f = s.start; f <= s.end; f++) plan[f] = { goal: goal(s, f) };
132
- const next = stances[i + 1], gap = next ? next.start - s.end : Infinity;
133
- if (next && gap - 1 <= edges[i].after + edges[i + 1].before) {
134
- for (let f = s.end + 1; f < next.start; f++) {
135
- const t = smoothstep((f - s.end) / gap);
136
- plan[f] = fade(f, edges[i].last.map((v, k) => v + (edges[i + 1].first[k] - v) * t), Math.max(s.level, next.level));
137
- }
138
- } else {
139
- const n = edges[i].after;
140
- for (let f = s.end + 1; f <= Math.min(T - 1, s.end + n); f++) plan[f] = fade(f, scale(edges[i].last, smoothstep(1 - (f - s.end) / (n + 1))), s.level);
141
- if (next) {
142
- const m = edges[i + 1].before;
143
- for (let f = next.start - m; f < next.start; f++) plan[f] = fade(f, scale(edges[i + 1].first, smoothstep(1 - (next.start - f) / (m + 1))), next.level);
144
- }
145
- }
146
- if (i === 0) {
147
- const m = edges[0].before;
148
- for (let f = Math.max(0, s.start - m); f < s.start; f++) plan[f] = fade(f, scale(edges[0].first, smoothstep(1 - (s.start - f) / (m + 1))), s.level);
149
- }
150
- });
151
- return plan;
152
- }
153
-
154
- function detectStances(motion, leg, { floorY, boxes, maxHeight, maxSpeed, minStanceFrames, minSwingFrames }) {
155
- const T = motion.frames, at = (f, j) => vec(motion.posedJoints, (f * 27 + j) * 3);
156
- const ankle = Array.from({ length: T }, (_, f) => at(f, leg.ankle));
157
- const toe = Array.from({ length: T }, (_, f) => at(f, leg.toe));
158
- // The slower one-sided step: a stance's first/last frame has one slow side.
159
- const step = (f, g) => Math.hypot(ankle[g][0] - ankle[f][0], ankle[g][2] - ankle[f][2]) * motion.fps;
160
- const speed = ankle.map((_, f) => Math.min(f > 0 ? step(f - 1, f) : Infinity, f + 1 < T ? step(f, f + 1) : Infinity, T === 1 ? 0 : Infinity));
161
- const support = ankle.map((p, f) => {
162
- const lowY = Math.min(p[1], toe[f][1]), s = supportUnder([p, toe[f]], lowY, floorY, boxes, maxHeight);
163
- return lowY - s <= maxHeight && speed[f] <= maxSpeed ? s : null;
164
- });
165
- const runs = [];
166
- for (let f = 0; f < T;) {
167
- if (support[f] === null) { f += 1; continue; }
168
- let end = f;
169
- while (end + 1 < T && support[end + 1] !== null && Math.abs(support[end + 1] - support[f]) < 0.01) end += 1;
170
- const last = runs.at(-1);
171
- // A foot cannot lift and replant in fewer than minSwingFrames: one fast
172
- // frame inside a sliding stance is not a step.
173
- if (last && f - last.end - 1 < minSwingFrames && Math.abs(last.level - support[f]) < 0.01) last.end = end;
174
- else runs.push({ start: f, end, level: support[f] });
175
- f = end + 1;
176
- }
177
- const stances = [];
178
- for (const { start, end, level } of runs) {
179
- if (end - start + 1 >= minStanceFrames) {
180
- const frames = Array.from({ length: end - start + 1 }, (_, i) => start + i);
181
- const mean = (k) => frames.reduce((sum, i) => sum + ankle[i][k], 0) / frames.length;
182
- // Flat-foot ankle height above the toe; heel-off raises the ankle above it.
183
- const flat = median(frames.map((i) => ankle[i][1] - toe[i][1]));
184
- const height = (i) => level + Math.max(flat, ankle[i][1] - toe[i][1]);
185
- stances.push({ foot: leg.foot, start, end, anchor: [mean(0), level + flat, mean(2)], level, height });
186
- }
187
- }
188
- return stances;
189
- }
190
-
191
- /**
192
- * Stance = ankle/toe within maxHeight of the floor or a box top AND ankle
193
- * horizontal speed <= maxSpeed for >= minStanceFrames. maxSpeed defaults to
194
- * 0.4 m/s, not 0.25: the fitted takes this pass repairs slide 25-30 cm/s on
195
- * truly planted frames, so 0.25 misses about half of each stance (measured on
196
- * the 11 gt+cube items: mean skate 9.7 cm/s at 0.25 vs 8.2 at 0.4).
197
- * @param {object} motion cskel27 motion ({ frames, fps, rotMats, rootPos, posedJoints, boneScale })
198
- * @returns {{ motion, diagnostics: { stances, maxResidualM, maxAdjustDeg } }}
199
- */
200
- export function lockFeet(motion, { floorY = 0, boxes = [], maxHeight = 0.06, maxSpeed = 0.4, minStanceFrames = 3, blendFrames = 3,
201
- minSwingFrames = 4, maxBlendStepM = 0.01, softness = 0.03, offsets = bodyOffsets(motion.boneScale) } = {}) {
202
- if (!(blendFrames >= 0) || !(minStanceFrames >= 1) || !(minSwingFrames >= 0) || !(maxBlendStepM > 0) || !(softness >= 0)) {
203
- throw new Error("lockFeet: blendFrames >= 0, minStanceFrames >= 1, minSwingFrames >= 0, maxBlendStepM > 0, softness >= 0 required");
204
- }
205
- const out = cloneMotion(motion), T = motion.frames;
206
- const detect = { floorY, boxes, maxHeight, maxSpeed, minStanceFrames, minSwingFrames };
207
- const plans = LEGS.map((leg) => {
208
- const stances = detectStances(motion, leg, detect);
209
- return { leg, stances, plan: planCorrections(motion, leg, stances, blendFrames, maxBlendStepM) };
210
- });
211
- let maxResidualM = 0, maxAdjustDeg = 0;
212
- for (let f = 0; f < T; f++) {
213
- const active = plans.filter(({ plan }) => plan[f] !== null);
214
- if (!active.length) continue;
215
- const original = localsAt(motion, f), locals = original.map((m) => m.map((row) => row.slice()));
216
- const root = vec(motion.rootPos, f * 3), G = globalRotations(locals), P = forwardKinematics(locals, offsets, root);
217
- const targets = active.map(({ leg, plan }) => {
218
- const target = plan[f].goal ?? P[leg.ankle].map((v, k) => v + plan[f].offset[k]);
219
- solveLeg(locals, G, P, leg, target, softness);
220
- return { leg, target };
221
- });
222
- const solved = forwardKinematics(locals, offsets, root);
223
- for (const { leg, target } of targets) {
224
- maxResidualM = Math.max(maxResidualM, norm(sub(solved[leg.ankle], target)));
225
- for (const j of [leg.hip, leg.knee, leg.ankle]) {
226
- maxAdjustDeg = Math.max(maxAdjustDeg, angleDeg(original[j], locals[j]));
227
- out.rotMats.set(locals[j].flat(), (f * 27 + j) * 9);
228
- }
229
- }
230
- for (let j = 0; j < 27; j++) out.posedJoints.set(solved[j], (f * 27 + j) * 3);
231
- }
232
- const stances = plans.flatMap(({ stances }) => stances.map(({ foot, start, end, anchor }) => ({ foot, start, end, anchor })));
233
- return { motion: out, diagnostics: { stances, maxResidualM, maxAdjustDeg } };
234
- }