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,171 @@
|
|
|
1
|
+
// The hand a fit puts on a scene body takes a grip on it (lane wrist). A fit only places a point of the
|
|
2
|
+
// hand (reachGrip's fingers on the anchor); with nothing asking for the hand's turn, the wrist stayed
|
|
3
|
+
// straight on the forearm and kept whatever roll the arm left it, so the palm did not face what it held
|
|
4
|
+
// (a box side, a guitar neck: 0 deg of wrist on every holding frame, bench S10, session df388a9b), or
|
|
5
|
+
// the agent keyed wflex/wtwist to fix it at the wrist alone. Now, while a fit's weight is on and its
|
|
6
|
+
// anchor is on a body (an object anchor's body, or any body within NEAR m of the anchor):
|
|
7
|
+
// fingers stay on the forearm's line: no flex or deviation is asked (a power grip turns the
|
|
8
|
+
// forearm, not the wrist: the wrist stays in its comfortable middle)
|
|
9
|
+
// roll the hand turns about the forearm: on a thin body (two half sizes under THIN m: a can, a
|
|
10
|
+
// cup, a bar, a handle, a neck) the knuckle line takes the body's long axis, a cylindrical
|
|
11
|
+
// grasp with the body across the palm, at the end the forearm turns to least; on anything
|
|
12
|
+
// else the end-state-comfort thumb (people take the grip that ends comfortable, Rosenbaum):
|
|
13
|
+
// up when the forearm is level, toward the midline when it points down or up. The turn
|
|
14
|
+
// stops where the forearm's whole twist leaves its comfortable range (arm-orient.js
|
|
15
|
+
// WRIST_COMFORT, +-50 deg: Morrey's daily-task range); the forearm bone takes its share and
|
|
16
|
+
// the wrist the rest (hold.js orient), by the fit's weight
|
|
17
|
+
// point the fingers' point stays where the fit put it, the elbow in the plane the arm had
|
|
18
|
+
// The spec wins: a hand whose wtwist_<k> or wflex_<k> is turned on the frame keeps it, a fit with
|
|
19
|
+
// orient: false is left alone, and a prop anchor is the second-hand grip's (second-hand.js). A hand
|
|
20
|
+
// that carries something keeps the carry's own grip (its grab offset) once the carry leads it. A keyed
|
|
21
|
+
// carry's grab offset is read off the plain solve of its grab frame (object-keys.js attach), so when it
|
|
22
|
+
// carries a body that is not thin (a box, a crate) the carrying hand takes the grip on the reach, on
|
|
23
|
+
// that frame and up to its first key (ownGrip): the offset the carry keeps is then a grip, not the
|
|
24
|
+
// forearm's neutral roll. A thin body (a cup, a can) keeps the neutral offset: its aim keys pick the
|
|
25
|
+
// roll the arm holds most easily from it, and the cylindrical grip measured worse on every bench cup.
|
|
26
|
+
// Those frames read the carried body where it rested before the grab, never a pose the hand moves.
|
|
27
|
+
import { add, sub, scale, norm, cross, dot, len, apply, ry, mul, T, axisRot } from "./math.js";
|
|
28
|
+
import { palmAxes, twoBone } from "./limbs.js";
|
|
29
|
+
import { aimArm, orient } from "./hold.js";
|
|
30
|
+
import { ELBOW_REST, WRIST_COMFORT, wristNeed } from "./arm-orient.js";
|
|
31
|
+
import { resolveAnchor } from "./anchors.js";
|
|
32
|
+
import { sdBox } from "../qa/geometry.js";
|
|
33
|
+
import { partsOf } from "../object-pose.js";
|
|
34
|
+
|
|
35
|
+
const NEAR = 0.06, THIN = 0.045, ALIGN = 0.3, PREFER = 30, DEG = 180 / Math.PI, ITER = 2, UP = [0, 1, 0];
|
|
36
|
+
const cols = (a, b, c) => [[a[0], b[0], c[0]], [a[1], b[1], c[1]], [a[2], b[2], c[2]]];
|
|
37
|
+
const flat = (v, n) => sub(v, scale(n, dot(v, n)));
|
|
38
|
+
|
|
39
|
+
/** The fit of side k whose weight is on most at channels c (null when none), its weight times the arm's IK weight. */
|
|
40
|
+
function activeFit(spec, c, k) {
|
|
41
|
+
let best = null;
|
|
42
|
+
for (const fit of spec.fits ?? []) {
|
|
43
|
+
const w = c[`w_${fit.anchor}_${fit.side}`] ?? 0;
|
|
44
|
+
if (fit.side === k && fit.orient !== false && w > 1e-4 && (!best || w > best.w)) best = { fit, w };
|
|
45
|
+
}
|
|
46
|
+
return best && { ...best, w: best.w * Math.min(1, Math.max(0, c[`armw_${k}`] ?? 0)) };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The fit's effector point on pose s (anchors.js measureProps' rule for a prop point; the joints' mean otherwise). */
|
|
50
|
+
function effectorAt(sk, spec, s, side, eff) {
|
|
51
|
+
if (eff.prop) {
|
|
52
|
+
const prop = spec.props?.[eff.prop];
|
|
53
|
+
if (!prop) return null;
|
|
54
|
+
const ax = palmAxes(sk, s, side), o = scale(prop.origin.map(b => s.P[sk.index[b]]).reduce(add, [0, 0, 0]), 1 / prop.origin.length);
|
|
55
|
+
const [d, l, n] = eff.point === "origin" ? [0, 0, 0] : prop.points?.[eff.point] ?? [0, 0, 0];
|
|
56
|
+
return add(o, add(scale(ax.d, d), add(scale(ax.l, l), scale(ax.n, n))));
|
|
57
|
+
}
|
|
58
|
+
const js = eff.joints ?? [`${side}HandIndex2`, `${side}HandMiddle2`];
|
|
59
|
+
return scale(js.map(j => s.P[sk.index[j]]).reduce(add, [0, 0, 0]), 1 / js.length);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The carry entry over body id (its own or a group's) and the side whose prop carries it, else null. */
|
|
63
|
+
function carryOf(ctx, id) {
|
|
64
|
+
const { spec } = ctx, o = ctx.scene?.objects?.[id];
|
|
65
|
+
for (const k of [id, ...(o?.ancestors ?? [])]) { const e = spec.objects?.[k]; if (e?.prop) return { key: k, entry: e, side: spec.props?.[e.prop]?.hand ?? null }; }
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Whether hand k takes the grip on keyed carry `key` at frame f although it carries it (see above): a keyed
|
|
71
|
+
* carry that keeps its grab offset, from its reach to its grab frame and up to its first key. Answers the
|
|
72
|
+
* frame to read the carried body at (before the grab), or null.
|
|
73
|
+
*/
|
|
74
|
+
function ownGrip(ctx, key, k, f) {
|
|
75
|
+
const o = ctx.spec.objects?.[key];
|
|
76
|
+
const half = ctx.scene?.objects?.[key]?.half;
|
|
77
|
+
if (!ctx.hold?.span || !o?.prop || ctx.spec.props?.[o.prop]?.hand !== k || !o.keys?.length || o.grab === undefined || o.at !== undefined || o.along !== undefined || (half && thinAxis(half) >= 0)) return null;
|
|
78
|
+
const [grab] = ctx.hold.span(key);
|
|
79
|
+
return grab >= 1 && f <= Math.max(grab, ctx.hold.firstKey(key) - 1) ? Math.min(f, grab - 1) : null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The body box the grip is on: the anchor's object (its nearest part) or the nearest body within NEAR m; null when none. */
|
|
83
|
+
function bodyAt(ctx, f, k, A, anchor, now) {
|
|
84
|
+
const scene = ctx.scene?.objects ?? {}, ids = anchor?.kind === "object" ? partsOf(ctx.scene, anchor.object) : Object.keys(scene);
|
|
85
|
+
let best = null;
|
|
86
|
+
for (const id of ids) {
|
|
87
|
+
const half = scene[id]?.half;
|
|
88
|
+
if (!half) continue;
|
|
89
|
+
const own = ownGrip(ctx, carryOf(ctx, id)?.key, k, f), pose = ctx.objectPose?.(id, own ?? f, own === null ? now : null) ?? scene[id];
|
|
90
|
+
if (!pose?.R) continue;
|
|
91
|
+
const box = { pos: pose.pos, R: pose.R, half }, d = sdBox(A, box);
|
|
92
|
+
if (!best || d < best.d) best = { box, d, id };
|
|
93
|
+
}
|
|
94
|
+
// a body this hand carries keeps the carry's own grip (its grab offset, which its keys are written on:
|
|
95
|
+
// the same with keys or without), and no other body takes its place
|
|
96
|
+
const carry = best && carryOf(ctx, best.id);
|
|
97
|
+
if (!best || (carry?.side === k && ownGrip(ctx, carry.key, k, f) === null)) return null;
|
|
98
|
+
return anchor?.kind === "object" || best.d <= NEAR ? best.box : null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const AXES = [0, 1, 2];
|
|
102
|
+
/** The long axis of a thin body (two half sizes under THIN m: a can, a cup, a bar, a handle, a neck), else -1. */
|
|
103
|
+
function thinAxis(half) {
|
|
104
|
+
const long = AXES.reduce((a, i) => (half[i] > half[a] ? i : a), 0);
|
|
105
|
+
return AXES.filter(i => i !== long).every(i => half[i] < THIN) ? long : -1;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The hand's palm frame (columns d, l, n of limbs.js palmAxes) for the grip: the fingers stay on the
|
|
110
|
+
* forearm's line (no flex or deviation added: a power grip turns the forearm, not the wrist) and the
|
|
111
|
+
* hand turns about it. On a thin body the knuckle line takes the body's long axis (a cylindrical grasp:
|
|
112
|
+
* the can, the bar, the neck across the palm) with the thumb at its comfortable end; on anything else,
|
|
113
|
+
* or a thin body along the forearm, the end-state-comfort thumb: up when the forearm is level, toward
|
|
114
|
+
* the midline when it points down or up.
|
|
115
|
+
*/
|
|
116
|
+
function gripFrame(side, d, now, yawDeg, axis) {
|
|
117
|
+
const right = side === "Right", medial = apply(ry(yawDeg ?? 0), [right ? 1 : -1, 0, 0]), vy = Math.abs(d[1]);
|
|
118
|
+
const thumb = flat(add(scale(UP, 1 - vy), scale(medial, vy)), d), want = norm(len(thumb) > 1e-3 ? thumb : flat(medial, d));
|
|
119
|
+
// a thin body lies across the palm, the knuckle line along its long axis: of its two ends the thumb takes the
|
|
120
|
+
// one the forearm turns to least (the end-state-comfort thumb counts PREFER deg in its favour)
|
|
121
|
+
const across = axis ? flat(axis, d) : null;
|
|
122
|
+
if (!across || len(across) <= ALIGN) return cols(d, want, cross(d, want));
|
|
123
|
+
const turn = l => Math.acos(Math.max(-1, Math.min(1, dot(l, now)))) * DEG - (dot(l, want) > 0 ? PREFER : 0);
|
|
124
|
+
const a = norm(across), l = turn(a) <= turn(scale(a, -1)) ? a : scale(a, -1);
|
|
125
|
+
return cols(d, l, cross(d, l));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Frame F (columns d, l, n) turned about its own d so the forearm's whole twist (from `twist` deg now) stays within the comfortable range: the roll stops where the forearm is comfortable. */
|
|
129
|
+
function comfortable(F, now, twist) {
|
|
130
|
+
const d = [F[0][0], F[1][0], F[2][0]], l = [F[0][1], F[1][1], F[2][1]];
|
|
131
|
+
const want = Math.atan2(dot(cross(now, l), d), dot(now, l)) * DEG, [lo, hi] = WRIST_COMFORT.twist;
|
|
132
|
+
const got = Math.min(hi, Math.max(lo, twist + want)) - twist;
|
|
133
|
+
if (Math.abs(got - want) < 1e-6) return F;
|
|
134
|
+
const r = apply(axisRot(d, got), now);
|
|
135
|
+
return cols(d, r, cross(d, r));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* gripHand(ctx, sk, s, side, k, c, f, diag): turn the hand of a fit onto the body under its anchor
|
|
140
|
+
* (see above); a no-op when nothing asks for it. diag.grip_<k> = { w, twist, flex, dev, body }.
|
|
141
|
+
*/
|
|
142
|
+
export function gripHand(ctx, sk, s, side, k, c, f, diag) {
|
|
143
|
+
// a wrist the spec turns on this frame is the agent's (or a skill's) own; a neutral 0 asks for nothing
|
|
144
|
+
if (Math.abs(c[`wtwist_${k}`] ?? 0) > 1e-3 || Math.abs(c[`wflex_${k}`] ?? 0) > 1e-3) return;
|
|
145
|
+
const on = activeFit(ctx.spec, c, k);
|
|
146
|
+
if (!on || on.w < 1e-3) return;
|
|
147
|
+
// a hand that carries something on this frame holds that (its carry's grip), whatever its fit's anchor
|
|
148
|
+
if (Object.keys(ctx.spec.objects ?? {}).some(id => ctx.spec.objects[id]?.prop && ctx.objectHand?.(id, f) === k && ownGrip(ctx, id, k, f) === null)) return;
|
|
149
|
+
const now = { s, diag, c }, anchor = ctx.spec.anchors?.[on.fit.anchor];
|
|
150
|
+
// a point of a prop another hand holds is the second-hand grip's (second-hand.js)
|
|
151
|
+
if (anchor?.kind === "prop") return;
|
|
152
|
+
const A = add(resolveAnchor(ctx, on.fit.anchor, s, diag, c, k), on.fit.offset ?? [0, 0, 0]);
|
|
153
|
+
const box = bodyAt(ctx, f, k, A, anchor, now);
|
|
154
|
+
const eff = box && effectorAt(sk, ctx.spec, s, side, on.fit.effector ?? {});
|
|
155
|
+
if (!eff) return;
|
|
156
|
+
const measure = st => ({ pos: effectorAt(sk, ctx.spec, st, side, on.fit.effector ?? {}), R: (({ d, l, n }) => cols(d, l, n))(palmAxes(sk, st, side)) });
|
|
157
|
+
const I = sk.index, a = I[`${side}Arm`], fa = I[`${side}ForeArm`], h = I[`${side}Hand`];
|
|
158
|
+
const long = thinAxis(box.half), axis = long >= 0 ? apply(box.R, AXES.map(i => (i === long ? 1 : 0))) : null;
|
|
159
|
+
const ax = palmAxes(sk, s, side), roll = norm(flat(ax.l, ax.d));
|
|
160
|
+
const axT = norm(sub(s.P[h], s.P[fa])), neutral = mul(s.R[fa], mul(T(sk.restWorldRot[fa]), sk.restWorldRot[h]));
|
|
161
|
+
const twist = wristNeed(neutral, s.R[h], axT, apply(mul(neutral, T(s.R[h])), ax.l)).twist;
|
|
162
|
+
const palm = { pos: eff, R: comfortable(gripFrame(side, ax.d, roll, c.yaw, axis), roll, twist) };
|
|
163
|
+
const { goal, plane, wristAt } = aimArm(sk, s, side, palm, measure(s), on.w, "palm");
|
|
164
|
+
let need = { twist: 0, flex: 0, dev: 0 };
|
|
165
|
+
for (let i = 0; i < ITER; i++) {
|
|
166
|
+
// the elbow keeps the plane the arm had (bendPlane), the fingers' point goes back where the fit put it
|
|
167
|
+
twoBone(sk, s, a, fa, h, wristAt(i ? s.R[h] : goal), plane, ELBOW_REST, "elbow");
|
|
168
|
+
need = orient(sk, s, side, h, fa, goal);
|
|
169
|
+
}
|
|
170
|
+
diag[`grip_${k}`] = { w: on.w, ...need };
|
|
171
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// While a walk runs the head keeps the gaze steadier than the trunk: the trunk's quick sway (the root's and the
|
|
2
|
+
// spine's yaw, pitch and roll less their moving average over about a stride, so a turn, a lean or a bow the keys
|
|
3
|
+
// hold is not swayed against) is taken back by the head, a part of it (STEADY) and no more than LIMIT degrees; little
|
|
4
|
+
// of the pitch: a person's head nods with the trunk's dip at each step.
|
|
5
|
+
// Only from a walk's start to its last step (walk.js tags them "<tag> start", "<tag> step <n>"; the settle after
|
|
6
|
+
// holds still, and a take cut in it ends the same), faded in and out over FADE frames, and never on a head axis the
|
|
7
|
+
// spec's own keys move during that walk: a keyed look wins. The average holds the take's end value past its end.
|
|
8
|
+
const STEADY = { yaw: 0.5, pitch: 0.2, roll: 0.7 }, LIMIT = { yaw: 4, pitch: 6, roll: 10 }, WINDOW = 33, FADE = 8;
|
|
9
|
+
const AXES = ["yaw", "pitch", "roll"];
|
|
10
|
+
|
|
11
|
+
/** The walk spans in the keys, start to last step: [[f0, f1], ...]. */
|
|
12
|
+
function walkSpans(keys) {
|
|
13
|
+
const spans = new Map();
|
|
14
|
+
for (const k of keys) {
|
|
15
|
+
const m = / (start|step \d+)$/.exec(k.src ?? ""), tag = m && k.src.slice(0, -m[0].length);
|
|
16
|
+
if (!m || !/\bwalk(To)?$/.test(tag)) continue;
|
|
17
|
+
const span = spans.get(tag) ?? [Infinity, -Infinity];
|
|
18
|
+
spans.set(tag, [Math.min(span[0], k.f), Math.max(span[1], k.f)]);
|
|
19
|
+
}
|
|
20
|
+
return [...spans.values()].filter(([a, b]) => b > a);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const movingAverage = (xs, w) => {
|
|
24
|
+
const half = Math.floor(w / 2), at = i => xs[Math.max(0, Math.min(xs.length - 1, i))];
|
|
25
|
+
return xs.map((_, f) => { let s = 0; for (let i = f - half; i <= f + half; i++) s += at(i); return s / (2 * half + 1); });
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** Adds the head's share of the trunk's sway to the baked head channels inside every walk (in place). */
|
|
29
|
+
export function steadyHead(baked, keys, frameCount) {
|
|
30
|
+
const spans = walkSpans(keys);
|
|
31
|
+
if (!spans.length) return;
|
|
32
|
+
const series = name => baked[name] ?? new Array(frameCount).fill(0);
|
|
33
|
+
for (const axis of AXES) {
|
|
34
|
+
const spine = series(`spine_${axis}`), trunk = series(axis).map((v, f) => v + spine[f]);
|
|
35
|
+
const mean = movingAverage(trunk, WINDOW);
|
|
36
|
+
let head = null;
|
|
37
|
+
for (const [f0, f1] of spans) {
|
|
38
|
+
const keyed = keys.some(k => k.f >= f0 && k.f <= f1 && k.set?.[`head_${axis}`] !== undefined && !/\bwalk(To)? (start|step \d+|end)$/.test(k.src ?? ""));
|
|
39
|
+
if (keyed) continue;
|
|
40
|
+
head ??= series(`head_${axis}`).slice();
|
|
41
|
+
for (let f = Math.max(0, f0); f <= Math.min(frameCount - 1, f1); f++) {
|
|
42
|
+
const fade = Math.min(1, (f - f0) / FADE, (f1 - f) / FADE), sway = Math.max(-LIMIT[axis], Math.min(LIMIT[axis], trunk[f] - mean[f]));
|
|
43
|
+
head[f] -= STEADY[axis] * sway * fade * fade * (3 - 2 * fade);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (head) baked[`head_${axis}`] = head;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// The hand follows a held object (docs/motion-api-v3.1.md C14). On frames where
|
|
2
|
+
// spec.objects[id].keys lead (../object-keys.js), pose.js calls holdHand after the plain arm
|
|
3
|
+
// (IK or FK, wrist channels, fingers): the wrist target and the hand's orientation are solved so
|
|
4
|
+
// the grip (the prop's palm frame) lands on the pose the object keys ask for, hand = object o
|
|
5
|
+
// attach^-1. The whole arm turns the object (arm-orient.js, lane K8): the elbow swings about the
|
|
6
|
+
// shoulder-wrist axis to the pole that leaves the wrist least strained, the forearm takes its
|
|
7
|
+
// share of the turn about its own axis, and the wrist adds the rest within anatomical limits
|
|
8
|
+
// relative to the forearm: twist about the forearm, flex about the knuckle line, deviation toward
|
|
9
|
+
// the thumb (+, radial) or the little finger (-, ulnar). What the arm or the wrist cannot do is
|
|
10
|
+
// left as a residual: QA measures it on the object (qa/key-checks.js) and reads the reasons from
|
|
11
|
+
// diag.hold_<side>.
|
|
12
|
+
import { add, sub, scale, norm, dot, len, mul, T, apply, axisRot, angle, quatFromMat, matFromQuat } from "./math.js";
|
|
13
|
+
import { twoBone, palmAxes, carried, askedFold, reachAt, ELBOW_FOLD } from "./limbs.js";
|
|
14
|
+
import { rotateSubtree } from "./skeleton.js";
|
|
15
|
+
import { slerp } from "./quat.js";
|
|
16
|
+
import { bodyFor, segmentDepth } from "../body.js";
|
|
17
|
+
import { WRIST_LIMITS, FOREARM_SHARE, ELBOW_REST, wristNeed, swivel, landscape, shoulderPrice, comfortPrice } from "./arm-orient.js";
|
|
18
|
+
|
|
19
|
+
// STRAIGHT: a plain arm whose elbow sits less than this (m) off its shoulder-wrist midpoint is
|
|
20
|
+
// straight and has no bend plane of its own. A straight FK arm keeps 1 to 3 mm there from its bone
|
|
21
|
+
// lengths (Y Bot: the upper arm is 2 mm shorter than the forearm, so the elbow sits 1 mm up the
|
|
22
|
+
// arm), and that direction is no bend: as the swivel's reference it put the elbow "up", so the
|
|
23
|
+
// limit 100 deg from it ruled out an elbow pointing down (session d5c110a7's batting stance).
|
|
24
|
+
// ROLL_MARGIN: the share of each wrist half range by which holdCost tightens the limits, so the
|
|
25
|
+
// roll picked for a key leaves the frames between keys (they turn between the picks) some room.
|
|
26
|
+
// The fold price (holdCost): an arm folded near flat turns the hand any way at all, so without a price
|
|
27
|
+
// the roll of a stance key at the shoulder folded it (session d5c110a7: wrist 6-9 cm from the shoulder,
|
|
28
|
+
// 162 deg), and the frames between keys, where the wrist moves a few cm, broke away from it. twoBone
|
|
29
|
+
// now stops the fold (limbs.js ELBOW_FOLD, ELBOW_STOP); the price still steers the roll off it.
|
|
30
|
+
const ITERATIONS = 3, REACH_PRICE = 1, STRAIGHT = 0.005, ROLL_MARGIN = 0.15, BODY_PRICE = 4;
|
|
31
|
+
const clampTo = (v, [lo, hi]) => Math.min(hi, Math.max(lo, v));
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Turn the hand (and its fingers) about the wrist so its world rotation is `goal`, as far as the
|
|
35
|
+
* limits let it: the forearm bone turns FOREARM_SHARE of the twist about its own axis, the hand
|
|
36
|
+
* the rest plus flex and deviation. Returns what it needed, degrees { twist, flex, dev }.
|
|
37
|
+
*/
|
|
38
|
+
export function orient(sk, s, side, h, fa, goal) {
|
|
39
|
+
const axT = scale(sub(s.P[h], s.P[fa]), 1 / len(sub(s.P[h], s.P[fa]))), neutral = mul(s.R[fa], mul(T(sk.restWorldRot[fa]), sk.restWorldRot[h]));
|
|
40
|
+
const { axF, axD, ...need } = wristNeed(neutral, goal, axT, apply(mul(neutral, T(s.R[h])), palmAxes(sk, s, side).l));
|
|
41
|
+
const got = Object.fromEntries(Object.entries(need).map(([k, v]) => [k, clampTo(v, WRIST_LIMITS[k])]));
|
|
42
|
+
const want = mul(mul(axisRot(add(scale(axF, got.flex), scale(axD, got.dev)), Math.hypot(got.flex, got.dev)), axisRot(axT, got.twist)), neutral);
|
|
43
|
+
rotateSubtree(sk, s, fa, s.P[fa].slice(), axisRot(axT, FOREARM_SHARE * got.twist));
|
|
44
|
+
rotateSubtree(sk, s, h, s.P[h].slice(), mul(want, T(s.R[h])));
|
|
45
|
+
return need;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The bend direction the channels gave the arm of `side` on pose s, what its swivel turns from: the
|
|
50
|
+
* elbow off the shoulder-wrist midpoint, or for a straight arm (STRAIGHT) where its elbow would
|
|
51
|
+
* point if it bent, the upper arm's rest bend carried by the bone, so the swivel's limits sit on
|
|
52
|
+
* the upper arm's own roll.
|
|
53
|
+
*/
|
|
54
|
+
export function bendPlane(sk, s, side) {
|
|
55
|
+
const I = sk.index, a = I[`${side}Arm`], bend = sub(s.P[I[`${side}ForeArm`]], scale(add(s.P[a], s.P[I[`${side}Hand`]]), 0.5));
|
|
56
|
+
return len(bend) > STRAIGHT ? bend : carried(sk, s, a, ELBOW_REST);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* What holding `palm` with weight w needs on pose s: the hand's goal rotation, the wrist target for
|
|
61
|
+
* a hand rotation (pin "wrist": where it is; "palm": where the palm stays put), the bend direction
|
|
62
|
+
* (bendPlane).
|
|
63
|
+
*/
|
|
64
|
+
export function aimArm(sk, s, side, palm, now, w, pin) {
|
|
65
|
+
const I = sk.index, h = I[`${side}Hand`];
|
|
66
|
+
// the palm frame in the hand bone's frame: fixed for this frame's finger curl
|
|
67
|
+
const localR = mul(T(s.R[h]), now.R), localP = apply(T(s.R[h]), sub(now.pos, s.P[h]));
|
|
68
|
+
const goal = matFromQuat(slerp(quatFromMat(s.R[h]), quatFromMat(mul(palm.R, T(localR))), w));
|
|
69
|
+
const from = s.P[h].slice();
|
|
70
|
+
// the wrist goes where the grip lands with the orientation the wrist reached (the goal at first)
|
|
71
|
+
const grip = R => sub(palm.pos, apply(R, localP));
|
|
72
|
+
return { goal, plane: bendPlane(sk, s, side), wristAt: pin === "wrist" ? () => from : pin === "palm" ? grip : R => add(from, scale(sub(grip(R), from), w)) };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* held = { w, palm: { pos, R }, measure(s) -> { pos, R }, pole?, second?, pin? }: blend weight
|
|
77
|
+
* (1 inside the keyed span), the palm frame the keys ask for, the function that measures it on a
|
|
78
|
+
* pose, the planned elbow pole (../hold-plan.js; searched here when absent) and, for a second hand
|
|
79
|
+
* (../second-hand.js), the id of the object it grips and what stays put (pin "wrist" or "palm").
|
|
80
|
+
*/
|
|
81
|
+
export function holdHand(sk, s, side, held, restBend, diag, k) {
|
|
82
|
+
const I = sk.index, a = I[`${side}Arm`], fa = I[`${side}ForeArm`], h = I[`${side}Hand`], { w } = held;
|
|
83
|
+
const { goal, plane, wristAt } = aimArm(sk, s, side, held.palm, held.measure(s), w, held.pin);
|
|
84
|
+
const arm = swivel(sk, s, side, goal, wristAt(goal), plane, restBend, { toward: held.pole, w, comfort: held.comfort });
|
|
85
|
+
let miss = 0, fold = 0, need = { twist: 0, flex: 0, dev: 0 };
|
|
86
|
+
for (let i = 0; i < ITERATIONS; i++) {
|
|
87
|
+
const W = wristAt(i ? s.R[h] : goal);
|
|
88
|
+
fold = askedFold(sk, s, side, W);
|
|
89
|
+
miss = twoBone(sk, s, a, fa, h, W, arm.pole, restBend, "elbow");
|
|
90
|
+
need = orient(sk, s, side, h, fa, goal);
|
|
91
|
+
}
|
|
92
|
+
// fold: the flexion (deg) the wrist asked when past ELBOW_FOLD; miss is then how far inside the fold it is
|
|
93
|
+
diag[`hold_${k}`] = { w, miss: Math.max(0, miss), ...need, swivel: swungFrom(s, a, fa, h, plane), ...(fold > ELBOW_FOLD ? { fold } : {}), ...(held.second ? { second: held.second } : {}) };
|
|
94
|
+
diag[`arm_miss_${k}`] *= 1 - w;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The swivel landscape holdHand searches for `held` on pose s (the plain arm of `side`): what
|
|
99
|
+
* ../hold-plan.js plans the elbow's path on.
|
|
100
|
+
*/
|
|
101
|
+
export function holdLandscape(sk, s, side, held) {
|
|
102
|
+
const { goal, plane, wristAt } = aimArm(sk, s, side, held.palm, held.measure(s), held.w);
|
|
103
|
+
return landscape(sk, s, side, goal, wristAt(goal), plane, ELBOW_REST, 0, held.comfort);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The arm's cost of holding the palm frame `palm` on pose s (the plain arm of `side`, `now` its
|
|
108
|
+
* palm frame there): the wrist strain at the pole holdHand would take, its limits ROLL_MARGIN
|
|
109
|
+
* tighter, plus REACH_PRICE per squared centimetre the arm is short or the wrist nearer the shoulder
|
|
110
|
+
* than the elbow folds (ELBOW_FOLD), plus the shoulder's price with opts.shoulder. An aim key without roll takes the cheapest roll (key-aim.js).
|
|
111
|
+
*/
|
|
112
|
+
export function holdCost(sk, s, side, palm, now, { shoulder = false } = {}) {
|
|
113
|
+
const { goal, plane, wristAt } = aimArm(sk, s, side, palm, now, 1), W = wristAt(goal);
|
|
114
|
+
const arm = swivel(sk, s, side, goal, W, plane, ELBOW_REST, { margin: ROLL_MARGIN });
|
|
115
|
+
// the wrist no nearer the shoulder than the elbow folds (ELBOW_FOLD): an arm folded flat holds anything, and nothing between keys
|
|
116
|
+
const I = sk.index, S = s.P[I[`${side}Arm`]], A = len(sub(s.P[I[`${side}ForeArm`]], S)), B = len(sub(s.P[I[`${side}Hand`]], s.P[I[`${side}ForeArm`]]));
|
|
117
|
+
const fold = Math.max(0, reachAt(A, B, ELBOW_FOLD) - len(sub(W, S)));
|
|
118
|
+
// opts.shoulder adds the shoulder's price at that elbow (arm-orient.js shoulderPrice) and the arm's depth into the body
|
|
119
|
+
// (body.js, priced as grip-choice.js prices it): a leaning aim key's pick (key-aim.js) keeps the elbow down, not in the chest
|
|
120
|
+
const arm2 = shoulder ? swivel(sk, s, side, goal, W, plane, ELBOW_REST, { comfort: true }) : arm;
|
|
121
|
+
return (shoulder ? comfortPrice(arm2.need) + shoulderPrice(S, arm2.elbow) + intoBody(sk, s, side, S, arm2.elbow, W) : arm.cost) + REACH_PRICE * ((arm.miss * 100) ** 2 + (fold * 100) ** 2);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** BODY_PRICE per squared centimetre the upper arm or the forearm at elbow E and wrist W goes into the body (body.js). */
|
|
125
|
+
function intoBody(sk, s, side, S, E, W) {
|
|
126
|
+
const body = bodyFor(sk), I = sk.index;
|
|
127
|
+
return body ? BODY_PRICE * (Math.max(segmentDepth(body, sk, s, I[`${side}Arm`], S, E), segmentDepth(body, sk, s, I[`${side}ForeArm`], E, W)) * 100) ** 2 : 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Degrees the elbow swung about the shoulder-wrist axis from the bend direction `plane` the channels gave. */
|
|
131
|
+
function swungFrom(s, a, fa, h, plane) {
|
|
132
|
+
const u = norm(sub(s.P[h], s.P[a])), off = v => sub(v, scale(u, dot(v, u)));
|
|
133
|
+
return angle(off(sub(s.P[fa], s.P[a])), off(plane));
|
|
134
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// The idle layer: a body that holds its place is never a statue. Wherever the take holds a pose (no
|
|
2
|
+
// keyed channel changes: not a hand, a door or a finger) with its hands free (no arm on IK, no hand
|
|
3
|
+
// target or fit weight, nothing carried), it breathes (the chest lifts and settles:
|
|
4
|
+
// spine_pitch), lets its head drift a little (head_yaw, head_pitch) and, standing, shifts its weight
|
|
5
|
+
// from foot to foot (the hips sway over the planted feet, the pelvis lists toward the unloaded side and
|
|
6
|
+
// the spine rights it: root, roll, spine_roll). The layer only adds to channels that carry no contact:
|
|
7
|
+
// the feet, hand targets, anchors and objects are left as keyed, so a planted foot stays put and a hand
|
|
8
|
+
// on a target stays on it. It fades in RAMP s after the take starts or the body stops, out before it
|
|
9
|
+
// moves again, and stays off across the span of a `still` goal. Deterministic: its phases come from where the character
|
|
10
|
+
// first stands, so a recompile bakes the same take. spec.idle: false turns it off. The offsets ride on
|
|
11
|
+
// the solved pose only (compile.js solveFrame): out.baked keeps the keyed values, so QA's causes and
|
|
12
|
+
// patches still name the keys.
|
|
13
|
+
const BREATH = { period: 3.8, deg: 0.8 }, HEAD = { yaw: 0.9, pitch: 0.6 }, SWAY = { period: 5.6, m: 0.012, list: 1.4 };
|
|
14
|
+
const RAMP = 0.5, MOVE = 1e-4, TAU = 2 * Math.PI;
|
|
15
|
+
const smoothstep = x => x * x * (3 - 2 * x);
|
|
16
|
+
const differs = (a, b) => (Array.isArray(a) ? a.some((v, i) => Math.abs(v - b[i]) > MOVE)
|
|
17
|
+
: a && typeof a === "object" ? Object.keys({ ...a, ...b }).some(k => Math.abs((a[k] ?? 0) - (b?.[k] ?? 0)) > 1e-3) : Math.abs((a ?? 0) - (b ?? 0)) > 0.01);
|
|
18
|
+
|
|
19
|
+
/** Phases in [0, TAU) from a text (FNV-1a hash, then mulberry32). */
|
|
20
|
+
function phases(text, n) {
|
|
21
|
+
let h = 2166136261;
|
|
22
|
+
for (let i = 0; i < text.length; i++) h = Math.imul(h ^ text.charCodeAt(i), 16777619);
|
|
23
|
+
let a = h >>> 0;
|
|
24
|
+
return Array.from({ length: n }, () => {
|
|
25
|
+
a = (a + 0x6d2b79f5) >>> 0;
|
|
26
|
+
let t = Math.imul(a ^ (a >>> 15), a | 1);
|
|
27
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
28
|
+
return (((t ^ (t >>> 14)) >>> 0) / 4294967296) * TAU;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The weight of the layer per frame: 0 while anything keyed moves, a hand works or a still goal holds the body, easing to 1 RAMP s into a hold. */
|
|
33
|
+
function holdWeights(baked, spec, frameCount, fps) {
|
|
34
|
+
// the take's first frame is its keyed pose: the layer fades in from there
|
|
35
|
+
const moving = new Array(frameCount).fill(false), watch = Object.keys(baked);
|
|
36
|
+
moving[0] = true;
|
|
37
|
+
for (let f = 1; f < frameCount; f++) if (watch.some(ch => differs(baked[ch][f], baked[ch][f - 1]))) moving[f] = moving[f - 1] = true;
|
|
38
|
+
// a working hand: an arm on IK, a hand target or fit weight, or an object carried from its grab on
|
|
39
|
+
const hands = watch.filter(ch => /^(armw_[lr]|w_.+_[lr])$/.test(ch));
|
|
40
|
+
for (let f = 0; f < frameCount; f++) if (hands.some(ch => baked[ch][f] > 1e-3)) moving[f] = true;
|
|
41
|
+
for (const o of Object.values(spec.objects ?? {})) {
|
|
42
|
+
if (!Number.isFinite(o?.grab)) continue;
|
|
43
|
+
for (let f = Math.max(0, o.grab); f < Math.min(frameCount, Number.isFinite(o.release) ? o.release : frameCount); f++) moving[f] = true;
|
|
44
|
+
}
|
|
45
|
+
for (const g of spec.goals ?? []) {
|
|
46
|
+
const at = Array.isArray(g?.at) ? g.at : null;
|
|
47
|
+
if (g?.still === undefined || !at?.length) continue;
|
|
48
|
+
for (let f = Math.max(0, Math.round(at[0])); f <= Math.min(frameCount - 1, Math.round(at[at.length - 1])); f++) moving[f] = true;
|
|
49
|
+
}
|
|
50
|
+
const ramp = Math.max(1, Math.round(RAMP * fps)), d = new Array(frameCount).fill(Infinity);
|
|
51
|
+
for (let f = 0, last = -Infinity; f < frameCount; f++) { if (moving[f]) last = f; d[f] = f - last; }
|
|
52
|
+
for (let f = frameCount - 1, next = Infinity; f >= 0; f--) { if (moving[f]) next = f; d[f] = Math.min(d[f], next - f); }
|
|
53
|
+
return d.map(x => smoothstep(Math.min(1, x / ramp)));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Per hand, how much it works at each frame: 1 while its arm is on IK, a hand target or fit weight of that hand is
|
|
58
|
+
* on, or it carries an object from its grab on; easing to 0 over RAMP s away from that. pose.js keeps a working
|
|
59
|
+
* hand's forearm on its slant (arm_hang), eased so the arm does not jump as it goes onto IK. null for an idle hand.
|
|
60
|
+
*/
|
|
61
|
+
export function busyHands(baked, spec, frameCount, fps) {
|
|
62
|
+
const out = {}, ramp = Math.max(1, Math.round(RAMP * fps));
|
|
63
|
+
for (const k of ["l", "r"]) {
|
|
64
|
+
const own = Object.keys(baked).filter(ch => ch === `armw_${k}` || new RegExp(`^w_.+_${k}$`).test(ch));
|
|
65
|
+
const busy = Array.from({ length: frameCount }, (_, f) => own.some(ch => baked[ch][f] > 1e-3));
|
|
66
|
+
for (const o of Object.values(spec.objects ?? {})) {
|
|
67
|
+
if (!Number.isFinite(o?.grab) || spec.props?.[o.prop]?.hand !== k) continue;
|
|
68
|
+
for (let f = Math.max(0, o.grab); f < Math.min(frameCount, Number.isFinite(o.release) ? o.release : frameCount); f++) busy[f] = true;
|
|
69
|
+
}
|
|
70
|
+
if (!busy.some(Boolean)) { out[k] = null; continue; }
|
|
71
|
+
const d = new Array(frameCount).fill(Infinity);
|
|
72
|
+
for (let f = 0, last = -Infinity; f < frameCount; f++) { if (busy[f]) last = f; d[f] = f - last; }
|
|
73
|
+
for (let f = frameCount - 1, next = Infinity; f >= 0; f--) { if (busy[f]) next = f; d[f] = Math.min(d[f], next - f); }
|
|
74
|
+
out[k] = d.map(x => 1 - smoothstep(Math.min(1, x / ramp)));
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The idle offsets per frame ({ channel: delta }, root a world [dx, 0, dz]) for the baked channels, or null (compile.js rebake). */
|
|
80
|
+
export function idleLayer(baked, { spec, sk, frameCount }) {
|
|
81
|
+
if (spec.idle === false || !baked.root || !baked.foot_l || !baked.foot_r) return null;
|
|
82
|
+
const fps = spec.fps ?? 30, w = holdWeights(baked, spec, frameCount, fps);
|
|
83
|
+
if (!w.some(x => x > 0)) return null;
|
|
84
|
+
const P = sk.restWorldPos, standY = P[sk.index.Hips][1], ankY = P[sk.index.LeftFoot][1];
|
|
85
|
+
const [pb, py, pp, ps, py2] = phases(JSON.stringify([baked.root[0].map(v => v.toFixed(3)), (baked.yaw?.[0] ?? 0).toFixed(1)]), 5);
|
|
86
|
+
const out = new Array(frameCount).fill(null);
|
|
87
|
+
for (let f = 0; f < frameCount; f++) {
|
|
88
|
+
if (!(w[f] > 0)) continue;
|
|
89
|
+
const t = f / fps, k = w[f];
|
|
90
|
+
// breathing in lifts the chest (spine_pitch + bends forward)
|
|
91
|
+
const d = out[f] = { spine_pitch: -k * BREATH.deg * Math.sin((TAU * t) / BREATH.period + pb),
|
|
92
|
+
head_yaw: k * HEAD.yaw * (0.6 * Math.sin((TAU * t) / 7.3 + py) + 0.4 * Math.sin((TAU * t) / 11.9 + py2)),
|
|
93
|
+
head_pitch: k * HEAD.pitch * Math.sin((TAU * t) / 9.1 + pp) };
|
|
94
|
+
// the weight shift: standing only (both feet flat on the floor, the hips at standing height)
|
|
95
|
+
const feet = ["l", "r"].every(s => baked[`foot_${s}`][f][1] < ankY + 0.03 && Math.abs(baked[`fpitch_${s}`]?.[f] ?? 0) < 2);
|
|
96
|
+
if (!feet || baked.root[f][1] < standY - 0.15) continue;
|
|
97
|
+
const s = k * Math.sin((TAU * t) / SWAY.period + ps), yaw = ((baked.yaw?.[f] ?? 0) * Math.PI) / 180;
|
|
98
|
+
d.root = [Math.cos(yaw) * SWAY.m * s, 0, -Math.sin(yaw) * SWAY.m * s];
|
|
99
|
+
// the hips over the left foot drop the right side (roll +), the spine stays upright
|
|
100
|
+
d.roll = SWAY.list * s;
|
|
101
|
+
d.spine_roll = -SWAY.list * s;
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Frame f's channels with the idle offsets added (a new object; c itself is left alone). */
|
|
107
|
+
export function withIdle(c, layer, f) {
|
|
108
|
+
const d = layer?.[f];
|
|
109
|
+
if (!d) return c;
|
|
110
|
+
const out = { ...c };
|
|
111
|
+
for (const [ch, v] of Object.entries(d)) out[ch] = Array.isArray(v) ? (Array.isArray(c[ch]) ? c[ch].map((x, i) => x + v[i]) : c[ch]) : (Number(c[ch]) || 0) + v;
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// Two-bone limb IK, foot orientation, wrist and finger posing on a world-space state.
|
|
2
|
+
// Every edit rotates a bone's subtree about that bone's own joint, so the result maps
|
|
3
|
+
// exactly back to local quaternions (skeleton.js toLocal).
|
|
4
|
+
import { add, sub, scale, dot, len, norm, cross, clamp, apply, T, align, axisRot, ry, rx } from "./math.js";
|
|
5
|
+
import { rotateSubtree } from "./skeleton.js";
|
|
6
|
+
|
|
7
|
+
/** Direction fixed to bone b that points along `restDir` in the rest pose. */
|
|
8
|
+
export const carried = (sk, s, b, restDir) => apply(s.R[b], apply(T(sk.restWorldRot[b]), restDir));
|
|
9
|
+
|
|
10
|
+
// Elbow flexion: degrees between the upper arm and the forearm (0 straight), read on the joints as QA
|
|
11
|
+
// reads it (Arm->ForeArm against ForeArm->Hand). Active flexion ends near 145 deg (clinical norms
|
|
12
|
+
// 140-150: AMA 140, Kendall 145, AAOS 150), where the forearm's muscle meets the upper arm's; the
|
|
13
|
+
// passive fold stops a few degrees on. An arm IK follows its wrist target freely to ELBOW_FOLD, eases
|
|
14
|
+
// onto ELBOW_STOP (armReach) and leaves a target nearer the shoulder as a residual (REACH), like one
|
|
15
|
+
// out of reach. Before, twoBone folded an arm flat: the forearm lay in the upper arm and the bend plane
|
|
16
|
+
// was any plane at all (session df6acdcc, 171 deg).
|
|
17
|
+
export const ELBOW_FOLD = 145, ELBOW_STOP = 150;
|
|
18
|
+
// Knee flexion, read on the joints as QA reads it (UpLeg->Leg against Leg->Foot, 0 straight). Active
|
|
19
|
+
// flexion ends near 135-140 deg (AAOS 135); past it the knee folds only passively, under the body's
|
|
20
|
+
// weight, until the thigh rests on the calf: a deep squat with the heels down reaches 150-160 deg, and
|
|
21
|
+
// passive flexion stops near 160 (the heel at the buttock). A leg IK follows its ankle target freely to
|
|
22
|
+
// KNEE_FOLD (the squats the sessions keyed bend 125-138 deg), eases onto KNEE_STOP as the thigh presses
|
|
23
|
+
// on the calf, and leaves a target nearer the hip as a residual (REACH leg_miss). Before, twoBone folded
|
|
24
|
+
// a knee flat: the shin went into the thigh.
|
|
25
|
+
export const KNEE_FOLD = 150, KNEE_STOP = 160;
|
|
26
|
+
/** The flexion (deg) that puts the wrist d metres from the shoulder of an arm with bones a, b (180 at |a - b| or nearer). */
|
|
27
|
+
export const flexionAt = (a, b, d) => (Math.acos(clamp((d * d - a * a - b * b) / (2 * a * b), -1, 1)) * 180) / Math.PI;
|
|
28
|
+
/** The shoulder-wrist distance of an arm with bones a, b folded `deg` degrees. */
|
|
29
|
+
export const reachAt = (a, b, deg) => Math.sqrt(a * a + b * b + 2 * a * b * Math.cos((deg * Math.PI) / 180));
|
|
30
|
+
/**
|
|
31
|
+
* The shoulder-wrist distance an arm with bones a, b takes for a wrist target d metres away: d while
|
|
32
|
+
* the elbow folds ELBOW_FOLD or less (d >= k), then d + (k - d)^2 / 4w (w = k - the ELBOW_STOP distance)
|
|
33
|
+
* down to d = k - 2w, and the stop beyond. The wrist slows at a constant rate (C1, no kink as it
|
|
34
|
+
* engages: half the peak deceleration of an exponential ease as wide) and sits on the stop once the
|
|
35
|
+
* target is 2w inside. A leg passes KNEE_FOLD and KNEE_STOP (the hip-ankle distance).
|
|
36
|
+
*/
|
|
37
|
+
export function armReach(a, b, d, fold = ELBOW_FOLD, stop = ELBOW_STOP) {
|
|
38
|
+
const k = reachAt(a, b, fold);
|
|
39
|
+
if (d >= k) return d;
|
|
40
|
+
const w = k - reachAt(a, b, stop);
|
|
41
|
+
return d > k - 2 * w ? d + ((k - d) * (k - d)) / (4 * w) : k - w;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The wrist point an arm (shoulder S, bones a, b) reaches for a wrist target T: T while the elbow
|
|
45
|
+
* folds ELBOW_FOLD or less, else the point armReach(|T - S|) from the shoulder on the ray from the fold
|
|
46
|
+
* centre C through T. No point can stand for every target inside the stop and still move smoothly (a
|
|
47
|
+
* ball has no continuous retraction onto its boundary): the nearest point (the ray from the shoulder)
|
|
48
|
+
* whips round the shoulder as a target passes near it (session df6acdcc: 120 deg in 2 frames). So C
|
|
49
|
+
* sits on the stop's sphere straight behind the shoulder (`back`, the chest's -Z), in the back, where
|
|
50
|
+
* no hand reaches, and a target passing in front of, above, below or beside the shoulder glides.
|
|
51
|
+
* A leg passes the knee's fold and stop, and `back` is the pelvis's up (upOf): no foot goes into the
|
|
52
|
+
* pelvis, and a squat's ankle target under the hip goes on down from it.
|
|
53
|
+
*/
|
|
54
|
+
export function foldPoint(S, T, a, b, back, fold = ELBOW_FOLD, stop = ELBOW_STOP) {
|
|
55
|
+
const k = reachAt(a, b, fold), d = len(sub(T, S));
|
|
56
|
+
if (d >= k) return T;
|
|
57
|
+
const r = armReach(a, b, d, fold, stop), m = scale(back, reachAt(a, b, stop)), C = add(S, m), e = sub(T, C);
|
|
58
|
+
const u = len(e) > 1e-9 ? norm(e) : scale(back, -1), em = dot(u, m);
|
|
59
|
+
return add(C, scale(u, -em + Math.sqrt(Math.max(0, em * em - dot(m, m) + r * r))));
|
|
60
|
+
}
|
|
61
|
+
/** The direction behind the shoulders on pose s (the chest bone's rest -Z), where foldPoint's rays start. */
|
|
62
|
+
export const backOf = (sk, s) => carried(sk, s, sk.index.Spine2 ?? sk.index.Hips, [0, 0, -1]);
|
|
63
|
+
/** The direction up from the hips on pose s (the pelvis's rest +Y), where a knee's foldPoint rays start. */
|
|
64
|
+
const upOf = (sk, s) => carried(sk, s, sk.index.Hips, [0, 1, 0]);
|
|
65
|
+
/** The flexion (deg) an arm with bones a, b takes when `deg` is asked (armReach as an angle). */
|
|
66
|
+
export const elbowFold = (a, b, deg) => flexionAt(a, b, armReach(a, b, reachAt(a, b, deg)));
|
|
67
|
+
/** The flexion (deg) a wrist target asks of the arm of `side` on pose s (a leg's ankle target: bones UpLeg, Leg, Foot). */
|
|
68
|
+
export function askedFold(sk, s, side, target, [u, m, e] = ["Arm", "ForeArm", "Hand"]) {
|
|
69
|
+
const S = s.P[sk.index[`${side}${u}`]], E = s.P[sk.index[`${side}${m}`]], W = s.P[sk.index[`${side}${e}`]];
|
|
70
|
+
return flexionAt(len(sub(E, S)), len(sub(W, E)), len(sub(target, S)));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Ease an elbow folded past ELBOW_FOLD (an FK abend) back as the IK would (elbowFold), the forearm turning in its bend plane. */
|
|
74
|
+
export function limitFold(sk, s, side) {
|
|
75
|
+
const a = sk.index[`${side}Arm`], fa = sk.index[`${side}ForeArm`], up = sub(s.P[fa], s.P[a]), down = sub(s.P[sk.index[`${side}Hand`]], s.P[fa]);
|
|
76
|
+
const deg = (Math.acos(clamp(dot(norm(up), norm(down)), -1, 1)) * 180) / Math.PI, n = cross(up, down);
|
|
77
|
+
if (deg > ELBOW_FOLD && len(n) > 1e-12) rotateSubtree(sk, s, fa, s.P[fa].slice(), axisRot(n, elbowFold(len(up), len(down), deg) - deg));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The stops twoBone keeps to: the free fold and the stop (deg), and the fold centre's direction on pose s. */
|
|
81
|
+
const STOPS = { elbow: [ELBOW_FOLD, ELBOW_STOP, backOf], knee: [KNEE_FOLD, KNEE_STOP, upOf] };
|
|
82
|
+
|
|
83
|
+
/** Solve root-mid-end so `end` lands on `target` (clamped to reach), bending toward `pole`.
|
|
84
|
+
* `pole0` is the limb's current bend reference carried by the upper bone. Returns the miss (m).
|
|
85
|
+
* `stop`: "elbow" or "knee", the mid joint folds no further than armReach allows with that joint's
|
|
86
|
+
* fold and stop (foldPoint), and the miss is the residual either way (out of reach, or nearer the
|
|
87
|
+
* shoulder or hip than the fold). */
|
|
88
|
+
export function twoBone(sk, s, root, mid, end, target, pole, restBend, stop = null) {
|
|
89
|
+
const Rt = s.P[root], M = s.P[mid], E = s.P[end];
|
|
90
|
+
const a = len(sub(M, Rt)), b = len(sub(E, M)), lim = STOPS[stop];
|
|
91
|
+
// a joint folds no further than armReach lets it: the end goes to foldPoint, and misses the target by the rest
|
|
92
|
+
const W = lim ? foldPoint(Rt, target, a, b, lim[2](sk, s), lim[0], lim[1]) : target, inside = W === target ? 0 : len(sub(W, target));
|
|
93
|
+
const v = sub(W, Rt), d = len(v), u = scale(v, 1 / Math.max(d, 1e-9));
|
|
94
|
+
const dc = clamp(d, Math.abs(a - b) + 1e-4, (a + b) * 0.9995);
|
|
95
|
+
const l = (a * a - b * b + dc * dc) / (2 * dc), h = Math.sqrt(Math.max(a * a - l * l, 0));
|
|
96
|
+
const pv = norm(sub(pole, scale(u, dot(pole, u))));
|
|
97
|
+
const Mn = add(Rt, add(scale(u, l), scale(pv, h)));
|
|
98
|
+
const p0 = carried(sk, s, root, restBend);
|
|
99
|
+
rotateSubtree(sk, s, root, Rt.slice(), align(sub(M, Rt), p0, sub(Mn, Rt), pv));
|
|
100
|
+
const p1 = carried(sk, s, mid, restBend);
|
|
101
|
+
const Tn = add(Rt, scale(u, dc));
|
|
102
|
+
rotateSubtree(sk, s, mid, s.P[mid].slice(), align(sub(s.P[end], s.P[mid]), p1, sub(Tn, s.P[mid]), pv));
|
|
103
|
+
return inside || d - dc;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Aim a foot: Foot->ToeBase along yaw/pitch of its rest direction, sole kept level. */
|
|
107
|
+
export function orientFoot(sk, s, side, yawDeg, pitchDeg) {
|
|
108
|
+
const foot = sk.index[`${side}Foot`], toe = sk.index[`${side}ToeBase`];
|
|
109
|
+
const rest = sub(sk.restWorldPos[toe], sk.restWorldPos[foot]);
|
|
110
|
+
const want = apply(ry(yawDeg), apply(rx(pitchDeg), rest));
|
|
111
|
+
// level the sole: the foot's own carried up vector goes to world up (no roll from the leg IK)
|
|
112
|
+
rotateSubtree(sk, s, foot, s.P[foot].slice(), align(sub(s.P[toe], s.P[foot]), carried(sk, s, foot, [0, 1, 0]), want, [0, 1, 0]));
|
|
113
|
+
// Toe-off: bend the toe joint so the toes stay flat on the ground while the heel rises.
|
|
114
|
+
const tip = sk.index[`${side}Toe_End`];
|
|
115
|
+
if (pitchDeg > 0 && tip !== undefined) {
|
|
116
|
+
const seg = sub(s.P[tip], s.P[toe]), flat = [seg[0], 0, seg[2]];
|
|
117
|
+
if (len(flat) > 1e-6) rotateSubtree(sk, s, toe, s.P[toe].slice(), align(seg, [0, 1, 0], flat, [0, 1, 0]));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function palmAxes(sk, s, side) {
|
|
122
|
+
const h = sk.index[`${side}Hand`], m1 = sk.index[`${side}HandMiddle1`];
|
|
123
|
+
const d = norm(sub(s.P[m1], s.P[h]));
|
|
124
|
+
let l = sub(s.P[sk.index[`${side}HandIndex1`]], s.P[sk.index[`${side}HandPinky1`]]);
|
|
125
|
+
l = norm(sub(l, scale(d, dot(l, d))));
|
|
126
|
+
return { d, l, n: cross(d, l) };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Turn the elbow `deg` about the shoulder-wrist line: the hand keeps its place and its world turn. */
|
|
130
|
+
export function swivelElbow(sk, s, side, deg) {
|
|
131
|
+
const a = sk.index[`${side}Arm`], h = sk.index[`${side}Hand`], R = axisRot(sub(s.P[h], s.P[a]), deg);
|
|
132
|
+
rotateSubtree(sk, s, a, s.P[a].slice(), R);
|
|
133
|
+
rotateSubtree(sk, s, h, s.P[h].slice(), T(R));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Wrist twist (about the forearm) and flex (about the knuckle line), degrees, clamped. */
|
|
137
|
+
export function wrist(sk, s, side, twistDeg, flexDeg) {
|
|
138
|
+
if (!twistDeg && !flexDeg) return;
|
|
139
|
+
const h = sk.index[`${side}Hand`], fa = sk.index[`${side}ForeArm`];
|
|
140
|
+
const axT = norm(sub(s.P[h], s.P[fa]));
|
|
141
|
+
const { l } = palmAxes(sk, s, side);
|
|
142
|
+
const axF = norm(sub(l, scale(axT, dot(l, axT))));
|
|
143
|
+
rotateSubtree(sk, s, h, s.P[h].slice(), axisRot(axF, clamp(flexDeg, -70, 70)));
|
|
144
|
+
rotateSubtree(sk, s, h, s.P[h].slice(), axisRot(axT, clamp(twistDeg, -85, 85)));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Finger curl presets, degrees per phalanx (1, 2, 3); thumb is [opposition, curl, curl].
|
|
148
|
+
const FINGER_PRESETS = {
|
|
149
|
+
relaxed: { Index: [12, 18, 10], Middle: [15, 20, 12], Ring: [18, 22, 12], Pinky: [20, 24, 14], Thumb: [15, 10, 8] },
|
|
150
|
+
fist: { Index: [80, 95, 60], Middle: [85, 95, 60], Ring: [85, 95, 60], Pinky: [85, 95, 60], Thumb: [45, 30, 35] },
|
|
151
|
+
grip: { Index: [55, 60, 35], Middle: [60, 62, 35], Ring: [62, 62, 35], Pinky: [65, 62, 35], Thumb: [40, 20, 20] },
|
|
152
|
+
cigarette: { Index: [8, 10, 6], Middle: [10, 12, 8], Ring: [60, 75, 45], Pinky: [65, 80, 45], Thumb: [35, 25, 20] },
|
|
153
|
+
point: { Index: [0, 0, 0], Middle: [80, 95, 60], Ring: [85, 95, 60], Pinky: [85, 95, 60], Thumb: [40, 30, 30] },
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/** Blend presets by weight over `relaxed` and curl every finger toward the palm. */
|
|
157
|
+
export function fingers(sk, s, side, weights) {
|
|
158
|
+
const base = FINGER_PRESETS.relaxed, sign = side === "Left" ? -1 : 1;
|
|
159
|
+
const curl = (f, k) => base[f][k] + Object.entries(weights ?? {}).reduce((acc, [p, w]) => acc + (w || 0) * ((FINGER_PRESETS[p]?.[f]?.[k] ?? base[f][k]) - base[f][k]), 0);
|
|
160
|
+
for (const f of ["Index", "Middle", "Ring", "Pinky", "Thumb"]) {
|
|
161
|
+
for (let k = 0; k < 3; k++) {
|
|
162
|
+
const j = sk.index[`${side}Hand${f}${k + 1}`];
|
|
163
|
+
if (j === undefined) continue;
|
|
164
|
+
const { l, d } = palmAxes(sk, s, side);
|
|
165
|
+
const ax = f === "Thumb" && k === 0 ? d : l;
|
|
166
|
+
rotateSubtree(sk, s, j, s.P[j].slice(), axisRot(ax, sign * curl(f, k)));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|