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,80 @@
|
|
|
1
|
+
// What each skill takes and does, for motion.describe { topic: "skills" } and the expander (an
|
|
2
|
+
// arg not listed here is refused; a lenient reload ignores it): args (JSON Schema fragments),
|
|
3
|
+
// required args, what must come before it (needs), how many frames it runs from f0 and what it
|
|
4
|
+
// adds to the spec. A test keeps the names equal to SKILL_NAMES (skills/index.js). `tune` names
|
|
5
|
+
// the numeric args repair may nudge (arg -> one step) when a near-limit QA group names the skill
|
|
6
|
+
// as its cause (repair-tune.js); the start value is the arg as sent, else its schema default.
|
|
7
|
+
// `place` names the args that set where the body stands (arg -> one step, m): repair-tune.js
|
|
8
|
+
// steps them when a GOAL group misses and this skill placed the body last.
|
|
9
|
+
import { WALK_FIELDS } from "../schema/fields.js";
|
|
10
|
+
import { WALK_TYPES, num, int, str, side, ref } from "../schema/types.js";
|
|
11
|
+
import { FINGER_PRESETS } from "../compiler/vocab.js";
|
|
12
|
+
|
|
13
|
+
/** The last frame of a walk (generator or walkTo): feet together 18 frames after the last step. */
|
|
14
|
+
export const walkEnd = gen => gen.f0 + 16 + (gen.steps ?? 8) * (gen.period ?? 20) + 18;
|
|
15
|
+
|
|
16
|
+
const xz = d => ({ type: "array", items: { type: "number" }, minItems: 2, maxItems: 2, description: d });
|
|
17
|
+
const carArgs = names => Object.fromEntries(names.map(n => [n, str(n === "car" ? "car prefix: fills <car>_Seat_Driver, _Seat_Driver_Back, _Door_L/R, _Door_Handle, _Steering_Wheel" : "scene object id (overrides the car default)")]));
|
|
18
|
+
const walkArgs = Object.fromEntries(WALK_FIELDS.filter(f => f !== "type" && f !== "f0").map(f => [f, WALK_TYPES[f]]));
|
|
19
|
+
|
|
20
|
+
export const SKILL_INFO = {
|
|
21
|
+
standIdle: {
|
|
22
|
+
args: { at: xz("[x, z] world m; default the root keyed before f0"), heading: num("degrees (yaw); default the keyed yaw"), until: int("hold the pose to this frame") },
|
|
23
|
+
needs: "args.at, or a root keyed before f0", frames: "1 (to args.until when given)",
|
|
24
|
+
adds: ["keys: a standing pose, FK arms down"], example: { f0: 0, args: { at: [0, 0], heading: 0 } },
|
|
25
|
+
place: { at: 0.03 },
|
|
26
|
+
},
|
|
27
|
+
walkTo: {
|
|
28
|
+
args: { to: xz("[x, z] world m: the heading turns toward it and the steps cover the distance"), ...walkArgs,
|
|
29
|
+
heading: { oneOf: [num(), WALK_TYPES.heading], description: "degrees: h1, or [h0, h1]" },
|
|
30
|
+
arrive: int("with to and no steps/period/step/swing: the frame to end by (feet together); the engine picks the cadence (observations.planned)") },
|
|
31
|
+
needs: "a standing pose keyed before f0", frames: "16 + steps * period + 18",
|
|
32
|
+
adds: ["keys: a walk ending feet together"], example: { f0: 10, args: { to: [0.5, 1.5] } },
|
|
33
|
+
tune: { lift: 0.01, halfWidth: 0.01, firstStep: 0.01, step: 0.01 }, place: { firstStep: 0.03, step: 0.03 },
|
|
34
|
+
},
|
|
35
|
+
reachGrip: {
|
|
36
|
+
args: { anchor: str("not ending in _l/_r"), side: side("the hand"), reach: int("frames to reach", { default: 20 }), hold: int("frames held", { default: 22 }),
|
|
37
|
+
release: int("frames to let go", { default: 12 }), elbow: ref("vec3", "elbow pole"), fingers: { enum: FINGER_PRESETS, default: "grip" },
|
|
38
|
+
define: { type: "object", description: "an anchor (topic anchors) named args.anchor" }, orient: { type: "boolean", description: "false: no grip turn" } },
|
|
39
|
+
required: ["anchor", "side"], needs: "args.anchor in spec.anchors or args.define", frames: "reach + hold + release (54 by default)",
|
|
40
|
+
adds: ["props.<anchor>Grip (fingers point)", "fits: fingers onto the anchor", "checks.contacts \"fingers on <anchor>\"", "checks.allow hand on the anchor's object",
|
|
41
|
+
"keys: w_<anchor>_<side>, fingers_<side>, armw_<side>"],
|
|
42
|
+
example: { f0: 10, args: { anchor: "bar", side: "r", define: { kind: "world", p: [-0.2, 1.2, 0.33] } } },
|
|
43
|
+
tune: { reach: 2, release: 2 },
|
|
44
|
+
},
|
|
45
|
+
sitInSeat: {
|
|
46
|
+
args: { ...carArgs(["car", "seat", "seatBack", "wheel"]), offset: { ...ref("vec3", "hips from the seat top, seat frame"), default: [0, 0.17, 0.02] }, floor: num("floor y under the feet, m") },
|
|
47
|
+
needs: "args.car or args.seat (a scene box)", frames: "1", adds: ["keys: seated, hands at the wheel", "checks.supports seat", "checks.allow seat, back, wheel"],
|
|
48
|
+
example: { f0: 0, args: { car: "Car" } },
|
|
49
|
+
},
|
|
50
|
+
exitCar: {
|
|
51
|
+
args: { ...carArgs(["car", "seat", "door", "handle"]), side: side("driver door side, default l"), channel: str("the door hinge channel", { default: "door" }) },
|
|
52
|
+
needs: "seated in the car before f0 (sitInSeat); a car on the ground (seat base y 0.4)", frames: "246",
|
|
53
|
+
adds: ["objects: door and handle hinges", "anchors handle, lthigh, rthigh", "props.grip", "fits, checks.contacts, checks.allow"],
|
|
54
|
+
example: { f0: 0, args: { car: "Car" } },
|
|
55
|
+
},
|
|
56
|
+
flip: {
|
|
57
|
+
args: { kind: { enum: ["back", "front", "cartwheel"], default: "back", description: "a standing back or front tuck, or a cartwheel" },
|
|
58
|
+
travel: num("back/front: m along the heading at landing (default back -0.25, front 0.4)"),
|
|
59
|
+
side: side("cartwheel: toward the character's l (default) or r"), heading: num("degrees (yaw); default the keyed yaw") },
|
|
60
|
+
needs: "a standing pose keyed before f0", frames: "back/front 58 (one turn, the hips 0.5 m up); cartwheel 68",
|
|
61
|
+
adds: ["keys every frame: back/front crouch, push-off, a ballistic tucked flight turning the whole body (pitch to -360 back, +360 front) and a landing that stands up",
|
|
62
|
+
"cartwheel: a side step, then the whole body rolls 360 like a wheel on the straddled legs and the arms overhead, the hands planted in turn",
|
|
63
|
+
"whole-body pitch and roll take full turns in any key (no range); past 60 deg the knee and elbow poles and the feet turn with the body"],
|
|
64
|
+
example: { f0: 10, args: { kind: "back" } },
|
|
65
|
+
},
|
|
66
|
+
smokeCigarette: {
|
|
67
|
+
args: { until: int("hold to this frame; default the last frame; under f0+264 the beats are paced to fit"),
|
|
68
|
+
kit: { type: "object", properties: { cig: str("the cigarette kit's rootId (object.createKit)"), lighter: str("the lighter kit's rootId"), exhale: str("the exhale kit's rootId (or an effect): it follows the mouth") },
|
|
69
|
+
required: ["cig", "lighter"], additionalProperties: false, description: "kits the skill carries and lights: it adds their spec.objects entries itself" } },
|
|
70
|
+
needs: "a stand or squat keyed before f0", frames: "265 nominal; fits 199..264 by pacing",
|
|
71
|
+
adds: ["anchors lpocket, rpocket, hold, mouth, flame", "props cig, lighter", "channels cig_vis, lit_vis, flame_vis, ember_vis, exhale, ember",
|
|
72
|
+
"checks contacts, orient, below, appear, mouthPoints"],
|
|
73
|
+
example: { f0: 0, args: {} },
|
|
74
|
+
},
|
|
75
|
+
hitReaction: {
|
|
76
|
+
args: { dir: xz("[x, z] the way the body is thrown"), from: str("or what hits it: object:<id> | person:<id> (thrown away from it)"),
|
|
77
|
+
speed: num("launch speed, m/s (a shove 2, a car at 30 km/h about 8)", { default: 4, exclusiveMinimum: 0 }), land: { enum: ["back", "front"], description: "default: back when hit from the front" } },
|
|
78
|
+
needs: "a pose; dir or from", frames: "~40", adds: ["keys: thrown to rest"], example: { f0: 10, args: { dir: [0, -1], speed: 3 } },
|
|
79
|
+
},
|
|
80
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// smokeCigarette from any posture, and dressed with kits (docs/motion-api-v3.1.md C15).
|
|
2
|
+
// args.kit = { cig, lighter, exhale? }: the kit roots object.createKit made (src/kits/) ride the
|
|
3
|
+
// props cig and lighter at the grip (at/along pin the root to the palm), and their lit parts
|
|
4
|
+
// switch with the skill's channels. A part is found by what it is, not by its name: under the
|
|
5
|
+
// cigarette a glowing body and a light (the ember: ember_vis, level ember) and an effect (the
|
|
6
|
+
// tip smoke: ember_vis); under the lighter a glowing body (the flame) and a light (flame_vis).
|
|
7
|
+
// exhale is the exhale kit's root (or any effect): it follows the mouth, facing forward, and shows
|
|
8
|
+
// while the exhale channel is on (C16: { follow: "landmark:mouth", aim: "face", visible: "exhale" }).
|
|
9
|
+
// squat: with the hips keyed low before f0 the cigarette rests over the right knee, not beside
|
|
10
|
+
// the hip where the thigh is, and the hand leaves (and comes back to) the lips straight ahead
|
|
11
|
+
// first (anchor mouthOut): a straight move between the lips and the knee would cross the hand's
|
|
12
|
+
// detour circle around the leaning torso going down, where the detour jumps.
|
|
13
|
+
import { skillError } from "./book.js";
|
|
14
|
+
import { groupsOf } from "../scene.js";
|
|
15
|
+
import { pose } from "../compiler/pose.js";
|
|
16
|
+
import { bonePoint } from "../compiler/skeleton.js";
|
|
17
|
+
import { add, sub, scale, len, apply, ry } from "../compiler/math.js";
|
|
18
|
+
import { HAND_CLEAR, keepOut, wristBody } from "../body.js";
|
|
19
|
+
|
|
20
|
+
const KIT_FIELDS = ["cig", "lighter", "exhale"];
|
|
21
|
+
/** The exhale emitter's entry: at the lips, its +Y (the puff) along the face, shown while breathing out. */
|
|
22
|
+
const EXHALE = { follow: "landmark:mouth", aim: "face", visible: "exhale" };
|
|
23
|
+
const KIT_OF = { cig: "cigarette", lighter: "lighter" };
|
|
24
|
+
/** Below this share of the rig's standing hips height the pose before f0 is a squat. */
|
|
25
|
+
const SQUAT_HIPS = 0.7;
|
|
26
|
+
/** The resting cigarette hand over the right knee, from the knee joint in the body frame (m): out, up, forward. */
|
|
27
|
+
const OVER_KNEE = [-0.05, 0.07, 0.1];
|
|
28
|
+
/** A knee farther than this share of the arm's length from the shoulder: the hand rests on the thigh short of it. */
|
|
29
|
+
const REST_REACH = 0.88;
|
|
30
|
+
/** Where the hand goes first from the lips in a squat: this far straight ahead of the mouth (m, body frame). */
|
|
31
|
+
const OUT_AHEAD = 0.2;
|
|
32
|
+
/** A squat's side pocket from the root (body frame, the left side; m): out, up, ahead. */
|
|
33
|
+
const SIDE_POCKET = [0.245, 0.08, 0.02];
|
|
34
|
+
/** The pockets on the standing body (body frame: left +x), where the hands fetch the kit. */
|
|
35
|
+
export const POCKET = { l: [0.13, -0.035, 0.08], r: [-0.13, -0.035, 0.08] };
|
|
36
|
+
|
|
37
|
+
/** [id, entry, body] of every record under group `root` in a motion scene (bodies and box-less records). */
|
|
38
|
+
function partsUnder(scene, root) {
|
|
39
|
+
const rows = Object.entries(scene?.objects ?? {}).map(([id, o]) => [id, o, true]).concat(Object.entries(scene?.others ?? {}).map(([id, o]) => [id, o, false]));
|
|
40
|
+
return rows.filter(([, o]) => o.ancestors?.includes(root));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function rootOf(ctx, kit, role) {
|
|
44
|
+
const id = kit[role];
|
|
45
|
+
if (typeof id !== "string" || !groupsOf(ctx.scene)[id]) skillError(ctx.tag, `args.kit.${role} ${JSON.stringify(id)} is not a group in the scene; object.createKit { kit: "${KIT_OF[role]}" } answers its rootId`);
|
|
46
|
+
return id;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** How deep a kit may sit in the thigh its pocket is on, and in the hip over it (a squat folds the pocket into the hip crease): it comes out of (and goes back into) the pocket there. */
|
|
50
|
+
const POCKET_DEPTH = { cig: 0.03, lighter: 0.045 };
|
|
51
|
+
/** Squatting, the kit comes out of a side pocket past the fat leg capsules (thigh, hips, shin) the scene check reads. */
|
|
52
|
+
const SQUAT_POCKET = 0.07;
|
|
53
|
+
/** ...and the hand in a side pocket touches the thigh and the hip that deep (m). */
|
|
54
|
+
const POCKET_TOUCH = 0.04;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The spec.objects entries args.kit asks for and the pocket allowances (null without it). at:
|
|
58
|
+
* absolute frames { cig, lit, unlit } the cigarette and the lighter appear and the lighter goes back; squat: the squat pockets.
|
|
59
|
+
*/
|
|
60
|
+
export function kitObjects(ctx, at) {
|
|
61
|
+
const kit = ctx.args.kit;
|
|
62
|
+
if (kit === undefined) return null;
|
|
63
|
+
if (!kit || typeof kit !== "object" || Array.isArray(kit)) skillError(ctx.tag, `args.kit must be { cig, lighter, exhale? } (kit root ids), got ${JSON.stringify(kit)}`);
|
|
64
|
+
for (const f of Object.keys(kit)) if (!KIT_FIELDS.includes(f)) skillError(ctx.tag, `args.kit has unknown field "${f}"; fields: ${KIT_FIELDS.join(", ")}`);
|
|
65
|
+
const cig = rootOf(ctx, kit, "cig"), lighter = rootOf(ctx, kit, "lighter");
|
|
66
|
+
const objects = {
|
|
67
|
+
[cig]: { prop: "cig", grab: at.cig, at: [0, 0, 0], along: [0, 0, 1], visible: "cig_vis" },
|
|
68
|
+
[lighter]: { prop: "lighter", grab: at.lit, release: at.unlit, at: [0, 0, 0], along: [0, 1, 0], visible: "lit_vis" },
|
|
69
|
+
};
|
|
70
|
+
for (const [id, o, body] of partsUnder(ctx.scene, cig)) {
|
|
71
|
+
if ((body && o.glow) || (!body && o.renderer === "light")) objects[id] = { visible: "ember_vis", level: "ember" };
|
|
72
|
+
else if (!body && o.renderer === "effect") objects[id] = { visible: "ember_vis" };
|
|
73
|
+
}
|
|
74
|
+
for (const [id, o, body] of partsUnder(ctx.scene, lighter)) if ((body && o.glow) || (!body && o.renderer === "light")) objects[id] = { visible: "flame_vis" };
|
|
75
|
+
if (kit.exhale !== undefined) {
|
|
76
|
+
if (typeof kit.exhale !== "string" || !(ctx.scene?.others?.[kit.exhale] || ctx.scene?.objects?.[kit.exhale] || groupsOf(ctx.scene)[kit.exhale])) {
|
|
77
|
+
skillError(ctx.tag, `args.kit.exhale ${JSON.stringify(kit.exhale)} is not a scene object; object.createKit { kit: "exhale" } answers its rootId (it follows the mouth)`);
|
|
78
|
+
}
|
|
79
|
+
objects[kit.exhale] = { ...EXHALE };
|
|
80
|
+
}
|
|
81
|
+
const deep = d => (at.squat ? SQUAT_POCKET : d);
|
|
82
|
+
const allow = [["thigh_r", cig, deep(POCKET_DEPTH.cig)], ["thigh_l", lighter, deep(POCKET_DEPTH.lighter)], ["hips", cig, deep(POCKET_DEPTH.cig)], ["hips", lighter, deep(POCKET_DEPTH.lighter)],
|
|
83
|
+
...(at.squat ? [["shin", cig, SQUAT_POCKET], ["shin", lighter, SQUAT_POCKET],
|
|
84
|
+
// the hands in the side pockets brush the thigh and the hip (qa/body-checks.js)
|
|
85
|
+
["hand_l", "thigh_l", POCKET_TOUCH], ["hand_r", "thigh_r", POCKET_TOUCH], ["hand", "hips", POCKET_TOUCH]] : [])].map(([part, object, depth]) => ({ part, object, depth }));
|
|
86
|
+
return { objects, allow };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* For a squat keyed before f0, the body-frame anchors { hold, mouthOut }: the resting hand over the
|
|
91
|
+
* right knee and the point the hand leaves the lips toward; null when the pose is not a squat.
|
|
92
|
+
*/
|
|
93
|
+
export function squatAnchors(ctx) {
|
|
94
|
+
const { sk, state } = ctx, I = sk.index, P = sk.restWorldPos;
|
|
95
|
+
if (!(state.root[1] < SQUAT_HIPS * P[I.Hips][1])) return null;
|
|
96
|
+
const { s } = pose(sk, state), yaw = state.yaw ?? 0, shoulder = s.P[I.RightArm], body = p => apply(ry(-yaw), sub(p, state.root));
|
|
97
|
+
const arm = len(sub(P[I.RightForeArm], P[I.RightArm])) + len(sub(P[I.RightHand], P[I.RightForeArm]));
|
|
98
|
+
const over = add(s.P[I.RightLeg], apply(ry(yaw), OVER_KNEE)), far = len(sub(over, shoulder));
|
|
99
|
+
const hold = body(far > REST_REACH * arm ? add(shoulder, scale(sub(over, shoulder), REST_REACH * arm / far)) : over);
|
|
100
|
+
const out = add(body(bonePoint(s, I.Head, sk.landmarks.mouth)), [0, 0, OUT_AHEAD]);
|
|
101
|
+
// squatting, the front pockets fold into the hip crease under the thighs: with fix.self (smoke.js) the hands go to the side pockets,
|
|
102
|
+
// SIDE_POCKET from the root (body frame), kept off the skin (the hand comes in from the side)
|
|
103
|
+
const skin = wristBody(sk), pocket = (side, sg) => {
|
|
104
|
+
const at = add(state.root, apply(ry(yaw), [sg * SIDE_POCKET[0], SIDE_POCKET[1], SIDE_POCKET[2]]));
|
|
105
|
+
return body(skin ? keepOut(skin.body, sk, s, at, HAND_CLEAR, skin.skip) : at);
|
|
106
|
+
};
|
|
107
|
+
const r3 = p => p.map(v => Math.round(v * 1000) / 1000);
|
|
108
|
+
return { hold: r3(hold), mouthOut: r3(out), lpocket: r3(pocket("Left", 1)), rpocket: r3(pocket("Right", -1)) };
|
|
109
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// smokeCigarette, extracted from the car-exit fixture: the right hand takes a cigarette from the
|
|
2
|
+
// right pocket to the lips, the left hand brings a lighter from the left pocket under the tip,
|
|
3
|
+
// lights it, puts it back; exhale, look around, a second drag, hold until `until`. Everything is
|
|
4
|
+
// body-relative (pockets, holding point) or measured (mouth), so it works wherever the character
|
|
5
|
+
// stands, and from any pose keyed before f0 (a squat): the head turns from its own pitch, the
|
|
6
|
+
// right arm's poles lean with the torso (pitch + spine_pitch), and a squat rests the hand over
|
|
7
|
+
// the knee (smoke-kit.js). Channels exhale (1 while breathing out) and ember (the tip's level:
|
|
8
|
+
// 0.6 idle, 2 on a drag) drive kits; args.kit binds them (smoke-kit.js). It runs 265 frames from
|
|
9
|
+
// f0 (f0..f0+264); a shorter span down to 75% of that (199 frames) paces every beat to fit.
|
|
10
|
+
import { keyBook, skillError } from "./book.js";
|
|
11
|
+
import { kitObjects, squatAnchors, POCKET } from "./smoke-kit.js";
|
|
12
|
+
import { apply, rx } from "../compiler/math.js";
|
|
13
|
+
|
|
14
|
+
// Wrist twist / flex per hand pose (degrees), found by sweeping them against the QA checks.
|
|
15
|
+
const WR = { TW_M: 40, FX_M: 0, TW_H: 0, FX_H: 10, TW_L: -80, FX_L: -50 };
|
|
16
|
+
// with fix.self on, the wrist at the mouth bends toward the back of the hand (15 deg standing, 20 squatting over the
|
|
17
|
+
// leaning chest): with the filter on the lips that brings the forearm off the chest (straight, it ran 3-6 cm into the
|
|
18
|
+
// body on the fitted skin, qa/body-checks.js); past 20 standing the knuckles reach the face
|
|
19
|
+
const FX_M_SELF = 15, FX_M_SQUAT = 20;
|
|
20
|
+
/** Lighting brings the hands together: the lighter's hand under the cigarette's, touching it (qa/body-checks.js). */
|
|
21
|
+
const LIGHTING = [["hand_l", "hand_r", 0.045], ["hand_r", "hand_l", 0.045], ["farm_r", "hand_l", 0.02], ["hand_l", "farm_r", 0.02]].map(([part, object, depth]) => ({ part, object, depth }));
|
|
22
|
+
const EL_MOUTH_R = [0.4, -1, 0.6], EL_HOLD_R = [-0.4, -1, -0.3], EL_LIGHT_L = [0.9, -0.6, 0.3];
|
|
23
|
+
const ALL = ["armw_r", "armw_l", "w_rpocket_r", "w_lpocket_l", "w_hold_r", "w_mouth_r", "w_flame_l", "elbow_r", "elbow_l",
|
|
24
|
+
"wtwist_r", "wflex_r", "wtwist_l", "wflex_l", "head_pitch", "head_yaw", "spine_pitch", "root", "fingers_l", "fingers_r",
|
|
25
|
+
"cig_vis", "lit_vis", "flame_vis", "ember_vis", "exhale", "ember"];
|
|
26
|
+
/** The last key at its own pace is f0 + NOMINAL; a span down to PACE_MIN of it is paced to fit. */
|
|
27
|
+
const NOMINAL = 264, PACE_MIN = 0.75, MIN_SPAN = Math.ceil(NOMINAL * PACE_MIN);
|
|
28
|
+
/** motion.compile takes only the timeline rate (motion3.js), so seconds in a refusal are at 24 fps. */
|
|
29
|
+
const secs = n => (n / 24).toFixed(1);
|
|
30
|
+
const DEFAULTS = { armw_r: 0, armw_l: 0, w_rpocket_r: 0, w_lpocket_l: 0, w_hold_r: 0, w_mouth_r: 0, w_flame_l: 0, elbow_r: [-0.5, -0.4, -1], elbow_l: [0.5, -0.4, -1],
|
|
31
|
+
wtwist_r: 0, wflex_r: 0, wtwist_l: 0, wflex_l: 0, head_pitch: 0, head_yaw: 0, spine_pitch: 0, fingers_l: { grip: 0, fist: 0 }, fingers_r: { cigarette: 0 },
|
|
32
|
+
cig_vis: 0, lit_vis: 0, flame_vis: 0, ember_vis: 0, exhale: 0, ember: 0 };
|
|
33
|
+
|
|
34
|
+
export function smokeCigarette(ctx) {
|
|
35
|
+
// by default it holds to the last frame (or ends with its own motion while frames is "auto"); a
|
|
36
|
+
// span short of NOMINAL down to MIN_SPAN is paced, a shorter one refused with what it needs
|
|
37
|
+
const until = ctx.args.until ?? (ctx.auto ? ctx.f0 + NOMINAL : ctx.frames - 1), span = until - ctx.f0;
|
|
38
|
+
if (span < MIN_SPAN) skillError(ctx.tag, `needs frames f${ctx.f0}..f${ctx.f0 + MIN_SPAN} at least: ${MIN_SPAN + 1} frames, ${secs(MIN_SPAN + 1)} s at 24 fps, paced ${PACE_MIN} `
|
|
39
|
+
+ `(${NOMINAL + 1} frames, ${secs(NOMINAL + 1)} s, at its own pace); it has f${ctx.f0}..f${until}, `
|
|
40
|
+
+ (ctx.args.until === undefined ? `so set frames >= ${ctx.f0 + MIN_SPAN + 1} or frames: "auto"` : `so set args.until >= ${ctx.f0 + MIN_SPAN}`));
|
|
41
|
+
if (!Array.isArray(ctx.state.root)) skillError(ctx.tag, "needs a pose keyed before f0 (root): standing or squatting");
|
|
42
|
+
const fresh = Object.fromEntries(Object.entries(DEFAULTS).filter(([c]) => ctx.state[c] === undefined));
|
|
43
|
+
if (Object.keys(fresh).length) Object.assign(ctx.state, structuredClone(fresh));
|
|
44
|
+
ctx.state.head_yaw = 0;
|
|
45
|
+
// the pose before f0 stays: the head pitches from its own angle, the right arm's poles lean with the torso
|
|
46
|
+
const head0 = ctx.state.head_pitch, lean = (ctx.state.pitch ?? 0) + ctx.state.spine_pitch, H = v => v + head0;
|
|
47
|
+
const EL_MOUTH = lean ? apply(rx(lean), EL_MOUTH_R) : EL_MOUTH_R, EL_HOLD = lean ? apply(rx(lean), EL_HOLD_R) : EL_HOLD_R;
|
|
48
|
+
const squat = squatAnchors(ctx), FX_M = ctx.self ? (squat ? FX_M_SQUAT : FX_M_SELF) : WR.FX_M;
|
|
49
|
+
if (squat) ctx.state.w_mouthOut_r = 0;
|
|
50
|
+
// a short span scales every beat's frame by `pace` (>= 0.75): rounding keeps the keys in order,
|
|
51
|
+
// and a gap of 2 frames or more stays at least 1 (the lighter reaches the tip 3 frames before
|
|
52
|
+
// the flame shows, the pocket 2 before it hides), so each switch still happens with the hand
|
|
53
|
+
// where the contacts and appear checks want it; the steps themselves are not stretched
|
|
54
|
+
const pace = span < NOMINAL ? span / NOMINAL : 1, t = f => Math.round(f * pace), book = keyBook(ctx.state, ctx.f0);
|
|
55
|
+
const k = pace === 1 ? book : { ...book, at: (f, set) => book.at(t(f), set), hold: (f, ...names) => book.hold(t(f), ...names), step: (f, ch, v) => book.step(t(f), ch, v) };
|
|
56
|
+
const root0 = ctx.state.root.slice(), yaw = ctx.state.yaw ?? 0;
|
|
57
|
+
/** The hand from the lips to the rest over f0+a..f0+b, and back; in a squat through mouthOut (smoke-kit.js). */
|
|
58
|
+
const raise = (a, b) => {
|
|
59
|
+
k.hold(a, "w_mouth_r", "w_hold_r", "wtwist_r", "wflex_r", "elbow_r", "head_pitch", "root", ...(squat ? ["w_mouthOut_r"] : []));
|
|
60
|
+
if (squat) k.at(Math.round((a + b) / 2), { w_hold_r: 0, w_mouthOut_r: 1, w_mouth_r: 0 });
|
|
61
|
+
k.at(b, { w_mouth_r: 1, w_hold_r: 0, ...(squat ? { w_mouthOut_r: 0 } : {}), wtwist_r: WR.TW_M, wflex_r: FX_M, elbow_r: EL_MOUTH, head_pitch: H(6), root: root0 });
|
|
62
|
+
};
|
|
63
|
+
const lower = (a, b) => {
|
|
64
|
+
k.hold(a, "w_mouth_r", "w_hold_r", "wtwist_r", "wflex_r", "elbow_r", "head_pitch", ...(squat ? ["w_mouthOut_r"] : []));
|
|
65
|
+
if (squat) k.at(Math.round((a + b) / 2), { w_mouth_r: 0, w_mouthOut_r: 1, w_hold_r: 0 });
|
|
66
|
+
k.at(b, { w_mouth_r: 0, w_hold_r: 1, ...(squat ? { w_mouthOut_r: 0 } : {}), wtwist_r: WR.TW_H, wflex_r: WR.FX_H, elbow_r: EL_HOLD, head_pitch: H(0) });
|
|
67
|
+
};
|
|
68
|
+
k.hold(0, ...ALL);
|
|
69
|
+
// right hand: pocket -> the cigarette appears -> mouth
|
|
70
|
+
k.at(2, { w_rpocket_r: 1, elbow_r: [-0.5, -0.4, -1] });
|
|
71
|
+
k.hold(6, "armw_r", "w_rpocket_r", "elbow_r", "fingers_r");
|
|
72
|
+
k.at(20, { armw_r: 1 });
|
|
73
|
+
k.at(28, { fingers_r: { cigarette: 1 } });
|
|
74
|
+
k.step(28, "cig_vis", 1);
|
|
75
|
+
k.hold(36, "w_rpocket_r", "w_mouth_r", "wtwist_r", "wflex_r", "elbow_r", "head_pitch");
|
|
76
|
+
k.at(58, { w_rpocket_r: 0, w_mouth_r: 1, wtwist_r: WR.TW_M, wflex_r: FX_M, elbow_r: EL_MOUTH, head_pitch: H(6) });
|
|
77
|
+
// left hand: pocket -> the lighter appears -> under the cigarette tip
|
|
78
|
+
k.at(2, { w_lpocket_l: 1, elbow_l: [0.5, -0.4, -1] });
|
|
79
|
+
k.hold(12, "armw_l", "w_lpocket_l", "elbow_l", "fingers_l");
|
|
80
|
+
k.at(26, { armw_l: 1 });
|
|
81
|
+
k.at(34, { fingers_l: { grip: 0, fist: 1 } });
|
|
82
|
+
k.step(35, "lit_vis", 1);
|
|
83
|
+
k.hold(44, "w_lpocket_l", "w_flame_l", "wtwist_l", "wflex_l", "elbow_l");
|
|
84
|
+
k.at(60, { w_lpocket_l: 0, w_flame_l: 1, wtwist_l: WR.TW_L, wflex_l: WR.FX_L, elbow_l: EL_LIGHT_L });
|
|
85
|
+
// light it: flame on, the ember appears, inhale
|
|
86
|
+
k.step(63, "flame_vis", 1);
|
|
87
|
+
k.step(73, "ember_vis", 1);
|
|
88
|
+
k.step(79, "flame_vis", 0);
|
|
89
|
+
// the ember catches, glows up while she draws (f80-f90, f200-f212) and settles to 0.6; she breathes out twice
|
|
90
|
+
k.step(73, "ember", 1);
|
|
91
|
+
k.at(80, { ember: 2 });
|
|
92
|
+
k.hold(90, "ember");
|
|
93
|
+
k.at(104, { ember: 0.6 });
|
|
94
|
+
k.hold(194, "ember");
|
|
95
|
+
k.at(200, { ember: 2 });
|
|
96
|
+
k.hold(212, "ember");
|
|
97
|
+
k.at(224, { ember: 0.6 });
|
|
98
|
+
k.step(116, "exhale", 1);
|
|
99
|
+
k.step(138, "exhale", 0);
|
|
100
|
+
k.step(238, "exhale", 1);
|
|
101
|
+
k.step(260, "exhale", 0);
|
|
102
|
+
// the left hand puts the lighter back and drops
|
|
103
|
+
k.hold(80, "w_flame_l", "w_lpocket_l", "wtwist_l", "wflex_l", "elbow_l");
|
|
104
|
+
// a squat brings the lighter back to the pocket a little slower (its path crosses the hand's detour circle on the way
|
|
105
|
+
// down); with fix.self to the side pocket slower still (at 96 the forearm pops), the lighter going in when the hand gets there
|
|
106
|
+
const back = squat && ctx.self ? 8 : 0;
|
|
107
|
+
k.at(back ? 96 + back : squat ? 100 : 96, { w_flame_l: 0, w_lpocket_l: 1, wtwist_l: 0, wflex_l: 0, elbow_l: [0.5, -0.4, -1] });
|
|
108
|
+
k.hold(101 + back, "fingers_l");
|
|
109
|
+
k.step(102 + back, "lit_vis", 0);
|
|
110
|
+
k.hold(104 + back, "armw_l", "w_lpocket_l");
|
|
111
|
+
k.at(112, { fingers_l: { grip: 0, fist: 0 } });
|
|
112
|
+
k.at(120, { armw_l: 0, w_lpocket_l: 1 });
|
|
113
|
+
k.at(123, { w_lpocket_l: 0 });
|
|
114
|
+
// the right hand lowers to the holding position; exhale with the head up
|
|
115
|
+
lower(92, 114);
|
|
116
|
+
k.at(124, { head_pitch: H(-14) });
|
|
117
|
+
k.at(140, { head_pitch: H(0) });
|
|
118
|
+
// idle: weight onto the left leg, look around
|
|
119
|
+
const lv = [Math.cos(yaw * Math.PI / 180), 0, -Math.sin(yaw * Math.PI / 180)];
|
|
120
|
+
k.hold(120, "root", "head_yaw");
|
|
121
|
+
k.at(146, { root: root0.map((v, i) => v + 0.03 * lv[i] - (i === 1 ? 0.005 : 0)), head_yaw: -22 });
|
|
122
|
+
k.at(168, { head_yaw: 0 });
|
|
123
|
+
// second drag
|
|
124
|
+
raise(170, 194);
|
|
125
|
+
lower(212, 236);
|
|
126
|
+
k.at(246, { head_pitch: H(-14) });
|
|
127
|
+
k.at(264, { head_pitch: H(0) });
|
|
128
|
+
book.hold(span, ...ALL, ...(squat ? ["w_mouthOut_r"] : []));
|
|
129
|
+
const side = Boolean(squat) && ctx.self;
|
|
130
|
+
const kit = kitObjects(ctx, { cig: ctx.f0 + t(28), lit: ctx.f0 + t(35), unlit: ctx.f0 + t(side ? 110 : 102), squat: side });
|
|
131
|
+
return {
|
|
132
|
+
keys: book.keys, end: until, ...(pace < 1 ? { paced: Math.floor(pace * 100) / 100 } : {}), channels: ["exhale", "ember"], ...(kit ? { objects: kit.objects } : {}),
|
|
133
|
+
anchors: {
|
|
134
|
+
lpocket: { kind: "body", p: (ctx.self && squat?.lpocket) || POCKET.l }, rpocket: { kind: "body", p: (ctx.self && squat?.rpocket) || POCKET.r }, hold: { kind: "body", p: squat?.hold ?? [-0.24, 0.03, 0.06] },
|
|
135
|
+
...(squat ? { mouthOut: { kind: "body", p: squat.mouthOut } } : {}),
|
|
136
|
+
mouth: { kind: "landmark", name: "mouth" }, flame: { kind: "prop", prop: "cig", point: "tip", offset: [0, -0.02, 0] },
|
|
137
|
+
},
|
|
138
|
+
props: {
|
|
139
|
+
cig: { hand: "r", origin: ["RightHandIndex2", "RightHandMiddle2"], points: { filter: [0, 0, -0.026], tip: [0, 0, 0.058] } },
|
|
140
|
+
lighter: { hand: "l", origin: ["LeftHand", "LeftHandMiddle1"], points: { body: [0.01, 0, 0], flame: [0.01, 0.036, 0] } },
|
|
141
|
+
},
|
|
142
|
+
fits: [{ side: "r", anchor: "mouth", effector: { prop: "cig", point: "filter" } }, { side: "l", anchor: "flame", effector: { prop: "lighter", point: "flame" } }],
|
|
143
|
+
checks: {
|
|
144
|
+
contacts: [
|
|
145
|
+
{ name: "filter on the lips", point: "prop:cig.filter", target: "anchor:mouth", tol: 0.01, when: "w_mouth_r" },
|
|
146
|
+
{ name: "flame under the cigarette tip", point: "prop:lighter.flame", target: "anchor:flame", tol: 0.01, when: "flame_vis" },
|
|
147
|
+
],
|
|
148
|
+
orient: [
|
|
149
|
+
{ name: "lit end points away from the face", from: "prop:cig.filter", to: "prop:cig.tip", along: "face", minCos: 0.64, when: "w_mouth_r" },
|
|
150
|
+
{ name: "lighter upright", from: "prop:lighter.body", to: "prop:lighter.flame", along: "up", minCos: 0.85, when: "flame_vis" },
|
|
151
|
+
],
|
|
152
|
+
below: [{ name: "no chicken-wing elbow", joint: "RightForeArm", ref: "RightArm", min: 0.08, when: "w_mouth_r" }],
|
|
153
|
+
appear: [
|
|
154
|
+
{ name: "cigarette from the right pocket", channel: "cig_vis", joint: "RightHand", anchor: "rpocket", tol: 0.035 },
|
|
155
|
+
{ name: "lighter from the left pocket", channel: "lit_vis", joint: "LeftHand", anchor: "lpocket", tol: 0.035 },
|
|
156
|
+
],
|
|
157
|
+
mouthPoints: ["cig.filter"], allow: [...(kit?.allow ?? []), ...LIGHTING],
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// C7. Spec ids and the editor's spec store. specIdOf is final (W0). The store is an LRU of
|
|
2
|
+
// SPEC_STORE_SIZE entries (lane D2): put, get, keepParts and parts all count as a use, and the
|
|
3
|
+
// least recently used entry goes first. It lives in the editor page's memory: a reload forgets
|
|
4
|
+
// it, and the agent then sends the whole spec again (motion3.js answers an unknown base or
|
|
5
|
+
// specId with that advice).
|
|
6
|
+
// frames: "auto" is an intent the stored spec cannot carry: the spec a specId names (and a take
|
|
7
|
+
// keeps) holds the resolved number. The pipeline marks such a spec (markFramesAuto) and the store
|
|
8
|
+
// remembers the mark beside the entry, so get() answers frames: "auto" again and base + patch
|
|
9
|
+
// re-resolves the length (keys appended past the old end extend it instead of failing).
|
|
10
|
+
|
|
11
|
+
export const SPEC_STORE_SIZE = 64;
|
|
12
|
+
const resolvedFromAuto = new WeakSet();
|
|
13
|
+
|
|
14
|
+
/** Marks a spec whose numeric frames were resolved from frames: "auto" (pipeline.js); answers it. */
|
|
15
|
+
export function markFramesAuto(spec) {
|
|
16
|
+
resolvedFromAuto.add(spec);
|
|
17
|
+
return spec;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Canonical JSON: object keys sorted at every level (arrays keep their order). */
|
|
21
|
+
const canonical = v => (Array.isArray(v) ? `[${v.map(canonical).join(",")}]`
|
|
22
|
+
: v && typeof v === "object" ? `{${Object.keys(v).sort().filter(k => v[k] !== undefined).map(k => `${JSON.stringify(k)}:${canonical(v[k])}`).join(",")}}`
|
|
23
|
+
: JSON.stringify(v) ?? "null");
|
|
24
|
+
|
|
25
|
+
/** "spec-" + the first 12 hex digits of the FNV-1a 64-bit hash of the spec's canonical JSON. */
|
|
26
|
+
export function specIdOf(spec) {
|
|
27
|
+
let h = 0xcbf29ce484222325n;
|
|
28
|
+
for (const byte of new TextEncoder().encode(canonical(spec))) h = BigInt.asUintN(64, (h ^ BigInt(byte)) * 0x100000001b3n);
|
|
29
|
+
return `spec-${h.toString(16).padStart(16, "0").slice(0, 12)}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The editor's store of compiled specs and their full output parts (C7).
|
|
34
|
+
* put(spec) -> specId remember a compiled spec (the most recent SPEC_STORE_SIZE)
|
|
35
|
+
* get(specId) -> spec | null a copy of a remembered spec; frames: "auto" when it was
|
|
36
|
+
* resolved from "auto" (the mark sticks to the specId)
|
|
37
|
+
* keepParts(specId, parts) the overflow hook: { groups, timeline, probes } in full,
|
|
38
|
+
* before fitOutput cuts them (report.js)
|
|
39
|
+
* parts(specId) -> parts | null what motion.report pages and delta compares against
|
|
40
|
+
*/
|
|
41
|
+
export function createSpecStore(max = SPEC_STORE_SIZE) {
|
|
42
|
+
const entries = new Map(), size = Math.max(1, max);
|
|
43
|
+
// a Map keeps insertion order: re-inserting moves an entry to the end, the first is the oldest
|
|
44
|
+
const touch = (id, entry) => { entries.delete(id); entries.set(id, entry); while (entries.size > size) entries.delete(entries.keys().next().value); return entry; };
|
|
45
|
+
const seen = id => (entries.has(id) ? touch(id, entries.get(id)) : null);
|
|
46
|
+
return {
|
|
47
|
+
put(spec) {
|
|
48
|
+
const id = specIdOf(spec), was = entries.get(id);
|
|
49
|
+
touch(id, { ...was, spec: structuredClone(spec), framesAuto: resolvedFromAuto.has(spec) || was?.framesAuto === true });
|
|
50
|
+
return id;
|
|
51
|
+
},
|
|
52
|
+
get(id) {
|
|
53
|
+
const entry = seen(id);
|
|
54
|
+
return entry?.spec ? { ...structuredClone(entry.spec), ...(entry.framesAuto ? { frames: "auto" } : {}) } : null;
|
|
55
|
+
},
|
|
56
|
+
keepParts(id, parts) { touch(id, { ...entries.get(id), parts }); },
|
|
57
|
+
parts(id) { return seen(id)?.parts ?? null; },
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// Timing edits on an installed take v3: the take is cut into segments, each playing a SOURCE
|
|
2
|
+
// frame range at its own speed (0.1x..4x), and segments can be removed or the take trimmed.
|
|
3
|
+
// The edit never touches the compiled take: renderTakeEdit resamples the source arrays (local
|
|
4
|
+
// bone quaternions, hips, object tracks) onto the timeline, so undo, reload and a new compile
|
|
5
|
+
// all start from the same untouched source. Frame numbers the timeline shows are edited frames;
|
|
6
|
+
// keys pinned to them (IK corrections) ride remapFrameKeys when the edit changes.
|
|
7
|
+
|
|
8
|
+
const SPEED_MIN = 0.1, SPEED_MAX = 4, SPEED_STEP = 0.1;
|
|
9
|
+
export const TRIM_MIN_FRAMES = 12;
|
|
10
|
+
const normalizeSpeed = speed => Number((Math.round(Number(speed) / SPEED_STEP) * SPEED_STEP).toFixed(1));
|
|
11
|
+
const sourceFrames = segment => segment.sourceEnd - segment.sourceStart + 1;
|
|
12
|
+
const timelineFrames = segment => Math.max(1, Math.round(sourceFrames(segment) / segment.speed));
|
|
13
|
+
|
|
14
|
+
export function createTakeEdit(frames) {
|
|
15
|
+
if (!Number.isInteger(frames) || frames < 1) throw new Error("createTakeEdit: frames must be a positive integer");
|
|
16
|
+
return [{ id: "motion-0", sourceStart: 0, sourceEnd: frames - 1, speed: 1 }];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const takeEditDuration = edit => (Array.isArray(edit) ? edit.reduce((total, segment) => total + timelineFrames(segment), 0) : 0);
|
|
20
|
+
|
|
21
|
+
/** Each segment with where it sits on the timeline (inclusive frames). */
|
|
22
|
+
export function takeEditLayout(edit) {
|
|
23
|
+
let cursor = 0;
|
|
24
|
+
return edit.map(segment => {
|
|
25
|
+
const frames = timelineFrames(segment), item = { ...segment, timelineStart: cursor, timelineEnd: cursor + frames - 1, timelineFrames: frames };
|
|
26
|
+
cursor += frames;
|
|
27
|
+
return item;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The (fractional) source frame a timeline frame shows. */
|
|
32
|
+
export function timelineToSource(edit, frame) {
|
|
33
|
+
const layout = takeEditLayout(edit);
|
|
34
|
+
const at = Math.max(0, Math.min(Number.isFinite(frame) ? frame : 0, layout.at(-1).timelineEnd));
|
|
35
|
+
const segment = layout.find(item => at <= item.timelineEnd) ?? layout.at(-1);
|
|
36
|
+
if (segment.timelineFrames <= 1) return segment.sourceStart;
|
|
37
|
+
return segment.sourceStart + ((at - segment.timelineStart) / (segment.timelineFrames - 1)) * (segment.sourceEnd - segment.sourceStart);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The timeline frame that shows a source frame; null when no segment covers it (cut away). */
|
|
41
|
+
export function sourceToTimeline(edit, source) {
|
|
42
|
+
const segment = takeEditLayout(edit).find(item => source >= item.sourceStart && source <= item.sourceEnd);
|
|
43
|
+
if (!segment) return null;
|
|
44
|
+
if (segment.sourceEnd === segment.sourceStart) return segment.timelineStart;
|
|
45
|
+
return Math.round(segment.timelineStart + ((source - segment.sourceStart) / (segment.sourceEnd - segment.sourceStart)) * (segment.timelineFrames - 1));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const remapTimelineFrame = (previous, next, frame) => sourceToTimeline(next, timelineToSource(previous, frame));
|
|
49
|
+
|
|
50
|
+
/** Encoded frame keys ([{ frame, ... }]) through an edit change: the earlier old frame wins a
|
|
51
|
+
* shared new frame, and keys whose source frames were cut away drop out. */
|
|
52
|
+
export function remapFrameKeys(keys, previous, next) {
|
|
53
|
+
const out = new Map();
|
|
54
|
+
for (const key of [...keys].sort((a, b) => a.frame - b.frame)) {
|
|
55
|
+
const frame = remapTimelineFrame(previous, next, key.frame);
|
|
56
|
+
if (frame !== null && !out.has(frame)) out.set(frame, { ...key, frame });
|
|
57
|
+
}
|
|
58
|
+
return [...out.values()];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Split the segment under timeline frame `frame` there. */
|
|
62
|
+
export function splitTakeEdit(edit, frame) {
|
|
63
|
+
const layout = takeEditLayout(edit), cut = Math.round(frame);
|
|
64
|
+
if (cut <= 0 || cut >= takeEditDuration(edit)) return edit;
|
|
65
|
+
const index = layout.findIndex(segment => cut > segment.timelineStart && cut <= segment.timelineEnd);
|
|
66
|
+
if (index < 0) return edit;
|
|
67
|
+
const segment = layout[index];
|
|
68
|
+
const sourceCut = Math.max(segment.sourceStart + 1, Math.min(segment.sourceEnd, Math.round(timelineToSource(edit, cut))));
|
|
69
|
+
if (sourceCut > segment.sourceEnd) return edit;
|
|
70
|
+
const next = [...edit];
|
|
71
|
+
next.splice(index, 1,
|
|
72
|
+
{ id: `${segment.id}-a`, sourceStart: segment.sourceStart, sourceEnd: sourceCut - 1, speed: segment.speed },
|
|
73
|
+
{ id: `${segment.id}-b`, sourceStart: sourceCut, sourceEnd: segment.sourceEnd, speed: segment.speed });
|
|
74
|
+
return next;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function setSegmentSpeed(edit, id, speed) {
|
|
78
|
+
const normalized = normalizeSpeed(speed);
|
|
79
|
+
if (!Number.isFinite(normalized) || normalized < SPEED_MIN || normalized > SPEED_MAX) throw new Error(`speed must be between ${SPEED_MIN} and ${SPEED_MAX} in ${SPEED_STEP} steps`);
|
|
80
|
+
return edit.map(segment => (segment.id === id ? { ...segment, speed: normalized } : segment));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Drop one segment; the last one stays (no motion is motion.clear's job). */
|
|
84
|
+
export function removeSegment(edit, id) {
|
|
85
|
+
if (edit.length < 2) return edit;
|
|
86
|
+
const next = edit.filter(segment => segment.id !== id);
|
|
87
|
+
return next.length === edit.length ? edit : next;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The speed that plays a segment across `frames` timeline frames (the stretch grip). */
|
|
91
|
+
export function segmentSpeedForFrames(segment, frames) {
|
|
92
|
+
const raw = sourceFrames(segment) / Math.max(1, Math.round(Number(frames) || 1));
|
|
93
|
+
return normalizeSpeed(Math.min(SPEED_MAX, Math.max(SPEED_MIN, raw)));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Keep timeline frames start..end (inclusive) of the edited take. */
|
|
97
|
+
export function trimTakeEdit(edit, start, end) {
|
|
98
|
+
const duration = takeEditDuration(edit);
|
|
99
|
+
if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end >= duration || start > end) throw new Error(`trim range ${start}..${end} is outside 0..${duration - 1}`);
|
|
100
|
+
return takeEditLayout(edit).flatMap(segment => {
|
|
101
|
+
const from = Math.max(start, segment.timelineStart), to = Math.min(end, segment.timelineEnd);
|
|
102
|
+
if (from > to) return [];
|
|
103
|
+
const sourceStart = Math.max(segment.sourceStart, Math.ceil(timelineToSource(edit, from) - 1e-9));
|
|
104
|
+
const sourceEnd = Math.min(segment.sourceEnd, Math.floor(timelineToSource(edit, to) + 1e-9));
|
|
105
|
+
return sourceStart <= sourceEnd ? [{ id: `${segment.id}-t${sourceStart}`, sourceStart, sourceEnd, speed: segment.speed }] : [];
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const isIdentityEdit = (edit, frames) => !edit || (edit.length === 1 && edit[0].sourceStart === 0 && edit[0].sourceEnd === frames - 1 && edit[0].speed === 1);
|
|
110
|
+
|
|
111
|
+
/** A saved edit checked against a take of `frames` source frames; null when it is not one. */
|
|
112
|
+
export function normalizeTakeEdit(value, frames) {
|
|
113
|
+
if (!Array.isArray(value) || !value.length) return null;
|
|
114
|
+
const ok = value.every(segment => segment && typeof segment.id === "string" && Number.isInteger(segment.sourceStart) && Number.isInteger(segment.sourceEnd)
|
|
115
|
+
&& segment.sourceStart >= 0 && segment.sourceStart <= segment.sourceEnd && (!Number.isFinite(frames) || segment.sourceEnd < frames)
|
|
116
|
+
&& Number.isFinite(segment.speed) && segment.speed >= SPEED_MIN && segment.speed <= SPEED_MAX);
|
|
117
|
+
return ok ? value.map(({ id, sourceStart, sourceEnd, speed }) => ({ id, sourceStart, sourceEnd, speed })) : null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function nlerp(out, o, a, ia, b, ib, t) {
|
|
121
|
+
const sign = a[ia] * b[ib] + a[ia + 1] * b[ib + 1] + a[ia + 2] * b[ib + 2] + a[ia + 3] * b[ib + 3] < 0 ? -1 : 1;
|
|
122
|
+
let n = 0;
|
|
123
|
+
for (let k = 0; k < 4; k++) { out[o + k] = a[ia + k] * (1 - t) + b[ib + k] * sign * t; n += out[o + k] ** 2; }
|
|
124
|
+
n = Math.sqrt(n) || 1;
|
|
125
|
+
for (let k = 0; k < 4; k++) out[o + k] /= n;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function lerp(out, o, a, ia, b, ib, t, width) {
|
|
129
|
+
for (let k = 0; k < width; k++) out[o + k] = a[ia + k] + (b[ib + k] - a[ia + k]) * t;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The take as the edit plays it: the same take object when there is no edit. */
|
|
133
|
+
export function renderTakeEdit(take) {
|
|
134
|
+
const edit = take && !take.editRendered ? normalizeTakeEdit(take.edit, take.frameCount) : null;
|
|
135
|
+
if (!edit || isIdentityEdit(edit, take.frameCount)) return take;
|
|
136
|
+
const D = takeEditDuration(edit), B = take.bones.length, F = take.frameCount;
|
|
137
|
+
const at = Array.from({ length: D }, (_, f) => {
|
|
138
|
+
const s = timelineToSource(edit, f), i0 = Math.min(F - 1, Math.floor(s));
|
|
139
|
+
return { i0, i1: Math.min(F - 1, i0 + 1), t: s - i0, near: Math.min(F - 1, Math.round(s)) };
|
|
140
|
+
});
|
|
141
|
+
const rot = new Float32Array(D * B * 4), hips = new Float32Array(D * 3);
|
|
142
|
+
at.forEach(({ i0, i1, t }, f) => {
|
|
143
|
+
for (let b = 0; b < B; b++) nlerp(rot, (f * B + b) * 4, take.rot, (i0 * B + b) * 4, take.rot, (i1 * B + b) * 4, t);
|
|
144
|
+
lerp(hips, f * 3, take.hips, i0 * 3, take.hips, i1 * 3, t, 3);
|
|
145
|
+
});
|
|
146
|
+
const objects = Object.fromEntries(Object.entries(take.objects ?? {}).map(([id, track]) => {
|
|
147
|
+
const next = {};
|
|
148
|
+
if (track.pos) { next.pos = new Float32Array(D * 3); at.forEach(({ i0, i1, t }, f) => lerp(next.pos, f * 3, track.pos, i0 * 3, track.pos, i1 * 3, t, 3)); }
|
|
149
|
+
if (track.quat) { next.quat = new Float32Array(D * 4); at.forEach(({ i0, i1, t }, f) => nlerp(next.quat, f * 4, track.quat, i0 * 4, track.quat, i1 * 4, t)); }
|
|
150
|
+
if (track.lvl) { next.lvl = new Float32Array(D); at.forEach(({ i0, i1, t }, f) => lerp(next.lvl, f, track.lvl, i0, track.lvl, i1, t, 1)); }
|
|
151
|
+
for (const key of ["vis", "hold"]) if (track[key]) { next[key] = new Uint8Array(D); at.forEach(({ near }, f) => { next[key][f] = track[key][near]; }); }
|
|
152
|
+
return [id, next];
|
|
153
|
+
}));
|
|
154
|
+
return { ...take, frameCount: D, frames: D, rot, hips, objects, edit, editRendered: true };
|
|
155
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Take v3: what the compiler bakes and the player reads. Bone rotations are LOCAL quaternions
|
|
2
|
+
// on the Mixamo control bones (the first depth-first bone of each name), so playback writes
|
|
3
|
+
// them straight onto the rig; the hips also carry a position. Everything is expressed in the
|
|
4
|
+
// character's placement frame (its base position and yaw), in metres, so moving or turning
|
|
5
|
+
// the character carries the take with it. Scene objects the motion animates ride along in
|
|
6
|
+
// world space: one track per spec.objects key (a group: its root only). A switch entry (C15)
|
|
7
|
+
// bakes only vis and lvl, no pos/quat: its record keeps its own place (object-travel.js). A follow
|
|
8
|
+
// entry (C16) bakes pos/quat with hold on every frame: its track is world space throughout. A free entry
|
|
9
|
+
// (C17) bakes pos/quat (and vis/lvl when it names them), world space, no hold.
|
|
10
|
+
import { toLocal } from "./compiler/skeleton.js";
|
|
11
|
+
import { sub, apply, ry, quatFromMat, mul } from "./compiler/math.js";
|
|
12
|
+
import { isCarried, isFollow, isSwitch } from "./carried.js";
|
|
13
|
+
import { isFree } from "./free-motion.js";
|
|
14
|
+
import { rootPoseAt, spanOf } from "./object-pose.js";
|
|
15
|
+
|
|
16
|
+
const TAKE_SCHEMA = "cozyclay.take.v3";
|
|
17
|
+
|
|
18
|
+
/** out = compileMotion(...) result; placement = { position: [x,y,z], yaw } of the character. */
|
|
19
|
+
export function bakeTake(sk, out, placement = { position: [0, 0, 0], yaw: 0 }, meta = {}) {
|
|
20
|
+
const F = out.frames.length, B = sk.names.length;
|
|
21
|
+
const rot = new Float32Array(F * B * 4), hips = new Float32Array(F * 3);
|
|
22
|
+
const unYaw = ry(-(placement.yaw ?? 0));
|
|
23
|
+
for (let f = 0; f < F; f++) {
|
|
24
|
+
const s = out.frames[f].s;
|
|
25
|
+
const local = { P: s.P.map(p => apply(unYaw, sub(p, placement.position))), R: s.R.map(R => mul(unYaw, R)) };
|
|
26
|
+
const { quats, hips: h } = toLocal(sk, local);
|
|
27
|
+
quats.forEach((q, b) => rot.set(q, (f * B + b) * 4));
|
|
28
|
+
hips.set(h, f * 3);
|
|
29
|
+
}
|
|
30
|
+
// one track per spec.objects key: a body, or a group root its parts ride (object-pose.js);
|
|
31
|
+
// `hold` marks the frames a carried record is in hand, and every frame of a follow entry (world
|
|
32
|
+
// space: object-travel.js cuts the carry of its own parents there); a switch bakes vis/lvl only
|
|
33
|
+
const objects = {}, f0 = out.f0 ?? 0;
|
|
34
|
+
for (const [id, spec] of Object.entries(out.ctx.spec.objects ?? {})) {
|
|
35
|
+
const carried = isCarried(spec), follows = isFollow(spec), rides = carried || follows, moves = !isSwitch(spec);
|
|
36
|
+
const pos = moves ? new Float32Array(F * 3) : null, quat = moves ? new Float32Array(F * 4) : null;
|
|
37
|
+
const named = rides || !moves || isFree(spec), vis = named && spec.visible ? new Uint8Array(F) : null, hold = rides ? new Uint8Array(F) : null;
|
|
38
|
+
const lvl = named && spec.level ? new Float32Array(F) : null;
|
|
39
|
+
const [grab, release] = carried ? spanOf(out, spec) : [0, 0];
|
|
40
|
+
for (let f = 0; f < F; f++) {
|
|
41
|
+
if (vis) vis[f] = (out.baked?.[spec.visible]?.[f + f0] ?? 0) > 0.5 ? 1 : 0;
|
|
42
|
+
if (lvl) lvl[f] = Math.max(0, Math.min(4, out.baked?.[spec.level]?.[f + f0] ?? 1));
|
|
43
|
+
if (!moves) continue;
|
|
44
|
+
const o = rootPoseAt(out, id, f + f0);
|
|
45
|
+
if (!o) continue;
|
|
46
|
+
pos.set(o.pos, f * 3); quat.set(quatFromMat(o.R), f * 4);
|
|
47
|
+
if (hold) hold[f] = follows || (f + f0 >= grab && f + f0 <= release) ? 1 : 0;
|
|
48
|
+
}
|
|
49
|
+
objects[id] = { ...(moves ? { pos, quat } : {}), ...(vis ? { vis } : {}), ...(hold ? { hold } : {}), ...(lvl ? { lvl } : {}) };
|
|
50
|
+
}
|
|
51
|
+
return { schema: TAKE_SCHEMA, fps: out.fps, frameCount: F, rig: sk.model, bones: sk.names.slice(), rot, hips, objects, placement, ...meta };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Local quaternion [x,y,z,w] of bone b at frame f (nearest frame, clamped). */
|
|
55
|
+
export function takeQuat(take, f, b) {
|
|
56
|
+
const ff = Math.max(0, Math.min(take.frameCount - 1, Math.round(f))), i = (ff * take.bones.length + b) * 4;
|
|
57
|
+
return [take.rot[i], take.rot[i + 1], take.rot[i + 2], take.rot[i + 3]];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function takeHips(take, f) {
|
|
61
|
+
const ff = Math.max(0, Math.min(take.frameCount - 1, Math.round(f))) * 3;
|
|
62
|
+
return [take.hips[ff], take.hips[ff + 1], take.hips[ff + 2]];
|
|
63
|
+
}
|