cozyclay 2.1.1 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +88 -0
- package/README.md +12 -30
- package/bin/agent/agent-routes.mjs +43 -116
- package/bin/agent/agent-runner.mjs +14 -15
- 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/session-store.mjs +2 -2
- package/bin/agent/studio-context-budget.mjs +93 -0
- 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-BWCqdgWA.js +1 -0
- package/dist/assets/app-DfTCxaKk.js +5087 -0
- package/dist/assets/app-f-fkV5uP.css +1 -0
- package/dist/assets/compile-worker-BxKXh07s.js +5 -0
- package/dist/assets/fflate.module-KbGj5-H6.js +5 -0
- package/dist/assets/{first-shot-handoff-msLI71g1.js → first-shot-handoff-DNO8kv_N.js} +1 -1
- package/dist/assets/{landing-ChUvB_zL.js → landing-yfZaJkWc.js} +1 -1
- package/dist/assets/scene-asset-sync-U3kS2rvK.js +20 -0
- package/dist/assets/scene-asset-sync-_S8NQl5B.css +1 -0
- package/dist/assets/workflow-D78eiKTa.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 +151 -2625
- package/src/agent-progress.js +80 -0
- package/src/agent-turn-mark.js +30 -0
- package/src/analytics.js +1 -19
- package/src/app-context.js +12 -2
- 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 +10 -8
- 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 +214 -0
- package/src/commands/objects.js +6 -4
- package/src/commands/shot.js +55 -13
- 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 +53 -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 +47 -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 +60 -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 +79 -0
- package/src/motion3/compile-worker.js +10 -0
- package/src/motion3/compiler/anchors.js +113 -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 +197 -0
- package/src/motion3/compiler/math.js +72 -0
- package/src/motion3/compiler/pose.js +177 -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 +144 -0
- package/src/motion3/compiler/validate-parts.js +118 -0
- package/src/motion3/compiler/validate.js +122 -0
- package/src/motion3/compiler/vocab.js +74 -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 +177 -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 +204 -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/people.js +82 -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 +125 -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 +195 -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 +44 -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/people-checks.js +37 -0
- package/src/motion3/qa/severity.js +38 -0
- package/src/motion3/qa/spec-checks.js +65 -0
- package/src/motion3/qa/style.js +228 -0
- package/src/motion3/qa/wrist-checks.js +69 -0
- package/src/motion3/qa/wrist-obs.js +60 -0
- package/src/motion3/refs.js +129 -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 +113 -0
- package/src/motion3/schema/types.js +149 -0
- package/src/motion3/search.js +117 -0
- package/src/motion3/second-hand.js +112 -0
- package/src/motion3/settle.js +136 -0
- package/src/motion3/skills/basic.js +83 -0
- package/src/motion3/skills/book.js +76 -0
- package/src/motion3/skills/car.js +145 -0
- package/src/motion3/skills/flip.js +200 -0
- package/src/motion3/skills/frames-auto.js +29 -0
- package/src/motion3/skills/impact.js +109 -0
- package/src/motion3/skills/index.js +85 -0
- package/src/motion3/skills/info.js +80 -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/qa-label.js +25 -0
- 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 +16 -14
- package/src/studio-agent-commands.js +19 -16
- package/src/studio-agent-motion.js +9 -9
- package/src/studio-agent-protocol.js +47 -77
- package/src/studio-app-binding.js +27 -47
- 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 +15 -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 +109 -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 +9 -125
- 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,117 @@
|
|
|
1
|
+
// Open parameters: any number in a spec (generator params, key channel values or vector
|
|
2
|
+
// components, anchor offsets, skill args) may be written as { range: [lo, hi] } (or
|
|
3
|
+
// { search: [lo, hi] }, + int: true). motion.compile then searches the values deterministically:
|
|
4
|
+
// the midpoint first, a grid (one parameter) or a Halton set (several), then a shrinking pattern
|
|
5
|
+
// search around the best, at most spec.search.budget (default 32, max 64) compiles, stopping at
|
|
6
|
+
// the first value with QA 0. Each trial compiles only the frames the parameters can change.
|
|
7
|
+
import { compileMotion } from "./compiler/compile.js";
|
|
8
|
+
import { bakeChannels, flowOptions } from "./compiler/channels.js";
|
|
9
|
+
import { expandGenerators } from "./compiler/walk.js";
|
|
10
|
+
import { runMotionQa } from "./qa/index.js";
|
|
11
|
+
import { weightOf } from "./qa/severity.js";
|
|
12
|
+
|
|
13
|
+
const INT_FIELDS = new Set(["f", "f0", "steps", "period", "swing", "until", "reach", "hold", "release"]);
|
|
14
|
+
const isOpen = v => v && typeof v === "object" && !Array.isArray(v) && (v.range ?? v.search) !== undefined && Object.keys(v).every(k => ["range", "search", "int"].includes(k));
|
|
15
|
+
const pathText = path => path.reduce((s, p) => (typeof p === "number" ? `${s}[${p}]` : s ? `${s}.${p}` : p), "");
|
|
16
|
+
|
|
17
|
+
/** Every { range } in the spec: [{ path, lo, hi, int }]. */
|
|
18
|
+
export function findParams(spec) {
|
|
19
|
+
const out = [];
|
|
20
|
+
const walk = (v, path) => {
|
|
21
|
+
if (isOpen(v)) {
|
|
22
|
+
const r = v.range ?? v.search;
|
|
23
|
+
if (!(Array.isArray(r) && r.length === 2 && r.every(Number.isFinite) && r[0] < r[1])) throw new Error(`MOTION_SPEC_INVALID: ${pathText(path)} range must be [lo, hi] with lo < hi, got ${JSON.stringify(r)}`);
|
|
24
|
+
out.push({ path, lo: r[0], hi: r[1], int: v.int ?? INT_FIELDS.has(path[path.length - 1]) });
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (Array.isArray(v)) v.forEach((x, i) => walk(x, [...path, i]));
|
|
28
|
+
else if (v && typeof v === "object") for (const [k, x] of Object.entries(v)) walk(x, [...path, k]);
|
|
29
|
+
};
|
|
30
|
+
for (const [k, v] of Object.entries(spec ?? {})) if (!["schema", "fps", "frames", "search"].includes(k)) walk(v, [k]);
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const snap = (p, x) => { const v = Math.min(p.hi, Math.max(p.lo, x)); return p.int ? Math.round(v) : Math.round(v * 1e4) / 1e4; };
|
|
35
|
+
|
|
36
|
+
function resolveParams(spec, params, values) {
|
|
37
|
+
const s = structuredClone(spec);
|
|
38
|
+
params.forEach((p, i) => { let o = s; for (const k of p.path.slice(0, -1)) o = o[k]; o[p.path[p.path.length - 1]] = values[i]; });
|
|
39
|
+
return s;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** QA groups -> one number: the sum of their severities (qa/severity.js, unrounded). */
|
|
43
|
+
const costOf = qa => qa.groups.reduce((c, g) => c + weightOf(g), 0);
|
|
44
|
+
|
|
45
|
+
/** Frames a parameter can change (baked channels differ between its ends), or null for all. */
|
|
46
|
+
function dirtyWindow(spec, params, mid, prepare, sk) {
|
|
47
|
+
const expand = values => prepare(resolveParams(spec, params, values));
|
|
48
|
+
const base = expand(mid), parts = s => JSON.stringify({ ...s, keys: undefined, generators: undefined });
|
|
49
|
+
const bake = s => bakeChannels(expandGenerators(s, sk), s.frames, flowOptions(s.flow));
|
|
50
|
+
const b0 = bake(base);
|
|
51
|
+
let a = Infinity, b = -Infinity;
|
|
52
|
+
for (let i = 0; i < params.length; i++) for (const end of [params[i].lo, params[i].hi]) {
|
|
53
|
+
const s = expand(mid.map((v, j) => (j === i ? snap(params[i], end) : v)));
|
|
54
|
+
if (parts(s) !== parts(base)) return null;
|
|
55
|
+
const b1 = bake(s);
|
|
56
|
+
for (const ch of new Set([...Object.keys(b0), ...Object.keys(b1)])) {
|
|
57
|
+
const x = b0[ch], y = b1[ch];
|
|
58
|
+
for (let f = 0; f < spec.frames; f++) if (JSON.stringify(x?.[f]) !== JSON.stringify(y?.[f])) { a = Math.min(a, f); b = Math.max(b, f); }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return a > b ? null : [Math.max(0, a - 8), Math.min(spec.frames - 1, b + 8)];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Halton point i (1-based) in [0, 1]^k. */
|
|
65
|
+
const PRIMES = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29];
|
|
66
|
+
const halton = (i, k) => Array.from({ length: k }, (_, d) => { let f = 1, r = 0, n = i; const p = PRIMES[d % PRIMES.length]; while (n > 0) { f /= p; r += f * (n % p); n = Math.floor(n / p); } return r; });
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* prepare(spec) -> expanded + validated plain spec (pipeline.js). Returns the spec with the
|
|
70
|
+
* chosen values and a report { values: { path: value }, evals, cost (0 = clean), window, ms }.
|
|
71
|
+
*/
|
|
72
|
+
export function searchParams(spec, params, { sk, scene }, prepare) {
|
|
73
|
+
const t0 = performance.now(), budget = Math.max(1, Math.min(64, Math.round(spec.search?.budget ?? 32)));
|
|
74
|
+
const mid = params.map(p => snap(p, (p.lo + p.hi) / 2));
|
|
75
|
+
const window = dirtyWindow(spec, params, mid, prepare, sk);
|
|
76
|
+
const cache = new Map();
|
|
77
|
+
let evals = 0, best = null;
|
|
78
|
+
const evaluate = values => {
|
|
79
|
+
const key = values.join(",");
|
|
80
|
+
if (cache.has(key)) return cache.get(key);
|
|
81
|
+
if (evals >= budget) return Infinity;
|
|
82
|
+
evals++;
|
|
83
|
+
let cost;
|
|
84
|
+
try {
|
|
85
|
+
const exp = prepare(resolveParams(spec, params, values));
|
|
86
|
+
cost = costOf(runMotionQa(sk, compileMotion({ fps: 24, ...exp }, { sk, scene }, { window }), exp, { explain: false }));
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (!/MOTION_SPEC_INVALID/.test(error.message)) throw error;
|
|
89
|
+
cost = 1e9;
|
|
90
|
+
}
|
|
91
|
+
cache.set(key, cost);
|
|
92
|
+
if (!best || cost < best.cost) best = { values, cost };
|
|
93
|
+
return cost;
|
|
94
|
+
};
|
|
95
|
+
const done = () => best?.cost === 0 || evals >= budget;
|
|
96
|
+
const at = u => params.map((p, d) => snap(p, p.lo + (p.hi - p.lo) * u[d]));
|
|
97
|
+
evaluate(mid);
|
|
98
|
+
// coarse pass: a 9-point grid from the middle outward (one parameter) or Halton points
|
|
99
|
+
const coarse = params.length === 1 ? [4, 3, 5, 2, 6, 1, 7, 0, 8].map(i => [i / 8]) : Array.from({ length: Math.min(budget >> 1, 8 * params.length) }, (_, i) => halton(i + 1, params.length));
|
|
100
|
+
for (const u of coarse) { if (done()) break; evaluate(at(u)); }
|
|
101
|
+
// pattern search around the best, halving the step
|
|
102
|
+
let step = params.map(p => (p.hi - p.lo) / 8);
|
|
103
|
+
while (!done() && step.some((s, d) => s > (params[d].int ? 0.5 : (params[d].hi - params[d].lo) * 1e-3))) {
|
|
104
|
+
let moved = false;
|
|
105
|
+
for (let d = 0; d < params.length && !done(); d++) for (const sg of [-1, 1]) {
|
|
106
|
+
const v = best.values.slice(); v[d] = snap(params[d], v[d] + sg * step[d]);
|
|
107
|
+
const before = best.cost;
|
|
108
|
+
evaluate(v);
|
|
109
|
+
if (best.cost < before) moved = true;
|
|
110
|
+
}
|
|
111
|
+
if (!moved) step = step.map(s => s / 2);
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
spec: resolveParams(spec, params, best.values),
|
|
115
|
+
search: { values: Object.fromEntries(params.map((p, i) => [pathText(p.path), best.values[i]])), evals, cost: Math.round(best.cost * 100) / 100, window, ms: Math.round(performance.now() - t0) },
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// The second hand on a held object (C14, lane K8). While keys lead a carry the right hand holds
|
|
2
|
+
// (object-keys.js), a left hand anchored to that prop (w_<anchor>_l on a kind "prop" anchor of
|
|
3
|
+
// the same prop, with or without a fit) turns with the object too. The wrist stays where the
|
|
4
|
+
// anchor puts it (with a fit, the palm stays where the fit puts it). The hand takes the holding
|
|
5
|
+
// hand's grip mirrored across the plane of the object's axis and the holding fingers (the object's
|
|
6
|
+
// +Y runs through the left palm frame as through the right one, knuckles in line), turned about
|
|
7
|
+
// the axis to the roll its arm holds most easily (planned over the frames, object-keys.js and
|
|
8
|
+
// hold-plan.js). The arm follows as for the holding hand (compiler/hold.js): the elbow swivels
|
|
9
|
+
// about the shoulder-wrist axis (lane N1; it stayed in the plane the anchor's IK gave it, and the
|
|
10
|
+
// default pole, down and back, bent a left arm reaching across the chest into the torso), the
|
|
11
|
+
// forearm and the wrist turn. Each roll is scored by the wrist's strain at the best of POLES poles,
|
|
12
|
+
// plus CLEAR per squared centimetre the upper arm, the forearm or the hand goes into the torso
|
|
13
|
+
// capsules (the BODY check's), and DEEP per squared centimetre the hand goes past what QA allows a
|
|
14
|
+
// hand: into the torso past the BODY allowance, into the holding hand past the SELF one (lane Q1:
|
|
15
|
+
// at CLEAR alone, session 4db5d447 put the hand 0.9 cm into torso_lo at f35 to save 1 of strain,
|
|
16
|
+
// QA BODY). The elbow's path is planned like the holding elbow's. The hold
|
|
17
|
+
// eases in with the keys' weight times the anchor weight, and a grip the arm cannot hold fades out.
|
|
18
|
+
// Only a hand solved after the holding hand can follow it (pose.js solves the right arm first), so
|
|
19
|
+
// the second hand is the left.
|
|
20
|
+
import { palmAxes } from "./compiler/limbs.js";
|
|
21
|
+
import { SIDES } from "./compiler/pose.js";
|
|
22
|
+
import { add, sub, scale, norm, dot, cross, mul, T, apply, axisRot, len, clamp } from "./compiler/math.js";
|
|
23
|
+
import { landscape, ELBOW_REST } from "./compiler/arm-orient.js";
|
|
24
|
+
import { bendPlane } from "./compiler/hold.js";
|
|
25
|
+
import { CAPSULES } from "./qa/geometry.js";
|
|
26
|
+
import { QA_LIMITS } from "./qa/checks.js";
|
|
27
|
+
import { bodyFor, segmentDepth } from "./body.js";
|
|
28
|
+
|
|
29
|
+
const ROLLS = 36, TEMP = 0.05, PULL = 0.3, EASY = 3, SLACK = 10, POLES = 36, CLEAR = 1, DEEP = 100;
|
|
30
|
+
const TORSO = CAPSULES.filter(c => c[0].startsWith("torso")), RADIUS = Object.fromEntries(CAPSULES.map(c => [c[0], c[3]]));
|
|
31
|
+
const cols = (a, b, c) => [[a[0], b[0], c[0]], [a[1], b[1], c[1]], [a[2], b[2], c[2]]];
|
|
32
|
+
const col = (R, i) => [R[0][i], R[1][i], R[2][i]];
|
|
33
|
+
|
|
34
|
+
/** The distance between segments p1-q1 and p2-q2 (closest points, clamped to both). */
|
|
35
|
+
function segSeg(p1, q1, p2, q2) {
|
|
36
|
+
const d1 = sub(q1, p1), d2 = sub(q2, p2), r = sub(p1, p2), a = dot(d1, d1), e = dot(d2, d2), f = dot(d2, r);
|
|
37
|
+
if (a < 1e-12 || e < 1e-12) return len(r);
|
|
38
|
+
const c = dot(d1, r), b = dot(d1, d2), den = a * e - b * b;
|
|
39
|
+
let t = den > 1e-12 ? clamp((b * f - c * e) / den, 0, 1) : 0, u = (b * t + f) / e;
|
|
40
|
+
if (u < 0) { u = 0; t = clamp(-c / a, 0, 1); } else if (u > 1) { u = 1; t = clamp((b - c) / a, 0, 1); }
|
|
41
|
+
return len(sub(add(p1, scale(d1, t)), add(p2, scale(d2, u))));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The palm frame of `side` on pose s: the finger middles, axes d, l, n as columns. */
|
|
45
|
+
function palmFrame(sk, s, side) {
|
|
46
|
+
const I = sk.index, ax = palmAxes(sk, s, side);
|
|
47
|
+
return { pos: scale(add(s.P[I[`${side}HandIndex2`]], s.P[I[`${side}HandMiddle2`]]), 0.5), R: cols(ax.d, ax.l, ax.n) };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The left hand's grip on carry `id` at this frame, or null when the left hand is not anchored to
|
|
52
|
+
* its prop: s is the pose with the holding hand done and the left arm on its plain target, c the
|
|
53
|
+
* frame's channels, axis the object's +Y as drawn. The grip is the holding hand's mirrored (knuckles
|
|
54
|
+
* in line), turned `roll` radians about the axis; arm(roll) is the left arm holding it: at(th) ->
|
|
55
|
+
* { pv, elbow, cost } for the elbow th radians about the shoulder-wrist axis from its plane (the
|
|
56
|
+
* swivel landscape plus the torso price) and best { th, pole, cost }; cost(roll) is best's cost
|
|
57
|
+
* (pulled lightly toward the mirror), point(roll) the end of the hand that moves (the palm with the
|
|
58
|
+
* wrist pinned, the wrist with the palm pinned), softmin() the easiest roll.
|
|
59
|
+
*/
|
|
60
|
+
export function secondGrip(sk, spec, id, s, c, axis) {
|
|
61
|
+
const o = spec.objects[id];
|
|
62
|
+
if (spec.props[o.prop].hand !== "r") return null;
|
|
63
|
+
const weight = Object.entries(spec.anchors ?? {}).reduce((sum, [name, a]) => sum + (a.kind === "prop" && a.prop === o.prop ? Math.max(0, c[`w_${name}_l`] ?? 0) : 0), 0);
|
|
64
|
+
if (!(weight > 1e-4)) return null;
|
|
65
|
+
// the mirror of the holding palm frame across the plane of the axis and the holding fingers
|
|
66
|
+
const R = palmFrame(sk, s, SIDES.r).R, d = col(R, 0), p = norm(cross(axis, sub(d, scale(axis, dot(d, axis))))), mirror = v => sub(v, scale(p, 2 * dot(v, p)));
|
|
67
|
+
const base = cols(mirror(d), mirror(col(R, 1)), scale(mirror(col(R, 2)), -1));
|
|
68
|
+
const left = palmFrame(sk, s, SIDES.l), I = sk.index, h = I.LeftHand, wrist = s.P[h], local = mul(T(s.R[h]), left.R), localP = apply(T(s.R[h]), sub(left.pos, wrist));
|
|
69
|
+
// with a fit the palm (its effector) stays where the fit put it, else the wrist stays on the anchor
|
|
70
|
+
const pin = (spec.fits ?? []).some(x => x.side === "l" && spec.anchors?.[x.anchor]?.kind === "prop" && spec.anchors[x.anchor].prop === o.prop && c[`w_${x.anchor}_l`] > 1e-4) ? "palm" : "wrist";
|
|
71
|
+
const rolled = r => mul(axisRot(axis, r * 180 / Math.PI), base), hand = r => mul(rolled(r), T(local));
|
|
72
|
+
const point = r => (pin === "wrist" ? add(wrist, apply(hand(r), localP)) : sub(left.pos, apply(hand(r), localP)));
|
|
73
|
+
// the wrist for a roll, the knuckles off it, and how deep a segment goes into the torso (m)
|
|
74
|
+
const wristAt = r => (pin === "wrist" ? wrist : point(r)), knuckle = apply(T(s.R[h]), sub(s.P[I.LeftHandMiddle3], wrist)), S = s.P[I.LeftArm], plane = bendPlane(sk, s, SIDES.l);
|
|
75
|
+
// on the fitted skin when the rig has one and spec.fix.self is on (body.js, as QA's BODY measures it), else on the torso capsules
|
|
76
|
+
const skin = spec.fix === false || spec.fix?.self === false ? null : bodyFor(sk), BONE = { uarm_l: I.LeftArm, farm_l: I.LeftForeArm, hand_l: I.LeftHand };
|
|
77
|
+
const into = (a, b, part) => (skin ? segmentDepth(skin, sk, s, BONE[part], a, b) : Math.max(0, ...TORSO.map(([, ta, tb, rt]) => RADIUS[part] + rt - segSeg(a, b, s.P[I[ta]], s.P[I[tb]]))));
|
|
78
|
+
const arms = new Map();
|
|
79
|
+
const arm = r => {
|
|
80
|
+
if (arms.has(r)) return arms.get(r);
|
|
81
|
+
const W = wristAt(r), land = landscape(sk, s, SIDES.l, hand(r), W, plane, ELBOW_REST);
|
|
82
|
+
const at = th => { const q = land.at(th); return { pv: q.pv, elbow: q.elbow, cost: q.cost + CLEAR * ((100 * into(S, q.elbow, "uarm_l")) ** 2 + (100 * into(q.elbow, W, "farm_l")) ** 2) }; };
|
|
83
|
+
let best = { th: 0, cost: Infinity };
|
|
84
|
+
for (let i = 0; i < POLES; i++) { const th = (2 * Math.PI * i) / POLES - Math.PI, v = at(th).cost; if (v < best.cost) best = { th, cost: v }; }
|
|
85
|
+
const K = add(W, apply(hand(r), knuckle)), deep = into(W, K, "hand_l"), past = Math.max(0, deep - QA_LIMITS.body);
|
|
86
|
+
// the holding hand is done: how far this hand's capsule goes into it (QA SELF "hands overlap")
|
|
87
|
+
const touch = 2 * RADIUS.hand_l - segSeg(W, K, s.P[I.RightHand], s.P[I.RightHandMiddle3]), over = Math.max(0, touch - QA_LIMITS.hands);
|
|
88
|
+
const fixed = CLEAR * ((100 * deep) ** 2 + (100 * land.miss) ** 2) + (past > 0 ? DEEP * (100 * past) ** 2 : 0) + (over > 0 ? DEEP * (100 * over) ** 2 : 0);
|
|
89
|
+
const a = { at, best: { th: best.th, pole: at(best.th).pv, cost: best.cost + fixed } };
|
|
90
|
+
arms.set(r, a);
|
|
91
|
+
return a;
|
|
92
|
+
};
|
|
93
|
+
const cost = r => arm(r).best.cost + PULL * (1 - Math.cos(r));
|
|
94
|
+
const softmin = () => {
|
|
95
|
+
const all = Array.from({ length: ROLLS }, (_, i) => { const r = (2 * Math.PI * i) / ROLLS - Math.PI; return [r, cost(r)]; }), low = Math.min(...all.map(x => x[1]));
|
|
96
|
+
const m = all.reduce((acc, [r, v]) => { const e = Math.exp(-(v - low) / TEMP); return [acc[0] + e * Math.cos(r), acc[1] + e * Math.sin(r)]; }, [0, 0]);
|
|
97
|
+
return Math.atan2(m[1], m[0]);
|
|
98
|
+
};
|
|
99
|
+
return { id, weight, pin, at: left.pos, rolled, arm, cost, point, softmin, measure: s2 => palmFrame(sk, s2, SIDES.l) };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** How much of a grip at `roll` the arm can take: 1 while its cost is under EASY, fading by SLACK past it. */
|
|
103
|
+
export const gripGate = (grip, roll) => Math.exp(-Math.max(0, grip.cost(roll) - EASY) / SLACK);
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The hold ({ w, palm, measure, pole, pin, second } for compiler/hold.js) of a grip at `roll`
|
|
107
|
+
* radians with the elbow toward `pole` (a world direction), w the keys' lead weight, gate the
|
|
108
|
+
* planned gripGate: a grip the arm cannot hold fades out, so the hand stays nearer where the plain
|
|
109
|
+
* arm put it instead of turning to a limit and flipping there.
|
|
110
|
+
*/
|
|
111
|
+
export const secondHold = (grip, w, roll, gate, pole) => ({ w: w * Math.min(1, grip.weight) * gate, second: grip.id, pin: grip.pin,
|
|
112
|
+
pole, palm: { pos: grip.at, R: grip.rolled(roll) }, measure: grip.measure });
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// How a falling object (C17, free-motion.js) comes to rest. It falls with its centre on a parabola, turning at the spin it
|
|
2
|
+
// was let go with, and lands on its lowest point. Then it rolls about what touches the support. Landing on an edge or a
|
|
3
|
+
// corner it rocks back onto the face nearest down, or falls over onto the face it leans to when it leans past that edge.
|
|
4
|
+
// Then it goes over an edge onto a lower face while the landing has the energy (a share TIP of the drop; only one that hit
|
|
5
|
+
// tilted or spinning, since a flat landing has nothing to tip it), or at once from a precarious face (one it can tilt less
|
|
6
|
+
// than PRECARIOUS before its centre passes the edge). So a phone on its edge falls flat, a tilted box comes down on a face,
|
|
7
|
+
// a can let go at a slant lies down, and a can or cup dropped upright stays upright. It only rolls onto a face that lowers
|
|
8
|
+
// its centre by at least LOWER of its height, so a near cube stays the way it lands. The shape is the box around the
|
|
9
|
+
// object's parts in its own frame, and its centre is the parts' volume-weighted centre. Every step is a rotation about a
|
|
10
|
+
// pivot on the support, paced by gravity: closed form and deterministic.
|
|
11
|
+
import { add, sub, scale, dot, cross, len, norm, mul, apply, axisRot, clamp } from "./compiler/math.js";
|
|
12
|
+
import { G } from "./free-motion.js";
|
|
13
|
+
|
|
14
|
+
/** The share of the drop height a landing spends tipping the object over an edge. */
|
|
15
|
+
export const TIP = 0.3;
|
|
16
|
+
const LOWER = 0.2, FLAT = 0.5, PRECARIOUS = 15, MIN_SPIN = 1, MAX_SPIN = 6 * Math.PI, MAX_STEPS = 3;
|
|
17
|
+
const DOWN = [0, -1, 0];
|
|
18
|
+
const AXES = [[1, 0, 0], [-1, 0, 0], [0, 1, 0], [0, -1, 0], [0, 0, 1], [0, 0, -1]];
|
|
19
|
+
const deg = r => (r * 180) / Math.PI;
|
|
20
|
+
|
|
21
|
+
/** The shape of part boxes { pos, R, half } given in the root frame: { com, lo, hi } (box around the corners); null without volume. */
|
|
22
|
+
export function shapeOf(boxes) {
|
|
23
|
+
const solid = boxes.filter(b => b.half[0] * b.half[1] * b.half[2] > 1e-12);
|
|
24
|
+
if (!solid.length) return null;
|
|
25
|
+
let vol = 0, c = [0, 0, 0];
|
|
26
|
+
const corners = [];
|
|
27
|
+
for (const b of solid) {
|
|
28
|
+
const v = b.half[0] * b.half[1] * b.half[2];
|
|
29
|
+
vol += v;
|
|
30
|
+
c = add(c, scale(add(b.pos, apply(b.R, [0, b.half[1], 0])), v));
|
|
31
|
+
for (const x of [-1, 1]) for (const y of [0, 2]) for (const z of [-1, 1]) corners.push(add(b.pos, apply(b.R, [x * b.half[0], y * b.half[1], z * b.half[2]])));
|
|
32
|
+
}
|
|
33
|
+
return { com: scale(c, 1 / vol), lo: [0, 1, 2].map(i => Math.min(...corners.map(p => p[i]))), hi: [0, 1, 2].map(i => Math.max(...corners.map(p => p[i]))) };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const boxCorners = s => [0, 1, 2, 3, 4, 5, 6, 7].map(i => [i & 1 ? s.hi[0] : s.lo[0], i & 2 ? s.hi[1] : s.lo[1], i & 4 ? s.hi[2] : s.lo[2]]);
|
|
37
|
+
const world = (pose, p) => add(pose.pos, apply(pose.R, p));
|
|
38
|
+
const lowest = (s, pose) => Math.min(...boxCorners(s).map(p => world(pose, p)[1]));
|
|
39
|
+
/** The pose moved up or down so its lowest corner sits on the support top. */
|
|
40
|
+
const snap = (s, pose, top) => ({ pos: [pose.pos[0], pose.pos[1] + top - lowest(s, pose), pose.pos[2]], R: pose.R });
|
|
41
|
+
const about = (pose, pivot, Q) => ({ pos: add(pivot, apply(Q, sub(pose.pos, pivot))), R: mul(Q, pose.R) });
|
|
42
|
+
/** The face whose outward normal points most nearly down. */
|
|
43
|
+
const downFace = R => AXES.reduce((best, n, k) => (dot(apply(R, n), DOWN) > dot(apply(R, AXES[best]), DOWN) ? k : best), 0);
|
|
44
|
+
/** Where the plane of face k lies along its axis, and the centre's distance to it (its height when it rests on k). */
|
|
45
|
+
const facePlane = (s, k) => { const a = k >> 1; return AXES[k][a] > 0 ? s.hi[a] : s.lo[a]; };
|
|
46
|
+
const heightOn = (s, k) => Math.abs(facePlane(s, k) - s.com[k >> 1]);
|
|
47
|
+
|
|
48
|
+
/** The spin (rad/s, world axis times rate) that takes rotation Ra to Rb in one frame at fps; capped at MAX_SPIN, none under MIN_SPIN (a hand's jitter). */
|
|
49
|
+
export function spinOf(Ra, Rb, fps) {
|
|
50
|
+
const D = mul(Rb, [[Ra[0][0], Ra[1][0], Ra[2][0]], [Ra[0][1], Ra[1][1], Ra[2][1]], [Ra[0][2], Ra[1][2], Ra[2][2]]]);
|
|
51
|
+
const a = Math.acos(clamp((D[0][0] + D[1][1] + D[2][2] - 1) / 2, -1, 1));
|
|
52
|
+
if (a * fps < MIN_SPIN) return [0, 0, 0];
|
|
53
|
+
const axis = norm([D[2][1] - D[1][2], D[0][2] - D[2][0], D[1][0] - D[0][1]]), rate = Math.min(MAX_SPIN, a * fps);
|
|
54
|
+
return len(axis) > 0 ? scale(axis, rate) : [0, 0, 0];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The fall through the air from `start` onto the support top: { tl (s), at(t) } with the centre on a parabola, turning at `spin`. */
|
|
58
|
+
export function airPath(s, start, spin, top, fps) {
|
|
59
|
+
const rate = len(spin), com0 = world(start, s.com);
|
|
60
|
+
const at = t => {
|
|
61
|
+
const R = rate > 0 ? mul(axisRot(spin, deg(rate * t)), start.R) : start.R;
|
|
62
|
+
return { pos: sub(add(com0, [0, -0.5 * G * t * t, 0]), apply(R, s.com)), R };
|
|
63
|
+
};
|
|
64
|
+
const h = Math.max(0, lowest(s, start) - top);
|
|
65
|
+
if (rate === 0) return { tl: Math.sqrt((2 * h) / G), at };
|
|
66
|
+
// spinning: the first sub-frame step whose lowest corner reaches the top, then halved down to the touch
|
|
67
|
+
const dt = 1 / (fps * 8), cap = Math.sqrt((2 * (h + len(sub(s.hi, s.lo)))) / G) + dt;
|
|
68
|
+
let a = 0, b = dt;
|
|
69
|
+
while (b < cap && lowest(s, at(b)) > top) { a = b; b += dt; }
|
|
70
|
+
for (let i = 0; i < 24 && b - a > 1e-7; i++) { const m = (a + b) / 2; if (lowest(s, at(m)) > top) a = m; else b = m; }
|
|
71
|
+
return { tl: b, at };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** A step's duration (s): gravity-paced by the centre's height and the angle it rolls through. */
|
|
75
|
+
const stepTime = (height, angle) => 0.12 + 0.9 * Math.sqrt(Math.max(height, 1e-4) / G) * Math.sqrt(angle / 90);
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* From the pose it lands in, how it rolls to rest on the support top with `energy` (m of drop) to tip over edges:
|
|
79
|
+
* { steps: [{ from, pivot, axis, angle, time }], rest, time }. `lean` (horizontal) is the way a spin tips it: it gives a flat
|
|
80
|
+
* landing the energy and breaks a tie between edges.
|
|
81
|
+
*/
|
|
82
|
+
export function settlePath(s, impact, top, energy, lean = [0, 0, 0]) {
|
|
83
|
+
const steps = [];
|
|
84
|
+
let pose = snap(s, impact, top), time = 0;
|
|
85
|
+
const push = (pivot, axis, angle) => {
|
|
86
|
+
const height = world(pose, s.com)[1] - top, t = stepTime(height, angle);
|
|
87
|
+
steps.push({ from: pose, pivot, axis, angle, time: t });
|
|
88
|
+
pose = snap(s, about(pose, pivot, axisRot(axis, angle)), top);
|
|
89
|
+
time += t;
|
|
90
|
+
};
|
|
91
|
+
// landed on an edge or a corner: back onto the face nearest down, or over onto the face it leans to when it leans past
|
|
92
|
+
// that edge (more than the angle its centre needs to pass it), rolling about the corners on the support
|
|
93
|
+
let k = downFace(pose.R);
|
|
94
|
+
const tiltOf = j => deg(Math.acos(clamp(dot(apply(pose.R, AXES[j]), DOWN), -1, 1))), tilt = tiltOf(k);
|
|
95
|
+
if (tilt > FLAT) {
|
|
96
|
+
const side = [0, 1, 2, 3, 4, 5].filter(j => j >> 1 !== k >> 1).reduce((a, j) => (tiltOf(j) < tiltOf(a) ? j : a), k === 0 || k === 1 ? 2 : 0);
|
|
97
|
+
if (tilt > deg(Math.atan2(heightOn(s, side), heightOn(s, k)))) k = side;
|
|
98
|
+
const touching = boxCorners(s).map(p => world(pose, p)).filter(p => p[1] <= top + 1e-4);
|
|
99
|
+
const n = apply(pose.R, AXES[k]);
|
|
100
|
+
push(scale(touching.reduce(add, [0, 0, 0]), 1 / touching.length), norm(cross(n, DOWN)), tiltOf(k));
|
|
101
|
+
}
|
|
102
|
+
// over an edge onto a lower face while the landing has the energy, at once from a precarious face (or a centre past the
|
|
103
|
+
// edge); a flat landing with no spin has none
|
|
104
|
+
let left = tilt > FLAT || len(lean) > 0 ? energy : 0;
|
|
105
|
+
for (let i = 0; i < MAX_STEPS; i++) {
|
|
106
|
+
const com = world(pose, s.com), cK = com[1] - top;
|
|
107
|
+
let best = null;
|
|
108
|
+
for (let j = 0; j < 6; j++) {
|
|
109
|
+
if (j >> 1 === k >> 1) continue;
|
|
110
|
+
const out = apply(pose.R, AXES[j]), edge = world(pose, AXES[j].map((v, a) => (v ? facePlane(s, j) : a === k >> 1 ? facePlane(s, k) : s.com[a])));
|
|
111
|
+
const w = -dot(sub(com, edge), out), cJ = heightOn(s, j);
|
|
112
|
+
if (!(w < 0 || cJ < cK * (1 - LOWER))) continue;
|
|
113
|
+
const cost = w < 0 || deg(Math.atan2(w, cK)) < PRECARIOUS ? 0 : Math.hypot(cK, w) - cK, side = dot(out, lean);
|
|
114
|
+
if (!best || cost < best.cost - 1e-9 || (Math.abs(cost - best.cost) <= 1e-9 && side > best.side)) best = { j, out, edge, cost, side };
|
|
115
|
+
}
|
|
116
|
+
if (!best || best.cost > left) break;
|
|
117
|
+
left -= best.cost;
|
|
118
|
+
const touching = boxCorners(s).map(p => world(pose, p)).filter(p => p[1] <= top + 1e-4 && dot(sub(p, best.edge), best.out) >= -1e-4);
|
|
119
|
+
push(touching.length ? scale(touching.reduce(add, [0, 0, 0]), 1 / touching.length) : [best.edge[0], top, best.edge[2]], norm(cross(best.out, DOWN)), 90);
|
|
120
|
+
k = best.j;
|
|
121
|
+
}
|
|
122
|
+
return { steps, rest: pose, time };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** The pose `u` seconds after it landed, along the settle path: each step rolls about its pivot, accelerating like a fall. */
|
|
126
|
+
export function settledAt(path, u) {
|
|
127
|
+
let t = u;
|
|
128
|
+
for (const st of path.steps) {
|
|
129
|
+
if (t < st.time) { const s = t / st.time; return about(st.from, st.pivot, axisRot(st.axis, st.angle * s * s)); }
|
|
130
|
+
t -= st.time;
|
|
131
|
+
}
|
|
132
|
+
return path.rest;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The energy (m of drop) a spin adds: its kinetic energy over m g, for the box's radius of gyration about its centre. */
|
|
136
|
+
export const spinEnergy = (s, spin) => (len(spin) ** 2 * ((s.hi[0] - s.lo[0]) ** 2 + (s.hi[1] - s.lo[1]) ** 2 + (s.hi[2] - s.lo[2]) ** 2)) / (24 * G);
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Basic skills: stand, walk, reach-and-grip. Each reads the channel state the earlier keys
|
|
2
|
+
// leave at its f0 and returns keys plus whatever anchors, props, fits and checks it needs.
|
|
3
|
+
import { add, apply, ry } from "../compiler/math.js";
|
|
4
|
+
import { walk } from "../compiler/walk.js";
|
|
5
|
+
import { keyBook, skillError } from "./book.js";
|
|
6
|
+
import { walkEnd } from "./info.js";
|
|
7
|
+
|
|
8
|
+
/** standIdle({ at: [x, z], heading = 0, until }): a neutral standing pose, arms down (FK). */
|
|
9
|
+
export function standIdle(ctx) {
|
|
10
|
+
const { sk, args, state } = ctx, I = sk.index, P = sk.restWorldPos, ank = P[I.LeftFoot][1];
|
|
11
|
+
const h = args.heading ?? state.yaw ?? 0, base = args.at ?? (state.root ? [state.root[0], state.root[2]] : null);
|
|
12
|
+
if (!base) skillError(ctx.tag, "needs args.at: [x, z] (no root is keyed before f0)");
|
|
13
|
+
const c = [base[0], 0, base[base.length - 1]], R = ry(h), foot = v => { const p = add(c, apply(R, v)); p[1] = ank; return p; };
|
|
14
|
+
const set = { root: [c[0], P[I.Hips][1] - 0.01, c[2]], yaw: h, pitch: 0, roll: 0, spine_yaw: 0, spine_pitch: 0, spine_roll: 0, head_yaw: 0, head_pitch: 0,
|
|
15
|
+
foot_l: foot([0.09, 0, -0.03]), foot_r: foot([-0.09, 0, 0.03]), fyaw_l: h, fyaw_r: h, fpitch_l: 0, fpitch_r: 0, knee_l: [0.05, 0, 1], knee_r: [-0.05, 0, 1],
|
|
16
|
+
armw_l: 0, armw_r: 0, aswing_l: 0, aswing_r: 0, abend_l: 0, abend_r: 0, arm_hang: 1 };
|
|
17
|
+
const keys = [{ f: ctx.f0, set }];
|
|
18
|
+
if (args.until !== undefined) keys.push({ f: args.until, set: structuredClone(set) });
|
|
19
|
+
return { keys, end: args.until ?? ctx.f0 };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* walkTo({ to: [x, z] } | { heading: h | [h0, h1] }, steps, step = 0.5, turnDistance, ...): the
|
|
24
|
+
* walk generator from the pose at f0. With `to` the heading turns toward the point and the step
|
|
25
|
+
* count covers the distance. Ends 16 + steps * period + 18 frames after f0, feet together; a
|
|
26
|
+
* take that ends after the last step holds that pose to its end. The steps keep a natural rhythm
|
|
27
|
+
* (compiler/gait-rhythm.js); vary: 0 makes every step the same.
|
|
28
|
+
*/
|
|
29
|
+
export function walkTo(ctx) {
|
|
30
|
+
const { args, state } = ctx;
|
|
31
|
+
if (!state.foot_l || !state.foot_r || !state.root) skillError(ctx.tag, "needs a standing pose keyed before f0 (use standIdle or another skill first)");
|
|
32
|
+
const gen = { type: "walk", f0: ctx.f0, ...args };
|
|
33
|
+
delete gen.to; delete gen.arrive;
|
|
34
|
+
const h0 = state.yaw ?? 0, step = args.step ?? 0.5, first = args.firstStep ?? 0.4;
|
|
35
|
+
if (args.to) {
|
|
36
|
+
const c0 = [(state.foot_l[0] + state.foot_r[0]) / 2, (state.foot_l[2] + state.foot_r[2]) / 2];
|
|
37
|
+
const dx = args.to[0] - c0[0], dz = args.to[args.to.length - 1] - c0[1], d = Math.hypot(dx, dz);
|
|
38
|
+
let h1 = Math.atan2(dx, dz) * 180 / Math.PI;
|
|
39
|
+
while (h1 - h0 > 180) h1 -= 360;
|
|
40
|
+
while (h1 - h0 < -180) h1 += 360;
|
|
41
|
+
gen.heading = args.heading ?? [h0, h1];
|
|
42
|
+
gen.steps = args.steps ?? Math.max(2, 2 + Math.round((d - first) / step));
|
|
43
|
+
}
|
|
44
|
+
if (typeof gen.heading === "number") gen.heading = [h0, gen.heading];
|
|
45
|
+
const keys = walk(gen, ctx.keys, ctx.sk, ctx.tag), end = walkEnd(gen), last = ctx.frames - 1;
|
|
46
|
+
// the settle after the last step only holds the pose: a take that ends in it ends the walk there
|
|
47
|
+
if (end > last && Math.max(...keys.filter(k => k.f < end).map(k => k.f)) <= last) return { keys: keys.filter(k => k.f <= last), end: last, cut: `settle cut at f${last} (hold to the end)` };
|
|
48
|
+
return { keys, end };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* reachGrip({ anchor, side, reach = 20, hold = 22, release = 12, elbow, fingers = "grip",
|
|
53
|
+
* define, orient = true }): the hand goes to the anchor (defined elsewhere, or by `define`) over
|
|
54
|
+
* `reach` frames with the fingers closing, holds, and lets go. Adds a fit of the fingertips on the
|
|
55
|
+
* anchor (on a body it also turns the hand into a grip, compiler/grip.js; orient: false leaves the
|
|
56
|
+
* hand's turn alone) and a contact check. A take that ends during the release holds the grip to its end.
|
|
57
|
+
*/
|
|
58
|
+
export function reachGrip(ctx) {
|
|
59
|
+
const { args, state } = ctx, k = args.side, name = args.anchor;
|
|
60
|
+
if (k !== "l" && k !== "r") skillError(ctx.tag, `args.side must be "l" or "r", got ${JSON.stringify(k)}`);
|
|
61
|
+
if (typeof name !== "string" || /_[lr]$/.test(name)) skillError(ctx.tag, "args.anchor must name an anchor (not ending in _l/_r)");
|
|
62
|
+
const reach = args.reach ?? 20, hold = args.hold ?? 22, release = args.release ?? 12, preset = args.fingers ?? "grip";
|
|
63
|
+
const w = `w_${name}_${k}`, fg = `fingers_${k}`, armw = `armw_${k}`, el = `elbow_${k}`, wasFk = !(state[armw] > 0.5);
|
|
64
|
+
// a take that ends after the hold but before the hand lets go cuts the release: the grip holds to the last frame
|
|
65
|
+
const last = ctx.frames - 1, cut = ctx.f0 + reach + hold <= last && ctx.f0 + reach + hold + release > last;
|
|
66
|
+
for (const [c, v] of [[w, 0], [fg, { [preset]: 0 }], [armw, 0]]) if (state[c] === undefined) state[c] = v;
|
|
67
|
+
const b = keyBook(state, ctx.f0);
|
|
68
|
+
b.hold(0, w, fg, armw, el);
|
|
69
|
+
b.at(reach, { [w]: 1, [fg]: { ...state[fg], [preset]: 1 }, [armw]: 1, ...(args.elbow ? { [el]: args.elbow } : {}) });
|
|
70
|
+
b.hold(reach + hold, w, fg, armw, el);
|
|
71
|
+
if (!cut) b.at(reach + hold + release, { [w]: 0, [fg]: { ...state[fg], [preset]: 0 }, ...(wasFk ? { [armw]: 0 } : {}) });
|
|
72
|
+
const S = k === "l" ? "Left" : "Right", prop = `${name}Grip`;
|
|
73
|
+
const def = args.define ? { [name]: args.define } : {};
|
|
74
|
+
// the hand may touch what it grips: an object, or the other person (people.js) whose joint the anchor is on
|
|
75
|
+
const a = args.define ?? ctx.anchors?.[name], obj = a?.kind === "object" ? a.object : a?.kind === "person" ? `person:${a.person}` : null;
|
|
76
|
+
return {
|
|
77
|
+
keys: b.keys, end: cut ? last : ctx.f0 + reach + hold + release, ...(cut ? { cut: `release cut at f${last} (hold to the end)` } : {}), anchors: def,
|
|
78
|
+
props: { [prop]: { hand: k, origin: [`${S}HandIndex2`, `${S}HandMiddle2`], points: { fingers: [0, 0, 0] } } },
|
|
79
|
+
fits: [{ side: k, anchor: name, effector: { prop, point: "fingers" }, ...(args.orient === false ? { orient: false } : {}) }],
|
|
80
|
+
checks: { contacts: [{ name: `fingers on ${name}`, point: `prop:${prop}.fingers`, target: `anchor:${name}`, tol: 0.04, when: w }],
|
|
81
|
+
allow: obj ? [{ part: "hand", object: obj, depth: 0.04 }] : [] },
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Skill plumbing: a key book that tracks the channel state while a skill writes keys, and the
|
|
2
|
+
// placement of choreography written in a template frame (e.g. a car's own frame, driver side
|
|
3
|
+
// on +X) into the scene: translate, turn by a yaw, and mirror left/right for the other side.
|
|
4
|
+
import { add, apply, ry } from "../compiler/math.js";
|
|
5
|
+
|
|
6
|
+
const SIDE_SWAP = { _l: "_r", _r: "_l" };
|
|
7
|
+
const NEGATE_ON_MIRROR = /^(yaw|roll|spine_yaw|spine_roll|head_yaw|head_roll|fyaw_[lr]|wtwist_[lr]|wflex_[lr])$/;
|
|
8
|
+
const POINT = /^(root|foot_[lr]|hand_[lr])$/, POLE = /^(knee|elbow)_[lr]$/, YAW = /^(yaw|fyaw_[lr])$/;
|
|
9
|
+
|
|
10
|
+
const clone = v => (v && typeof v === "object" ? structuredClone(v) : v);
|
|
11
|
+
export const swapSide = name => name.replace(/_(l|r)$/, m => SIDE_SWAP[m]).replace(/^(Left|Right)/, m => (m === "Left" ? "Right" : "Left"));
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A frame: { origin: [x,y,z], yaw (deg), mirror (bool), negate: Set of extra channels (hinge
|
|
15
|
+
* angles) to negate, names: { anchor: mirroredAnchor } }. Template points p map to
|
|
16
|
+
* origin + ry(yaw)(mirror ? [-x, y, z] : p).
|
|
17
|
+
*/
|
|
18
|
+
export const identityFrame = () => ({ origin: [0, 0, 0], yaw: 0, mirror: false, negate: new Set(), names: {} });
|
|
19
|
+
|
|
20
|
+
const placePoint = (fr, p) => add(fr.origin, apply(ry(fr.yaw), fr.mirror ? [-p[0], p[1], p[2]] : p));
|
|
21
|
+
export const placeDir = (fr, v) => (fr.mirror ? [-v[0], v[1], v[2]] : v.slice());
|
|
22
|
+
|
|
23
|
+
/** A channel name in the placed frame (sides swap and anchor names rename when mirrored). */
|
|
24
|
+
export function placeChannel(fr, ch) {
|
|
25
|
+
if (!fr.mirror) return ch;
|
|
26
|
+
const w = /^w_(.+)_([lr])$/.exec(ch);
|
|
27
|
+
if (w) return `w_${fr.names[w[1]] ?? w[1]}_${w[2] === "l" ? "r" : "l"}`;
|
|
28
|
+
return /_[lr]$/.test(ch) ? swapSide(ch) : ch;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function placeSet(fr, set) {
|
|
32
|
+
const out = {};
|
|
33
|
+
for (const [ch, v0] of Object.entries(set)) {
|
|
34
|
+
let v = clone(v0);
|
|
35
|
+
if (POINT.test(ch)) v = placePoint(fr, v);
|
|
36
|
+
else if (POLE.test(ch)) v = placeDir(fr, v);
|
|
37
|
+
else if (typeof v === "number") {
|
|
38
|
+
if (fr.mirror && (NEGATE_ON_MIRROR.test(ch) || fr.negate.has(ch))) v = -v;
|
|
39
|
+
if (YAW.test(ch)) v += fr.yaw;
|
|
40
|
+
}
|
|
41
|
+
out[placeChannel(fr, ch)] = v;
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Keys relative to f0 (book.at(12, ...) writes frame f0 + 12). `at` and `step` take template
|
|
48
|
+
* values and place them; `hold` re-keys the current (already placed) values.
|
|
49
|
+
*/
|
|
50
|
+
export function keyBook(state, f0 = 0, fr = identityFrame()) {
|
|
51
|
+
const keys = [], name = ch => placeChannel(fr, ch);
|
|
52
|
+
return {
|
|
53
|
+
state, keys, frame: fr,
|
|
54
|
+
at(f, set) { const placed = placeSet(fr, set); Object.assign(state, clone(placed)); keys.push({ f: f0 + f, set: placed }); },
|
|
55
|
+
hold(f, ...names) { keys.push({ f: f0 + f, set: Object.fromEntries(names.filter(n => state[name(n)] !== undefined).map(n => [name(n), clone(state[name(n)])])) }); },
|
|
56
|
+
step(f, ch, value) {
|
|
57
|
+
const n = name(ch);
|
|
58
|
+
keys.push({ f: f0 + f - 1, set: { [n]: state[n] } });
|
|
59
|
+
state[n] = value;
|
|
60
|
+
keys.push({ f: f0 + f, set: { [n]: value }, step: [n] });
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Throws the skill's own MOTION_SPEC_INVALID message. */
|
|
66
|
+
export const skillError = (tag, msg) => { throw new Error(`MOTION_SPEC_INVALID: ${tag}: ${msg}`); };
|
|
67
|
+
|
|
68
|
+
/** Scene box by id or a skill error naming the arg. */
|
|
69
|
+
export function sceneBox(ctx, arg, id) {
|
|
70
|
+
const box = ctx.scene?.objects?.[id];
|
|
71
|
+
if (!box) skillError(ctx.tag, `args.${arg} "${id}" is not a scene object`);
|
|
72
|
+
return box;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Yaw (deg) of a box's rotation matrix about +Y. */
|
|
76
|
+
export const yawOf = R => Math.atan2(R[0][2], R[2][2]) * 180 / Math.PI;
|