cozyclay 2.1.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +60 -0
- package/README.md +12 -30
- package/bin/agent/agent-routes.mjs +40 -113
- package/bin/agent/agent-runner.mjs +9 -10
- package/bin/agent/cliproxy-provider.mjs +135 -0
- package/bin/agent/credential-store.mjs +30 -7
- package/bin/agent/pi-tools.mjs +4 -1
- package/bin/agent/provider-keys.mjs +32 -0
- package/bin/agent/provider-login.mjs +110 -0
- package/bin/agent/provider-routes.mjs +99 -0
- package/bin/agent/providers.mjs +19 -97
- package/bin/agent/script-tool.mjs +17 -4
- package/bin/agent/studio-motion-reference.mjs +23 -0
- package/bin/agent/studio-prompt.mjs +1 -1
- package/bin/agent/studio-rebase-text.mjs +32 -0
- package/bin/agent/studio-rebase.mjs +26 -39
- package/bin/agent/studio-tools.mjs +9 -28
- package/bin/agent/tool-arguments.mjs +49 -0
- package/bin/agent/turn-frames.mjs +66 -0
- package/bin/codex-auth.mjs +9 -2
- package/bin/cozyclay.mjs +10 -116
- package/bin/live/cli.mjs +3 -3
- package/bin/mcp-runtime.mjs +2 -12
- package/bin/package-signature.mjs +0 -4
- package/dist/app/index.html +7 -6
- package/dist/assets/EXRLoader-CvVIvGVE.js +1 -0
- package/dist/assets/analytics-C9sUnndd.js +1 -0
- package/dist/assets/app-CCh--1Hc.js +5087 -0
- package/dist/assets/app-f-fkV5uP.css +1 -0
- package/dist/assets/compile-worker-CkxiD6lH.js +5 -0
- package/dist/assets/fflate.module-KbGj5-H6.js +5 -0
- package/dist/assets/{first-shot-handoff-msLI71g1.js → first-shot-handoff-CC4xe4HM.js} +1 -1
- package/dist/assets/{landing-ChUvB_zL.js → landing-DjHD8u7k.js} +1 -1
- package/dist/assets/scene-asset-sync-Hx_FzdOH.js +20 -0
- package/dist/assets/scene-asset-sync-_S8NQl5B.css +1 -0
- package/dist/assets/workflow-C4yKH1p4.js +179 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/index.html +3 -3
- package/dist/sw.js +3 -6
- package/dist/workflow/index.html +5 -4
- package/mcp/LIVE-PROTOCOL.md +7 -47
- package/mcp/README.md +16 -26
- package/mcp/live-hub.mjs +6 -10
- package/mcp/package.json +1 -3
- package/mcp/tool-handlers.mjs +4 -168
- package/package.json +3 -22
- package/src/App.jsx +150 -2624
- package/src/agent-progress.js +79 -0
- package/src/agent-turn-mark.js +30 -0
- package/src/analytics.js +1 -19
- package/src/app-context.js +5 -0
- package/src/app-stage.jsx +2 -34
- package/src/asset-pane.jsx +6 -5
- package/src/camera-block.js +21 -0
- package/src/camera-direction.js +120 -0
- package/src/character-kind.js +3 -9
- package/src/command-bus.js +8 -6
- package/src/commands/cast.js +5 -34
- package/src/commands/elements/character.js +1 -1
- package/src/commands/elements/motion.js +1 -6
- package/src/commands/elements/object.js +6 -2
- package/src/commands/elements/shot.js +4 -1
- package/src/commands/elements.js +38 -14
- package/src/commands/index.js +3 -1
- package/src/commands/kits.js +37 -0
- package/src/commands/motion-timing.js +57 -0
- package/src/commands/motion.js +9 -152
- package/src/commands/motion3-compile.js +65 -0
- package/src/commands/motion3.js +207 -0
- package/src/commands/objects.js +6 -4
- package/src/commands/shot.js +49 -11
- package/src/commands/stage.js +14 -1
- package/src/commands/view.js +4 -1
- package/src/document-mirrors.js +42 -0
- package/src/document-store.js +4 -0
- package/src/domains/cast.js +19 -194
- package/src/domains/motion/motion-range-pin-rules.js +1 -1
- package/src/domains/motion/take-source.js +51 -0
- package/src/domains/motion.js +60 -1838
- package/src/domains/scenes.js +12 -55
- package/src/domains/shots.js +2 -2
- package/src/domains/stage.js +4 -2
- package/src/draft-ghost.js +46 -0
- package/src/draft-ghost.jsx +129 -0
- package/src/dualview.jsx +4 -3
- package/src/effects/EffectsLayer.jsx +112 -0
- package/src/effects/effect-meshes.js +232 -0
- package/src/effects/effect-record.js +103 -0
- package/src/effects/emitter.js +59 -0
- package/src/effects/simulate.js +261 -0
- package/src/effects/state.js +59 -0
- package/src/execution-telemetry.js +2 -3
- package/src/first-success-guide.jsx +5 -5
- package/src/ghost-set.js +257 -0
- package/src/held-object-edit.jsx +113 -0
- package/src/held-object-pose.js +175 -0
- package/src/hierarchy-model.js +1 -1
- package/src/hierarchy-panel.jsx +1 -1
- package/src/imported-mesh.jsx +103 -0
- package/src/kits/held.js +84 -0
- package/src/kits/index.js +70 -0
- package/src/kits/smoking.js +57 -0
- package/src/light-blockers.js +93 -0
- package/src/motion/beat-board-model.js +84 -0
- package/src/motion/beat-board.css +124 -0
- package/src/motion/beat-board.jsx +59 -0
- package/src/motion/center-of-mass.js +66 -0
- package/src/motion/fix-collisions.js +850 -0
- package/src/motion/physics-review.js +118 -0
- package/src/{ardy → motion}/physics-support.js +1 -76
- package/src/motion/rig-snapshot.js +22 -0
- package/src/motion/take-segments.jsx +140 -0
- package/src/{ardy → motion}/timeline-coordinates.js +0 -21
- package/src/{ardy → motion}/timeline.css +1 -2
- package/src/{ardy → motion}/timeline.jsx +42 -413
- package/src/motion/waypoints.js +76 -0
- package/src/motion3/beats.js +83 -0
- package/src/motion3/body.js +158 -0
- package/src/motion3/bones.js +47 -0
- package/src/motion3/carried.js +47 -0
- package/src/motion3/compile-job.js +75 -0
- package/src/motion3/compile-worker.js +10 -0
- package/src/motion3/compiler/anchors.js +105 -0
- package/src/motion3/compiler/arm-orient.js +128 -0
- package/src/motion3/compiler/channels.js +125 -0
- package/src/motion3/compiler/compile.js +205 -0
- package/src/motion3/compiler/gait-rhythm.js +97 -0
- package/src/motion3/compiler/grip.js +171 -0
- package/src/motion3/compiler/head-steady.js +48 -0
- package/src/motion3/compiler/hold.js +134 -0
- package/src/motion3/compiler/idle.js +113 -0
- package/src/motion3/compiler/limbs.js +170 -0
- package/src/motion3/compiler/math.js +72 -0
- package/src/motion3/compiler/pose.js +162 -0
- package/src/motion3/compiler/quat.js +49 -0
- package/src/motion3/compiler/skeleton.js +74 -0
- package/src/motion3/compiler/validate-common.js +32 -0
- package/src/motion3/compiler/validate-goals.js +118 -0
- package/src/motion3/compiler/validate-objects.js +143 -0
- package/src/motion3/compiler/validate-parts.js +116 -0
- package/src/motion3/compiler/validate.js +122 -0
- package/src/motion3/compiler/vocab.js +73 -0
- package/src/motion3/compiler/walk-pelvis.js +275 -0
- package/src/motion3/compiler/walk.js +207 -0
- package/src/motion3/compiler/wrist-clamp.js +32 -0
- package/src/motion3/delta.js +47 -0
- package/src/motion3/describe.js +174 -0
- package/src/motion3/docs.js +27 -0
- package/src/motion3/edit-carry.js +21 -0
- package/src/motion3/fix/ease.js +16 -0
- package/src/motion3/fix/ground.js +37 -0
- package/src/motion3/fix/index.js +52 -0
- package/src/motion3/fix/push.js +71 -0
- package/src/motion3/fix/reach.js +46 -0
- package/src/motion3/fix/retime.js +79 -0
- package/src/motion3/fix/self.js +211 -0
- package/src/motion3/fix/slide.js +70 -0
- package/src/motion3/fix/turn.js +51 -0
- package/src/motion3/follow.js +39 -0
- package/src/motion3/free-motion.js +173 -0
- package/src/motion3/grip-choice.js +130 -0
- package/src/motion3/hold-plan.js +157 -0
- package/src/motion3/json-patch.js +77 -0
- package/src/motion3/key-aim.js +105 -0
- package/src/motion3/key-edit.js +82 -0
- package/src/motion3/lenient.js +127 -0
- package/src/motion3/object-keys.js +236 -0
- package/src/motion3/object-pose.js +182 -0
- package/src/motion3/object-tracks.js +12 -0
- package/src/motion3/pipeline.js +102 -0
- package/src/motion3/playback.js +51 -0
- package/src/motion3/probes.js +91 -0
- package/src/motion3/qa/body-checks.js +34 -0
- package/src/motion3/qa/check-names.js +14 -0
- package/src/motion3/qa/checks.js +121 -0
- package/src/motion3/qa/contact-checks.js +51 -0
- package/src/motion3/qa/explain.js +80 -0
- package/src/motion3/qa/fmt.js +5 -0
- package/src/motion3/qa/foot-checks.js +72 -0
- package/src/motion3/qa/geometry.js +77 -0
- package/src/motion3/qa/goals.js +191 -0
- package/src/motion3/qa/ground.js +60 -0
- package/src/motion3/qa/hints-contact.js +179 -0
- package/src/motion3/qa/hints-goal.js +160 -0
- package/src/motion3/qa/hints-knee.js +24 -0
- package/src/motion3/qa/hints-motion.js +209 -0
- package/src/motion3/qa/hints-object.js +91 -0
- package/src/motion3/qa/index.js +42 -0
- package/src/motion3/qa/key-checks.js +82 -0
- package/src/motion3/qa/near.js +96 -0
- package/src/motion3/qa/object-checks.js +135 -0
- package/src/motion3/qa/patch.js +161 -0
- package/src/motion3/qa/severity.js +38 -0
- package/src/motion3/qa/spec-checks.js +65 -0
- package/src/motion3/qa/style.js +218 -0
- package/src/motion3/qa/wrist-checks.js +69 -0
- package/src/motion3/qa/wrist-obs.js +60 -0
- package/src/motion3/refs.js +120 -0
- package/src/motion3/repair-tune.js +121 -0
- package/src/motion3/repair.js +83 -0
- package/src/motion3/report.js +67 -0
- package/src/motion3/rigs/index.js +14 -0
- package/src/motion3/rigs/x-bot-tpose-body.json +1 -0
- package/src/motion3/rigs/x-bot-tpose.json +1 -0
- package/src/motion3/rigs/y-bot-tpose-body.json +1 -0
- package/src/motion3/rigs/y-bot-tpose.json +1 -0
- package/src/motion3/scene.js +60 -0
- package/src/motion3/schema/brief.js +89 -0
- package/src/motion3/schema/fields.js +44 -0
- package/src/motion3/schema/spec-examples.js +46 -0
- package/src/motion3/schema/topics.js +111 -0
- package/src/motion3/schema/types.js +147 -0
- package/src/motion3/search.js +117 -0
- package/src/motion3/second-hand.js +112 -0
- package/src/motion3/skills/basic.js +82 -0
- package/src/motion3/skills/book.js +76 -0
- package/src/motion3/skills/car.js +145 -0
- package/src/motion3/skills/frames-auto.js +29 -0
- package/src/motion3/skills/index.js +83 -0
- package/src/motion3/skills/info.js +65 -0
- package/src/motion3/skills/smoke-kit.js +109 -0
- package/src/motion3/skills/smoke.js +160 -0
- package/src/motion3/spec-store.js +59 -0
- package/src/motion3/take-edit.js +155 -0
- package/src/motion3/take.js +63 -0
- package/src/motion3/timeline.js +127 -0
- package/src/motion3/views.js +121 -0
- package/src/motion3/walk-plan.js +119 -0
- package/src/object-catalog.jsx +7 -1
- package/src/object-colors.js +98 -0
- package/src/object-gizmo-parts.js +115 -0
- package/src/object-gizmo.jsx +37 -122
- package/src/object-glow.js +139 -0
- package/src/object-hidden.js +33 -0
- package/src/object-look.js +196 -0
- package/src/object-size.js +10 -0
- package/src/object-travel.js +71 -6
- package/src/panels/CameraPanel.jsx +7 -7
- package/src/panels/HdrEnvironmentControls.jsx +59 -0
- package/src/panels/LightPanel.jsx +25 -4
- package/src/panels/ObjectLookPanel.jsx +88 -0
- package/src/panels/PosePanel.jsx +2 -17
- package/src/panels/RigControlPanel.jsx +5 -53
- package/src/panels/SubjectBox.jsx +4 -1
- package/src/panels/details.css +14 -0
- package/src/posestudio.jsx +4 -27
- package/src/project-browser.jsx +3 -4
- package/src/project-resources.js +18 -31
- package/src/project.js +13 -104
- package/src/props.jsx +25 -280
- package/src/rail-dot-drag.js +23 -0
- package/src/render-passes.js +10 -0
- package/src/resource-status.jsx +1 -0
- package/src/result-modal.jsx +4 -19
- package/src/room.jsx +20 -12
- package/src/scene-assets.js +8 -6
- package/src/scene-light-place.js +72 -0
- package/src/scene-lights.jsx +148 -0
- package/src/scene-objects.js +31 -139
- package/src/scenes.js +15 -105
- package/src/semantic-edit.js +2 -5
- package/src/set-pieces.jsx +183 -0
- package/src/settings-menu.jsx +3 -11
- package/src/shell/BottomDock.jsx +9 -51
- package/src/shell/DetailsSlot.jsx +16 -200
- package/src/shell/GhostsMenuGroup.jsx +74 -0
- package/src/shell/LibrarySlot.jsx +1 -3
- package/src/shell/MenuBar.jsx +3 -3
- package/src/shell/PreferencesDialog.jsx +15 -264
- package/src/shell/PreferencesSlot.jsx +2 -8
- package/src/shell/TopBar.jsx +5 -84
- package/src/shell/ViewportToolbar.jsx +10 -70
- package/src/shell/agent-glass.css +19 -6
- package/src/shell/details-selection.js +42 -0
- package/src/shell/ghosts-menu.css +22 -0
- package/src/shell/glass-regions.css +29 -3
- package/src/shell/preferences.css +6 -1
- package/src/shot-authoring.js +4 -7
- package/src/soft-shadows.js +37 -0
- package/src/stage-attach-frames.js +12 -12
- package/src/stage-canvas-rigs.jsx +3 -3
- package/src/stage-capture.jsx +42 -8
- package/src/stage-characters.jsx +22 -216
- package/src/stage-editor-gizmos.jsx +6 -6
- package/src/stage-environment.js +93 -0
- package/src/stage-hdr-builtins.js +107 -0
- package/src/stage-hdr-image.js +116 -0
- package/src/stage-hdr-library.js +70 -0
- package/src/stage-hdr-map.js +94 -0
- package/src/stage-hdr.js +130 -0
- package/src/stage-lighting.js +149 -0
- package/src/stage-occlusion.js +210 -0
- package/src/stage-sky.jsx +159 -0
- package/src/stage-startup-defaults.js +5 -9
- package/src/studio-actions.js +12 -14
- package/src/studio-agent-commands.js +19 -16
- package/src/studio-agent-motion.js +9 -9
- package/src/studio-agent-protocol.js +41 -71
- package/src/studio-app-binding.js +25 -45
- package/src/studio-asset-list.js +17 -0
- package/src/studio-contact-sheet.js +37 -5
- package/src/studio-elements.js +27 -7
- package/src/studio-entity-bounds.js +18 -0
- package/src/studio-error-codes.js +8 -0
- package/src/studio-export-framing.js +152 -0
- package/src/studio-lighting-check.js +103 -0
- package/src/studio-motion-visual.js +150 -0
- package/src/studio-schema-errors.js +80 -0
- package/src/studio-support-gap.js +74 -0
- package/src/styles.css +1 -19
- package/src/timeline-defaults.js +5 -0
- package/src/timeline-extent.js +8 -20
- package/src/tutorial-walk.js +17 -0
- package/src/ui-scrub.js +1 -1
- package/src/use-shot-camera-tracks.js +12 -0
- package/src/view-ghosts.js +97 -0
- package/src/view-ghosts.jsx +49 -0
- package/src/workflow/AgentPanel.jsx +47 -166
- package/src/workflow/AgentProgressCard.jsx +107 -0
- package/src/workflow/AgentReplyText.jsx +37 -0
- package/src/workflow/AgentTurnSteps.jsx +84 -0
- package/src/workflow/LoginPanel.jsx +140 -0
- package/src/workflow/WorkflowBuilder.jsx +7 -23
- package/src/workflow/agent-client.js +54 -51
- package/src/workflow/agent-panel.css +71 -1
- package/src/workflow/agent-progress-card.css +167 -0
- package/src/workflow/agent-progress-view.js +47 -0
- package/src/workflow/agent-turn-steps.css +113 -0
- package/src/workflow/agent-turn.js +191 -0
- package/src/workflow/login-client.js +34 -0
- package/src/workflow/motion-input.js +18 -31
- package/src/workflow/node-schema.js +1 -1
- package/src/workflow/reply-markdown.js +69 -0
- package/src/workflow/scene-asset-sync.js +8 -8
- package/tools/agent-session-stats.mjs +61 -0
- package/tools/dev-full.mjs +5 -56
- package/tools/eslint-baseline.json +12 -13
- package/tools/knip-baseline.json +0 -179
- package/tools/loc-baseline.json +31 -81
- package/tools/motion3/extract-body.mjs +103 -0
- package/tools/motion3/extract-rig.mjs +85 -0
- package/tools/process-supervisor.mjs +0 -88
- package/tools/run-tests.mjs +8 -124
- package/bin/agent/motion-runtime.mjs +0 -76
- package/dist/ardy/cskel27-rest.json +0 -557
- package/dist/assets/analytics-C9pMpW7s.js +0 -1
- package/dist/assets/app-B_2XB9rw.css +0 -1
- package/dist/assets/app-DAuvKPP8.js +0 -4920
- package/dist/assets/config-Bx7pYsD9.js +0 -1
- package/dist/assets/config-DeoFTYzb.css +0 -1
- package/dist/assets/demo-CvKuBgy5.js +0 -1
- package/dist/assets/scene-asset-sync-DKkpjKBV.js +0 -19
- package/dist/assets/scene-asset-sync-DjJ_cJ2R.css +0 -1
- package/dist/assets/ticket-BPk2Ni95.js +0 -1
- package/dist/assets/workflow-jJRBR94Z.js +0 -179
- package/dist/demo/walk-then-stop.npz +0 -0
- package/mcp/ardy-prompts.mjs +0 -218
- package/src/ardy/auto-fix-panel.css +0 -108
- package/src/ardy/auto-physics.js +0 -1194
- package/src/ardy/client.js +0 -153
- package/src/ardy/convert.js +0 -313
- package/src/ardy/cskel27-neutral.js +0 -39
- package/src/ardy/cskel27.js +0 -77
- package/src/ardy/export.js +0 -165
- package/src/ardy/fix-collisions.js +0 -2257
- package/src/ardy/foot-lock.js +0 -230
- package/src/ardy/motion-calibration.js +0 -118
- package/src/ardy/motion-edit.js +0 -213
- package/src/ardy/motion-url.js +0 -55
- package/src/ardy/mp4-duration.js +0 -102
- package/src/ardy/npz.js +0 -595
- package/src/ardy/physics-panel.css +0 -21
- package/src/ardy/physics-panel.jsx +0 -89
- package/src/ardy/physics-refine.js +0 -111
- package/src/ardy/physics-review.js +0 -567
- package/src/ardy/physics-temporal.js +0 -54
- package/src/ardy/platform-fit-panel.css +0 -64
- package/src/ardy/platform-fit-panel.jsx +0 -41
- package/src/ardy/platform-fit.js +0 -326
- package/src/ardy/playback.js +0 -711
- package/src/ardy/pose-pin.js +0 -97
- package/src/ardy/prompt-clips.js +0 -30
- package/src/ardy/retime.js +0 -211
- package/src/ardy/root-drop.js +0 -369
- package/src/ardy/timeline-resize.js +0 -13
- package/src/ardy/to-cskel27.js +0 -225
- package/src/ardy/trim.js +0 -33
- package/src/ardy/waypoints.js +0 -369
- package/src/ardy-bridge-contract.js +0 -281
- package/src/domains/motion/motion-video-capture.js +0 -664
- package/src/i2v-motion-client.js +0 -77
- package/src/i2v-motion-studio.jsx +0 -180
- package/src/line-edit.js +0 -1348
- package/src/motion/generation.js +0 -105
- package/src/motion-readiness-ui.jsx +0 -67
- package/src/motion-readiness.js +0 -40
- package/src/motion-resources.js +0 -177
- package/src/motion-store.js +0 -65
- package/src/motion-trail.js +0 -601
- package/src/multimodel-ingest.js +0 -394
- package/src/panels/PromptBlocksPanel.jsx +0 -444
- package/src/panels/VideoCapturePanel.jsx +0 -181
- package/src/pose-extract/browser.js +0 -42
- package/src/pose-extract/detector.js +0 -100
- package/src/pose-extract/fit.js +0 -227
- package/src/pose-extract/image-frame.js +0 -51
- package/src/pose-extract/index.js +0 -58
- package/src/pose-extract/keyframes.js +0 -74
- package/src/pose-extract/landmarks.js +0 -162
- package/src/pose-extract/mirror.js +0 -153
- package/src/pose-extract/take.js +0 -131
- package/src/pose-extract/video-frames.js +0 -91
- package/src/take-recipe.js +0 -165
- package/src/trail-key-conflicts.js +0 -39
- package/src/trail-pick.js +0 -59
- package/src/use-i2v-capture-modal.js +0 -15
- package/tools/ardy/BRIDGE.md +0 -37
- package/tools/ardy/GVHMR-NPZ.md +0 -68
- package/tools/ardy/README.md +0 -124
- package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
- package/tools/ardy/artifacts.mjs +0 -34
- package/tools/ardy/bridge.mjs +0 -1732
- package/tools/ardy/bvh-cskel27.mjs +0 -1265
- package/tools/ardy/cclay_constrained_generate.py +0 -561
- package/tools/ardy/cclay_gvhmr_worker.py +0 -264
- package/tools/ardy/cclay_motion_edit.py +0 -510
- package/tools/ardy/cclay_sequence_generate.py +0 -705
- package/tools/ardy/cclay_worker.py +0 -304
- package/tools/ardy/dump-npz.py +0 -205
- package/tools/ardy/extract-rest.mjs +0 -299
- package/tools/ardy/extract.mjs +0 -507
- package/tools/ardy/footage.mjs +0 -467
- package/tools/ardy/gvhmr-floor.mjs +0 -102
- package/tools/ardy/gvhmr-to-cskel27.mjs +0 -21
- package/tools/ardy/gvhmr_fastpath.py +0 -168
- package/tools/ardy/gvhmr_trajectory.py +0 -240
- package/tools/ardy/merge-text-encoder.py +0 -160
- package/tools/ardy/mocap-metrics.mjs +0 -200
- package/tools/ardy/mocap-quality-gate.mjs +0 -110
- package/tools/ardy/motion-stabilize.mjs +0 -348
- package/tools/ardy/npz.mjs +0 -414
- package/tools/ardy/pose-to-npz.mjs +0 -106
- package/tools/ardy/prompt-limits.mjs +0 -5
- package/tools/ardy/run-edit-on-box.sh +0 -73
- package/tools/ardy/run-local.mjs +0 -233
- package/tools/ardy/run-on-box.sh +0 -593
- package/tools/ardy/run-sequence-on-box.sh +0 -177
- package/tools/ardy/runners/gvhmr-worker.mjs +0 -206
- package/tools/ardy/runners/index.mjs +0 -18
- package/tools/ardy/runners/local.mjs +0 -455
- package/tools/ardy/runners/proc.mjs +0 -125
- package/tools/ardy/runners/remote.mjs +0 -252
- package/tools/ardy/setup-local.mjs +0 -189
- package/tools/ardy/setup-text-encoder-on-box.sh +0 -93
- package/tools/ardy/setup-text-encoder.py +0 -306
- package/tools/ardy/smpl-cskel27.mjs +0 -178
- package/tools/ardy/visual-qa.mjs +0 -196
- package/tools/ardy/vq-car.mjs +0 -52
- package/tools/bench/EXP3.md +0 -61
- package/tools/bench/cclay_bench_extract_incam.py +0 -125
- package/tools/bench/cclay_bench_extract_obs.py +0 -204
- package/tools/bench/cclay_bench_runner.py +0 -42
- package/tools/bench/cube-contact.mjs +0 -162
- package/tools/bench/exp3.mjs +0 -152
- package/tools/bench/extract-bench-lib.mjs +0 -216
- package/tools/bench/extract-bench.mjs +0 -302
- package/tools/bench/fal-generate.mjs +0 -88
- package/tools/bench/fit/README.md +0 -189
- package/tools/bench/fit/camera.mjs +0 -59
- package/tools/bench/fit/contact.mjs +0 -419
- package/tools/bench/fit/footlock.mjs +0 -234
- package/tools/bench/fit/motion.mjs +0 -85
- package/tools/bench/fit/pin.mjs +0 -27
- package/tools/bench/fit/remote.mjs +0 -105
- package/tools/bench/fit-bench.mjs +0 -126
- package/tools/bench/fit-sanity.mjs +0 -45
- package/tools/bench/metrics.mjs +0 -313
- package/tools/bench/obs/depth.mjs +0 -258
- package/tools/bench/obs/extrinsics.mjs +0 -175
- package/tools/bench/obs/fit_mannequin_betas.py +0 -340
- package/tools/bench/obs/ground.mjs +0 -294
- package/tools/bench/obs/heading.mjs +0 -151
- package/tools/bench/obs/ladder.mjs +0 -555
- package/tools/bench/obs/mannequin-betas.json +0 -124
- package/tools/bench/obs/remote.mjs +0 -83
- package/tools/bench/obs/rest_joints.py +0 -52
- package/tools/bench/obs/ybot-targets.mjs +0 -49
- package/tools/bench/obs-bench.mjs +0 -520
- package/tools/bench/score.mjs +0 -343
- package/tools/bench/summarize.mjs +0 -86
- package/tools/demo-worker/README.md +0 -137
- package/tools/demo-worker/api-client.mjs +0 -233
- package/tools/demo-worker/generate.mjs +0 -301
- package/tools/demo-worker/index.mjs +0 -466
- package/tools/demo-worker/sign-core.mjs +0 -112
- package/tools/demo-worker/sign.mjs +0 -161
- package/tools/gt-render/browser.mjs +0 -182
- package/tools/gt-render/camera-math.mjs +0 -276
- package/tools/gt-render/page.mjs +0 -267
- package/tools/gt-render/render.mjs +0 -514
- package/tools/gt-render/scene-box.mjs +0 -31
- package/tools/gt-render/take-transform.mjs +0 -87
- package/tools/kimodo/compare-seam.mjs +0 -108
- package/tools/kimodo/constraints.mjs +0 -244
- package/tools/kimodo/edit.mjs +0 -259
- package/tools/kimodo/effector-constraints.mjs +0 -200
- package/tools/kimodo/generate.mjs +0 -646
- package/tools/kimodo/local-output.mjs +0 -265
- package/tools/kimodo/measure-pose.mjs +0 -65
- package/tools/kimodo/measure-preserve.mjs +0 -899
- package/tools/kimodo/measure-waypoints.mjs +0 -55
- package/tools/kimodo/mlx-generate.py +0 -111
- package/tools/kimodo/pose-constraints.mjs +0 -248
- package/tools/kimodo/preserve-mask.mjs +0 -543
- package/tools/kimodo/read-npz.mjs +0 -189
- package/tools/kimodo/run-edit-on-box.mjs +0 -155
- package/tools/kimodo/run-sequence-on-box.mjs +0 -178
- package/tools/kimodo/runner.mjs +0 -284
- package/tools/kimodo/seam-batch.mjs +0 -77
- package/tools/kimodo/setup-local.mjs +0 -30
- package/tools/kimodo/setup-on-box.sh +0 -339
- package/tools/kimodo/soma77-to-cskel27.mjs +0 -305
- package/tools/morphgs/assets/gen_truth.py +0 -39
- package/tools/morphgs/assets/mesh_ori_rig.txt +0 -27
- package/tools/morphgs/assets/playback-check.mjs +0 -30
- package/tools/morphgs/demo-gate.sh +0 -26
- package/tools/morphgs/fbx2morphgs.mjs +0 -68
- package/tools/morphgs/morphgs-to-cskel27.mjs +0 -105
- package/tools/morphgs/patches/preprocess_src-none-mode.patch +0 -76
- package/tools/morphgs/setup-on-cluster.sh +0 -126
- package/tools/projflow/driver.py +0 -1069
- package/tools/projflow/generate.mjs +0 -805
- package/tools/projflow/hml22-to-cskel27.mjs +0 -735
- package/tools/projflow/line-edit-job.mjs +0 -547
- package/tools/projflow/replay.mjs +0 -452
- package/tools/projflow/runner.mjs +0 -269
- package/tools/projflow/service.mjs +0 -742
- package/tools/track/DESIGN.md +0 -513
- package/tools/track/backfill-provenance.mjs +0 -116
- package/tools/track/budget.mjs +0 -8
- package/tools/track/check-rig.mjs +0 -209
- package/tools/track/diagnostics.schema.json +0 -60
- package/tools/track/export-rig.mjs +0 -256
- package/tools/track/fallback.mjs +0 -7
- package/tools/track/fk-parity-fixture.mjs +0 -161
- package/tools/track/gate.mjs +0 -349
- package/tools/track/masks.mjs +0 -318
- package/tools/track/metrics.mjs +0 -201
- package/tools/track/publish-obs.mjs +0 -108
- package/tools/track/py/check_env.py +0 -61
- package/tools/track/py/eval_lr.py +0 -229
- package/tools/track/py/lr_viterbi.py +0 -294
- package/tools/track/py/masks.py +0 -405
- package/tools/track/py/objective.py +0 -382
- package/tools/track/py/rig.py +0 -508
- package/tools/track/py/scene.py +0 -326
- package/tools/track/py/test_joint_indices.py +0 -123
- package/tools/track/py/test_lr_viterbi.py +0 -195
- package/tools/track/py/test_masks.py +0 -229
- package/tools/track/py/test_rig.py +0 -288
- package/tools/track/py/test_scene.py +0 -354
- package/tools/track/py/test_track.py +0 -328
- package/tools/track/py/track.py +0 -411
- package/tools/track/remote.mjs +0 -140
- package/tools/track/rig-dump.mjs +0 -227
- package/tools/track/run-box-tests.mjs +0 -14
- package/tools/track/run-box.mjs +0 -168
- package/tools/track/setup-box.sh +0 -59
- package/tools/track/study-2d.mjs +0 -728
- /package/src/{ardy → motion}/collision-blockers.js +0 -0
- /package/src/{ardy → motion}/ground.js +0 -0
- /package/src/{ardy → motion}/ik-drag.js +0 -0
- /package/src/{ardy → motion}/ik-key-json.js +0 -0
- /package/src/{ardy → motion}/ik.js +0 -0
- /package/src/{ardy → motion}/physics-surface.js +0 -0
- /package/src/{ardy → motion}/playback-clock.js +0 -0
- /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
|
+
}
|