cozyclay 2.1.1 → 3.0.1

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 (585) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +12 -30
  3. package/bin/agent/agent-routes.mjs +43 -116
  4. package/bin/agent/agent-runner.mjs +14 -15
  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/session-store.mjs +2 -2
  14. package/bin/agent/studio-context-budget.mjs +93 -0
  15. package/bin/agent/studio-motion-reference.mjs +23 -0
  16. package/bin/agent/studio-prompt.mjs +1 -1
  17. package/bin/agent/studio-rebase-text.mjs +32 -0
  18. package/bin/agent/studio-rebase.mjs +26 -39
  19. package/bin/agent/studio-tools.mjs +9 -28
  20. package/bin/agent/tool-arguments.mjs +49 -0
  21. package/bin/agent/turn-frames.mjs +66 -0
  22. package/bin/codex-auth.mjs +9 -2
  23. package/bin/cozyclay.mjs +10 -116
  24. package/bin/live/cli.mjs +3 -3
  25. package/bin/mcp-runtime.mjs +2 -12
  26. package/bin/package-signature.mjs +0 -4
  27. package/dist/app/index.html +7 -6
  28. package/dist/assets/EXRLoader-CvVIvGVE.js +1 -0
  29. package/dist/assets/analytics-BWCqdgWA.js +1 -0
  30. package/dist/assets/app-DfTCxaKk.js +5087 -0
  31. package/dist/assets/app-f-fkV5uP.css +1 -0
  32. package/dist/assets/compile-worker-BxKXh07s.js +5 -0
  33. package/dist/assets/fflate.module-KbGj5-H6.js +5 -0
  34. package/dist/assets/{first-shot-handoff-msLI71g1.js → first-shot-handoff-DNO8kv_N.js} +1 -1
  35. package/dist/assets/{landing-ChUvB_zL.js → landing-yfZaJkWc.js} +1 -1
  36. package/dist/assets/scene-asset-sync-U3kS2rvK.js +20 -0
  37. package/dist/assets/scene-asset-sync-_S8NQl5B.css +1 -0
  38. package/dist/assets/workflow-D78eiKTa.js +179 -0
  39. package/dist/cozyclay-package.json +1 -1
  40. package/dist/index.html +3 -3
  41. package/dist/sw.js +3 -6
  42. package/dist/workflow/index.html +5 -4
  43. package/mcp/LIVE-PROTOCOL.md +7 -47
  44. package/mcp/README.md +16 -26
  45. package/mcp/live-hub.mjs +6 -10
  46. package/mcp/package.json +1 -3
  47. package/mcp/tool-handlers.mjs +4 -168
  48. package/package.json +3 -22
  49. package/src/App.jsx +151 -2625
  50. package/src/agent-progress.js +80 -0
  51. package/src/agent-turn-mark.js +30 -0
  52. package/src/analytics.js +1 -19
  53. package/src/app-context.js +12 -2
  54. package/src/app-stage.jsx +2 -34
  55. package/src/asset-pane.jsx +6 -5
  56. package/src/camera-block.js +21 -0
  57. package/src/camera-direction.js +120 -0
  58. package/src/character-kind.js +3 -9
  59. package/src/command-bus.js +10 -8
  60. package/src/commands/cast.js +5 -34
  61. package/src/commands/elements/character.js +1 -1
  62. package/src/commands/elements/motion.js +1 -6
  63. package/src/commands/elements/object.js +6 -2
  64. package/src/commands/elements/shot.js +4 -1
  65. package/src/commands/elements.js +38 -14
  66. package/src/commands/index.js +3 -1
  67. package/src/commands/kits.js +37 -0
  68. package/src/commands/motion-timing.js +57 -0
  69. package/src/commands/motion.js +9 -152
  70. package/src/commands/motion3-compile.js +65 -0
  71. package/src/commands/motion3.js +214 -0
  72. package/src/commands/objects.js +6 -4
  73. package/src/commands/shot.js +55 -13
  74. package/src/commands/stage.js +14 -1
  75. package/src/commands/view.js +4 -1
  76. package/src/document-mirrors.js +42 -0
  77. package/src/document-store.js +4 -0
  78. package/src/domains/cast.js +19 -194
  79. package/src/domains/motion/motion-range-pin-rules.js +1 -1
  80. package/src/domains/motion/take-source.js +53 -0
  81. package/src/domains/motion.js +60 -1838
  82. package/src/domains/scenes.js +12 -55
  83. package/src/domains/shots.js +2 -2
  84. package/src/domains/stage.js +4 -2
  85. package/src/draft-ghost.js +47 -0
  86. package/src/draft-ghost.jsx +129 -0
  87. package/src/dualview.jsx +4 -3
  88. package/src/effects/EffectsLayer.jsx +112 -0
  89. package/src/effects/effect-meshes.js +232 -0
  90. package/src/effects/effect-record.js +103 -0
  91. package/src/effects/emitter.js +59 -0
  92. package/src/effects/simulate.js +261 -0
  93. package/src/effects/state.js +59 -0
  94. package/src/execution-telemetry.js +2 -3
  95. package/src/first-success-guide.jsx +5 -5
  96. package/src/ghost-set.js +257 -0
  97. package/src/held-object-edit.jsx +113 -0
  98. package/src/held-object-pose.js +175 -0
  99. package/src/hierarchy-model.js +1 -1
  100. package/src/hierarchy-panel.jsx +1 -1
  101. package/src/imported-mesh.jsx +103 -0
  102. package/src/kits/held.js +84 -0
  103. package/src/kits/index.js +70 -0
  104. package/src/kits/smoking.js +57 -0
  105. package/src/light-blockers.js +93 -0
  106. package/src/motion/beat-board-model.js +84 -0
  107. package/src/motion/beat-board.css +124 -0
  108. package/src/motion/beat-board.jsx +60 -0
  109. package/src/motion/center-of-mass.js +66 -0
  110. package/src/motion/fix-collisions.js +850 -0
  111. package/src/motion/physics-review.js +118 -0
  112. package/src/{ardy → motion}/physics-support.js +1 -76
  113. package/src/motion/rig-snapshot.js +22 -0
  114. package/src/motion/take-segments.jsx +140 -0
  115. package/src/{ardy → motion}/timeline-coordinates.js +0 -21
  116. package/src/{ardy → motion}/timeline.css +1 -2
  117. package/src/{ardy → motion}/timeline.jsx +42 -413
  118. package/src/motion/waypoints.js +76 -0
  119. package/src/motion3/beats.js +83 -0
  120. package/src/motion3/body.js +158 -0
  121. package/src/motion3/bones.js +47 -0
  122. package/src/motion3/carried.js +47 -0
  123. package/src/motion3/compile-job.js +79 -0
  124. package/src/motion3/compile-worker.js +10 -0
  125. package/src/motion3/compiler/anchors.js +113 -0
  126. package/src/motion3/compiler/arm-orient.js +128 -0
  127. package/src/motion3/compiler/channels.js +125 -0
  128. package/src/motion3/compiler/compile.js +205 -0
  129. package/src/motion3/compiler/gait-rhythm.js +97 -0
  130. package/src/motion3/compiler/grip.js +171 -0
  131. package/src/motion3/compiler/head-steady.js +48 -0
  132. package/src/motion3/compiler/hold.js +134 -0
  133. package/src/motion3/compiler/idle.js +113 -0
  134. package/src/motion3/compiler/limbs.js +197 -0
  135. package/src/motion3/compiler/math.js +72 -0
  136. package/src/motion3/compiler/pose.js +177 -0
  137. package/src/motion3/compiler/quat.js +49 -0
  138. package/src/motion3/compiler/skeleton.js +74 -0
  139. package/src/motion3/compiler/validate-common.js +32 -0
  140. package/src/motion3/compiler/validate-goals.js +118 -0
  141. package/src/motion3/compiler/validate-objects.js +144 -0
  142. package/src/motion3/compiler/validate-parts.js +118 -0
  143. package/src/motion3/compiler/validate.js +122 -0
  144. package/src/motion3/compiler/vocab.js +74 -0
  145. package/src/motion3/compiler/walk-pelvis.js +275 -0
  146. package/src/motion3/compiler/walk.js +207 -0
  147. package/src/motion3/compiler/wrist-clamp.js +32 -0
  148. package/src/motion3/delta.js +47 -0
  149. package/src/motion3/describe.js +177 -0
  150. package/src/motion3/docs.js +27 -0
  151. package/src/motion3/edit-carry.js +21 -0
  152. package/src/motion3/fix/ease.js +16 -0
  153. package/src/motion3/fix/ground.js +37 -0
  154. package/src/motion3/fix/index.js +52 -0
  155. package/src/motion3/fix/push.js +71 -0
  156. package/src/motion3/fix/reach.js +46 -0
  157. package/src/motion3/fix/retime.js +79 -0
  158. package/src/motion3/fix/self.js +211 -0
  159. package/src/motion3/fix/slide.js +70 -0
  160. package/src/motion3/fix/turn.js +51 -0
  161. package/src/motion3/follow.js +39 -0
  162. package/src/motion3/free-motion.js +204 -0
  163. package/src/motion3/grip-choice.js +130 -0
  164. package/src/motion3/hold-plan.js +157 -0
  165. package/src/motion3/json-patch.js +77 -0
  166. package/src/motion3/key-aim.js +105 -0
  167. package/src/motion3/key-edit.js +82 -0
  168. package/src/motion3/lenient.js +127 -0
  169. package/src/motion3/object-keys.js +236 -0
  170. package/src/motion3/object-pose.js +182 -0
  171. package/src/motion3/object-tracks.js +12 -0
  172. package/src/motion3/people.js +82 -0
  173. package/src/motion3/pipeline.js +102 -0
  174. package/src/motion3/playback.js +51 -0
  175. package/src/motion3/probes.js +91 -0
  176. package/src/motion3/qa/body-checks.js +34 -0
  177. package/src/motion3/qa/check-names.js +14 -0
  178. package/src/motion3/qa/checks.js +125 -0
  179. package/src/motion3/qa/contact-checks.js +51 -0
  180. package/src/motion3/qa/explain.js +80 -0
  181. package/src/motion3/qa/fmt.js +5 -0
  182. package/src/motion3/qa/foot-checks.js +72 -0
  183. package/src/motion3/qa/geometry.js +77 -0
  184. package/src/motion3/qa/goals.js +191 -0
  185. package/src/motion3/qa/ground.js +60 -0
  186. package/src/motion3/qa/hints-contact.js +195 -0
  187. package/src/motion3/qa/hints-goal.js +160 -0
  188. package/src/motion3/qa/hints-knee.js +24 -0
  189. package/src/motion3/qa/hints-motion.js +209 -0
  190. package/src/motion3/qa/hints-object.js +91 -0
  191. package/src/motion3/qa/index.js +44 -0
  192. package/src/motion3/qa/key-checks.js +82 -0
  193. package/src/motion3/qa/near.js +96 -0
  194. package/src/motion3/qa/object-checks.js +135 -0
  195. package/src/motion3/qa/patch.js +161 -0
  196. package/src/motion3/qa/people-checks.js +37 -0
  197. package/src/motion3/qa/severity.js +38 -0
  198. package/src/motion3/qa/spec-checks.js +65 -0
  199. package/src/motion3/qa/style.js +228 -0
  200. package/src/motion3/qa/wrist-checks.js +69 -0
  201. package/src/motion3/qa/wrist-obs.js +60 -0
  202. package/src/motion3/refs.js +129 -0
  203. package/src/motion3/repair-tune.js +121 -0
  204. package/src/motion3/repair.js +83 -0
  205. package/src/motion3/report.js +67 -0
  206. package/src/motion3/rigs/index.js +14 -0
  207. package/src/motion3/rigs/x-bot-tpose-body.json +1 -0
  208. package/src/motion3/rigs/x-bot-tpose.json +1 -0
  209. package/src/motion3/rigs/y-bot-tpose-body.json +1 -0
  210. package/src/motion3/rigs/y-bot-tpose.json +1 -0
  211. package/src/motion3/scene.js +60 -0
  212. package/src/motion3/schema/brief.js +89 -0
  213. package/src/motion3/schema/fields.js +44 -0
  214. package/src/motion3/schema/spec-examples.js +46 -0
  215. package/src/motion3/schema/topics.js +113 -0
  216. package/src/motion3/schema/types.js +149 -0
  217. package/src/motion3/search.js +117 -0
  218. package/src/motion3/second-hand.js +112 -0
  219. package/src/motion3/settle.js +136 -0
  220. package/src/motion3/skills/basic.js +83 -0
  221. package/src/motion3/skills/book.js +76 -0
  222. package/src/motion3/skills/car.js +145 -0
  223. package/src/motion3/skills/flip.js +200 -0
  224. package/src/motion3/skills/frames-auto.js +29 -0
  225. package/src/motion3/skills/impact.js +109 -0
  226. package/src/motion3/skills/index.js +85 -0
  227. package/src/motion3/skills/info.js +80 -0
  228. package/src/motion3/skills/smoke-kit.js +109 -0
  229. package/src/motion3/skills/smoke.js +160 -0
  230. package/src/motion3/spec-store.js +59 -0
  231. package/src/motion3/take-edit.js +155 -0
  232. package/src/motion3/take.js +63 -0
  233. package/src/motion3/timeline.js +127 -0
  234. package/src/motion3/views.js +121 -0
  235. package/src/motion3/walk-plan.js +119 -0
  236. package/src/object-catalog.jsx +7 -1
  237. package/src/object-colors.js +98 -0
  238. package/src/object-gizmo-parts.js +115 -0
  239. package/src/object-gizmo.jsx +37 -122
  240. package/src/object-glow.js +139 -0
  241. package/src/object-hidden.js +33 -0
  242. package/src/object-look.js +196 -0
  243. package/src/object-size.js +10 -0
  244. package/src/object-travel.js +71 -6
  245. package/src/panels/CameraPanel.jsx +7 -7
  246. package/src/panels/HdrEnvironmentControls.jsx +59 -0
  247. package/src/panels/LightPanel.jsx +25 -4
  248. package/src/panels/ObjectLookPanel.jsx +88 -0
  249. package/src/panels/PosePanel.jsx +2 -17
  250. package/src/panels/RigControlPanel.jsx +5 -53
  251. package/src/panels/SubjectBox.jsx +4 -1
  252. package/src/panels/details.css +14 -0
  253. package/src/posestudio.jsx +4 -27
  254. package/src/project-browser.jsx +3 -4
  255. package/src/project-resources.js +18 -31
  256. package/src/project.js +13 -104
  257. package/src/props.jsx +25 -280
  258. package/src/qa-label.js +25 -0
  259. package/src/rail-dot-drag.js +23 -0
  260. package/src/render-passes.js +10 -0
  261. package/src/resource-status.jsx +1 -0
  262. package/src/result-modal.jsx +4 -19
  263. package/src/room.jsx +20 -12
  264. package/src/scene-assets.js +8 -6
  265. package/src/scene-light-place.js +72 -0
  266. package/src/scene-lights.jsx +148 -0
  267. package/src/scene-objects.js +31 -139
  268. package/src/scenes.js +15 -105
  269. package/src/semantic-edit.js +2 -5
  270. package/src/set-pieces.jsx +183 -0
  271. package/src/settings-menu.jsx +3 -11
  272. package/src/shell/BottomDock.jsx +9 -51
  273. package/src/shell/DetailsSlot.jsx +16 -200
  274. package/src/shell/GhostsMenuGroup.jsx +74 -0
  275. package/src/shell/LibrarySlot.jsx +1 -3
  276. package/src/shell/MenuBar.jsx +3 -3
  277. package/src/shell/PreferencesDialog.jsx +15 -264
  278. package/src/shell/PreferencesSlot.jsx +2 -8
  279. package/src/shell/TopBar.jsx +5 -84
  280. package/src/shell/ViewportToolbar.jsx +10 -70
  281. package/src/shell/agent-glass.css +19 -6
  282. package/src/shell/details-selection.js +42 -0
  283. package/src/shell/ghosts-menu.css +22 -0
  284. package/src/shell/glass-regions.css +29 -3
  285. package/src/shell/preferences.css +6 -1
  286. package/src/shot-authoring.js +4 -7
  287. package/src/soft-shadows.js +37 -0
  288. package/src/stage-attach-frames.js +12 -12
  289. package/src/stage-canvas-rigs.jsx +3 -3
  290. package/src/stage-capture.jsx +42 -8
  291. package/src/stage-characters.jsx +22 -216
  292. package/src/stage-editor-gizmos.jsx +6 -6
  293. package/src/stage-environment.js +93 -0
  294. package/src/stage-hdr-builtins.js +107 -0
  295. package/src/stage-hdr-image.js +116 -0
  296. package/src/stage-hdr-library.js +70 -0
  297. package/src/stage-hdr-map.js +94 -0
  298. package/src/stage-hdr.js +130 -0
  299. package/src/stage-lighting.js +149 -0
  300. package/src/stage-occlusion.js +210 -0
  301. package/src/stage-sky.jsx +159 -0
  302. package/src/stage-startup-defaults.js +5 -9
  303. package/src/studio-actions.js +16 -14
  304. package/src/studio-agent-commands.js +19 -16
  305. package/src/studio-agent-motion.js +9 -9
  306. package/src/studio-agent-protocol.js +47 -77
  307. package/src/studio-app-binding.js +27 -47
  308. package/src/studio-asset-list.js +17 -0
  309. package/src/studio-contact-sheet.js +37 -5
  310. package/src/studio-elements.js +27 -7
  311. package/src/studio-entity-bounds.js +18 -0
  312. package/src/studio-error-codes.js +15 -0
  313. package/src/studio-export-framing.js +152 -0
  314. package/src/studio-lighting-check.js +103 -0
  315. package/src/studio-motion-visual.js +150 -0
  316. package/src/studio-schema-errors.js +80 -0
  317. package/src/studio-support-gap.js +74 -0
  318. package/src/styles.css +1 -19
  319. package/src/timeline-defaults.js +5 -0
  320. package/src/timeline-extent.js +8 -20
  321. package/src/tutorial-walk.js +17 -0
  322. package/src/ui-scrub.js +1 -1
  323. package/src/use-shot-camera-tracks.js +12 -0
  324. package/src/view-ghosts.js +97 -0
  325. package/src/view-ghosts.jsx +49 -0
  326. package/src/workflow/AgentPanel.jsx +47 -166
  327. package/src/workflow/AgentProgressCard.jsx +109 -0
  328. package/src/workflow/AgentReplyText.jsx +37 -0
  329. package/src/workflow/AgentTurnSteps.jsx +84 -0
  330. package/src/workflow/LoginPanel.jsx +140 -0
  331. package/src/workflow/WorkflowBuilder.jsx +7 -23
  332. package/src/workflow/agent-client.js +54 -51
  333. package/src/workflow/agent-panel.css +71 -1
  334. package/src/workflow/agent-progress-card.css +167 -0
  335. package/src/workflow/agent-progress-view.js +47 -0
  336. package/src/workflow/agent-turn-steps.css +113 -0
  337. package/src/workflow/agent-turn.js +191 -0
  338. package/src/workflow/login-client.js +34 -0
  339. package/src/workflow/motion-input.js +18 -31
  340. package/src/workflow/node-schema.js +1 -1
  341. package/src/workflow/reply-markdown.js +69 -0
  342. package/src/workflow/scene-asset-sync.js +8 -8
  343. package/tools/agent-session-stats.mjs +61 -0
  344. package/tools/dev-full.mjs +5 -56
  345. package/tools/eslint-baseline.json +12 -13
  346. package/tools/knip-baseline.json +0 -179
  347. package/tools/loc-baseline.json +31 -81
  348. package/tools/motion3/extract-body.mjs +103 -0
  349. package/tools/motion3/extract-rig.mjs +85 -0
  350. package/tools/process-supervisor.mjs +0 -88
  351. package/tools/run-tests.mjs +9 -125
  352. package/bin/agent/motion-runtime.mjs +0 -76
  353. package/dist/ardy/cskel27-rest.json +0 -557
  354. package/dist/assets/analytics-C9pMpW7s.js +0 -1
  355. package/dist/assets/app-B_2XB9rw.css +0 -1
  356. package/dist/assets/app-DAuvKPP8.js +0 -4920
  357. package/dist/assets/config-Bx7pYsD9.js +0 -1
  358. package/dist/assets/config-DeoFTYzb.css +0 -1
  359. package/dist/assets/demo-CvKuBgy5.js +0 -1
  360. package/dist/assets/scene-asset-sync-DKkpjKBV.js +0 -19
  361. package/dist/assets/scene-asset-sync-DjJ_cJ2R.css +0 -1
  362. package/dist/assets/ticket-BPk2Ni95.js +0 -1
  363. package/dist/assets/workflow-jJRBR94Z.js +0 -179
  364. package/dist/demo/walk-then-stop.npz +0 -0
  365. package/mcp/ardy-prompts.mjs +0 -218
  366. package/src/ardy/auto-fix-panel.css +0 -108
  367. package/src/ardy/auto-physics.js +0 -1194
  368. package/src/ardy/client.js +0 -153
  369. package/src/ardy/convert.js +0 -313
  370. package/src/ardy/cskel27-neutral.js +0 -39
  371. package/src/ardy/cskel27.js +0 -77
  372. package/src/ardy/export.js +0 -165
  373. package/src/ardy/fix-collisions.js +0 -2257
  374. package/src/ardy/foot-lock.js +0 -230
  375. package/src/ardy/motion-calibration.js +0 -118
  376. package/src/ardy/motion-edit.js +0 -213
  377. package/src/ardy/motion-url.js +0 -55
  378. package/src/ardy/mp4-duration.js +0 -102
  379. package/src/ardy/npz.js +0 -595
  380. package/src/ardy/physics-panel.css +0 -21
  381. package/src/ardy/physics-panel.jsx +0 -89
  382. package/src/ardy/physics-refine.js +0 -111
  383. package/src/ardy/physics-review.js +0 -567
  384. package/src/ardy/physics-temporal.js +0 -54
  385. package/src/ardy/platform-fit-panel.css +0 -64
  386. package/src/ardy/platform-fit-panel.jsx +0 -41
  387. package/src/ardy/platform-fit.js +0 -326
  388. package/src/ardy/playback.js +0 -711
  389. package/src/ardy/pose-pin.js +0 -97
  390. package/src/ardy/prompt-clips.js +0 -30
  391. package/src/ardy/retime.js +0 -211
  392. package/src/ardy/root-drop.js +0 -369
  393. package/src/ardy/timeline-resize.js +0 -13
  394. package/src/ardy/to-cskel27.js +0 -225
  395. package/src/ardy/trim.js +0 -33
  396. package/src/ardy/waypoints.js +0 -369
  397. package/src/ardy-bridge-contract.js +0 -281
  398. package/src/domains/motion/motion-video-capture.js +0 -664
  399. package/src/i2v-motion-client.js +0 -77
  400. package/src/i2v-motion-studio.jsx +0 -180
  401. package/src/line-edit.js +0 -1348
  402. package/src/motion/generation.js +0 -105
  403. package/src/motion-readiness-ui.jsx +0 -67
  404. package/src/motion-readiness.js +0 -40
  405. package/src/motion-resources.js +0 -177
  406. package/src/motion-store.js +0 -65
  407. package/src/motion-trail.js +0 -601
  408. package/src/multimodel-ingest.js +0 -394
  409. package/src/panels/PromptBlocksPanel.jsx +0 -444
  410. package/src/panels/VideoCapturePanel.jsx +0 -181
  411. package/src/pose-extract/browser.js +0 -42
  412. package/src/pose-extract/detector.js +0 -100
  413. package/src/pose-extract/fit.js +0 -227
  414. package/src/pose-extract/image-frame.js +0 -51
  415. package/src/pose-extract/index.js +0 -58
  416. package/src/pose-extract/keyframes.js +0 -74
  417. package/src/pose-extract/landmarks.js +0 -162
  418. package/src/pose-extract/mirror.js +0 -153
  419. package/src/pose-extract/take.js +0 -131
  420. package/src/pose-extract/video-frames.js +0 -91
  421. package/src/take-recipe.js +0 -165
  422. package/src/trail-key-conflicts.js +0 -39
  423. package/src/trail-pick.js +0 -59
  424. package/src/use-i2v-capture-modal.js +0 -15
  425. package/tools/ardy/BRIDGE.md +0 -37
  426. package/tools/ardy/GVHMR-NPZ.md +0 -68
  427. package/tools/ardy/README.md +0 -124
  428. package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
  429. package/tools/ardy/artifacts.mjs +0 -34
  430. package/tools/ardy/bridge.mjs +0 -1732
  431. package/tools/ardy/bvh-cskel27.mjs +0 -1265
  432. package/tools/ardy/cclay_constrained_generate.py +0 -561
  433. package/tools/ardy/cclay_gvhmr_worker.py +0 -264
  434. package/tools/ardy/cclay_motion_edit.py +0 -510
  435. package/tools/ardy/cclay_sequence_generate.py +0 -705
  436. package/tools/ardy/cclay_worker.py +0 -304
  437. package/tools/ardy/dump-npz.py +0 -205
  438. package/tools/ardy/extract-rest.mjs +0 -299
  439. package/tools/ardy/extract.mjs +0 -507
  440. package/tools/ardy/footage.mjs +0 -467
  441. package/tools/ardy/gvhmr-floor.mjs +0 -102
  442. package/tools/ardy/gvhmr-to-cskel27.mjs +0 -21
  443. package/tools/ardy/gvhmr_fastpath.py +0 -168
  444. package/tools/ardy/gvhmr_trajectory.py +0 -240
  445. package/tools/ardy/merge-text-encoder.py +0 -160
  446. package/tools/ardy/mocap-metrics.mjs +0 -200
  447. package/tools/ardy/mocap-quality-gate.mjs +0 -110
  448. package/tools/ardy/motion-stabilize.mjs +0 -348
  449. package/tools/ardy/npz.mjs +0 -414
  450. package/tools/ardy/pose-to-npz.mjs +0 -106
  451. package/tools/ardy/prompt-limits.mjs +0 -5
  452. package/tools/ardy/run-edit-on-box.sh +0 -73
  453. package/tools/ardy/run-local.mjs +0 -233
  454. package/tools/ardy/run-on-box.sh +0 -593
  455. package/tools/ardy/run-sequence-on-box.sh +0 -177
  456. package/tools/ardy/runners/gvhmr-worker.mjs +0 -206
  457. package/tools/ardy/runners/index.mjs +0 -18
  458. package/tools/ardy/runners/local.mjs +0 -455
  459. package/tools/ardy/runners/proc.mjs +0 -125
  460. package/tools/ardy/runners/remote.mjs +0 -252
  461. package/tools/ardy/setup-local.mjs +0 -189
  462. package/tools/ardy/setup-text-encoder-on-box.sh +0 -93
  463. package/tools/ardy/setup-text-encoder.py +0 -306
  464. package/tools/ardy/smpl-cskel27.mjs +0 -178
  465. package/tools/ardy/visual-qa.mjs +0 -196
  466. package/tools/ardy/vq-car.mjs +0 -52
  467. package/tools/bench/EXP3.md +0 -61
  468. package/tools/bench/cclay_bench_extract_incam.py +0 -125
  469. package/tools/bench/cclay_bench_extract_obs.py +0 -204
  470. package/tools/bench/cclay_bench_runner.py +0 -42
  471. package/tools/bench/cube-contact.mjs +0 -162
  472. package/tools/bench/exp3.mjs +0 -152
  473. package/tools/bench/extract-bench-lib.mjs +0 -216
  474. package/tools/bench/extract-bench.mjs +0 -302
  475. package/tools/bench/fal-generate.mjs +0 -88
  476. package/tools/bench/fit/README.md +0 -189
  477. package/tools/bench/fit/camera.mjs +0 -59
  478. package/tools/bench/fit/contact.mjs +0 -419
  479. package/tools/bench/fit/footlock.mjs +0 -234
  480. package/tools/bench/fit/motion.mjs +0 -85
  481. package/tools/bench/fit/pin.mjs +0 -27
  482. package/tools/bench/fit/remote.mjs +0 -105
  483. package/tools/bench/fit-bench.mjs +0 -126
  484. package/tools/bench/fit-sanity.mjs +0 -45
  485. package/tools/bench/metrics.mjs +0 -313
  486. package/tools/bench/obs/depth.mjs +0 -258
  487. package/tools/bench/obs/extrinsics.mjs +0 -175
  488. package/tools/bench/obs/fit_mannequin_betas.py +0 -340
  489. package/tools/bench/obs/ground.mjs +0 -294
  490. package/tools/bench/obs/heading.mjs +0 -151
  491. package/tools/bench/obs/ladder.mjs +0 -555
  492. package/tools/bench/obs/mannequin-betas.json +0 -124
  493. package/tools/bench/obs/remote.mjs +0 -83
  494. package/tools/bench/obs/rest_joints.py +0 -52
  495. package/tools/bench/obs/ybot-targets.mjs +0 -49
  496. package/tools/bench/obs-bench.mjs +0 -520
  497. package/tools/bench/score.mjs +0 -343
  498. package/tools/bench/summarize.mjs +0 -86
  499. package/tools/demo-worker/README.md +0 -137
  500. package/tools/demo-worker/api-client.mjs +0 -233
  501. package/tools/demo-worker/generate.mjs +0 -301
  502. package/tools/demo-worker/index.mjs +0 -466
  503. package/tools/demo-worker/sign-core.mjs +0 -112
  504. package/tools/demo-worker/sign.mjs +0 -161
  505. package/tools/gt-render/browser.mjs +0 -182
  506. package/tools/gt-render/camera-math.mjs +0 -276
  507. package/tools/gt-render/page.mjs +0 -267
  508. package/tools/gt-render/render.mjs +0 -514
  509. package/tools/gt-render/scene-box.mjs +0 -31
  510. package/tools/gt-render/take-transform.mjs +0 -87
  511. package/tools/kimodo/compare-seam.mjs +0 -108
  512. package/tools/kimodo/constraints.mjs +0 -244
  513. package/tools/kimodo/edit.mjs +0 -259
  514. package/tools/kimodo/effector-constraints.mjs +0 -200
  515. package/tools/kimodo/generate.mjs +0 -646
  516. package/tools/kimodo/local-output.mjs +0 -265
  517. package/tools/kimodo/measure-pose.mjs +0 -65
  518. package/tools/kimodo/measure-preserve.mjs +0 -899
  519. package/tools/kimodo/measure-waypoints.mjs +0 -55
  520. package/tools/kimodo/mlx-generate.py +0 -111
  521. package/tools/kimodo/pose-constraints.mjs +0 -248
  522. package/tools/kimodo/preserve-mask.mjs +0 -543
  523. package/tools/kimodo/read-npz.mjs +0 -189
  524. package/tools/kimodo/run-edit-on-box.mjs +0 -155
  525. package/tools/kimodo/run-sequence-on-box.mjs +0 -178
  526. package/tools/kimodo/runner.mjs +0 -284
  527. package/tools/kimodo/seam-batch.mjs +0 -77
  528. package/tools/kimodo/setup-local.mjs +0 -30
  529. package/tools/kimodo/setup-on-box.sh +0 -339
  530. package/tools/kimodo/soma77-to-cskel27.mjs +0 -305
  531. package/tools/morphgs/assets/gen_truth.py +0 -39
  532. package/tools/morphgs/assets/mesh_ori_rig.txt +0 -27
  533. package/tools/morphgs/assets/playback-check.mjs +0 -30
  534. package/tools/morphgs/demo-gate.sh +0 -26
  535. package/tools/morphgs/fbx2morphgs.mjs +0 -68
  536. package/tools/morphgs/morphgs-to-cskel27.mjs +0 -105
  537. package/tools/morphgs/patches/preprocess_src-none-mode.patch +0 -76
  538. package/tools/morphgs/setup-on-cluster.sh +0 -126
  539. package/tools/projflow/driver.py +0 -1069
  540. package/tools/projflow/generate.mjs +0 -805
  541. package/tools/projflow/hml22-to-cskel27.mjs +0 -735
  542. package/tools/projflow/line-edit-job.mjs +0 -547
  543. package/tools/projflow/replay.mjs +0 -452
  544. package/tools/projflow/runner.mjs +0 -269
  545. package/tools/projflow/service.mjs +0 -742
  546. package/tools/track/DESIGN.md +0 -513
  547. package/tools/track/backfill-provenance.mjs +0 -116
  548. package/tools/track/budget.mjs +0 -8
  549. package/tools/track/check-rig.mjs +0 -209
  550. package/tools/track/diagnostics.schema.json +0 -60
  551. package/tools/track/export-rig.mjs +0 -256
  552. package/tools/track/fallback.mjs +0 -7
  553. package/tools/track/fk-parity-fixture.mjs +0 -161
  554. package/tools/track/gate.mjs +0 -349
  555. package/tools/track/masks.mjs +0 -318
  556. package/tools/track/metrics.mjs +0 -201
  557. package/tools/track/publish-obs.mjs +0 -108
  558. package/tools/track/py/check_env.py +0 -61
  559. package/tools/track/py/eval_lr.py +0 -229
  560. package/tools/track/py/lr_viterbi.py +0 -294
  561. package/tools/track/py/masks.py +0 -405
  562. package/tools/track/py/objective.py +0 -382
  563. package/tools/track/py/rig.py +0 -508
  564. package/tools/track/py/scene.py +0 -326
  565. package/tools/track/py/test_joint_indices.py +0 -123
  566. package/tools/track/py/test_lr_viterbi.py +0 -195
  567. package/tools/track/py/test_masks.py +0 -229
  568. package/tools/track/py/test_rig.py +0 -288
  569. package/tools/track/py/test_scene.py +0 -354
  570. package/tools/track/py/test_track.py +0 -328
  571. package/tools/track/py/track.py +0 -411
  572. package/tools/track/remote.mjs +0 -140
  573. package/tools/track/rig-dump.mjs +0 -227
  574. package/tools/track/run-box-tests.mjs +0 -14
  575. package/tools/track/run-box.mjs +0 -168
  576. package/tools/track/setup-box.sh +0 -59
  577. package/tools/track/study-2d.mjs +0 -728
  578. /package/src/{ardy → motion}/collision-blockers.js +0 -0
  579. /package/src/{ardy → motion}/ground.js +0 -0
  580. /package/src/{ardy → motion}/ik-drag.js +0 -0
  581. /package/src/{ardy → motion}/ik-key-json.js +0 -0
  582. /package/src/{ardy → motion}/ik.js +0 -0
  583. /package/src/{ardy → motion}/physics-surface.js +0 -0
  584. /package/src/{ardy → motion}/playback-clock.js +0 -0
  585. /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
- }