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
@@ -0,0 +1,850 @@
1
+ import * as THREE from "three";
2
+ import {
3
+ findBone,
4
+ measureContactRadii,
5
+ hasBindPose,
6
+ bindWorldPosition,
7
+ restWorldPosition,
8
+ } from "./ik.js";
9
+
10
+ /**
11
+ * Body-collision DETECTION: capsule proxies per body segment (bone → child
12
+ * bone axis, radius measured from the bind-pose skinned mesh by
13
+ * measureContactRadii) and the penetrations between them and external
14
+ * blockers. Read-only: motion verification (studio-agent-motion.js) and the
15
+ * collision blockers build on it. The 2.x solver that pushed limbs apart was
16
+ * removed with ARDY; motion3 QA (src/motion3/qa) reports collisions for
17
+ * compiled takes.
18
+ *
19
+ * Torso/head capsules are static; limb capsules name the IK chain they ride.
20
+ */
21
+
22
+ /**
23
+ * Body capsules. `movable.chain`/`movable.joint` name the IK chain and the
24
+ * joint of that chain to drive ("mid" = elbow/knee, "effector" = wrist/
25
+ * ankle). Torso, chest and head have no chain: they are blockers the limbs
26
+ * get pushed out of. `radiusJoint` keys into the measured contact radii.
27
+ *
28
+ * `endOptional` marks an end bone the rig is allowed not to have: the hands
29
+ * span wrist → middle-finger base so a fingertip driven into a thigh is
30
+ * judged at the FINGERS, not at the wrist a palm's length behind them (a
31
+ * wrist-only sphere reads that pose as clean). Rigs exported without finger
32
+ * bones simply fall back to the wrist sphere instead of disqualifying the
33
+ * whole tool.
34
+ */
35
+ // `maxRadius` caps the measured radius where the mesh extent is not a
36
+ // thickness: the head measurement runs crown-to-chin (a length), the foot
37
+ // measurement follows the toe (also a length). Uncapped, those fat capsules
38
+ // flag permanent false contacts with the torso and with the other foot at
39
+ // any normal stance.
40
+ const CAPSULE_DEFS = [
41
+ { id: "torso", start: "mixamorigHips", end: "mixamorigSpine1", radiusJoint: "Spine", maxRadius: 0.17, movable: null },
42
+ { id: "chest", start: "mixamorigSpine1", end: "mixamorigSpine2", radiusJoint: "Spine", maxRadius: 0.16, movable: null },
43
+ { id: "head", start: "mixamorigNeck", end: "mixamorigHead", radiusJoint: "Head", maxRadius: 0.11, movable: null },
44
+ { id: "leftUpperArm", start: "mixamorigLeftArm", end: "mixamorigLeftForeArm", radiusJoint: "LeftArm", movable: { chain: "leftHand", joint: "mid" }, priority: 2 },
45
+ { id: "leftForeArm", start: "mixamorigLeftForeArm", end: "mixamorigLeftHand", radiusJoint: "LeftForeArm", movable: { chain: "leftHand", joint: "effector" }, priority: 2 },
46
+ { id: "leftHand", start: "mixamorigLeftHand", end: "mixamorigLeftHandMiddle1", endOptional: true, radiusJoint: "LeftHand", maxRadius: 0.06, movable: { chain: "leftHand", joint: "effector" }, priority: 2 },
47
+ { id: "rightUpperArm", start: "mixamorigRightArm", end: "mixamorigRightForeArm", radiusJoint: "RightArm", movable: { chain: "rightHand", joint: "mid" }, priority: 2 },
48
+ { id: "rightForeArm", start: "mixamorigRightForeArm", end: "mixamorigRightHand", radiusJoint: "RightForeArm", movable: { chain: "rightHand", joint: "effector" }, priority: 2 },
49
+ { id: "rightHand", start: "mixamorigRightHand", end: "mixamorigRightHandMiddle1", endOptional: true, radiusJoint: "RightHand", maxRadius: 0.06, movable: { chain: "rightHand", joint: "effector" }, priority: 2 },
50
+ { id: "leftThigh", start: "mixamorigLeftUpLeg", end: "mixamorigLeftLeg", radiusJoint: "LeftUpLeg", movable: { chain: "leftFoot", joint: "mid" }, priority: 1 },
51
+ { id: "leftShin", start: "mixamorigLeftLeg", end: "mixamorigLeftFoot", radiusJoint: "LeftLeg", movable: { chain: "leftFoot", joint: "effector" }, priority: 1 },
52
+ { id: "leftFoot", start: "mixamorigLeftFoot", end: "mixamorigLeftToeBase", radiusJoint: "LeftFoot", maxRadius: 0.07, movable: { chain: "leftFoot", joint: "effector" }, priority: 1 },
53
+ { id: "rightThigh", start: "mixamorigRightUpLeg", end: "mixamorigRightLeg", radiusJoint: "RightUpLeg", movable: { chain: "rightFoot", joint: "mid" }, priority: 1 },
54
+ { id: "rightShin", start: "mixamorigRightLeg", end: "mixamorigRightFoot", radiusJoint: "RightLeg", movable: { chain: "rightFoot", joint: "effector" }, priority: 1 },
55
+ { id: "rightFoot", start: "mixamorigRightFoot", end: "mixamorigRightToeBase", radiusJoint: "RightFoot", maxRadius: 0.07, movable: { chain: "rightFoot", joint: "effector" }, priority: 1 },
56
+ ];
57
+
58
+ /**
59
+ * FINGER CAPSULES. The hand capsule spans wrist → middle-finger base, which is
60
+ * a palm: it still reads a pose where the FINGERS are buried in a thigh and the
61
+ * palm is not as clean, because the fingers stick out another 8 cm past its end
62
+ * cap. Mixamo hands carry mixamorig{Side}Hand{Finger}1..4, so each finger gets
63
+ * its own thin capsule from the base joint to the tip.
64
+ *
65
+ * OPTIONAL, ONE FINGER AT A TIME. Game rigs ship every mixture of these bones:
66
+ * all four joints, three, a thumb only, none at all. A finger falls back from
67
+ * 1→4 to 1→3 and, failing both, is simply left out — `optional` means a MISSING
68
+ * finger is not a missing capsule but a missing entry, so a fingerless rig is
69
+ * still fully supported (supportsCollisionCleanup ignores optional defs) and
70
+ * every other proxy is built exactly as before.
71
+ *
72
+ * RADIUS is a constant. measureContactRadii's CONTACT_JOINTS carry no finger
73
+ * entries — there is nothing to look up — and a finger is the one body part
74
+ * whose thickness barely varies between characters.
75
+ */
76
+ export const FINGER_RADIUS = 0.011;
77
+
78
+ const FINGER_NAMES = ["Thumb", "Index", "Middle", "Ring", "Pinky"];
79
+
80
+ for (const side of ["Left", "Right"]) {
81
+ const lower = side === "Left" ? "left" : "right";
82
+ for (const finger of FINGER_NAMES) {
83
+ CAPSULE_DEFS.push({
84
+ id: `${lower}Hand${finger}`,
85
+ start: `mixamorig${side}Hand${finger}1`,
86
+ // First spelling that resolves wins: the tip, else the last knuckle.
87
+ ends: [`mixamorig${side}Hand${finger}4`, `mixamorig${side}Hand${finger}3`],
88
+ optional: true,
89
+ fixedRadius: FINGER_RADIUS,
90
+ // The two ids a finger must never be tested against beyond its own
91
+ // kind — see pairExcluded.
92
+ handId: `${lower}Hand`,
93
+ foreArmId: `${lower}ForeArm`,
94
+ movable: { chain: `${lower}Hand`, joint: "effector" },
95
+ priority: 2,
96
+ });
97
+ }
98
+ }
99
+
100
+ /** Fallback radius when no measurement exists (matches ik.js's floor). */
101
+ const RADIUS_FALLBACK = 0.01;
102
+
103
+ /** Soft-tissue compression allowance, as a fraction of the thinner capsule.
104
+ * See detectPenetrations for why this is 0.25 and not something roomier. */
105
+ export const DEFAULT_SLACK_FACTOR = 0.25;
106
+
107
+ /** How much closer than its BIND pose a pair must come before the extra
108
+ * closeness counts as a collision. See restPairOverlaps. 5 mm is under the
109
+ * amplitude of the proxy error itself and far under any visible interpenetration. */
110
+ export const PAIR_TOLERANCE = 0.005;
111
+
112
+ /**
113
+ * REPORTING FLOOR. A capsule proxy is a coarse stand-in for a specific mesh, and
114
+ * its agreement with that mesh is worth a few millimetres at best; the pushes it
115
+ * takes to resolve one are not small (a 0.2 mm thigh × thigh reading during a
116
+ * normal walk stride moved the pose 16 mm and keyed the frame). So a pair under
117
+ * this depth is not reported at all — not by detection, not to the fixer, not to
118
+ * the readout — and the tool spends its passes on contact a viewer can actually
119
+ * see.
120
+ */
121
+ export const MIN_DEPTH = 0.004;
122
+
123
+ /**
124
+ * True when the rig carries every bone the capsule table REQUIRES — the
125
+ * cheap pre-flight the UI runs to decide whether to offer the tool at all.
126
+ * Bone lookups only: no mesh measurement, so it is safe to call per render.
127
+ * Optional ends (the finger bones) never count against a rig.
128
+ */
129
+ export function supportsCollisionCleanup(rig) {
130
+ if (!rig) return false;
131
+ for (const def of CAPSULE_DEFS) {
132
+ // An OPTIONAL capsule (the fingers) is an extra proxy, never a
133
+ // requirement: a rig without them is described just as well without them.
134
+ if (def.optional) continue;
135
+ if (!findBone(rig, def.start)) return false;
136
+ if (def.end && !def.endOptional && !findBone(rig, def.end)) return false;
137
+ }
138
+ return true;
139
+ }
140
+
141
+ /** The capsule table against an arbitrary source of bone positions — the
142
+ * current pose for detection, the bind pose for calibration. Returns null when
143
+ * a REQUIRED bone is missing. */
144
+ function buildCapsules(rig, radiiMap, positionOf) {
145
+ const capsules = new Map();
146
+ for (const def of CAPSULE_DEFS) {
147
+ const start = findBone(rig, def.start);
148
+ // An OPTIONAL capsule the rig has no bones for is left out, not fatal.
149
+ if (!start) {
150
+ if (def.optional) continue;
151
+ return null;
152
+ }
153
+ let end = null;
154
+ if (def.ends) {
155
+ for (const name of def.ends) {
156
+ end = findBone(rig, name);
157
+ if (end) break;
158
+ }
159
+ // A finger with a base joint but no further joint has no segment worth
160
+ // testing: skip it rather than park a knuckle-sized sphere on the hand.
161
+ if (!end) continue;
162
+ } else if (def.end) {
163
+ end = findBone(rig, def.end);
164
+ // A missing OPTIONAL end degrades to the wrist sphere; a missing
165
+ // required end means this is not a rig we can build proxies for.
166
+ if (!end && !def.endOptional) return null;
167
+ }
168
+ const a = positionOf(start);
169
+ const b = end ? positionOf(end) : a.clone();
170
+ const measured = Math.max(RADIUS_FALLBACK, radiiMap[def.radiusJoint] ?? RADIUS_FALLBACK);
171
+ const radius = def.fixedRadius ?? (def.maxRadius ? Math.min(measured, def.maxRadius) : measured);
172
+ capsules.set(def.id, {
173
+ def,
174
+ bones: end ? [start, end] : [start],
175
+ a,
176
+ b,
177
+ radius,
178
+ });
179
+ }
180
+ return capsules;
181
+ }
182
+
183
+ /**
184
+ * Build world-space capsules for the rig's CURRENT pose. Cheap (a bone
185
+ * lookup + two world positions per entry), so callers rebuild per frame.
186
+ * `radii` overrides the measured map (tests, or rigs without skin weights).
187
+ * Returns null when a REQUIRED bone is missing — a non-Mixamo rig the tool
188
+ * cannot describe, same policy as resolveIkRig. Callers must tell that apart
189
+ * from "found no collision".
190
+ */
191
+ export function buildCollisionCapsules(rig, radii = null) {
192
+ if (!rig) return null;
193
+ rig.updateMatrixWorld(true);
194
+ const radiiMap = radii ?? measureContactRadii(rig);
195
+ const capsules = buildCapsules(rig, radiiMap, (bone) => bone.getWorldPosition(new THREE.Vector3()));
196
+ // Ride the pair calibration along on the map so detectPenetrations gets it
197
+ // for free — it is a property of the RIG, which detection alone cannot see.
198
+ if (capsules) capsules.pairAllowances = restPairOverlaps(rig, radiiMap);
199
+ return capsules;
200
+ }
201
+
202
+ /** Stable key for an unordered capsule pair. */
203
+ function pairKey(idA, idB) {
204
+ return idA < idB ? `${idA}|${idB}` : `${idB}|${idA}`;
205
+ }
206
+
207
+ // rig → (radii object identity → pair overlap map). The reference poses never
208
+ // change, so this is measured once per character.
209
+ const restOverlapCache = new WeakMap();
210
+
211
+ /** Every testable pair's overlap in one reference pose, positive entries only,
212
+ * merged into `into` by taking the larger of the two. Measured through the SAME
213
+ * pairGeometry detection uses, so a trimmed (hinged) pair is calibrated on the
214
+ * portions it will actually be judged on — the natural fold a character rests
215
+ * in is a legitimate overlap there exactly as it is anywhere else. */
216
+ function collectPairOverlaps(rig, radiiMap, positionOf, into) {
217
+ const capsules = buildCapsules(rig, radiiMap, positionOf);
218
+ if (!capsules) return into;
219
+ const list = [...capsules.values()];
220
+ const pa = new THREE.Vector3();
221
+ const pb = new THREE.Vector3();
222
+ for (let i = 0; i < list.length; i += 1) {
223
+ for (let j = i + 1; j < list.length; j += 1) {
224
+ const geo = pairGeometry(list[i], list[j]);
225
+ if (!geo) continue;
226
+ closestPointsSegmentSegment(geo.a0, geo.a1, geo.b0, geo.b1, pa, pb);
227
+ const overlap = list[i].radius + list[j].radius - pa.distanceTo(pb);
228
+ if (overlap <= 0) continue;
229
+ const key = pairKey(list[i].def.id, list[j].def.id);
230
+ if (overlap > (into.get(key) ?? 0)) into.set(key, overlap);
231
+ }
232
+ }
233
+ return into;
234
+ }
235
+
236
+ /**
237
+ * How much each non-adjacent capsule pair ALREADY overlaps in a pose the
238
+ * character legitimately holds, in metres (positive entries only; a pair that is
239
+ * clear in every reference pose has no entry). Returns null on a rig with no
240
+ * bind snapshot to compose from.
241
+ *
242
+ * Capsules are a coarse proxy for a specific mesh, and on real characters the
243
+ * proxy error is not small: on the shipped default rig the REST pose reports
244
+ * torso × upper arm at 9.2 mm of "penetration" (torso 0.17 + upper arm 0.05447
245
+ * + offset 0.002 against a 0.19547 axis distance, minus a 21.8 mm slack
246
+ * allowance). One press of Fix collisions on a brand-new project baked a phantom
247
+ * key and moved the rig 13.7 mm. Slack cannot fix this: the rig would need
248
+ * slackFactor ≥ 0.569 to read clean, and a global allowance that loose re-masks
249
+ * every real contact on every other pair.
250
+ *
251
+ * So the pair's own reference overlap becomes its zero point — the same
252
+ * clip-relative-ground move auto-physics made for the floor.
253
+ *
254
+ * TWO REFERENCE POSES, and the second is the one that matters. `poseBind` is a
255
+ * T-POSE: arms straight out, nowhere near the torso (LeftForeArm at x = 0.4616).
256
+ * The pose the app actually shows is DEFAULT_POSE — arms hanging at the sides,
257
+ * the same forearm at x = 0.1876 — and that is where the arm capsules graze the
258
+ * torso. Calibrating against bind alone therefore recorded only leftThigh ×
259
+ * rightThigh and left torso × upper arm at zero allowance, which is exactly the
260
+ * pair that false-positives. Both poses are measured and the LARGER allowance
261
+ * wins, so a T-posed character and a standing one both read clean, while a pair
262
+ * that overlaps in neither (a hand driven into a thigh) still fires at full
263
+ * sensitivity. Both are composed virtually through ik.js, so measuring never
264
+ * disturbs — or depends on — the pose currently on screen.
265
+ *
266
+ * The cost, stated plainly: contact that the rest pose already has is invisible
267
+ * here. An arm pressed flat against the ribs cannot be distinguished from an arm
268
+ * hanging beside them, because the capsule proxies genuinely cannot tell those
269
+ * apart. Excluding those pairs outright (the alternative) would lose the deep
270
+ * hits too; this keeps them.
271
+ */
272
+ function restPairOverlaps(rig, radii = null) {
273
+ if (!rig || !hasBindPose(rig)) return null;
274
+ const radiiMap = radii ?? measureContactRadii(rig);
275
+ let perRadii = restOverlapCache.get(rig);
276
+ if (!perRadii) restOverlapCache.set(rig, (perRadii = new Map()));
277
+ const cached = perRadii.get(radiiMap);
278
+ if (cached) return cached;
279
+ rig.updateMatrixWorld(true);
280
+ const overlaps = new Map();
281
+ collectPairOverlaps(rig, radiiMap, (bone) => bindWorldPosition(rig, bone, new THREE.Vector3()), overlaps);
282
+ collectPairOverlaps(rig, radiiMap, (bone) => restWorldPosition(rig, bone, new THREE.Vector3()), overlaps);
283
+ perRadii.set(radiiMap, overlaps);
284
+ return overlaps;
285
+ }
286
+
287
+ /** Closest points between two segments (Ericson, Real-Time Collision
288
+ * Detection — the textbook segment/segment distance). Writes into outA/outB
289
+ * and returns the squared distance. */
290
+ function closestPointsSegmentSegment(p1, q1, p2, q2, outA, outB) {
291
+ const d1 = q1.clone().sub(p1);
292
+ const d2 = q2.clone().sub(p2);
293
+ const r = p1.clone().sub(p2);
294
+ const a = d1.dot(d1);
295
+ const e = d2.dot(d2);
296
+ const f = d2.dot(r);
297
+ let s;
298
+ let t;
299
+ if (a <= 1e-12 && e <= 1e-12) {
300
+ outA.copy(p1); outB.copy(p2);
301
+ return r.dot(r);
302
+ }
303
+ if (a <= 1e-12) {
304
+ s = 0;
305
+ t = Math.max(0, Math.min(1, f / e));
306
+ } else {
307
+ const c = d1.dot(r);
308
+ if (e <= 1e-12) {
309
+ t = 0;
310
+ s = Math.max(0, Math.min(1, -c / a));
311
+ } else {
312
+ const b = d1.dot(d2);
313
+ const denom = a * e - b * b;
314
+ s = denom > 1e-12 ? Math.max(0, Math.min(1, (b * f - c * e) / denom)) : 0;
315
+ t = (b * s + f) / e;
316
+ if (t < 0) { t = 0; s = Math.max(0, Math.min(1, -c / a)); }
317
+ else if (t > 1) { t = 1; s = Math.max(0, Math.min(1, (b - c) / a)); }
318
+ }
319
+ }
320
+ outA.copy(p1).addScaledVector(d1, s);
321
+ outB.copy(p2).addScaledVector(d2, t);
322
+ return outA.distanceToSquared(outB);
323
+ }
324
+
325
+ /** Ancestor hop count between two bones, or Infinity when neither is an
326
+ * ancestor of the other within `limit` hops. */
327
+ const HOP_SEARCH = 8;
328
+ function boneHops(b0, b1, limit = HOP_SEARCH) {
329
+ let hops = 0;
330
+ for (let node = b0; node && hops <= limit; node = node.parent, hops += 1) {
331
+ if (node === b1) return hops;
332
+ }
333
+ hops = 0;
334
+ for (let node = b1; node && hops <= limit; node = node.parent, hops += 1) {
335
+ if (node === b0) return hops;
336
+ }
337
+ return Infinity;
338
+ }
339
+
340
+ /** Capsules whose endpoints sit within HOP_LIMIT hops on the skeleton are
341
+ * NEIGHBOURS (upper arm against chest at the shoulder, thigh against the
342
+ * pelvis, forearm against upper arm at the elbow): they always overlap AT the
343
+ * joint they share, so they get the trimmed test below rather than the plain
344
+ * one. */
345
+ const HOP_LIMIT = 2;
346
+ function bonesRelated(b0, b1) {
347
+ return boneHops(b0, b1, HOP_LIMIT) <= HOP_LIMIT;
348
+ }
349
+
350
+ function capsulesAdjacent(ca, cb) {
351
+ return ca.bones.some((ba) => cb.bones.some((bb) => bonesRelated(ba, bb)));
352
+ }
353
+
354
+ /**
355
+ * HINGED PAIRS. A blanket "skip everything within HOP_LIMIT" made whole classes
356
+ * of real interpenetration invisible: a knee driven into the belly is
357
+ * thigh × torso, an arm folded past its own elbow is forearm × upper arm, and
358
+ * neither pair was ever tested. The skip existed for a good reason — the two
359
+ * capsules MEET at the joint they share, so an untrimmed test reports a
360
+ * permanent collision at every pose and pins the fixer forever — but that
361
+ * reason only covers the part of each capsule NEXT TO the joint.
362
+ *
363
+ * So the pair is tested on the portions FAR from the shared joint: TRIM_FRACTION
364
+ * of each segment is cut away from the joint side, and the segment-segment test
365
+ * runs on what is left. A normally bent elbow keeps its far portions well apart;
366
+ * a fully folded one lays them on top of each other, which is exactly the pose
367
+ * that has to be caught.
368
+ *
369
+ * The joint side is the endpoint CLOSER to the other capsule in the hierarchy
370
+ * (fewest ancestor hops), so the trim always eats the shared end and never the
371
+ * free one.
372
+ */
373
+ export const TRIM_FRACTION = 0.45;
374
+
375
+ /**
376
+ * Compression allowance for a pair that SHARES a bone — the two segments of one
377
+ * hinge (elbow, knee, ankle, wrist, waist). Flesh-to-flesh contact is the normal
378
+ * state of a deep fold there, and the capsule proxies overlap by construction
379
+ * long before the pose is wrong, so the thin-capsule slack rule is the wrong
380
+ * ruler: this one is a fraction of the COMBINED radii, and only a fold that puts
381
+ * the far end of one segment inside the body of the other clears it.
382
+ */
383
+ export const HINGE_SLACK_FACTOR = 0.65;
384
+
385
+ /** Which end of `ca` faces `cb` on the skeleton: true = the START endpoint. */
386
+ function jointSideIsStart(ca, cb) {
387
+ const hopsOf = (bone) => Math.min(...cb.bones.map((other) => boneHops(bone, other)));
388
+ const first = hopsOf(ca.bones[0]);
389
+ const last = ca.bones.length > 1 ? hopsOf(ca.bones[ca.bones.length - 1]) : first;
390
+ return first <= last;
391
+ }
392
+
393
+ /** `ca` with the joint-side TRIM_FRACTION cut off, written into out0/out1. */
394
+ function trimTowardJoint(ca, cb, out0, out1) {
395
+ if (jointSideIsStart(ca, cb)) {
396
+ out0.copy(ca.a).lerp(ca.b, TRIM_FRACTION);
397
+ out1.copy(ca.b);
398
+ } else {
399
+ out0.copy(ca.a);
400
+ out1.copy(ca.b).lerp(ca.a, TRIM_FRACTION);
401
+ }
402
+ }
403
+
404
+ /** How many bones the two capsules have in common (0, 1 or 2). */
405
+ function sharedBoneCount(ca, cb) {
406
+ return ca.bones.filter((bone) => cb.bones.includes(bone)).length;
407
+ }
408
+
409
+ /**
410
+ * Pairs that must never be tested, whatever the pose.
411
+ *
412
+ * Adjacency already covers most of the finger cases (a finger base is one hop
413
+ * from the hand, two from the forearm), but the trimmed test above means
414
+ * "adjacent" no longer means "skipped", so the finger rules are spelled out
415
+ * rather than inherited: a finger against its own hand, its own forearm, or any
416
+ * other finger is a pose the proxies cannot judge — fingers touch each other and
417
+ * lie against the palm constantly — while a finger against a thigh, a shin, the
418
+ * torso, the chest, the head or the opposite arm is exactly what the finger
419
+ * capsules were added to catch.
420
+ */
421
+ function pairExcluded(ca, cb) {
422
+ const fa = ca.def.handId;
423
+ const fb = cb.def.handId;
424
+ if (fa && fb) return true; // finger × finger, either hand
425
+ if (fa && (cb.def.id === fa || cb.def.id === ca.def.foreArmId)) return true;
426
+ if (fb && (ca.def.id === fb || ca.def.id === cb.def.foreArmId)) return true;
427
+ // SAME CHAIN, no shared joint: a hand against its own upper arm, a foot
428
+ // against its own thigh, a finger against its own forearm. Two things are
429
+ // true of every such pair and neither is true of a hinge: the pose is
430
+ // legitimate (a hand rests on its own shoulder, a heel touches its own
431
+ // thigh) and the fixer could not separate them anyway, because ONE solver
432
+ // drives both sides and each push undoes the other — measured as a fold
433
+ // that oscillated to maxIterations without converging. The fold that IS
434
+ // impossible — the segments either side of the elbow or the knee — shares a
435
+ // bone, and stays tested (trimmed) by the rule above.
436
+ const chainA = ca.def.movable?.chain;
437
+ const chainB = cb.def.movable?.chain;
438
+ if (chainA && chainA === chainB && sharedBoneCount(ca, cb) === 0) return true;
439
+ return false;
440
+ }
441
+
442
+ /** Scratch segments for the pair geometry below. One pair is measured at a time
443
+ * (detection is a plain double loop), so these can be module-level. */
444
+ const geoA0 = new THREE.Vector3();
445
+ const geoA1 = new THREE.Vector3();
446
+ const geoB0 = new THREE.Vector3();
447
+ const geoB1 = new THREE.Vector3();
448
+ const geometry = { a0: geoA0, a1: geoA1, b0: geoB0, b1: geoB1, hinge: false };
449
+
450
+ /**
451
+ * The segments to test for one capsule pair, or null when the pair is not
452
+ * testable at all. Unrelated capsules are tested whole; neighbours are tested
453
+ * on their trimmed portions (see HINGED PAIRS). The returned object is REUSED
454
+ * between calls — read it before the next call.
455
+ */
456
+ function pairGeometry(ca, cb) {
457
+ if (pairExcluded(ca, cb)) return null;
458
+ if (!capsulesAdjacent(ca, cb)) {
459
+ geoA0.copy(ca.a); geoA1.copy(ca.b);
460
+ geoB0.copy(cb.a); geoB1.copy(cb.b);
461
+ geometry.hinge = false;
462
+ return geometry;
463
+ }
464
+ // Same segment, or two capsules over the same two bones: there is no "far
465
+ // portion" to compare and nothing a fixer could do about it either way.
466
+ const shared = sharedBoneCount(ca, cb);
467
+ if (shared >= 2 || ca.bones.length === 0 || cb.bones.length === 0) return null;
468
+ // Two static blockers meeting at the spine (torso × chest, chest × head) are
469
+ // permanent by construction and unfixable by anything: no limb chain drives
470
+ // them, so reporting them would only add noise.
471
+ if (!ca.def.movable && !cb.def.movable) return null;
472
+ trimTowardJoint(ca, cb, geoA0, geoA1);
473
+ trimTowardJoint(cb, ca, geoB0, geoB1);
474
+ geometry.hinge = shared === 1;
475
+ return geometry;
476
+ }
477
+
478
+ /**
479
+ * EXTERNAL BLOCKERS. Everything above is the character against itself; a hand
480
+ * swinging through a chair is the same problem against the SET. Callers pass
481
+ * `blockers`: world-space static shapes, already posed for the frame being
482
+ * solved (the caller owns their animation — this owns the push).
483
+ *
484
+ * capsule: { id, kind: "capsule", a: Vector3, b: Vector3, radius }
485
+ * box: { id, kind: "box", center: Vector3, halfExtents: Vector3, yaw }
486
+ *
487
+ * `yaw` is radians about world +Y, the only rotation a prop standing on a floor
488
+ * normally needs and the only one the closest-point test below has to invert.
489
+ *
490
+ * They behave exactly like torso and head: never movable, never adjacent to
491
+ * anything, and never rest-calibrated — a character's rest pose says nothing
492
+ * about where the furniture is. The full slack rule applies, with one wrinkle: a
493
+ * box has no radius to take the min against, so the movable capsule's own slack
494
+ * is used (a box is a surface, not a soft body).
495
+ *
496
+ * A blocker's records carry its id VERBATIM, so the existing pair readouts
497
+ * ("leftHand×obj:chair") identify which prop was hit without any caller change.
498
+ * The id is the caller's to namespace — collision-blockers.js already ships
499
+ * "obj:cube" and "char:char-2:torso" — and a prefix added here would only double
500
+ * it up ("obj:obj:cube", as QA read out).
501
+ */
502
+ function blockerEntry(shape) {
503
+ if (!shape || typeof shape.id !== "string") return null;
504
+ const point = (value) => (value && Number.isFinite(value.x) && Number.isFinite(value.y) && Number.isFinite(value.z)
505
+ ? new THREE.Vector3(value.x, value.y, value.z)
506
+ : null);
507
+ const def = { id: shape.id, movable: null, blocker: true };
508
+ if (shape.kind === "capsule") {
509
+ const a = point(shape.a);
510
+ const b = point(shape.b);
511
+ if (!a || !b || !Number.isFinite(shape.radius) || shape.radius < 0) return null;
512
+ return { def, kind: "capsule", bones: [], a, b, radius: shape.radius };
513
+ }
514
+ if (shape.kind === "box") {
515
+ const center = point(shape.center);
516
+ const halfExtents = point(shape.halfExtents);
517
+ if (!center || !halfExtents) return null;
518
+ halfExtents.set(Math.abs(halfExtents.x), Math.abs(halfExtents.y), Math.abs(halfExtents.z));
519
+ return {
520
+ def,
521
+ kind: "box",
522
+ bones: [],
523
+ center,
524
+ halfExtents,
525
+ yaw: Number.isFinite(shape.yaw) ? shape.yaw : 0,
526
+ // The slack rule reads `radius`; a box surface has none.
527
+ radius: 0,
528
+ };
529
+ }
530
+ return null;
531
+ }
532
+
533
+ /** The capsule whose START is a chain's root joint (shoulder / hip) — the
534
+ * anchor a box escape aims back toward. */
535
+ const CHAIN_ROOT_CAPSULE = {
536
+ leftHand: "leftUpperArm",
537
+ rightHand: "rightUpperArm",
538
+ leftFoot: "leftThigh",
539
+ rightFoot: "rightThigh",
540
+ };
541
+
542
+ function normalizeBlockers(blockers) {
543
+ if (!Array.isArray(blockers) || blockers.length === 0) return [];
544
+ const out = [];
545
+ for (const shape of blockers) {
546
+ const entry = blockerEntry(shape);
547
+ if (entry) out.push(entry);
548
+ }
549
+ return out;
550
+ }
551
+
552
+ /**
553
+ * Signed distance from `point` to an oriented box: positive outside, NEGATIVE
554
+ * inside (the depth to the nearest face). Writes the closest SURFACE point into
555
+ * `outSurface` and the outward surface normal there into `outNormal`.
556
+ *
557
+ * The box is axis-aligned once the yaw is undone, so the whole thing is a clamp
558
+ * in box space plus a rotation back.
559
+ */
560
+ const boxLocal = new THREE.Vector3();
561
+ const boxClamped = new THREE.Vector3();
562
+ const boxPrefer = new THREE.Vector3();
563
+ function boxSignedDistance(box, point, outSurface, outNormal, prefer = null) {
564
+ const cos = Math.cos(box.yaw);
565
+ const sin = Math.sin(box.yaw);
566
+ const dx = point.x - box.center.x;
567
+ const dy = point.y - box.center.y;
568
+ const dz = point.z - box.center.z;
569
+ // world → box: rotate by −yaw about Y.
570
+ boxLocal.set(cos * dx - sin * dz, dy, sin * dx + cos * dz);
571
+ const h = box.halfExtents;
572
+ const outward = (local) => {
573
+ // box → world.
574
+ outSurface.set(cos * local.x + sin * local.z, local.y, -sin * local.x + cos * local.z).add(box.center);
575
+ };
576
+ const outsideX = Math.abs(boxLocal.x) - h.x;
577
+ const outsideY = Math.abs(boxLocal.y) - h.y;
578
+ const outsideZ = Math.abs(boxLocal.z) - h.z;
579
+ if (outsideX > 0 || outsideY > 0 || outsideZ > 0) {
580
+ boxClamped.set(
581
+ Math.max(-h.x, Math.min(h.x, boxLocal.x)),
582
+ Math.max(-h.y, Math.min(h.y, boxLocal.y)),
583
+ Math.max(-h.z, Math.min(h.z, boxLocal.z)),
584
+ );
585
+ const distance = boxLocal.distanceTo(boxClamped);
586
+ outward(boxClamped);
587
+ if (distance > 1e-9) {
588
+ const nx = (boxLocal.x - boxClamped.x) / distance;
589
+ const ny = (boxLocal.y - boxClamped.y) / distance;
590
+ const nz = (boxLocal.z - boxClamped.z) / distance;
591
+ outNormal.set(cos * nx + sin * nz, ny, -sin * nx + cos * nz).normalize();
592
+ } else {
593
+ // Exactly ON the surface: fall through to the face normal.
594
+ const gaps = [h.x - Math.abs(boxLocal.x), h.y - Math.abs(boxLocal.y), h.z - Math.abs(boxLocal.z)];
595
+ const axis = gaps.indexOf(Math.min(...gaps));
596
+ const sign = [boxLocal.x, boxLocal.y, boxLocal.z][axis] < 0 ? -1 : 1;
597
+ const local = [0, 0, 0];
598
+ local[axis] = sign;
599
+ outNormal.set(cos * local[0] + sin * local[2], local[1], -sin * local[0] + cos * local[2]).normalize();
600
+ }
601
+ return distance;
602
+ }
603
+ // Inside: THE EXIT FACE, which is not always the nearest one.
604
+ //
605
+ // A limb buried in a crate is nearest to whichever face it happened to sink
606
+ // past, and for a deep hit that is regularly the FAR one — a push out through
607
+ // it drags the whole arm through the box (QA measured 16 bones inside after a
608
+ // "fix"). So of the six faces the exit prefers one the limb's own ROOT is on:
609
+ // the shallowest whose outward normal points back toward the shoulder or hip.
610
+ // With no root hint — or with the root itself inside the box — the nearest
611
+ // face is still the answer, which is the plain signed distance.
612
+ //
613
+ // `prefer` therefore changes the DIRECTION and the depth of the escape, never
614
+ // the geometry: the plain nearest-face value is what the segment search below
615
+ // minimises (a max of linear functions, so convex, which is what makes the
616
+ // ternary search exact), and the preference is applied once, to the point that
617
+ // search returns.
618
+ const components = [boxLocal.x, boxLocal.y, boxLocal.z];
619
+ const extents = [h.x, h.y, h.z];
620
+ let bestAxis = 0;
621
+ let bestSign = 1;
622
+ let bestExit = Infinity;
623
+ let bestAligned = false;
624
+ for (let axis = 0; axis < 3; axis += 1) {
625
+ for (const sign of [-1, 1]) {
626
+ const exit = extents[axis] - sign * components[axis];
627
+ let aligned = false;
628
+ if (prefer) {
629
+ boxPrefer.set(
630
+ cos * (axis === 0 ? sign : 0) + sin * (axis === 2 ? sign : 0),
631
+ axis === 1 ? sign : 0,
632
+ -sin * (axis === 0 ? sign : 0) + cos * (axis === 2 ? sign : 0),
633
+ );
634
+ aligned = boxPrefer.dot(prefer) > 0;
635
+ }
636
+ // An aligned face beats an unaligned one outright; within a class the
637
+ // shallower exit wins.
638
+ if ((aligned && !bestAligned) || (aligned === bestAligned && exit < bestExit)) {
639
+ bestAxis = axis;
640
+ bestSign = sign;
641
+ bestExit = exit;
642
+ bestAligned = aligned;
643
+ }
644
+ }
645
+ }
646
+ boxClamped.copy(boxLocal);
647
+ if (bestAxis === 0) boxClamped.x = bestSign * h.x;
648
+ else if (bestAxis === 1) boxClamped.y = bestSign * h.y;
649
+ else boxClamped.z = bestSign * h.z;
650
+ outward(boxClamped);
651
+ outNormal.set(
652
+ cos * (bestAxis === 0 ? bestSign : 0) + sin * (bestAxis === 2 ? bestSign : 0),
653
+ bestAxis === 1 ? bestSign : 0,
654
+ -sin * (bestAxis === 0 ? bestSign : 0) + cos * (bestAxis === 2 ? bestSign : 0),
655
+ ).normalize();
656
+ return -bestExit;
657
+ }
658
+
659
+ /** How many ternary-search steps the segment/box solve takes. Each step keeps
660
+ * 2/3 of the interval, so 48 of them pin the parameter to ~1e-8 of the segment
661
+ * — far under the millimetre the depths are reported in. */
662
+ const BOX_SEARCH_STEPS = 48;
663
+
664
+ /**
665
+ * Closest approach between a segment and an oriented box. The distance from a
666
+ * point to a convex set is convex along a line, and the SIGNED distance of a
667
+ * convex set is convex too (which is what makes the inside case work), so a
668
+ * ternary search on the parameter finds the minimum without any case analysis of
669
+ * edges, faces and corners.
670
+ *
671
+ * Writes the segment point into `outA` and the box's surface point into `outB`,
672
+ * with the outward surface normal in `outNormal`, and returns the SIGNED
673
+ * distance (negative when the segment runs through the box).
674
+ */
675
+ const boxProbe = new THREE.Vector3();
676
+ const boxSurface = new THREE.Vector3();
677
+ const boxNormal = new THREE.Vector3();
678
+ function closestSegmentBox(p, q, box, outA, outB, outNormal, prefer = null) {
679
+ const at = (t) => {
680
+ boxProbe.copy(p).lerp(q, t);
681
+ return boxSignedDistance(box, boxProbe, boxSurface, boxNormal);
682
+ };
683
+ let lo = 0;
684
+ let hi = 1;
685
+ for (let step = 0; step < BOX_SEARCH_STEPS; step += 1) {
686
+ const third = (hi - lo) / 3;
687
+ const m1 = lo + third;
688
+ const m2 = hi - third;
689
+ if (at(m1) < at(m2)) hi = m2;
690
+ else lo = m1;
691
+ }
692
+ const t = (lo + hi) / 2;
693
+ let distance = at(t);
694
+ if (distance < 0 && prefer) {
695
+ // Deepest point found; now pick the face it leaves by.
696
+ boxProbe.copy(p).lerp(q, t);
697
+ distance = boxSignedDistance(box, boxProbe, boxSurface, boxNormal, prefer);
698
+ }
699
+ outA.copy(p).lerp(q, t);
700
+ outB.copy(boxSurface);
701
+ outNormal.copy(boxNormal);
702
+ return distance;
703
+ }
704
+
705
+ /**
706
+ * One capsule against one blocker. Returns { gap, normal, pointA, pointB } with
707
+ * `gap` the signed distance from the capsule's AXIS to the blocker's SURFACE and
708
+ * `normal` pointing from the blocker toward the capsule — the same b → a
709
+ * convention detectPenetrations uses everywhere else.
710
+ */
711
+ const blockNormal = new THREE.Vector3();
712
+ const blockPrefer = new THREE.Vector3();
713
+ function blockerContact(capsule, blocker, outA, outB, rootAnchor = null) {
714
+ if (blocker.kind === "box") {
715
+ // The way OUT of a box is the way the limb came in, and the limb's root is
716
+ // the only anchor that knows which way that was.
717
+ let prefer = null;
718
+ if (rootAnchor) {
719
+ blockPrefer.copy(rootAnchor).sub(capsule.a.clone().lerp(capsule.b, 0.5));
720
+ if (blockPrefer.lengthSq() > 1e-12) prefer = blockPrefer.normalize();
721
+ }
722
+ const gap = closestSegmentBox(capsule.a, capsule.b, blocker, outA, outB, blockNormal, prefer);
723
+ return { gap, normal: blockNormal.clone() };
724
+ }
725
+ closestPointsSegmentSegment(capsule.a, capsule.b, blocker.a, blocker.b, outA, outB);
726
+ const axisGap = outA.distanceTo(outB);
727
+ const normal = outA.clone().sub(outB);
728
+ if (normal.lengthSq() < 1e-12) {
729
+ normal.set(0, 0, 1).cross(capsule.b.clone().sub(capsule.a));
730
+ if (normal.lengthSq() < 1e-12) normal.set(0, 1, 0);
731
+ }
732
+ normal.normalize();
733
+ // Report the contact ON the blocker's surface, not on its axis.
734
+ outB.addScaledVector(normal, blocker.radius);
735
+ return { gap: axisGap - blocker.radius, normal };
736
+ }
737
+
738
+ /**
739
+ * Find every penetrating capsule pair in the current pose. Returns records
740
+ * { a, b, depth, normal, pointA, pointB } sorted deepest-first. `normal`
741
+ * points from b toward a. `offset` inflates the contact distance HERE, in
742
+ * the test itself — a pair counts as penetrating once it is closer than
743
+ * rA + rB + offset — so the same gap the fixer pushes out to is the gap it
744
+ * checks for. Honouring offset only in the push distance would make the
745
+ * fixer chase a target detection never asked for, and the very gap it just
746
+ * opened would read as clean at exactly the surface-grazing pose it exists
747
+ * to avoid.
748
+ *
749
+ * SLACK: capsules are rigid proxies for soft tissue — an upper arm resting
750
+ * against the torso, or thighs touching, legitimately overlaps the proxies.
751
+ * A pair only counts once it penetrates past `slackFactor × min(radius)`,
752
+ * the soft-tissue compression allowance; `depth` is the amount PAST that
753
+ * allowance, which is also the distance the fixer pushes.
754
+ *
755
+ * The default is 0.25, and it used to be 0.4. The looser value was doing two
756
+ * jobs: covering genuine soft-tissue compression AND covering the overlaps a
757
+ * character legitimately holds at rest, which the proxies cannot tell from a
758
+ * real hit. The rest/bind calibration below now does the second job precisely,
759
+ * per pair, so the global allowance no longer has to be blunt enough for the
760
+ * worst pair on the rig — and at 0.4 it hid a centimetre and a half of forearm
761
+ * inside a thigh. (Loosening it further was never an option either: at 0.6 a
762
+ * hand sunk most of the way into a thigh still fit inside the allowance and the
763
+ * tool answered "no collisions".)
764
+ *
765
+ * HINGED PAIRS use their own, wider allowance instead — see HINGE_SLACK_FACTOR —
766
+ * because two segments meeting at a joint are in flesh-to-flesh contact at any
767
+ * deep fold. BLOCKERS use the plain rule, with a box's missing radius standing
768
+ * in as the movable capsule's own.
769
+ *
770
+ * BIND CALIBRATION: on top of the global slack, a pair that already overlaps in
771
+ * the rig's bind pose gets that overlap plus PAIR_TOLERANCE as its own
772
+ * allowance (see restPairOverlaps), so a rest or bind pose can never report a
773
+ * collision. `pairAllowances` overrides the map buildCollisionCapsules attached;
774
+ * pass null to measure a pose against nothing but the slack rule.
775
+ *
776
+ * `blockers` adds the world-space static shapes described under EXTERNAL
777
+ * BLOCKERS; they are tested against every MOVABLE capsule and named `obj:<id>`
778
+ * in the records.
779
+ */
780
+ export function detectPenetrations(capsules, {
781
+ offset = 0.002,
782
+ slackFactor = DEFAULT_SLACK_FACTOR,
783
+ pairAllowances,
784
+ blockers = null,
785
+ } = {}) {
786
+ if (!capsules) return [];
787
+ const calibration = pairAllowances === undefined ? (capsules.pairAllowances ?? null) : pairAllowances;
788
+ const list = [...capsules.values()];
789
+ const out = [];
790
+ const pa = new THREE.Vector3();
791
+ const pb = new THREE.Vector3();
792
+ for (let i = 0; i < list.length; i += 1) {
793
+ for (let j = i + 1; j < list.length; j += 1) {
794
+ const ca = list[i];
795
+ const cb = list[j];
796
+ const geo = pairGeometry(ca, cb);
797
+ if (!geo) continue;
798
+ const hinge = geo.hinge;
799
+ closestPointsSegmentSegment(geo.a0, geo.a1, geo.b0, geo.b1, pa, pb);
800
+ const overlap = ca.radius + cb.radius + offset - pa.distanceTo(pb);
801
+ const slack = hinge
802
+ ? HINGE_SLACK_FACTOR * (ca.radius + cb.radius)
803
+ : slackFactor * Math.min(ca.radius, cb.radius);
804
+ const bindOverlap = calibration?.get(pairKey(ca.def.id, cb.def.id)) ?? 0;
805
+ const allowance = Math.max(slack, bindOverlap > 0 ? bindOverlap + PAIR_TOLERANCE : 0);
806
+ const depth = overlap - allowance;
807
+ // MIN_DEPTH, not epsilon: see the constant. A pair under it is not a
808
+ // contact anyone can see and not one worth moving a limb for.
809
+ if (depth < MIN_DEPTH) continue;
810
+ const normal = pa.clone().sub(pb);
811
+ if (normal.lengthSq() < 1e-12) {
812
+ // Coincident axes: any perpendicular direction separates them.
813
+ normal.set(0, 0, 1).cross(ca.b.clone().sub(ca.a));
814
+ if (normal.lengthSq() < 1e-12) normal.set(0, 1, 0);
815
+ }
816
+ normal.normalize();
817
+ out.push({ a: ca, b: cb, depth, normal, pointA: pa.clone(), pointB: pb.clone() });
818
+ }
819
+ }
820
+ // External blockers: only MOVABLE capsules are worth testing — a blocker
821
+ // against the torso is two immovable things touching, which no pass can fix.
822
+ const shapes = normalizeBlockers(blockers);
823
+ for (const blocker of shapes) {
824
+ for (const capsule of list) {
825
+ if (!capsule.def.movable) continue;
826
+ const rootId = CHAIN_ROOT_CAPSULE[capsule.def.movable.chain];
827
+ const rootAnchor = rootId ? capsules.get(rootId)?.a ?? null : null;
828
+ const contact = blockerContact(capsule, blocker, pa, pb, rootAnchor);
829
+ const overlap = capsule.radius + offset - contact.gap;
830
+ // A box has no radius to take the min against, so the limb's own
831
+ // slack is the whole allowance. No rest calibration: the character's
832
+ // reference poses say nothing about where the set dressing is.
833
+ const slack = slackFactor * (blocker.kind === "box"
834
+ ? capsule.radius
835
+ : Math.min(capsule.radius, blocker.radius));
836
+ const depth = overlap - slack;
837
+ if (depth < MIN_DEPTH) continue;
838
+ out.push({
839
+ a: capsule,
840
+ b: blocker,
841
+ depth,
842
+ normal: contact.normal.clone(),
843
+ pointA: pa.clone(),
844
+ pointB: pb.clone(),
845
+ });
846
+ }
847
+ }
848
+ out.sort((x, y) => y.depth - x.depth);
849
+ return out;
850
+ }