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,130 @@
|
|
|
1
|
+
// The grip a keyed carry keeps (C18, lane pathgrip). Without at/along a carry keeps the palm-object transform of
|
|
2
|
+
// its grab frame (object-keys.js attach), and nothing chose that grip: the hand met the object at whatever turn the
|
|
3
|
+
// reach left it, and the keys then took the cup to the mouth with the wrist past its comfortable range on 9-47
|
|
4
|
+
// frames in 6 of 8 bench S8 takes (50-58 deg of flex and 70 deg of forearm twist at the mouth). A person picks the
|
|
5
|
+
// grip for where the object is going (end-state comfort, Rosenbaum), so for a keyed carry whose grip is free the
|
|
6
|
+
// grab offset is chosen here over the carry's keyed path:
|
|
7
|
+
// cost at the grab (the hand takes the grip on the body at rest) and along the keys, the arm's wrist need at
|
|
8
|
+
// the elbow the hold solve takes (arm-orient.js landscape) priced against the comfortable ranges
|
|
9
|
+
// (WRIST_COMFORT: z^2 inside, STEEP to the anatomical limits, OVER past them), plus REACH per squared
|
|
10
|
+
// centimetre the arm is short or folded (a residual QA reports) and BODY per squared centimetre the arm
|
|
11
|
+
// or the hand goes into the body (body.js)
|
|
12
|
+
// faithful the keys as the compile resolves them (key-aim.js: an aim key without roll at the roll the arm's
|
|
13
|
+
// strain picks), priced every STEP frames; today's grip that keeps every frame comfortable stays
|
|
14
|
+
// search from today's grip, turns of STEPS degrees about the palm axes (the fingers' line, the palm normal,
|
|
15
|
+
// the knuckle line) while the rough cost (the grab and KEYS keys, an aim key at the best of ROLLS rolls
|
|
16
|
+
// by the strain) drops; the fingers' point stays where the fit put it
|
|
17
|
+
// pick the found grip only when its faithful cost beats today's by MARGIN or SHARE of it (no flip for less)
|
|
18
|
+
// Deterministic. The hand comes into the grip on its reach and leaves it on its release (object-keys.js).
|
|
19
|
+
import { sub, scale, norm, len, apply, mul, T, axisRot, matFromQuat, quatFromMat } from "./compiler/math.js";
|
|
20
|
+
import { qmul, qrot, qdot, axisQuat, arcQuat, qlog, slerp } from "./compiler/quat.js";
|
|
21
|
+
import { aimArm, holdCost } from "./compiler/hold.js";
|
|
22
|
+
import { curveOf, keyedPose } from "./object-keys.js";
|
|
23
|
+
import { landscape, ELBOW_REST, WRIST_COMFORT, WRIST_LIMITS } from "./compiler/arm-orient.js";
|
|
24
|
+
import { bodyFor, segmentDepth } from "./body.js";
|
|
25
|
+
import { reachAt, ELBOW_FOLD } from "./compiler/limbs.js";
|
|
26
|
+
import { compose, relative } from "./carried.js";
|
|
27
|
+
|
|
28
|
+
const THIN = 0.045, ROLLS = 4, KEYS = 8, STEPS = [30, 10], TRIES = 2, STEP = 4, MARGIN = 2, SHARE = 0.1, COMFORTABLE = 1.5, REACH = 20, BODY = 4, STEEP = 8, OVER = 400;
|
|
29
|
+
// the palm frame's columns (carried.js carriedPose): the fingers' line, the palm normal, the knuckle line (reversed)
|
|
30
|
+
const AXES = ["the fingers' line", "the palm normal", "the knuckle line"];
|
|
31
|
+
const col = (R, i) => [R[0][i], R[1][i], R[2][i]];
|
|
32
|
+
const invert = p => { const Rt = T(p.R); return { pos: scale(apply(Rt, p.pos), -1), R: Rt }; };
|
|
33
|
+
const r2 = v => Math.round(v * 100) / 100;
|
|
34
|
+
|
|
35
|
+
/** The rotation a fraction t of the way from A to B. */
|
|
36
|
+
export function between(A, B, t) {
|
|
37
|
+
const a = quatFromMat(A);
|
|
38
|
+
let b = quatFromMat(B);
|
|
39
|
+
if (qdot(a, b) < 0) b = b.map(v => -v);
|
|
40
|
+
return matFromQuat(slerp(a, b, t));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Price of one wrist turn v (deg) against its comfortable range and anatomical limits. */
|
|
44
|
+
function price(v, [clo, chi], [llo, lhi]) {
|
|
45
|
+
const c = v >= 0 ? chi : -clo, L = v >= 0 ? lhi : -llo, a = Math.abs(v);
|
|
46
|
+
if (a <= c) return (a / c) ** 2;
|
|
47
|
+
const e = (Math.min(a, L) - c) / Math.max(1e-6, L - c);
|
|
48
|
+
return 1 + STEEP * e * e + (a > L ? OVER * ((a - L) / L) ** 2 : 0);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The arm's comfort-priced cost of holding palm frame `palm` on pose s (the plain arm of `side`, `now` its palm frame there). */
|
|
52
|
+
function comfortCost(sk, s, side, palm, now) {
|
|
53
|
+
const { goal, plane, wristAt } = aimArm(sk, s, side, palm, now, 1), W = wristAt(goal), land = landscape(sk, s, side, goal, W, plane, ELBOW_REST);
|
|
54
|
+
const arm = land.at(land.softmin()), I = sk.index, S = s.P[I[`${side}Arm`]];
|
|
55
|
+
const A = len(sub(s.P[I[`${side}ForeArm`]], S)), B = len(sub(s.P[I[`${side}Hand`]], s.P[I[`${side}ForeArm`]]));
|
|
56
|
+
const fold = Math.max(0, reachAt(A, B, ELBOW_FOLD) - len(sub(W, S))), body = bodyFor(sk);
|
|
57
|
+
const into = body ? Math.max(segmentDepth(body, sk, s, I[`${side}Arm`], S, arm.elbow), segmentDepth(body, sk, s, I[`${side}ForeArm`], arm.elbow, W), segmentDepth(body, sk, s, I[`${side}Hand`], W, palm.pos)) : 0;
|
|
58
|
+
return ["twist", "flex", "dev"].reduce((c, k) => c + price(arm.need[k], WRIST_COMFORT[k], WRIST_LIMITS[k]), 0) + REACH * ((land.miss * 100) ** 2 + (fold * 100) ** 2) + BODY * (into * 100) ** 2;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The rough cost of key `key` with offset A: a rot key at its rot; an aim key turned from where the carry holds it (key-aim.js), at its roll or at the best of ROLLS rolls by the arm's strain (hold.js holdCost, as the compile picks it), priced by priceOf. */
|
|
62
|
+
function keyCost(sk, s, side, key, now, A, priceOf) {
|
|
63
|
+
if (key.aim === undefined) return priceOf({ pos: key.pos, R: matFromQuat(key.rot) });
|
|
64
|
+
const from = quatFromMat(compose(now, A).R), d = norm(key.aim), q0 = qmul(arcQuat(norm(qrot(from, [0, 1, 0])), d), from);
|
|
65
|
+
const pose = r => ({ pos: key.pos, R: matFromQuat(qmul(axisQuat(d, r), q0)) });
|
|
66
|
+
if (key.roll !== undefined) return priceOf(pose(key.roll));
|
|
67
|
+
let best = null;
|
|
68
|
+
for (let j = 0; j < ROLLS; j++) {
|
|
69
|
+
const p = pose(j * (360 / ROLLS) - 180), strain = holdCost(sk, s, side, compose(p, invert(A)), now);
|
|
70
|
+
if (!best || strain < best.strain) best = { p, strain };
|
|
71
|
+
}
|
|
72
|
+
return priceOf(best.p);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* True when body or group `id` is thin (two half sizes under THIN m: a cup, a can, a bar, a handle); a group by its
|
|
77
|
+
* largest part (a cup with a handle is held by its cup). A box keeps the grip grip.js gives it at the grab.
|
|
78
|
+
*/
|
|
79
|
+
export function thinBody(scene, id) {
|
|
80
|
+
const parts = Object.entries(scene?.objects ?? {}).filter(([k, o]) => (k === id || o.ancestors?.includes(id)) && o.half);
|
|
81
|
+
if (!parts.length) return false;
|
|
82
|
+
const half = parts.map(([, o]) => o.half).reduce((m, h) => (h[0] * h[1] * h[2] > m[0] * m[1] * m[2] ? h : m));
|
|
83
|
+
const long = [0, 1, 2].reduce((a, i) => (half[i] > half[a] ? i : a), 0);
|
|
84
|
+
return [0, 1, 2].filter(i => i !== long).every(i => half[i] < THIN);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** At most n of the keys, spread over them (the first and the last kept). */
|
|
88
|
+
const spread = (keys, n) => (keys.length <= n ? keys : Array.from({ length: n }, (_, i) => keys[Math.round((i * (keys.length - 1)) / (n - 1))]));
|
|
89
|
+
|
|
90
|
+
/** How the found rotation R differs from today's R0, in words: "today's grip turned 40 deg about the palm normal". */
|
|
91
|
+
function named(R0, R) {
|
|
92
|
+
const v = qlog(quatFromMat(mul(T(R0), R))), deg = Math.round((2 * len(v) * 180) / Math.PI), i = [0, 1, 2].reduce((m, j) => (Math.abs(v[j]) > Math.abs(v[m]) ? j : m), 0);
|
|
93
|
+
return `today's grip turned ${deg} deg, mostly about ${AXES[i]}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* chooseGrip({ sk, o, side, grab, rest, plain, palmOf, map }) -> { offset, name, cost, today }: the grab offset a
|
|
98
|
+
* keyed carry with a free grip keeps (see above). plain(f) solves frame f without keys, palmOf(s) is the carry's palm
|
|
99
|
+
* frame on a pose, rest the body's pose at the grab, map a key frame to its timeline frame.
|
|
100
|
+
*/
|
|
101
|
+
export function chooseGrip({ sk, o, side, grab, rest, plain, palmOf, map = f => f }) {
|
|
102
|
+
const solved = new Map(), at = f => { const g = Math.round(f); if (!solved.has(g)) { const s = plain(g); solved.set(g, { s, now: palmOf(s) }); } return solved.get(g); };
|
|
103
|
+
const { s: s0, now: now0 } = at(grab), keys = [...o.keys].sort((a, b) => a.f - b.f), some = spread(keys, KEYS);
|
|
104
|
+
const offsetOf = R => relative({ pos: now0.pos, R }, rest), held = (A, f, pose) => { const { s, now } = at(f); return comfortCost(sk, s, side, compose(pose, invert(A)), now); };
|
|
105
|
+
const grabCost = A => comfortCost(sk, s0, side, compose(rest, invert(A)), now0);
|
|
106
|
+
const rough = R => { const A = offsetOf(R); return grabCost(A) + some.reduce((sum, key) => { const { s, now } = at(map(key.f)); return sum + keyCost(sk, s, side, key, now, A, pose => held(A, map(key.f), pose)); }, 0); };
|
|
107
|
+
const faithful = R => {
|
|
108
|
+
const A = offsetOf(R), t0 = Math.round(map(keys[0].f)), t1 = Math.round(map(keys[keys.length - 1].f));
|
|
109
|
+
const curve = curveOf(o, map, t => quatFromMat(compose(at(t).now, A).R), (t, pos, q, opts) => { const { s, now } = at(t); return holdCost(sk, s, side, compose({ pos, R: matFromQuat(q) }, invert(A)), now, opts); }, 2 * (rest?.half?.[1] ?? 0));
|
|
110
|
+
let cost = grabCost(A), worst = cost;
|
|
111
|
+
for (let f = t0; f <= t1; f += STEP) { const c = held(A, f, keyedPose(curve, f)); cost += c; worst = Math.max(worst, c); }
|
|
112
|
+
return { A, cost, worst };
|
|
113
|
+
};
|
|
114
|
+
// today's grip that keeps every frame comfortable (each turn inside its range) stays as it is
|
|
115
|
+
const today = { name: "today", R: now0.R, ...faithful(now0.R) };
|
|
116
|
+
if (today.worst <= COMFORTABLE) return { offset: today.A, name: "today", cost: r2(today.cost), today: r2(today.cost) };
|
|
117
|
+
let cur = { R: now0.R, rough: rough(now0.R) };
|
|
118
|
+
for (const step of STEPS) {
|
|
119
|
+
for (let tries = 0, better = true; better && tries < TRIES; tries++) {
|
|
120
|
+
better = false;
|
|
121
|
+
for (const i of [0, 1, 2]) for (const sign of [1, -1]) {
|
|
122
|
+
const R = mul(axisRot(col(cur.R, i), sign * step), cur.R), c = rough(R);
|
|
123
|
+
if (c < cur.rough - 1e-6) { cur = { R, rough: c }; better = true; }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const found = cur.R === now0.R ? today : { name: named(now0.R, cur.R), ...faithful(cur.R) };
|
|
128
|
+
const pick = found.cost < today.cost - Math.max(MARGIN, SHARE * today.cost) ? found : today;
|
|
129
|
+
return { offset: pick.A, name: pick.name, cost: r2(pick.cost), today: r2(today.cost) };
|
|
130
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// The arms' paths over a keyed span (C14, lane K8). What the arm does on a frame the keys lead
|
|
2
|
+
// (the holding elbow's swivel, compiler/arm-orient.js; the second hand's roll about the object,
|
|
3
|
+
// second-hand.js) is read as a cost per frame on the pose that frame starts from, and the path
|
|
4
|
+
// minimises, over all those frames at once, the cost of each frame plus SMOOTH times the squared
|
|
5
|
+
// acceleration of the point that choice moves (the elbow; the free end of the second hand): where
|
|
6
|
+
// the cost barely cares the arm glides, where it does the arm goes where the wrist needs it. The
|
|
7
|
+
// best choice alone can move 20 deg from one frame to the next while a bat whips through the
|
|
8
|
+
// zone, and an arm that followed it would jerk (QA POP). Solved by NEWTON Gauss-Newton steps from
|
|
9
|
+
// each frame's soft minimum, each a banded (5-diagonal) linear solve. Every frame reads the same
|
|
10
|
+
// plan, so a frame still solves on its own (a windowed compile, a frame solved on demand) and the
|
|
11
|
+
// same spec gives the same take.
|
|
12
|
+
// The holding elbow's path never jumps (lane Q1, steadyPath). Where the cheap poles of two frames
|
|
13
|
+
// in a row lie on either side of a ridge, smoothPath from each frame's soft minimum stays in a
|
|
14
|
+
// local optimum that jumps across it (session 4db5d447, the follow-through: the elbow could hold
|
|
15
|
+
// the bat down-forward to f43 and up-forward from f44, and the planned elbow went 22 cm in one
|
|
16
|
+
// frame, QA POP and FLIP 78 deg). A path that jumps (jumpsIn) is solved again from the cheapest
|
|
17
|
+
// path that never turns more than JUMP a frame (continuousPath, dynamic programming over the
|
|
18
|
+
// poles), and if the cost pulls that back into a jump, with the smoothing BOOST times stronger
|
|
19
|
+
// around it: the elbow goes over the ridge in a few frames (QA-17 run: at most 10 cm a frame,
|
|
20
|
+
// the plane 26 deg a frame) and pays its strain there. A path that does not jump, and one these
|
|
21
|
+
// do not free of its jump (the keys jerk the hand there), is the plan as before, bit for bit.
|
|
22
|
+
import { holdLandscape } from "./compiler/hold.js";
|
|
23
|
+
import { add, sub, scale, dot, angle, len } from "./compiler/math.js";
|
|
24
|
+
|
|
25
|
+
const SMOOTH = 1000, NEWTON = 4, STEP = 0.03, MAX_STEP = 0.6, MIN_CURVE = 0.05, SIGMA = 2;
|
|
26
|
+
// A jump: the pole turns more than JUMP deg in a frame while the elbow moves more than SPIKE times
|
|
27
|
+
// as far as in the frames around and more than FLOOR m (QA POP's floor, accLimb). The continuous
|
|
28
|
+
// path prices each turn TURN per squared radian and OVER_TURN past JUMP; BOOST, AROUND as above.
|
|
29
|
+
const JUMP = 45, SPIKE = 2, FLOOR = 0.05, TURN = 10, OVER_TURN = 400, BOOST = 16, AROUND = 4;
|
|
30
|
+
/** The poles continuousPath reads (radians from the landscape's th 0), 5 deg apart. */
|
|
31
|
+
const POLE_GRID = Array.from({ length: 72 }, (_, i) => (2 * Math.PI * i) / 72 - Math.PI);
|
|
32
|
+
|
|
33
|
+
/** Solve the symmetric 5-diagonal system A x = b; A[i][k] is the entry (i, i + k - 2). */
|
|
34
|
+
function solveBand(A, b) {
|
|
35
|
+
const n = b.length, x = Array.from(b);
|
|
36
|
+
for (let i = 0; i < n; i++) {
|
|
37
|
+
for (let j = i + 1; j <= Math.min(i + 2, n - 1); j++) {
|
|
38
|
+
const m = A[j][i - j + 2] / A[i][2];
|
|
39
|
+
for (let k = i; k <= Math.min(i + 2, n - 1); k++) A[j][k - j + 2] -= m * A[i][k - i + 2];
|
|
40
|
+
x[j] -= m * x[i];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
44
|
+
for (let k = i + 1; k <= Math.min(i + 2, n - 1); k++) x[i] -= A[i][k - i + 2] * x[k];
|
|
45
|
+
x[i] /= A[i][2];
|
|
46
|
+
}
|
|
47
|
+
return x;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The angle per frame (radians, frames in a row) that minimises sum cost(i, x_i) + SMOOTH *
|
|
52
|
+
* sum weight_r |P(r) - 2 P(r+1) + P(r+2)|^2 with P(i) = point(i, x_i) (metres), from x0 (each
|
|
53
|
+
* frame's own best, unwrapped here so neighbours differ by less than half a turn); weight 1 when
|
|
54
|
+
* not given.
|
|
55
|
+
*/
|
|
56
|
+
export function smoothPath(cost, point, x0, weight = null) {
|
|
57
|
+
const n = x0.length, x = x0.slice();
|
|
58
|
+
for (let i = 1; i < n; i++) x[i] = x[i - 1] + Math.atan2(Math.sin(x[i] - x[i - 1]), Math.cos(x[i] - x[i - 1]));
|
|
59
|
+
for (let it = 0; it < NEWTON && n > 2; it++) {
|
|
60
|
+
const A = x.map(() => new Float64Array(5)), b = new Float64Array(n);
|
|
61
|
+
x.forEach((t, i) => {
|
|
62
|
+
const c0 = cost(i, t), cp = cost(i, t + STEP), cm = cost(i, t - STEP);
|
|
63
|
+
A[i][2] = Math.max(MIN_CURVE, (cp + cm - 2 * c0) / (STEP * STEP));
|
|
64
|
+
b[i] = -(cp - cm) / (2 * STEP);
|
|
65
|
+
});
|
|
66
|
+
// linearised: the point moves along tan = dP/dx
|
|
67
|
+
const P = x.map((t, i) => point(i, t)), tan = x.map((t, i) => scale(sub(point(i, t + STEP), point(i, t - STEP)), 1 / (2 * STEP)));
|
|
68
|
+
for (let r = 0; r + 2 < n; r++) {
|
|
69
|
+
const c = [1, -2, 1], acc = add(sub(P[r], scale(P[r + 1], 2)), P[r + 2]), k = weight ? 2 * SMOOTH * weight[r] : 2 * SMOOTH;
|
|
70
|
+
for (let p = 0; p < 3; p++) {
|
|
71
|
+
b[r + p] -= k * c[p] * dot(acc, tan[r + p]);
|
|
72
|
+
for (let q = 0; q < 3; q++) A[r + p][q - p + 2] += k * c[p] * c[q] * dot(tan[r + p], tan[r + q]);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const step = solveBand(A, b);
|
|
76
|
+
x.forEach((t, i) => { x[i] = t + Math.max(-MAX_STEP, Math.min(MAX_STEP, step[i])); });
|
|
77
|
+
}
|
|
78
|
+
return x;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The frames i where the path x jumps from frame i to i + 1: dir (a unit vector, the elbow's pole)
|
|
83
|
+
* turns more than JUMP degrees and the point (the elbow) moves more than SPIKE times as far as in
|
|
84
|
+
* the frame before or after, and more than FLOOR metres. A pole that turns while the arm is
|
|
85
|
+
* straight barely moves the elbow, and a swing moves it fast for several frames: neither is a jump.
|
|
86
|
+
*/
|
|
87
|
+
function jumpsIn(x, dir, point) {
|
|
88
|
+
const P = x.map((t, i) => point(i, t)), step = i => (i >= 0 && i + 1 < P.length ? len(sub(P[i + 1], P[i])) : 0);
|
|
89
|
+
return x.slice(1).map((t, i) => i).filter(i => step(i) > FLOOR && step(i) > SPIKE * Math.max(step(i - 1), step(i + 1)) && angle(dir(i, x[i]), dir(i + 1, x[i + 1])) > JUMP);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The path over the candidate angles `grid` that minimises sum cost(i, x_i) + TURN * sum a_i^2
|
|
94
|
+
* plus OVER_TURN * (a_i - JUMP)^2 past JUMP, a_i the turn (radians) of dir from frame i to i + 1
|
|
95
|
+
* (dynamic programming): the cheapest way through that never jumps, whichever valley it starts in.
|
|
96
|
+
*/
|
|
97
|
+
function continuousPath(cost, dir, n, grid) {
|
|
98
|
+
const K = grid.length, lim = JUMP * Math.PI / 180, C = [], U = [];
|
|
99
|
+
for (let i = 0; i < n; i++) { C.push(grid.map(t => cost(i, t))); U.push(grid.map(t => dir(i, t))); }
|
|
100
|
+
let total = C[0].slice();
|
|
101
|
+
const back = [];
|
|
102
|
+
for (let i = 1; i < n; i++) {
|
|
103
|
+
const next = new Array(K), from = new Int32Array(K);
|
|
104
|
+
for (let k = 0; k < K; k++) {
|
|
105
|
+
let best = Infinity;
|
|
106
|
+
for (let j = 0; j < K; j++) {
|
|
107
|
+
const a = Math.acos(Math.max(-1, Math.min(1, dot(U[i - 1][j], U[i][k])))), t = total[j] + TURN * a * a + (a > lim ? OVER_TURN * (a - lim) ** 2 : 0);
|
|
108
|
+
if (t < best) { best = t; from[k] = j; }
|
|
109
|
+
}
|
|
110
|
+
next[k] = best + C[i][k];
|
|
111
|
+
}
|
|
112
|
+
back.push(from);
|
|
113
|
+
total = next;
|
|
114
|
+
}
|
|
115
|
+
const pick = new Array(n);
|
|
116
|
+
pick[n - 1] = total.indexOf(Math.min(...total));
|
|
117
|
+
for (let i = n - 1; i > 0; i--) pick[i - 1] = back[i - 1][pick[i]];
|
|
118
|
+
return pick.map(k => grid[k]);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* smoothPath that never jumps (jumpsIn): dir(i, x) is the world direction choice x gives frame i
|
|
123
|
+
* (the elbow's pole), grid the candidate angles. When the path smoothPath finds from x0 jumps, it
|
|
124
|
+
* starts again from the continuous path (continuousPath); if the cost pulls that back into a jump,
|
|
125
|
+
* the acceleration terms within AROUND frames of it weigh BOOST times more. A path that does not
|
|
126
|
+
* jump is smoothPath's, bit for bit, and so is one these do not free of jumps (the keys themselves
|
|
127
|
+
* jerk the hand there).
|
|
128
|
+
*/
|
|
129
|
+
export function steadyPath(cost, point, x0, dir, grid = POLE_GRID) {
|
|
130
|
+
const x = smoothPath(cost, point, x0);
|
|
131
|
+
if (!jumpsIn(x, dir, point).length) return x;
|
|
132
|
+
const y0 = continuousPath(cost, dir, x.length, grid), y = smoothPath(cost, point, y0), jumps = jumpsIn(y, dir, point);
|
|
133
|
+
if (!jumps.length) return y;
|
|
134
|
+
const weight = new Float64Array(Math.max(0, y.length - 2)).map((_, r) => (jumps.some(j => j >= r - AROUND && j <= r + 1 + AROUND) ? BOOST : 1));
|
|
135
|
+
const z = smoothPath(cost, point, y0, weight);
|
|
136
|
+
return jumpsIn(z, dir, point).length ? x : z;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Values over frames in a row, smoothed by a Gaussian of SIGMA frames (the weights renormalised at the ends). */
|
|
140
|
+
export function smoothed(v) {
|
|
141
|
+
return v.map((_, i) => {
|
|
142
|
+
let sum = 0, wsum = 0;
|
|
143
|
+
for (let j = Math.max(0, i - 3 * SIGMA); j <= Math.min(v.length - 1, i + 3 * SIGMA); j++) { const g = Math.exp(-((j - i) ** 2) / (2 * SIGMA * SIGMA)); sum += g * v[j]; wsum += g; }
|
|
144
|
+
return sum / wsum;
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The holding elbow's planned pole per frame, Map f -> world direction, over `frames` (ascending
|
|
150
|
+
* and contiguous, the frames the keys lead). frameAt(f) -> { s, held }: the plain pose of frame f
|
|
151
|
+
* and what the hand holds there (object-keys.js lead).
|
|
152
|
+
*/
|
|
153
|
+
export function polePlan(sk, side, frames, frameAt) {
|
|
154
|
+
const lands = frames.map(f => { const { s, held } = frameAt(f); return holdLandscape(sk, s, side, held); });
|
|
155
|
+
const th = steadyPath((i, t) => lands[i].at(t).cost, (i, t) => lands[i].at(t).elbow, lands.map(l => l.softmin()), (i, t) => lands[i].at(t).pv);
|
|
156
|
+
return new Map(frames.map((f, i) => [f, lands[i].at(th[i]).pv]));
|
|
157
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// JSON Patch (RFC 6902) on a motion spec: add, remove, replace and test, with "-" to append to
|
|
2
|
+
// an array in add. Used for motion.compile { base, patch } (C7) and by repair (C6). Errors are
|
|
3
|
+
// MOTION_SPEC_INVALID (INVALID_ARGUMENT to the agent) and name the op and what is there:
|
|
4
|
+
// patch[1] (replace /keys/12/f): /keys/12 does not exist (keys has 9 entries)
|
|
5
|
+
// With a notes list (motion.compile, lenient.js), replace on a member an existing object lacks adds it
|
|
6
|
+
// and says so; repair (no notes) stays strict.
|
|
7
|
+
|
|
8
|
+
const OPS = ["add", "remove", "replace", "test"];
|
|
9
|
+
const bad = msg => { throw new Error(`MOTION_SPEC_INVALID: ${msg}`); };
|
|
10
|
+
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
11
|
+
|
|
12
|
+
/** "/keys/0/set" -> ["keys", "0", "set"] (~1 is "/", ~0 is "~"). */
|
|
13
|
+
const tokens = path => path.split("/").slice(1).map(t => t.replaceAll("~1", "/").replaceAll("~0", "~"));
|
|
14
|
+
const pointer = parts => parts.map(t => `/${String(t).replaceAll("~", "~0").replaceAll("/", "~1")}`).join("");
|
|
15
|
+
|
|
16
|
+
function what(container, name) {
|
|
17
|
+
if (Array.isArray(container)) return `${name} has ${container.length} entries`;
|
|
18
|
+
const keys = Object.keys(container);
|
|
19
|
+
return keys.length ? `${name} has ${keys.slice(0, 8).join(", ")}${keys.length > 8 ? ", ..." : ""}` : `${name} is empty`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The container holding the last token, walking every token before it. */
|
|
23
|
+
function parentOf(doc, parts, tag) {
|
|
24
|
+
let node = doc;
|
|
25
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
26
|
+
const t = parts[i], name = i ? parts[i - 1] : "the spec";
|
|
27
|
+
if (node === null || typeof node !== "object" || !Object.hasOwn(node, Array.isArray(node) ? Number(t) : t) || (Array.isArray(node) && !/^\d+$/.test(t)))
|
|
28
|
+
bad(`${tag}: ${pointer(parts.slice(0, i + 1))} does not exist${node && typeof node === "object" ? ` (${what(node, name)})` : ""}`);
|
|
29
|
+
node = node[Array.isArray(node) ? Number(t) : t];
|
|
30
|
+
}
|
|
31
|
+
if (node === null || typeof node !== "object") bad(`${tag}: ${pointer(parts.slice(0, -1))} is not an object or array`);
|
|
32
|
+
return node;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function applyOne(doc, op, i, notes) {
|
|
36
|
+
if (!op || typeof op !== "object" || !OPS.includes(op.op)) bad(`patch[${i}]: op must be ${OPS.join(", ")}, got ${JSON.stringify(op?.op)}`);
|
|
37
|
+
const tag = `patch[${i}] (${op.op} ${op.path})`;
|
|
38
|
+
if (typeof op.path !== "string" || (op.path !== "" && !op.path.startsWith("/"))) bad(`${tag}: path must be a JSON Pointer such as "/keys/0/f"`);
|
|
39
|
+
if (op.op !== "remove" && !Object.hasOwn(op, "value")) bad(`${tag}: needs a value`);
|
|
40
|
+
if (op.path === "") {
|
|
41
|
+
if (op.op === "replace") return structuredClone(op.value);
|
|
42
|
+
if (op.op === "test") { if (!same(doc, op.value)) bad(`${tag}: the spec differs from the test value`); return doc; }
|
|
43
|
+
bad(`${tag}: ${op.op} needs a path below the spec`);
|
|
44
|
+
}
|
|
45
|
+
const parts = tokens(op.path), last = parts[parts.length - 1], node = parentOf(doc, parts, tag);
|
|
46
|
+
const name = parts.length > 1 ? parts[parts.length - 2] : "the spec";
|
|
47
|
+
if (Array.isArray(node)) {
|
|
48
|
+
if (last === "-") { if (op.op !== "add") bad(`${tag}: "-" (append) works with add only`); node.push(structuredClone(op.value)); return doc; }
|
|
49
|
+
const k = /^\d+$/.test(last) ? Number(last) : NaN;
|
|
50
|
+
const max = op.op === "add" ? node.length : node.length - 1;
|
|
51
|
+
if (!(k >= 0 && k <= max)) bad(`${tag}: ${op.path} does not exist (${what(node, name)})`);
|
|
52
|
+
if (op.op === "add") node.splice(k, 0, structuredClone(op.value));
|
|
53
|
+
else if (op.op === "remove") node.splice(k, 1);
|
|
54
|
+
else if (op.op === "replace") node[k] = structuredClone(op.value);
|
|
55
|
+
else if (!same(node[k], op.value)) bad(`${tag}: value is ${JSON.stringify(node[k])?.slice(0, 60)}, not ${JSON.stringify(op.value)?.slice(0, 60)}`);
|
|
56
|
+
return doc;
|
|
57
|
+
}
|
|
58
|
+
if (op.op === "replace" && notes && !Object.hasOwn(node, last)) notes.push(`${tag}: ${op.path} did not exist (${what(node, name)}); added`);
|
|
59
|
+
else if (op.op !== "add" && !Object.hasOwn(node, last)) bad(`${tag}: ${op.path} does not exist (${what(node, name)})`);
|
|
60
|
+
if (op.op === "remove") delete node[last];
|
|
61
|
+
else if (op.op === "test") { if (!same(node[last], op.value)) bad(`${tag}: value is ${JSON.stringify(node[last])?.slice(0, 60)}, not ${JSON.stringify(op.value)?.slice(0, 60)}`); }
|
|
62
|
+
else node[last] = structuredClone(op.value);
|
|
63
|
+
return doc;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Applies ops in order to a copy of doc (doc is never changed) and returns the copy; the first
|
|
68
|
+
* failing op throws MOTION_SPEC_INVALID and nothing is applied. notes (optional) turns a replace on a
|
|
69
|
+
* missing object member into an add and collects a line for it.
|
|
70
|
+
* @param {object} doc
|
|
71
|
+
* @param {{ op: "add" | "remove" | "replace" | "test", path: string, value?: any }[]} ops
|
|
72
|
+
* @param {string[]} [notes]
|
|
73
|
+
*/
|
|
74
|
+
export function applyPatch(doc, ops, notes) {
|
|
75
|
+
if (!Array.isArray(ops)) bad("patch must be an array of { op, path, value }");
|
|
76
|
+
return ops.reduce((d, op, i) => applyOne(d, op, i, notes), structuredClone(doc));
|
|
77
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// C16. Aim keys: a held-object key (C14) may give aim [x, y, z], the world direction the object's
|
|
2
|
+
// +Y points (a bat: knob to barrel), with an optional roll (degrees about that direction,
|
|
3
|
+
// right-handed), in place of the quaternion rot. An aim key turns the object the shortest way
|
|
4
|
+
// from where the plain carry (no keys) holds it at that key's frame onto aim, then by roll.
|
|
5
|
+
// (Turning from the key before instead piles the roll up over a swing: the bat fixture's keys as
|
|
6
|
+
// aims, so chained, missed by 45.5 cm.)
|
|
7
|
+
// Without roll (lane K8) the key takes the roll the arm holds most easily: ROLLS candidate rolls
|
|
8
|
+
// about aim are scored by the strain of the arm that holds them (compiler/hold.js holdCost), a
|
|
9
|
+
// pass over the keys in order picks one per key with a light price on how far the object turns
|
|
10
|
+
// from the key before (so equally easy rolls do not alternate), and a golden-section search
|
|
11
|
+
// settles each pick within its step. roll given (0 included) and rot keys are used exactly.
|
|
12
|
+
// The quaternion is rounded to 1e-5 (the grid motion.keyObject writes) and kept on the hemisphere
|
|
13
|
+
// of the key before it.
|
|
14
|
+
// lean (degrees, lane elbow) lets the compiler lean aim itself up to that far: a cup brought to the mouth
|
|
15
|
+
// is tipped toward the face AND a little toward the hand's side, which lets the elbow stay down (a cup at
|
|
16
|
+
// the mouth aimed only at the face can be held with a comfortable wrist only with the elbow over the
|
|
17
|
+
// shoulder). The aim, and AZIMUTHS directions leaned lean/2 and lean, each at every roll (or at the
|
|
18
|
+
// given roll), are scored by the arm's strain plus the shoulder's price (hold.js holdCost opts.shoulder);
|
|
19
|
+
// the chain and the roll refinement run as above on the picked direction.
|
|
20
|
+
import { qmul, qrot, qdot, qnorm, arcQuat, axisQuat } from "./compiler/quat.js";
|
|
21
|
+
import { add, cross, norm, scale, sub } from "./compiler/math.js";
|
|
22
|
+
|
|
23
|
+
const ROLLS = 24, STEP = 360 / ROLLS, TURN = 0.2, GOLDEN = 12, AZIMUTHS = 8;
|
|
24
|
+
const round5 = v => Math.round(v * 1e5) / 1e5;
|
|
25
|
+
/** The turn between two unit quaternions, radians. */
|
|
26
|
+
const turn = (a, b) => 2 * Math.acos(Math.min(1, Math.abs(qdot(a, b))));
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Where a leaning key puts the object: a lean pivots about the object's top (its +Y end, `top` m above its
|
|
30
|
+
* base pivot), so a cup's rim stays where aim put it (at the lips) and its foot swings; any other key's pos.
|
|
31
|
+
*/
|
|
32
|
+
export const leanPos = (key, q, top) => (key.lean > 0 && key.aim && top > 0 ? add(key.pos, scale(sub(norm(key.aim), qrot(q, [0, 1, 0])), top)) : key.pos);
|
|
33
|
+
|
|
34
|
+
/** aim d and the directions leaned lean/2 and lean deg from it toward AZIMUTHS sides. */
|
|
35
|
+
function leanedAims(d, lean) {
|
|
36
|
+
const side = norm(cross(d, Math.abs(d[1]) < 0.9 ? [0, 1, 0] : [1, 0, 0])), up = cross(d, side), out = [d];
|
|
37
|
+
for (const deg of [lean / 2, lean]) for (let j = 0; j < AZIMUTHS; j++) {
|
|
38
|
+
const a = (2 * Math.PI * j) / AZIMUTHS, t = (Math.min(deg, 45) * Math.PI) / 180, w = add(scale(side, Math.cos(a)), scale(up, Math.sin(a)));
|
|
39
|
+
out.push(norm(add(scale(d, Math.cos(t)), scale(w, Math.sin(t)))));
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The pick per key: DP over candidates [{ q, cost }] per key, a TURN price on the squared turn between neighbours. */
|
|
45
|
+
function chain(cands) {
|
|
46
|
+
let best = cands[0].map(c => ({ total: c.cost, from: -1 }));
|
|
47
|
+
const back = [best];
|
|
48
|
+
for (let i = 1; i < cands.length; i++) {
|
|
49
|
+
best = cands[i].map(c => {
|
|
50
|
+
let pick = { total: Infinity, from: -1 };
|
|
51
|
+
cands[i - 1].forEach((p, j) => { const t = back[i - 1][j].total + TURN * turn(p.q, c.q) ** 2; if (t < pick.total) pick = { total: t, from: j }; });
|
|
52
|
+
return { total: pick.total + c.cost, from: pick.from };
|
|
53
|
+
});
|
|
54
|
+
back.push(best);
|
|
55
|
+
}
|
|
56
|
+
const picks = [];
|
|
57
|
+
for (let i = cands.length - 1, j = best.reduce((m, b, k) => (b.total < best[m].total ? k : m), 0); i >= 0; j = back[i][j].from, i--) picks[i] = j;
|
|
58
|
+
return picks;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Golden-section search of f on [lo, hi]. */
|
|
62
|
+
function golden(f, lo, hi) {
|
|
63
|
+
const g = (Math.sqrt(5) - 1) / 2;
|
|
64
|
+
let a = lo, b = hi, c = b - g * (b - a), d = a + g * (b - a), fc = f(c), fd = f(d);
|
|
65
|
+
for (let i = 0; i < GOLDEN; i++) {
|
|
66
|
+
if (fc < fd) { b = d; d = c; fd = fc; c = b - g * (b - a); fc = f(c); } else { a = c; c = d; fc = fd; d = a + g * (b - a); fd = f(d); }
|
|
67
|
+
}
|
|
68
|
+
return (a + b) / 2;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The rot of each key (keys sorted by f): a rot key's own, an aim key's resolved quaternion
|
|
73
|
+
* [x, y, z, w]. plain(i) is the rotation (a quaternion) the plain carry gives the object at key
|
|
74
|
+
* i's frame; it is asked for aim keys only. strain(i, q), when given, is the arm's cost of
|
|
75
|
+
* holding key i at rotation q; it picks the roll of aim keys that give none.
|
|
76
|
+
*/
|
|
77
|
+
export function keyRots(keys, plain, strain) {
|
|
78
|
+
const at = keys.map((k, i) => {
|
|
79
|
+
if (k.aim === undefined) return { fixed: qnorm(k.rot) };
|
|
80
|
+
const from = qnorm(plain(i)), aimed = d => { const q0 = qmul(arcQuat(norm(qrot(from, [0, 1, 0])), d), from); return r => qmul(axisQuat(d, r), q0); };
|
|
81
|
+
if (k.lean > 0 && strain) return { leans: leanedAims(norm(k.aim), k.lean).map(aimed), roll: k.roll };
|
|
82
|
+
const rolled = aimed(norm(k.aim));
|
|
83
|
+
return k.roll !== undefined || !strain ? { fixed: k.roll ? rolled(k.roll) : rolled(0) } : { rolled };
|
|
84
|
+
});
|
|
85
|
+
const rollsOf = a => (a.roll !== undefined ? [a.roll] : Array.from({ length: ROLLS }, (_, j) => j * STEP - 180));
|
|
86
|
+
const cands = at.map((a, i) => (a.fixed ? [{ q: a.fixed, cost: 0, r: 0 }]
|
|
87
|
+
: a.leans ? a.leans.flatMap(rolled => rollsOf(a).map(r => { const q = rolled(r); return { q, cost: strain(i, q), r, rolled, fixedRoll: a.roll !== undefined }; }))
|
|
88
|
+
: Array.from({ length: ROLLS }, (_, j) => { const r = j * STEP - 180, q = a.rolled(r); return { q, cost: strain(i, q), r }; })));
|
|
89
|
+
const picks = chain(cands), chosen = cands.map((c, i) => c[picks[i]]);
|
|
90
|
+
// a leaning key keeps the direction it picked; its roll is refined below like any other (unless given)
|
|
91
|
+
for (const [i, a] of at.entries()) if (a.leans) at[i] = chosen[i].fixedRoll ? { fixed: chosen[i].q } : { rolled: chosen[i].rolled };
|
|
92
|
+
const rots = [];
|
|
93
|
+
let prev = null;
|
|
94
|
+
for (const [i, a] of at.entries()) {
|
|
95
|
+
if (keys[i].aim === undefined) { rots.push(keys[i].rot); prev = a.fixed; continue; }
|
|
96
|
+
let q = (a.fixed ?? a.rolled(golden(r => {
|
|
97
|
+
const qr = a.rolled(r), near = [chosen[i - 1], chosen[i + 1]].filter(Boolean);
|
|
98
|
+
return strain(i, qr) + near.reduce((sum, n) => sum + TURN * turn(n.q, qr) ** 2, 0);
|
|
99
|
+
}, chosen[i].r - STEP / 2, chosen[i].r + STEP / 2))).map(round5);
|
|
100
|
+
if (prev && qdot(q, prev) < 0) q = q.map(v => -v);
|
|
101
|
+
rots.push(q);
|
|
102
|
+
prev = qnorm(q);
|
|
103
|
+
}
|
|
104
|
+
return rots;
|
|
105
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// motion.keyObject (C14): one key of an object the take moves, upserted into (or removed from) a
|
|
2
|
+
// copy of the take's spec. The caller compiles and installs the copy.
|
|
3
|
+
// carry objects.<id>.keys[{ f, pos, rot }] or [{ f, pos, aim, roll? }] (C16): the world pose of its
|
|
4
|
+
// base (a group: its root), only while it is in hand (grab..release); kept sorted by f; the
|
|
5
|
+
// last one removed drops `keys`
|
|
6
|
+
// hinge spec.keys[{ f, set: { <channel>: degrees } }]: `angle`, or the turn of `rot` (the drawn
|
|
7
|
+
// world rotation) about the hinge axis from the scene pose; a key at f is reused
|
|
8
|
+
// An object a skill moves (exitCar's door hinge) is keyed the same way: a hinge through its channel,
|
|
9
|
+
// a carry by copying the skill's entry into spec.objects with the keys (the spec's own entry wins).
|
|
10
|
+
// Frames are the spec's (absolute). Errors carry the MOTION_SPEC_INVALID: prefix (INVALID_ARGUMENT).
|
|
11
|
+
import { isCarried, isFollow, movesRecord } from "./carried.js";
|
|
12
|
+
import { groupsOf } from "./scene.js";
|
|
13
|
+
import { mul, T, norm, quatFromMat, matFromQuat } from "./compiler/math.js";
|
|
14
|
+
|
|
15
|
+
const bad = msg => { throw new Error(`MOTION_SPEC_INVALID: ${msg}`); };
|
|
16
|
+
const round = (v, d) => Math.round(v * d) / d;
|
|
17
|
+
const framesOf = list => (list.length ? list.map(f => `f${f}`).join(", ") : "none");
|
|
18
|
+
|
|
19
|
+
/** The spec.objects key that moves scene body `id` (itself or its nearest group; a switch moves nothing), or null. */
|
|
20
|
+
function moverOf(spec, scene, id) {
|
|
21
|
+
return [id, ...(scene?.objects?.[id]?.ancestors ?? [])].find(k => movesRecord(spec.objects?.[k])) ?? null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Degrees the world rotation `rot` turns about `axis` from the rest rotation (the twist, -180..180). */
|
|
25
|
+
function hingeAngle(rot, rest, axis) {
|
|
26
|
+
const q = quatFromMat(mul(matFromQuat(rot), T(rest.R))), a = norm(axis), sg = q[3] < 0 ? -1 : 1;
|
|
27
|
+
const deg = 2 * Math.atan2(sg * (q[0] * a[0] + q[1] * a[1] + q[2] * a[2]), sg * q[3]) * 180 / Math.PI;
|
|
28
|
+
return round(deg > 180 ? deg - 360 : deg, 100);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function carryKey(spec, id, o, { frame, pos, rot, aim, roll, remove }) {
|
|
32
|
+
const lo = o.grab ?? 0, hi = o.release ?? spec.frames - 1, keys = [...(o.keys ?? [])].filter(k => k.f !== frame);
|
|
33
|
+
if (frame < lo || frame > hi) bad(`"${id}" is in hand only f${lo}..f${hi} (grab..release); at f${frame} the take does not hold it, so drag it as a scene object, or change objects.${id}.grab/release first`);
|
|
34
|
+
if (remove) {
|
|
35
|
+
if (keys.length === (o.keys ?? []).length) bad(`"${id}" has no key at f${frame}; its keys: ${framesOf((o.keys ?? []).map(k => k.f))}`);
|
|
36
|
+
} else {
|
|
37
|
+
if (!Array.isArray(pos) || Array.isArray(rot) === Array.isArray(aim)) {
|
|
38
|
+
bad(`keying "${id}" (carried) needs pos [x, y, z] and rot [x, y, z, w] or aim [x, y, z] (+ roll), one of the two: the world pose of its base (a group: its root)`);
|
|
39
|
+
}
|
|
40
|
+
const turn = rot ? { rot: rot.map(v => round(v, 1e5)) } : { aim: aim.map(v => round(v, 1e5)), ...(Number.isFinite(roll) && roll ? { roll: round(roll, 100) } : {}) };
|
|
41
|
+
keys.push({ f: frame, pos: pos.map(v => round(v, 1e4)), ...turn });
|
|
42
|
+
}
|
|
43
|
+
const { keys: _old, ...rest } = o;
|
|
44
|
+
spec.objects[id] = keys.length ? { ...rest, keys: keys.sort((a, b) => a.f - b.f) } : rest;
|
|
45
|
+
return { kind: "carry", f: frame, ...(remove ? { removed: true } : {}), keys: keys.map(k => k.f) };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function hingeKey(spec, scene, id, o, { frame, rot, angle, remove }) {
|
|
49
|
+
const ch = o.channel, i = (spec.keys ?? []).findIndex(k => k?.f === frame && !k.src && k.set && ch in k.set);
|
|
50
|
+
if (remove) {
|
|
51
|
+
if (i < 0) bad(`"${id}" (hinge, channel "${ch}") has no key at f${frame}; ${ch} is keyed at ${framesOf((spec.keys ?? []).filter(k => k?.set && ch in k.set).map(k => k.f))}`);
|
|
52
|
+
const { [ch]: _gone, ...set } = spec.keys[i].set;
|
|
53
|
+
spec.keys = Object.keys(set).length ? spec.keys.map((k, j) => (j === i ? { ...k, set, ...(k.step ? { step: k.step.filter(c => c !== ch) } : {}) } : k)) : spec.keys.filter((_, j) => j !== i);
|
|
54
|
+
return { kind: "hinge", channel: ch, f: frame, removed: true };
|
|
55
|
+
}
|
|
56
|
+
const rest = scene?.objects?.[id] ?? groupsOf(scene)[id];
|
|
57
|
+
const value = Number.isFinite(angle) ? round(angle, 100) : Array.isArray(rot) && rest ? hingeAngle(rot, rest, o.axis ?? [0, 1, 0])
|
|
58
|
+
: bad(`keying "${id}" (a hinge on channel "${ch}") needs angle (degrees) or rot [x, y, z, w] (its drawn world rotation; the turn about the hinge axis is kept)`);
|
|
59
|
+
const at = (spec.keys ?? []).findIndex(k => k?.f === frame && !k.src);
|
|
60
|
+
spec.keys = at < 0 ? [...(spec.keys ?? []), { f: frame, set: { [ch]: value } }] : spec.keys.map((k, j) => (j === at ? { ...k, set: { ...k.set, [ch]: value } } : k));
|
|
61
|
+
return { kind: "hinge", channel: ch, f: frame, angle: value };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* keyObject(spec, scene, { objectId, frame, pos?, rot?, aim?, roll?, angle?, remove? }, skillObjects?) -> { spec, keyed }:
|
|
66
|
+
* a copy of the spec with the key written, and what was written ({ objectId, kind, f, ... }).
|
|
67
|
+
* skillObjects are the spec.objects entries its skills add (skills/index.js expandUse).
|
|
68
|
+
*/
|
|
69
|
+
export function keyObject(stored, scene, args, skillObjects = {}) {
|
|
70
|
+
const spec = structuredClone(stored), { objectId: id, frame } = args, moves = x => (movesRecord(x) ? x : undefined);
|
|
71
|
+
const o = moves(spec.objects?.[id]) ?? structuredClone(moves(skillObjects[id]));
|
|
72
|
+
if (!o) {
|
|
73
|
+
const mover = moverOf({ objects: { ...skillObjects, ...spec.objects } }, scene, id);
|
|
74
|
+
bad(mover ? `"${id}" moves with "${mover}" (spec.objects.${mover}); key "${mover}" with its root pose`
|
|
75
|
+
: `"${id}" is not in this take's spec.objects (${Object.keys({ ...skillObjects, ...spec.objects }).join(", ") || "none"}): the take neither carries nor hinges it, so drag it as a scene object`);
|
|
76
|
+
}
|
|
77
|
+
if (!(Number.isInteger(frame) && frame >= 0 && frame < spec.frames)) bad(`frame ${frame} is outside the take (0..${spec.frames - 1})`);
|
|
78
|
+
if (isFollow(o)) bad(`"${id}" follows ${o.follow} (spec.objects.${id}); it has no keys: change its follow, offset or aim`);
|
|
79
|
+
if (isCarried(o)) spec.objects = { ...spec.objects, [id]: o };
|
|
80
|
+
const keyed = isCarried(o) ? carryKey(spec, id, o, args) : hingeKey(spec, scene, id, o, args);
|
|
81
|
+
return { spec, keyed: { objectId: id, ...keyed } };
|
|
82
|
+
}
|