cozyclay 2.1.0 → 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 +65 -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 +22 -6
- 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-BlT33CGf.js → first-shot-handoff-CC4xe4HM.js} +1 -1
- package/dist/assets/{landing-BSesrCpL.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/llms.txt +14 -0
- 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 +174 -2639
- 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 +48 -419
- 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/PoseImagePanel.jsx +1 -1
- 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 +11 -53
- package/src/shell/CompositionGuideButton.jsx +20 -0
- 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/ShotPreviewHeader.jsx +17 -0
- package/src/shell/TopBar.jsx +5 -83
- package/src/shell/ViewportToolbar.jsx +23 -93
- 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/shell/viewport.css +12 -0
- 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 +46 -16
- package/src/studio-agent-motion.js +9 -9
- package/src/studio-agent-protocol.js +46 -73
- package/src/studio-app-binding.js +29 -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/headless-studio.mjs +85 -0
- package/tools/knip-baseline.json +0 -179
- package/tools/loc-baseline.json +33 -83
- 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 -124
- package/bin/agent/motion-runtime.mjs +0 -76
- package/dist/ardy/cskel27-rest.json +0 -557
- package/dist/assets/analytics-B41FmPYI.js +0 -1
- package/dist/assets/app-Do-Og5Pu.css +0 -1
- package/dist/assets/app-DwP-tl2x.js +0 -4920
- package/dist/assets/config-Bx7pYsD9.js +0 -1
- package/dist/assets/config-DeoFTYzb.css +0 -1
- package/dist/assets/demo-Dt0qZfS7.js +0 -1
- package/dist/assets/scene-asset-sync-BCWDJB1d.js +0 -19
- package/dist/assets/scene-asset-sync-DjJ_cJ2R.css +0 -1
- package/dist/assets/ticket-jqMhqL88.js +0 -1
- package/dist/assets/workflow-Br-K7Srs.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 -159
- 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
package/src/domains/motion.js
CHANGED
|
@@ -1,24 +1,11 @@
|
|
|
1
|
-
import { useState, useEffect, useSyncExternalStore,
|
|
2
|
-
import { AppContext } from '../app-context.js';
|
|
1
|
+
import { useState, useEffect, useSyncExternalStore, useRef, useMemo } from "react";
|
|
3
2
|
import { createDocumentStore } from '../document-store.js';
|
|
4
3
|
import { useDocumentDomain } from '../store/use-document-store.js';
|
|
5
|
-
import { createPhysicsProgress } from "../ardy/physics-panel.jsx";
|
|
6
4
|
import {
|
|
7
|
-
TIMELINE_FPS,
|
|
8
5
|
hierarchyIdForIkFocus,
|
|
9
|
-
ARDY_PRESERVE_DEFAULT,
|
|
10
6
|
REST_BONES,
|
|
11
|
-
ARDY_SEED_MAX,
|
|
12
|
-
lineTrackLabel,
|
|
13
|
-
ARDY_DURATION_MIN,
|
|
14
|
-
buildPromptSchedule,
|
|
15
|
-
posePlacementFrame,
|
|
16
|
-
toArdySegments,
|
|
17
|
-
ARDY_FPS,
|
|
18
|
-
toArdyFrame,
|
|
19
7
|
} from "../app-stage.jsx";
|
|
20
|
-
import { copyPhysicsKeys
|
|
21
|
-
import { fitPlatforms } from "../ardy/platform-fit.js";
|
|
8
|
+
import { copyPhysicsKeys } from "../motion/physics-review.js";
|
|
22
9
|
import {
|
|
23
10
|
createIkState,
|
|
24
11
|
ikTouch,
|
|
@@ -39,84 +26,46 @@ import {
|
|
|
39
26
|
ikBakeKeyframe,
|
|
40
27
|
ikEvaluate,
|
|
41
28
|
resolveIkRig,
|
|
42
|
-
} from "../
|
|
29
|
+
} from "../motion/ik.js";
|
|
43
30
|
import * as THREE from "three";
|
|
44
31
|
import { StudioProtocolError } from "../studio-agent-protocol.js";
|
|
45
|
-
|
|
32
|
+
|
|
33
|
+
import { applyTakeFrame } from "../motion3/playback.js";
|
|
46
34
|
import { isKo, ko } from "../locale.js";
|
|
47
|
-
import { characterScaleFor, loadMotionFromUrl } from "../ardy/npz.js";
|
|
48
|
-
import { supportHeightForObject, OBJECT_LIBRARY } from "../scene-objects.js";
|
|
49
|
-
import { applySupportRise, autoRoofDrop, applyAutoFall, applyRootDrop, normalizeRootDrop } from "../ardy/root-drop.js";
|
|
50
35
|
import { isProxyFigure } from "../scenes.js";
|
|
51
|
-
import {
|
|
52
|
-
import { retimeMotion } from "../ardy/retime.js";
|
|
53
|
-
import {
|
|
54
|
-
createMotionEdit,
|
|
55
|
-
motionEditLayout,
|
|
56
|
-
renderMotionEdit,
|
|
57
|
-
remapFrameKeyMap,
|
|
58
|
-
remapTimelineFrame,
|
|
59
|
-
} from "../ardy/motion-edit.js";
|
|
36
|
+
import { refuseRigOnly } from "../character-kind.js";
|
|
60
37
|
import { DEFAULT_POSE, restoreBindPositions, applyPose, applyHipsOffset } from "../poses.js";
|
|
61
|
-
import {
|
|
62
|
-
import {
|
|
63
|
-
import { fixCollisions, fixCollisionsRange } from "../ardy/fix-collisions.js";
|
|
64
|
-
import { trackFeature, startMotionRequest, motionPreflightReason, trackActivation } from "../analytics.js";
|
|
65
|
-
import { buildArdyPose } from "../ardy/export.js";
|
|
66
|
-
import { slateLine } from "../shot.js";
|
|
67
|
-
import { motionReadiness } from "../motion-readiness.js";
|
|
68
|
-
import { motionReadinessMessage } from "../motion-readiness-ui.jsx";
|
|
69
|
-
import {
|
|
70
|
-
resolveSeed,
|
|
71
|
-
stripSourceMotion,
|
|
72
|
-
blocksFromRequest,
|
|
73
|
-
replayPayload,
|
|
74
|
-
freshRecipe,
|
|
75
|
-
withLineEdit,
|
|
76
|
-
pushTakeVersion,
|
|
77
|
-
TAKE_VERSIONS_MAX,
|
|
78
|
-
} from "../take-recipe.js";
|
|
79
|
-
import { buildGenerationRequest, generationRefusal } from '../motion/generation.js';
|
|
80
|
-
import { planPosePin } from '../ardy/pose-pin.js';
|
|
81
|
-
import { TRAIL_TRACKS, applyTrailFalloffDelta, restoreTrailEdits, trailEditRange, worldDeltaToTrailClip } from "../motion-trail.js";
|
|
82
|
-
import { bakeIkDragKey, bakeIkDragRange, chainsChangedBy, ikDragRecord, ikDragTouch } from "../ardy/ik-drag.js";
|
|
83
|
-
import { ikKeyJson, ikTrackKeyFromJson } from "../ardy/ik-key-json.js";
|
|
84
|
-
import { findAbsoluteIkKeyConflicts } from "../trail-key-conflicts.js";
|
|
38
|
+
import { bakeIkDragKey, bakeIkDragRange, chainsChangedBy, ikDragRecord, ikDragTouch } from "../motion/ik-drag.js";
|
|
39
|
+
import { ikKeyJson, ikTrackKeyFromJson } from "../motion/ik-key-json.js";
|
|
85
40
|
import { chooseIkEntryPose } from "../ik-camera.js";
|
|
86
|
-
import {
|
|
87
|
-
import { isLineEditUnsupported } from "../line-edit.js";
|
|
88
|
-
import { openMotionDb, getMotion, putMotion } from "../motion-store.js";
|
|
89
|
-
import { resolveMotionSource, decodeMotionResource, encodeMotionResource, sha256Hex } from "../motion-resources.js";
|
|
90
|
-
import { applyRangePin, captureRangePinTarget, normalizeRangePin, rangePinTargetWorld, rangePinTracks, removeRangePinKeys } from "../ardy/range-pin.js";
|
|
41
|
+
import { applyRangePin, captureRangePinTarget, normalizeRangePin, rangePinTargetWorld, rangePinTracks, removeRangePinKeys } from "../motion/range-pin.js";
|
|
91
42
|
import { rangePinObjectMatrixAt } from "../range-pin-object-transform.js";
|
|
92
|
-
import { createMotionVideoCapture } from "./motion/motion-video-capture.js";
|
|
93
43
|
import { ensureRangePinState, overlappingRangePin, rangePinConflictFrames, rangePinOverlapMessage } from "./motion/motion-range-pin-rules.js";
|
|
94
44
|
|
|
45
|
+
import { takeSource, normalizeTakeSource, compileTakeSource, renderTakeEdit } from "./motion/take-source.js";
|
|
95
46
|
import { sameMotionIntent, emptyMotionLayer, encodeMotionKeys, decodeMotionKeys, cloneRangePin, encodeRangePins, decodeRangePins, encodeRangePinResiduals, decodeRangePinResiduals } from "./motion/motion-intent-codec.js";
|
|
96
47
|
// Plain intent owns take identities and JSON keys. Decoded buffers and rig
|
|
97
48
|
// preimages are copy-on-record runtime resources, never mutable store values.
|
|
98
49
|
export function createMotionDomain(appContext, characters) {
|
|
99
|
-
const takes = new Map(), decoded = new WeakMap(), rigImages = new WeakMap()
|
|
50
|
+
const takes = new Map(), decoded = new WeakMap(), rigImages = new WeakMap();
|
|
100
51
|
let running = 0;
|
|
101
52
|
const references = entries => entries.map(entry => {
|
|
102
|
-
const
|
|
103
|
-
const take = ref ? { resourceId: `restore:${entry.id}:${ref.motionId ?? ref.url}`, url: ref.url ?? null, anchorX: ref.anchorX, anchorZ: ref.anchorZ,
|
|
104
|
-
rotationDeg: ref.rotationDeg, prompt: ref.prompt ?? '', ...(ref.trailEdits ? { trailEdits: ref.trailEdits } : {}), ...(ref.studioTakeId ? { studioTakeId: ref.studioTakeId } : {}) } : null;
|
|
53
|
+
const saved = normalizeTakeSource(entry.motionTake), take = saved ? restoredTake(entry.id, saved) : null;
|
|
105
54
|
// The saved Pose-mode / IK edits come back with the scene (scenes.js ikEdits).
|
|
106
55
|
const edits = entry.ikEdits;
|
|
107
|
-
return { id: entry.id, take, fullTake: take,
|
|
108
|
-
ikKeys: edits?.keys ?? [], ikPins: edits?.pins ?? [], ikPinResiduals: edits?.pinResiduals ?? [] };
|
|
56
|
+
return { id: entry.id, take, fullTake: take, ikKeys: edits?.keys ?? [], ikPins: edits?.pins ?? [], ikPinResiduals: edits?.pinResiduals ?? [] };
|
|
109
57
|
});
|
|
110
|
-
|
|
58
|
+
// A take v3 keeps its source (spec, placement, scene) in the authored layer, so a reload
|
|
59
|
+
// or an undo past a pruned resource compiles the same bones again (take-source.js).
|
|
60
|
+
function snapshotTake(take, resourceId = crypto.randomUUID(), source = null, stored = true) {
|
|
111
61
|
if (!take) return null;
|
|
112
62
|
if (take.resourceId) return { ...take };
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return { resourceId, frames: take.frames, fps: take.fps,
|
|
116
|
-
anchorX: take.anchorX ?? 0, anchorZ: take.anchorZ ?? 0, anchorFrame: take.anchorFrame ?? 0, rotationDeg: take.rotationDeg ?? 0,
|
|
117
|
-
...(take.trailEdits ? { trailEdits: take.trailEdits } : {}),
|
|
118
|
-
editSegments: take.editSegments ?? createMotionEdit(take.frames), studioTakeId: take.studioTakeId ?? resourceId };
|
|
63
|
+
if (stored) takes.set(resourceId, structuredClone(take));
|
|
64
|
+
source ??= takeSource(take, appContext.shared.storeRef?.current?.objects ?? appContext.shared.sceneObjects);
|
|
65
|
+
return { resourceId, frames: take.frames, fps: take.fps, prompt: take.prompt ?? '', studioTakeId: take.studioTakeId ?? resourceId, ...(source ? { source } : {}), ...(take.edit ? { edit: take.edit } : {}) };
|
|
119
66
|
}
|
|
67
|
+
const restoredTake = (id, { takeId, edit, ...source }) => snapshotTake({ schema: source.schema, frames: Math.round(source.spec.frames), fps: 24,
|
|
68
|
+
prompt: source.spec.label ?? '', studioTakeId: takeId, edit }, `restore:${id}:${takeId ?? 'take-v3'}`, source, false);
|
|
120
69
|
const normalize = rows => rows.map(row => ({ ...emptyMotionLayer(row.id), ...row,
|
|
121
70
|
take: snapshotTake(row.take), fullTake: snapshotTake(row.fullTake ?? row.take),
|
|
122
71
|
ikKeys: row.ikKeys ?? [], ikPins: row.ikPins ?? [], ikPinResiduals: row.ikPinResiduals ?? [], committedIkEdits: row.committedIkEdits ?? [] }));
|
|
@@ -128,15 +77,19 @@ export function createMotionDomain(appContext, characters) {
|
|
|
128
77
|
const read = () => documentStore.read('motion');
|
|
129
78
|
const layer = id => read().find(row => row.id === id) ?? emptyMotionLayer(id);
|
|
130
79
|
function materialize(take) {
|
|
80
|
+
if (take?.source && !takes.has(take.resourceId)) {
|
|
81
|
+
try { const compiled = compileTakeSource(take.source); if (compiled) takes.set(take.resourceId, compiled); }
|
|
82
|
+
catch (error) { console.warn('[cozyclay] could not compile the saved take', error); }
|
|
83
|
+
}
|
|
131
84
|
if (!take || !takes.has(take.resourceId)) return null;
|
|
132
85
|
if (!decoded.has(take)) {
|
|
133
86
|
const { resourceId, ...metadata } = take;
|
|
134
|
-
decoded.set(take, { ...takes.get(resourceId), ...metadata });
|
|
87
|
+
decoded.set(take, renderTakeEdit({ ...takes.get(resourceId), ...metadata }));
|
|
135
88
|
}
|
|
136
89
|
return decoded.get(take);
|
|
137
90
|
}
|
|
138
91
|
const motionFor = id => materialize(layer(id).take), fullMotionFor = id => materialize(layer(id).fullTake);
|
|
139
|
-
const visibleMotion =
|
|
92
|
+
const visibleMotion = motionFor;
|
|
140
93
|
const frame = () => appContext.live.state?.timeline.currentFrame ?? 0;
|
|
141
94
|
const rigFor = id => appContext.shared.rigs[id];
|
|
142
95
|
// Rig-only tools refuse a capsule figure by name before looking for its rig.
|
|
@@ -160,9 +113,9 @@ export function createMotionDomain(appContext, characters) {
|
|
|
160
113
|
const rig = rigFor(id);
|
|
161
114
|
if (rig) {
|
|
162
115
|
const resolved = resolveIkRig(rig); if (resolved) Object.assign(state, resolved, { rig });
|
|
163
|
-
if (savedRigs?.has(id)
|
|
116
|
+
if (savedRigs?.has(id)) appContext.shared.restoreExportRig(savedRigs.get(id));
|
|
164
117
|
else if (resolved) {
|
|
165
|
-
if (take)
|
|
118
|
+
if (take) applyTakeFrame(rig, take, Math.min(frame(), take.frames - 1));
|
|
166
119
|
else {
|
|
167
120
|
const character = cast.read().find(entry => entry.id === id);
|
|
168
121
|
restoreBindPositions(rig); applyPose(rig, { ...REST_BONES, ...(character?.pose ?? DEFAULT_POSE).bones });
|
|
@@ -176,7 +129,6 @@ export function createMotionDomain(appContext, characters) {
|
|
|
176
129
|
const id = appContext.shared.loadedLayerCharRef.current;
|
|
177
130
|
appContext.shared.ikStateRef.current = states.get(id) ?? createIkState();
|
|
178
131
|
appContext.shared.bufferRef.current = { ...appContext.shared.bufferRef.current, motion: visibleMotion(id), ik: appContext.shared.ikStateRef.current };
|
|
179
|
-
appContext.shared.takeRecipeRef.current = layer(id).takeRecipe;
|
|
180
132
|
domain.onProject?.(); viewRevision++;
|
|
181
133
|
}
|
|
182
134
|
const unsubscribe = documentStore.subscribe(project);
|
|
@@ -270,68 +222,24 @@ export function createMotionDomain(appContext, characters) {
|
|
|
270
222
|
for (const [track, key] of incoming) entry.set(track, key); state.keys.set(at, entry);
|
|
271
223
|
});
|
|
272
224
|
}
|
|
273
|
-
function castWrite(update) { return appContext.recordAction('cast', () => appContext.storeDomain('cast').write(update), null, true); }
|
|
274
225
|
function synchronizeTimeline() { appContext.storeDomain('cast').syncTimeline(); }
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
previews.delete(id);
|
|
280
|
-
writeLayer(id, { take: snapshotTake(take), fullTake: snapshotTake(fullTake), ikKeys, ikPins, ikPinResiduals, takeRecipe: recipe, takeVersions: versions, committedIkEdits: [] });
|
|
226
|
+
// Install a take (motion.compile): one descriptor serves as both the visible and the full take.
|
|
227
|
+
function replace(id, take, { ikKeys = [], ikPins = [], ikPinResiduals = [] } = {}) {
|
|
228
|
+
const descriptor = snapshotTake(take);
|
|
229
|
+
writeLayer(id, { take: descriptor, fullTake: descriptor, ikKeys, ikPins, ikPinResiduals, committedIkEdits: [] });
|
|
281
230
|
synchronizeTimeline();
|
|
282
231
|
}
|
|
283
232
|
function clear(id) {
|
|
284
|
-
|
|
285
|
-
writeLayer(id, { take: null, fullTake: null, ikKeys: [], ikPins: [], ikPinResiduals: [], committedIkEdits: [], takeRecipe: null });
|
|
286
|
-
castWrite(rows => rows.map(row => row.id === id ? { ...row, scale: 1, motionRef: null,
|
|
287
|
-
layer: { ...row.layer, promptClips: [] } } : row));
|
|
233
|
+
writeLayer(id, { take: null, fullTake: null, ikKeys: [], ikPins: [], ikPinResiduals: [], committedIkEdits: [] });
|
|
288
234
|
synchronizeTimeline();
|
|
289
235
|
}
|
|
290
|
-
function editSegments(id, segments) {
|
|
291
|
-
const full = fullMotionFor(id), take = motionFor(id);
|
|
292
|
-
if (!full || !take) throw new StudioProtocolError('TARGET_NOT_READY', 'This character has no take to edit.');
|
|
293
|
-
const previous = take.editSegments, rendered = { ...renderMotionEdit(full, segments), url: null };
|
|
294
|
-
const keys = remapFrameKeyMap(decodeMotionKeys(layer(id).ikKeys), previous, segments);
|
|
295
|
-
writeLayer(id, { take: snapshotTake(rendered), ikKeys: encodeMotionKeys(keys) });
|
|
296
|
-
castWrite(rows => rows.map(row => row.id !== id ? row : { ...row, layer: { ...row.layer,
|
|
297
|
-
promptClips: row.layer.promptClips.flatMap(clip => {
|
|
298
|
-
const start = remapTimelineFrame(previous, segments, clip.startFrame), end = remapTimelineFrame(previous, segments, clip.endFrame);
|
|
299
|
-
if (start === null && end === null) return [];
|
|
300
|
-
const startFrame = Math.max(0, Math.min(start ?? 0, rendered.frames - 1));
|
|
301
|
-
const endFrame = Math.max(0, Math.min(end ?? rendered.frames - 1, rendered.frames - 1));
|
|
302
|
-
return startFrame <= endFrame ? [{ ...clip, startFrame, endFrame }] : [];
|
|
303
|
-
}) } }));
|
|
304
|
-
synchronizeTimeline();
|
|
305
|
-
}
|
|
306
|
-
function fix(id, scope = 'frame') {
|
|
307
|
-
rigOnly(id, 'collision');
|
|
308
|
-
const rig = rigFor(id), resolved = rig && resolveIkRig(rig);
|
|
309
|
-
if (!resolved) throw new StudioProtocolError('TARGET_NOT_READY', 'The character rig is not loaded.');
|
|
310
|
-
const state = appContext.shared.ikStatesRef.current.get(id) ?? { ...createIkState(), ...resolved };
|
|
311
|
-
const blockers = at => collisionBlockers({ rigs: appContext.shared.rigs, activeId: id, characterIds: appContext.live.characters,
|
|
312
|
-
sceneObjects: appContext.ports.read().objects, library: OBJECT_LIBRARY, frame: at, take: { frameCount: motionFor(id)?.frames ?? 1, fps: 24 } });
|
|
313
|
-
if (scope === 'clip') {
|
|
314
|
-
const take = motionFor(id); if (!take) throw new StudioProtocolError('TARGET_NOT_READY', 'Load a take first.');
|
|
315
|
-
const originals = rigSnapshots();
|
|
316
|
-
try {
|
|
317
|
-
fixCollisionsRange({ rig, ...resolved, ikState: state, startFrame: 0, endFrame: take.frames - 1,
|
|
318
|
-
applyFrame(at) { applyMotionFrame(rig, take, at); ikEvaluate(resolved.chains, state, at, resolved.fkJoints, 6); },
|
|
319
|
-
blockersAt(at) { for (const row of read()) if (row.id !== id && rigFor(row.id)) appContext.shared.poseMemberAtFrame(rigFor(row.id), motionFor(row.id), appContext.shared.ikStatesRef.current.get(row.id), at, 6); return blockers(at); } });
|
|
320
|
-
} finally { for (const snapshot of originals.values()) appContext.shared.restoreExportRig(snapshot); }
|
|
321
|
-
} else {
|
|
322
|
-
const result = fixCollisions(rig, resolved.chains, { ikState: state, fkJoints: resolved.fkJoints, blockers: blockers(frame()) });
|
|
323
|
-
if (!result.supported) throw new StudioProtocolError('TARGET_NOT_READY', 'This rig does not support collision cleanup.');
|
|
324
|
-
if (result.changed) ikBakeKeyframe(resolved.chains, state, frame(), resolved.fkJoints, result.touched, null, result.baseQuats);
|
|
325
|
-
}
|
|
326
|
-
setKeys(id, state.keys);
|
|
327
|
-
}
|
|
328
236
|
function bakeCurrentKey(id, at, dragIds = null) {
|
|
329
237
|
const rig = rigFor(id), resolved = rig && resolveIkRig(rig);
|
|
330
238
|
if (!resolved) return null;
|
|
331
239
|
const take = motionFor(id);
|
|
332
240
|
if (dragIds) {
|
|
333
241
|
const keyed = bakeIkDragKey(resolved.chains, resolved.fkJoints, at, dragIds, take
|
|
334
|
-
? (rawRig) =>
|
|
242
|
+
? (rawRig) => applyTakeFrame(rawRig, take, at)
|
|
335
243
|
: null);
|
|
336
244
|
if (keyed && take) for (const key of keyed.values()) key.blend = 6;
|
|
337
245
|
return keyed ? run('ik.setKey', { characterId: id, frame: at, tracks: ikKeyJson(keyed) }) : null;
|
|
@@ -351,71 +259,6 @@ export function createMotionDomain(appContext, characters) {
|
|
|
351
259
|
ikBakeKeyframe(resolved.chains, state, at, resolved.fkJoints);
|
|
352
260
|
});
|
|
353
261
|
}
|
|
354
|
-
function editTrail(id, { track = "hips", grabFrame, radiusFrames, delta }) {
|
|
355
|
-
rigOnly(id, 'trails');
|
|
356
|
-
const take = motionFor(id), character = appContext.storeDomain('cast').read().find(row => row.id === id);
|
|
357
|
-
if (!take) throw new StudioProtocolError('TARGET_NOT_READY', 'Load a take first.');
|
|
358
|
-
const scale = character.scale ?? 1;
|
|
359
|
-
const rig = rigFor(id);
|
|
360
|
-
const clipDelta = worldDeltaToTrailClip(take, delta, { track, grabFrame, radiusFrames, rig, scale });
|
|
361
|
-
const edited = snapshotTake(applyTrailFalloffDelta(take, { track, grabFrame, radiusFrames, clipDelta }));
|
|
362
|
-
writeLayer(id, { take: edited });
|
|
363
|
-
castWrite(rows => rows.map(row => row.id === id ? { ...row, motionRef: { ...row.motionRef, trailEdits: edited.trailEdits } } : row));
|
|
364
|
-
}
|
|
365
|
-
const physicsReviews = new Map();
|
|
366
|
-
let physicsJob = 0;
|
|
367
|
-
const physicsStamp = id => JSON.stringify([layer(id), appContext.storeDomain('cast').read().find(row => row.id === id)]);
|
|
368
|
-
function applyPhysics(id) {
|
|
369
|
-
rigOnly(id, 'physics');
|
|
370
|
-
const preview = physicsReviews.get(id);
|
|
371
|
-
if (!preview || preview.stamp !== physicsStamp(id)) throw new StudioProtocolError('STALE_TARGET', 'Analyse this take again before applying corrections.');
|
|
372
|
-
setKeys(id, preview.result.candidate.keys);
|
|
373
|
-
domain.onPhysicsPreview?.(null);
|
|
374
|
-
}
|
|
375
|
-
async function autoPhysics(id, options, context) {
|
|
376
|
-
rigOnly(id, 'physics');
|
|
377
|
-
const rig = rigFor(id), resolved = rig && resolveIkRig(rig), take = motionFor(id);
|
|
378
|
-
if (!resolved || !take) throw new StudioProtocolError('TARGET_NOT_READY', 'Load a take and character rig first.');
|
|
379
|
-
const job = ++physicsJob, stamp = physicsStamp(id), original = appContext.shared.snapshotExportRig(rig);
|
|
380
|
-
let lastYield = performance.now();
|
|
381
|
-
const check = () => { context.check(); if (job !== physicsJob) throw new StudioProtocolError('STALE_TARGET', 'Physics analysis was superseded.'); };
|
|
382
|
-
domain.onPhysicsRunning?.(true); domain.onPhysicsPreview?.(null);
|
|
383
|
-
try {
|
|
384
|
-
check();
|
|
385
|
-
const result = await reviewAutoPhysics({ rig, ...resolved, motion: take, sourceKeys: decodeMotionKeys(layer(id).ikKeys),
|
|
386
|
-
applyRaw: at => applyMotionFrame(rig, take, at), sceneObjects: appContext.ports.read().objects,
|
|
387
|
-
...options, cache: appContext.shared.physicsSourceCacheRef.current, onProgress: value => domain.onPhysicsProgress?.(value),
|
|
388
|
-
yieldFrame: async () => {
|
|
389
|
-
check(); if (performance.now() - lastYield < 16) return;
|
|
390
|
-
appContext.shared.restoreExportRig(original);
|
|
391
|
-
await new Promise(resolve => { const channel = new MessageChannel(); channel.port1.onmessage = () => { channel.port1.close(); channel.port2.close(); resolve(); }; channel.port2.postMessage(0); });
|
|
392
|
-
lastYield = performance.now(); check();
|
|
393
|
-
},
|
|
394
|
-
});
|
|
395
|
-
appContext.shared.restoreExportRig(original); check();
|
|
396
|
-
physicsReviews.set(id, { result, stamp });
|
|
397
|
-
if (options.apply) context.commit(() => applyPhysics(id)); else domain.onPhysicsPreview?.(result);
|
|
398
|
-
return { before: result.before, after: result.after, changedFrames: result.changedFrames.length, warnings: result.warnings, unresolved: result.unresolved };
|
|
399
|
-
} finally {
|
|
400
|
-
if (job === physicsJob) { if (stamp === physicsStamp(id)) appContext.shared.restoreExportRig(original); domain.onPhysicsRunning?.(false); }
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
function runPrepared(characterId, apply) {
|
|
404
|
-
if (running) return apply();
|
|
405
|
-
let result;
|
|
406
|
-
const token = crypto.randomUUID(); prepared.set(token, () => { result = apply(); return result; });
|
|
407
|
-
try { run('motion.applyPrepared', { characterId, token }); return result; }
|
|
408
|
-
finally { prepared.delete(token); }
|
|
409
|
-
}
|
|
410
|
-
function persistTake(take, motionRef) {
|
|
411
|
-
if (!take?.sourceBytes) return;
|
|
412
|
-
(async () => {
|
|
413
|
-
let record = appContext.shared.motionEncodingCacheRef.current.get(take.sourceBytes);
|
|
414
|
-
if (!record) { record = await encodeMotionResource(take.sourceBytes, { prompt: motionRef?.prompt, sourceUrl: motionRef?.url }); appContext.shared.motionEncodingCacheRef.current.set(take.sourceBytes, record); }
|
|
415
|
-
appContext.shared.projectMotionsRef.current.set(record.motionId.toLowerCase(), record);
|
|
416
|
-
const db = await openMotionDb(); try { await putMotion(db, record); } finally { db.close(); }
|
|
417
|
-
})().catch(error => console.warn('[cozyclay] could not cache motions', error));
|
|
418
|
-
}
|
|
419
262
|
function run(id, args) {
|
|
420
263
|
const result = appContext.bus.run(id, args);
|
|
421
264
|
const checked = receipt => { if (!receipt.ok) throw new StudioProtocolError(receipt.code, receipt.message); return receipt; };
|
|
@@ -445,15 +288,14 @@ export function createMotionDomain(appContext, characters) {
|
|
|
445
288
|
return run('run.update', { txId: gesture.txId, args });
|
|
446
289
|
}
|
|
447
290
|
const domain = { documentStore, read, write, layer, writeLayer, motionFor, fullMotionFor, visibleMotion, snapshotTake, project,
|
|
448
|
-
setKeys, setRangePinState, bakeRangePin, removePin, editKeys, setKey, replace, clear,
|
|
449
|
-
insideAction: () => running > 0, bakeCurrentKey, keyPose,
|
|
450
|
-
applyPrepared(token) { const apply = prepared.get(token); if (!apply) throw new StudioProtocolError('STALE_TARGET', 'Prepared motion is no longer available.'); return apply(); },
|
|
291
|
+
setKeys, setRangePinState, bakeRangePin, removePin, editKeys, setKey, replace, clear, run, edit, beginGesture, finishGesture, beginAction,
|
|
292
|
+
insideAction: () => running > 0, bakeCurrentKey, keyPose,
|
|
451
293
|
bindRender(context) { appContext = context; },
|
|
452
294
|
viewRevision: () => viewRevision, canUndo: id => documentStore.canUndo(id),
|
|
453
295
|
stepHistory: redo => { finishGesture(); return Boolean((redo ? documentStore.redo : documentStore.undo)()); },
|
|
454
296
|
document: () => ({ motion: read().map(row => {
|
|
455
297
|
const take = motionFor(row.id);
|
|
456
|
-
return take ? { ...row, take: { ...row.take, frames: take.frames, fps: take.fps
|
|
298
|
+
return take ? { ...row, take: { ...row.take, frames: take.frames, fps: take.fps } } : row;
|
|
457
299
|
}) }), sceneSlice: slices => slices.motion ?? references(slices.cast),
|
|
458
300
|
publish: rows => write(rows), commitDraft: write,
|
|
459
301
|
switchLayer(id) { appContext.shared.loadedLayerCharRef.current = id; project(); notify(); },
|
|
@@ -465,29 +307,13 @@ export function createMotionDomain(appContext, characters) {
|
|
|
465
307
|
frameCount: state.frameCount, committedIkEdits: layer(id).committedIkEdits, renderer: rig ? appContext.shared.snapshotExportRig(rig) : null };
|
|
466
308
|
},
|
|
467
309
|
removeLayer(id) { write(rows => rows.filter(row => row.id !== id)); },
|
|
468
|
-
|
|
469
|
-
hydrate(id, take, reference) {
|
|
470
|
-
const current = appContext.storeDomain('cast').read().find(row => row.id === id);
|
|
471
|
-
const descriptor = layer(id).take;
|
|
472
|
-
if (!descriptor || !sameMotionIntent(current?.motionRef, reference)) return false;
|
|
473
|
-
takes.set(descriptor.resourceId, structuredClone(take));
|
|
474
|
-
for (const rows of [read(), ...documentStore.history().past.map(entry => entry.snapshot.motion), ...documentStore.history().future.map(entry => entry.snapshot.motion)]) {
|
|
475
|
-
for (const row of rows) for (const value of [row.take, row.fullTake]) if (value?.resourceId === descriptor.resourceId) decoded.delete(value);
|
|
476
|
-
}
|
|
477
|
-
notify(); return true;
|
|
478
|
-
},
|
|
479
|
-
load(rows) { finishGesture(true); previews.clear(); release(); native.dispose(); native = createDocumentStore({ owned: { motion: normalize(rows) } }); release = native.subscribe(notify); pruneTakes(); notify(); },
|
|
310
|
+
load(rows) { finishGesture(true); release(); native.dispose(); native = createDocumentStore({ owned: { motion: normalize(rows) } }); release = native.subscribe(notify); pruneTakes(); notify(); },
|
|
480
311
|
dispose() { finishGesture(true); unregister(); unsubscribe(); release(); native.dispose(); listeners.clear(); takes.clear(); },
|
|
481
312
|
};
|
|
482
313
|
const unregister = appContext.registerStoreDomain('motion', domain);
|
|
483
314
|
return domain;
|
|
484
315
|
}
|
|
485
316
|
|
|
486
|
-
export function useMotionCommands() {
|
|
487
|
-
const app = useContext(AppContext), owner = app.storeDomain('motion');
|
|
488
|
-
return { run: (id, args = {}) => owner.run(id, { characterId: app.storeDomain('cast').activeId, ...args }) };
|
|
489
|
-
}
|
|
490
|
-
|
|
491
317
|
export function useMotion(appContext) {
|
|
492
318
|
const [domain] = useState(() => appContext.storeDomain('motion') ?? createMotionDomain(appContext, appContext.shared.characters));
|
|
493
319
|
domain.bindRender(appContext);
|
|
@@ -498,7 +324,6 @@ export function useMotion(appContext) {
|
|
|
498
324
|
const start = () => domain.beginGesture(); window.addEventListener('pointerdown', start, true);
|
|
499
325
|
return () => { window.removeEventListener('pointerdown', start, true); domain.finishGesture(true); };
|
|
500
326
|
}, [domain, appContext.shared.activeChar.id]);
|
|
501
|
-
const [i2vMotion, setI2vMotion] = useState({ a: null, b: null, job: null, status: "idle", error: "", instruction: "", dailyRemaining: null });
|
|
502
327
|
// IK posing for Subject 1: dragging a wrist/ankle handle FOCUSES that
|
|
503
328
|
// joint and solves its chain backward (two-bone analytic IK) on top of
|
|
504
329
|
// the current pose; joints never dragged stay purely on the FK pose.
|
|
@@ -534,44 +359,14 @@ export function useMotion(appContext) {
|
|
|
534
359
|
// hide the seam, short enough that a mid-clip fix stays visibly local.
|
|
535
360
|
const IK_CORRECTION_BLEND_FRAMES = 6;
|
|
536
361
|
|
|
537
|
-
const [autoPhysicsRunning, setAutoPhysicsRunning] = useState(false);
|
|
538
|
-
|
|
539
|
-
const [physicsPreview, setPhysicsPreview] = useState(null);
|
|
540
|
-
|
|
541
|
-
const [physicsShow, setPhysicsShow] = useState(true);
|
|
542
|
-
|
|
543
|
-
const [physicsProgress] = useState(createPhysicsProgress);
|
|
544
|
-
|
|
545
|
-
const setPhysicsProgress = physicsProgress.set;
|
|
546
|
-
|
|
547
|
-
const [physicsOptions, setPhysicsOptions] = useState({ overrides: [], protectedFrames: [], strength: 1 });
|
|
548
|
-
|
|
549
|
-
const [platformFitRunning, setPlatformFitRunning] = useState(false);
|
|
550
|
-
const [platformFitLast, setPlatformFitLast] = useState(null);
|
|
551
|
-
const [platformFitProgress] = useState(createPhysicsProgress);
|
|
552
|
-
const platformFitProgressSet = platformFitProgress.set;
|
|
553
|
-
const platformFitJobRef = useRef(0);
|
|
554
|
-
const platformFitAppliedRef = useRef(null);
|
|
555
|
-
const [platformFitApplied, setPlatformFitApplied] = useState(false);
|
|
556
|
-
|
|
557
362
|
const [ikTick, setIkTick] = useState(0);
|
|
558
363
|
|
|
559
364
|
const committedIkEdits = domain.layer(appContext.shared.activeChar.id).committedIkEdits;
|
|
560
365
|
const setCommittedIkEdits = update => domain.writeLayer(appContext.shared.activeChar.id, { committedIkEdits: typeof update === 'function' ? update(domain.layer(appContext.shared.activeChar.id).committedIkEdits) : update });
|
|
561
366
|
domain.onProject = () => setIkTick(value => value + 1);
|
|
562
367
|
|
|
563
|
-
|
|
564
|
-
* smoothstep falloff around the grab frame (pure local preview). The last
|
|
565
|
-
* finished drag stays pending so "Regenerate from trail edit" can send the
|
|
566
|
-
* auto-derived window through the existing motionEdit pipeline. */
|
|
567
|
-
const [trailFalloffS, setTrailFalloffS] = useState(0.5);
|
|
568
|
-
|
|
569
|
-
const [showTrails, setShowTrails] = useState(true);
|
|
570
|
-
|
|
368
|
+
// The Pose-mode tool: "ik" (drag handles) or "pin" (range pins).
|
|
571
369
|
const [ikEditTool, setIkEditTool] = useState("ik");
|
|
572
|
-
|
|
573
|
-
const [trailEdit, setTrailEdit] = useState(null);
|
|
574
|
-
const [trailTrackFocus, setTrailTrackFocus] = useState("hips");
|
|
575
370
|
const [rangePinSelection, setRangePinSelection] = useState(null);
|
|
576
371
|
const [rangePinPartPick, setRangePinPartPick] = useState(null);
|
|
577
372
|
const [rangePinPreview, setRangePinPreview] = useState(null);
|
|
@@ -579,8 +374,6 @@ export function useMotion(appContext) {
|
|
|
579
374
|
const rangePins = useMemo(() => encodeRangePins(pinLayer.ikPins), [pinLayer.ikPins]);
|
|
580
375
|
const rangePinResiduals = useMemo(() => decodeRangePinResiduals(pinLayer.ikPinResiduals), [pinLayer.ikPinResiduals]);
|
|
581
376
|
|
|
582
|
-
const trailFalloffFrames = Math.max(1, Math.round(trailFalloffS * TIMELINE_FPS));
|
|
583
|
-
|
|
584
377
|
function focusIkHandle(focus) {
|
|
585
378
|
setIkFocus(focus);
|
|
586
379
|
const hierarchyId = hierarchyIdForIkFocus(focus, appContext.shared.rowIdForCharIndex(appContext.shared.activeCharIndex));
|
|
@@ -589,17 +382,10 @@ export function useMotion(appContext) {
|
|
|
589
382
|
}
|
|
590
383
|
}
|
|
591
384
|
|
|
592
|
-
|
|
385
|
+
// An IK correction applies across the whole installed take.
|
|
386
|
+
function selectedIkRange() {
|
|
593
387
|
const count = Math.max(1, motion?.frames ?? appContext.shared.tlFrameCount ?? 1);
|
|
594
|
-
|
|
595
|
-
const edit = motion.editSegments ?? createMotionEdit(count);
|
|
596
|
-
const layout = motionEditLayout(edit);
|
|
597
|
-
const at = Math.max(0, Math.min(count - 1, Math.round(frame) || 0));
|
|
598
|
-
const selected = layout.find((segment) => at >= segment.timelineStart && at <= segment.timelineEnd) ?? layout[0];
|
|
599
|
-
return {
|
|
600
|
-
startFrame: Math.max(0, selected?.timelineStart ?? 0),
|
|
601
|
-
endFrame: Math.min(count - 1, selected?.timelineEnd ?? count - 1),
|
|
602
|
-
};
|
|
388
|
+
return { startFrame: 0, endFrame: count - 1 };
|
|
603
389
|
}
|
|
604
390
|
|
|
605
391
|
function setPendingIkEditValue(value) {
|
|
@@ -659,19 +445,6 @@ export function useMotion(appContext) {
|
|
|
659
445
|
return true;
|
|
660
446
|
}
|
|
661
447
|
|
|
662
|
-
function clearTrailTrackFocus() {
|
|
663
|
-
setTrailTrackFocus(null);
|
|
664
|
-
setIkFocus(null);
|
|
665
|
-
}
|
|
666
|
-
function selectTrailTrack(track) {
|
|
667
|
-
setTrailTrackFocus(track);
|
|
668
|
-
focusIkHandle(track);
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
useEffect(() => {
|
|
672
|
-
if (!ikMode || ikEditTool !== "trail") setTrailTrackFocus(TRAIL_TRACKS.some(track => track.id === ikFocus) ? ikFocus : "hips");
|
|
673
|
-
}, [ikMode, ikEditTool, ikFocus]);
|
|
674
|
-
|
|
675
448
|
function rangePinObjectWorldMatrix(objectId, frame) {
|
|
676
449
|
const object = appContext.shared.sceneObjects.find((entry) => entry.id === objectId);
|
|
677
450
|
if (!object) return null;
|
|
@@ -817,85 +590,6 @@ export function useMotion(appContext) {
|
|
|
817
590
|
return count;
|
|
818
591
|
}
|
|
819
592
|
|
|
820
|
-
const [bridge, setBridge] = useState(null);
|
|
821
|
-
|
|
822
|
-
const [bridgeChecking, setBridgeChecking] = useState(false);
|
|
823
|
-
|
|
824
|
-
const [motionSetupReveal, setMotionSetupReveal] = useState(0);
|
|
825
|
-
|
|
826
|
-
const [motionSetupKind, setMotionSetupKind] = useState("prompt");
|
|
827
|
-
|
|
828
|
-
const [ardyPrompt, setArdyPrompt] = useState("");
|
|
829
|
-
|
|
830
|
-
const [ardyDuration, setArdyDuration] = useState(4);
|
|
831
|
-
|
|
832
|
-
// Optional native-ARDY seed: empty string = omit from the request (the
|
|
833
|
-
// box picks a fresh random one each run); otherwise a plain integer in
|
|
834
|
-
// 0..2**31-1 to reproduce a result.
|
|
835
|
-
const [ardySeed, setArdySeed] = useState("");
|
|
836
|
-
|
|
837
|
-
// How much of the loaded take a regeneration keeps (scheduled inpainting).
|
|
838
|
-
// 1 = hold the take everywhere the user did not edit, 0 = ignore it and
|
|
839
|
-
// generate fresh. Only ever consulted when the take still has a bridge
|
|
840
|
-
// source to preserve FROM, so the control renders with the take, not with
|
|
841
|
-
// the panel.
|
|
842
|
-
const [preserveStrength, setPreserveStrength] = useState(ARDY_PRESERVE_DEFAULT);
|
|
843
|
-
|
|
844
|
-
// Off by default on purpose: pinning runs the box's pose mode, which builds
|
|
845
|
-
// on a fixed reference base, so it is a choice the operator makes when they
|
|
846
|
-
// actually want the pose in the generated clip.
|
|
847
|
-
const [ardyStartFromPose, setArdyStartFromPose] = useState(false);
|
|
848
|
-
|
|
849
|
-
// WHERE the pose lands in the clip. "start" leaves from it, "end" arrives at
|
|
850
|
-
// it, "middle" passes through it, "playhead" places it on the frame the
|
|
851
|
-
// operator scrubbed to — the box takes any destination frame.
|
|
852
|
-
const [ardyPosePlacement, setArdyPosePlacement] = useState("start");
|
|
853
|
-
|
|
854
|
-
/* The recipe of the take that is loaded RIGHT NOW: seed + prompt blocks +
|
|
855
|
-
* the line edits pulled on top of it. It is written in exactly one place
|
|
856
|
-
* (commitTakeRecipe, on a successful run) and read in two: the request
|
|
857
|
-
* assembly, which attaches it as C10 `replay`, and the version strip, which
|
|
858
|
-
* stores a copy beside every motionUrl so clicking v1 restores v1's recipe
|
|
859
|
-
* and not the one the artist has since edited into existence.
|
|
860
|
-
* The ref shadows the state because the recipe is consulted from inside an
|
|
861
|
-
* async job completion, where a stale closure would silently record the
|
|
862
|
-
* previous take's edits against this take's url. */
|
|
863
|
-
const takeRecipe = domain.layer(appContext.shared.activeChar.id).takeRecipe;
|
|
864
|
-
const setTakeRecipe = value => domain.writeLayer(appContext.shared.activeChar.id, { takeRecipe: value });
|
|
865
|
-
const takeVersions = domain.layer(appContext.shared.activeChar.id).takeVersions;
|
|
866
|
-
const setTakeVersions = update => domain.writeLayer(appContext.shared.activeChar.id, { takeVersions: typeof update === 'function' ? update(domain.layer(appContext.shared.activeChar.id).takeVersions) : update });
|
|
867
|
-
|
|
868
|
-
// Which C10 replay entries came back failed or boundary-warned. Non-blocking
|
|
869
|
-
// by contract: the take generated, one refinement may not have survived it,
|
|
870
|
-
// and that is worth a line next to the take rather than a toast that scrolls
|
|
871
|
-
// away before the artist has looked at the result.
|
|
872
|
-
const [replayNotices, setReplayNotices] = useState([]);
|
|
873
|
-
|
|
874
|
-
// Whether the Scene entry's action menu is open. The Refine entry needs no
|
|
875
|
-
// equivalent — it IS its action.
|
|
876
|
-
const [sceneMenuOpen, setSceneMenuOpen] = useState(false);
|
|
877
|
-
|
|
878
|
-
const [ardyRunning, setArdyRunning] = useState(false);
|
|
879
|
-
|
|
880
|
-
const [ardyStatus, setArdyStatus] = useState("");
|
|
881
|
-
|
|
882
|
-
// Keep the latest ARDY status inline with the Prompt Blocks controls. The
|
|
883
|
-
// former bottom Console history was removed because it duplicated this state
|
|
884
|
-
// and exposed an editor surface that is not part of the production workflow.
|
|
885
|
-
function reportArdyStatus(line) {
|
|
886
|
-
setArdyStatus(line);
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
const [ardyReport, setArdyReport] = useState(null);
|
|
890
|
-
|
|
891
|
-
const [ardyOutcome, setArdyOutcome] = useState(null);
|
|
892
|
-
|
|
893
|
-
// Wave-2 gate: the bridge only routes lineEdit once M4's routing lands.
|
|
894
|
-
// Until the /ardy/health payload says so, the request is never sent —
|
|
895
|
-
// today's bridge ignores unknown fields, so an ungated POST would quietly
|
|
896
|
-
// return a fresh unrelated take instead of an edit.
|
|
897
|
-
const [lineEditBackend, setLineEditBackend] = useState(false);
|
|
898
|
-
|
|
899
593
|
// Loaded motion: decoded arrays plus the world anchor captured at load.
|
|
900
594
|
const motion = domain.visibleMotion(appContext.shared.activeChar.id);
|
|
901
595
|
useEffect(() => {
|
|
@@ -907,19 +601,6 @@ export function useMotion(appContext) {
|
|
|
907
601
|
}, [appContext.shared.activeChar.id, appContext.shared.activeRig, motion, pinLayer, ikMode, ikEditTool]);
|
|
908
602
|
const rangePinRebuildRef = useRef(null);
|
|
909
603
|
const rangePinSourceStamp = JSON.stringify([pinLayer.take, appContext.shared.activeChar.id]);
|
|
910
|
-
// A fit is invalidated by a different take, trim or preview, not by key
|
|
911
|
-
// writes: every layer write re-snapshots the take descriptor, so the decoded
|
|
912
|
-
// `motion` object is new after the fit's own commit even though the take is
|
|
913
|
-
// unchanged. Key the reset on the descriptor's content instead.
|
|
914
|
-
const platformFitPreview = motion === domain.motionFor(appContext.shared.activeChar.id) ? null : motion;
|
|
915
|
-
useEffect(() => {
|
|
916
|
-
platformFitJobRef.current += 1;
|
|
917
|
-
setPlatformFitRunning(false);
|
|
918
|
-
setPlatformFitLast(null);
|
|
919
|
-
platformFitProgressSet(0);
|
|
920
|
-
platformFitAppliedRef.current = null;
|
|
921
|
-
setPlatformFitApplied(false);
|
|
922
|
-
}, [rangePinSourceStamp, platformFitPreview, appContext.shared.activeRig]);
|
|
923
604
|
// Object-bound pins follow edited prop transforms and paths, as in the
|
|
924
605
|
// source branch. Rebuilding derived keys must not create an undo entry.
|
|
925
606
|
useEffect(() => {
|
|
@@ -941,33 +622,7 @@ export function useMotion(appContext) {
|
|
|
941
622
|
}, [rangePinSourceStamp, ikChains]);
|
|
942
623
|
|
|
943
624
|
|
|
944
|
-
const [motionBusy, setMotionBusy] = useState(false);
|
|
945
|
-
|
|
946
|
-
const [motionError, setMotionError] = useState("");
|
|
947
|
-
|
|
948
|
-
const [multiModelUrl, setMultiModelUrl] = useState("");
|
|
949
|
-
|
|
950
|
-
const [multiModelSource, setMultiModelSource] = useState(null);
|
|
951
|
-
|
|
952
|
-
const [multiModelStatus, setMultiModelStatus] = useState("idle");
|
|
953
|
-
|
|
954
|
-
// The ingest is a real download + decode, so it owns real progress and a
|
|
955
|
-
// real receipt: the footage numbers below come from the decoded media.
|
|
956
|
-
const [multiModelStage, setMultiModelStage] = useState("idle");
|
|
957
|
-
|
|
958
|
-
const [multiModelProgress, setMultiModelProgress] = useState(null);
|
|
959
|
-
|
|
960
|
-
const [multiModelFootage, setMultiModelFootage] = useState(null);
|
|
961
|
-
|
|
962
|
-
const [multiModelError, setMultiModelError] = useState("");
|
|
963
|
-
|
|
964
|
-
const [multiModelTake, setMultiModelTake] = useState(null);
|
|
965
|
-
|
|
966
|
-
const [multiModelExtract, setMultiModelExtract] = useState("idle");
|
|
967
625
|
|
|
968
|
-
const [multiModelExtractProgress, setMultiModelExtractProgress] = useState(null);
|
|
969
|
-
|
|
970
|
-
const [multiModelExtractError, setMultiModelExtractError] = useState("");
|
|
971
626
|
|
|
972
627
|
function advanceFrame(steps = 1) {
|
|
973
628
|
const count = Math.max(1, Math.floor(steps));
|
|
@@ -1001,192 +656,11 @@ export function useMotion(appContext) {
|
|
|
1001
656
|
setIkFocus(null);
|
|
1002
657
|
}
|
|
1003
658
|
|
|
1004
|
-
|
|
1005
|
-
* take, a browser-baked one — does the same two things: drop out of IK
|
|
1006
|
-
* EDIT mode (playback is the new context; the IK KEYS stay and keep
|
|
1007
|
-
* correcting the clip layer-style) and swap the pre-playback bone
|
|
1008
|
-
* baseline, so a re-load never snapshots mid-animation and clearing
|
|
1009
|
-
* always returns to the blocking pose. */
|
|
1010
|
-
function beginPlaybackOn(rig) {
|
|
1011
|
-
leaveIkMode();
|
|
1012
|
-
const previous = appContext.shared.restoreRef.current;
|
|
1013
|
-
appContext.shared.restoreRef.current = null;
|
|
1014
|
-
if (previous) restorePlaybackBones(previous.rig, previous.bones);
|
|
1015
|
-
appContext.shared.restoreRef.current = { rig, bones: snapshotPlaybackBones(rig) };
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
/** Land takes 1..N-1 on the rest of the cast. Each extra take is its OWN
|
|
1019
|
-
* layer: it goes to that entry's sessionMotion, NOT through the editing
|
|
1020
|
-
* buffer, which holds the active character's clip alone. Returns how many
|
|
1021
|
-
* performers actually landed. */
|
|
1022
|
-
const authoredSupportDescriptors = () => appContext.shared.sceneObjects.map((object) => ({
|
|
1023
|
-
x: object.x,
|
|
1024
|
-
z: object.z,
|
|
1025
|
-
rotDeg: object.rot ?? 0,
|
|
1026
|
-
supportY: (object.y ?? 0) + supportHeightForObject(object) * (object.scaleY ?? 1),
|
|
1027
|
-
topY: (object.y ?? 0) + supportHeightForObject(object) * (object.scaleY ?? 1),
|
|
1028
|
-
width: (object.footprint?.width ?? 0) * (object.scaleX ?? 1),
|
|
1029
|
-
depth: (object.footprint?.depth ?? 0) * (object.scaleZ ?? 1),
|
|
1030
|
-
}));
|
|
1031
|
-
|
|
1032
|
-
const applyAuthoredSupportRise = (clip, anchor, rotationDeg, worldScale = characterScaleFor(clip)) => applySupportRise(clip, authoredSupportDescriptors(), {
|
|
1033
|
-
subjectX: anchor.x,
|
|
1034
|
-
subjectY: anchor.y ?? 0,
|
|
1035
|
-
subjectZ: anchor.z,
|
|
1036
|
-
rotationDeg,
|
|
1037
|
-
worldScale,
|
|
1038
|
-
});
|
|
1039
|
-
|
|
1040
|
-
function commitLoadedTake(characterId, take, { recipe = null, job = null, promptClips = null, scale = characterScaleFor(take) } = {}) {
|
|
1041
|
-
const owned = appContext.storeDomain('motion');
|
|
1042
|
-
const previous = owned.layer(characterId);
|
|
1043
|
-
const imported = recipe ?? (job ? previous.takeRecipe : { seed: null, blocks: [{ prompt: take.prompt, duration: take.frames / take.fps }], lineEdits: [] });
|
|
1044
|
-
const versions = recipe || job ? previous.takeVersions : pushTakeVersion(previous.takeVersions, {
|
|
1045
|
-
motionUrl: take.url, recipe: imported, savedAt: Date.now(), label: ko('Loaded', '불러옴'),
|
|
1046
|
-
}, TAKE_VERSIONS_MAX);
|
|
1047
|
-
owned.replace(characterId, take, { recipe: imported, versions });
|
|
1048
|
-
const motionRef = { url: take.url, prompt: take.prompt, rotationDeg: take.rotationDeg, anchorX: take.anchorX, anchorZ: take.anchorZ,
|
|
1049
|
-
calibration: take.sceneCalibration, ...(take.trailEdits ? { trailEdits: take.trailEdits } : {}), ...(take.motionId ? { motionId: take.motionId } : {}) };
|
|
1050
|
-
owned.castWrite(rows => rows.map(row => row.id === characterId ? { ...row, scale, motionRef,
|
|
1051
|
-
layer: promptClips ? { ...row.layer, promptClips } : row.layer } : row));
|
|
1052
|
-
if (job) {
|
|
1053
|
-
commitTakeRecipe(job, take.url);
|
|
1054
|
-
if (job.hasBlockEdits) owned.writeLayer(characterId, { committedIkEdits: job.committedEditKeys });
|
|
1055
|
-
}
|
|
1056
|
-
owned.persistTake(take, motionRef);
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
// Decoded motion + the world anchor: frame 0 always starts at Subject 1.
|
|
1060
|
-
// Authored root destinations are generated by ARDY as sparse constraints,
|
|
1061
|
-
// so playback consumes the returned trajectory without coordinate warping.
|
|
1062
|
-
async function loadMotion(
|
|
1063
|
-
url,
|
|
1064
|
-
prompt,
|
|
1065
|
-
rotationDeg = appContext.shared.charA.rot,
|
|
1066
|
-
drop = null,
|
|
1067
|
-
targetCharacterId = appContext.shared.activeChar.id,
|
|
1068
|
-
targetPromptClips = null,
|
|
1069
|
-
// `preview: true` means "put this clip on screen, do not treat it as a
|
|
1070
|
-
// new take". The line-edit preview loop swaps the viewport several times
|
|
1071
|
-
// a minute, and every announcement this function normally makes — the
|
|
1072
|
-
// load toast, the auto-drop toast, clearing the IK keys, snapping the
|
|
1073
|
-
// playhead back to 0 — is an announcement about a take CHANGING. A
|
|
1074
|
-
// preview is the same take seen a second time, so it makes none of them.
|
|
1075
|
-
{ preview = false, calibration = null, tutorialEpoch = null, commandContext = null, recipe = null, job = null, personScaleFallback = null } = {},
|
|
1076
|
-
) {
|
|
1077
|
-
setMotionBusy(true);
|
|
1078
|
-
setMotionError("");
|
|
1079
|
-
try {
|
|
1080
|
-
refuseRigOnly(appContext.live.characters.find((entry) => entry.id === targetCharacterId), "take");
|
|
1081
|
-
// Inbound boundary: an ARDY take (20 fps) or a filmed one (30/60)
|
|
1082
|
-
// becomes a production-clock clip here, once, before anything on
|
|
1083
|
-
// the timeline counts its frames. Same-rate input rides through.
|
|
1084
|
-
// A drop is staging applied to the clip itself, so it happens at
|
|
1085
|
-
// the same boundary — trims and IK then see the dropped take.
|
|
1086
|
-
const retimed = retimeMotion(await loadMotionFromUrl(url), TIMELINE_FPS);
|
|
1087
|
-
if (retimed.sourceBytes) retimed.motionId = await sha256Hex(retimed.sourceBytes);
|
|
1088
|
-
if (tutorialEpoch !== null && tutorialEpoch !== appContext.shared.tutorialProjectEpochRef.current) return null;
|
|
1089
|
-
const normalizedCalibration = normalizeMotionCalibration(calibration);
|
|
1090
|
-
// Scene yaw/XY translation belong to the character's scene transform.
|
|
1091
|
-
// Applying them to both the arrays and the Character group would rotate
|
|
1092
|
-
// the trajectory twice and leave rotMats facing the old direction.
|
|
1093
|
-
const playbackCalibration = { ...normalizedCalibration, yawDeg: 0, offsetX: 0, offsetZ: 0 };
|
|
1094
|
-
// Scene calibration is optional metadata from the capture boundary. It
|
|
1095
|
-
// runs before support/fall staging so every downstream measurement uses
|
|
1096
|
-
// the same scene-space coordinates.
|
|
1097
|
-
const raw = applyMotionCalibration(retimed, playbackCalibration).motion;
|
|
1098
|
-
// Staging descriptors are authored in scene metres while decoded
|
|
1099
|
-
// trajectories are canonical-body units. Resolve stature before any
|
|
1100
|
-
// support or fall math so a 0.8x/1.2x performer still lands exactly on
|
|
1101
|
-
// the same authored surface after playback multiplies the clip.
|
|
1102
|
-
const motionScale = characterScaleFor(raw);
|
|
1103
|
-
const targetCharacter = appContext.live.characters.find((entry) => entry.id === targetCharacterId);
|
|
1104
|
-
if (!targetCharacter) throw new Error(`Motion target ${targetCharacterId} no longer exists.`);
|
|
1105
|
-
const sceneAnchorX = targetCharacter.x + normalizedCalibration.offsetX;
|
|
1106
|
-
const sceneAnchorZ = targetCharacter.z + normalizedCalibration.offsetZ;
|
|
1107
|
-
const sceneRotationDeg = rotationDeg + normalizedCalibration.yawDeg;
|
|
1108
|
-
const rig = appContext.shared.rigs[targetCharacter.id] ?? await appContext.shared.waitForRig(targetCharacter.id);
|
|
1109
|
-
if (tutorialEpoch !== null && tutorialEpoch !== appContext.shared.tutorialProjectEpochRef.current) return null;
|
|
1110
|
-
// No explicit drop staged: a character standing on a raised object
|
|
1111
|
-
// whose take walks off the edge falls on its own — ARDY motion is
|
|
1112
|
-
// flat-ground, so the stage supplies the gravity.
|
|
1113
|
-
const supports = authoredSupportDescriptors();
|
|
1114
|
-
// A support's top is scene data, never guessed from the motion. Apply
|
|
1115
|
-
// only when the clip shows an upward root trend entering its footprint;
|
|
1116
|
-
// ordinary deck walks remain byte-for-byte unchanged.
|
|
1117
|
-
const raised = drop ? raw : applySupportRise(raw, supports, {
|
|
1118
|
-
subjectX: sceneAnchorX,
|
|
1119
|
-
subjectY: targetCharacter.y ?? 0,
|
|
1120
|
-
subjectZ: sceneAnchorZ,
|
|
1121
|
-
rotationDeg: sceneRotationDeg,
|
|
1122
|
-
worldScale: motionScale,
|
|
1123
|
-
});
|
|
1124
|
-
const staging = drop ?? autoRoofDrop(
|
|
1125
|
-
raised,
|
|
1126
|
-
{ x: sceneAnchorX, z: sceneAnchorZ, y: targetCharacter.y ?? 0, rotationDeg: sceneRotationDeg },
|
|
1127
|
-
supports,
|
|
1128
|
-
{ worldScale: motionScale },
|
|
1129
|
-
);
|
|
1130
|
-
const decoded = drop ? applyRootDrop(raised, staging, { worldScale: motionScale }) : applyAutoFall(raised, staging, { worldScale: motionScale });
|
|
1131
|
-
if (!drop && staging && !preview) {
|
|
1132
|
-
appContext.notify((isKo, ko) => ko(
|
|
1133
|
-
`Auto drop staged: the take leaves its support at ${staging.fromS.toFixed(1)}s and falls ${staging.meters.toFixed(1)}m`,
|
|
1134
|
-
`자동 낙하 적용: ${staging.fromS.toFixed(1)}초에 지지면을 벗어나 ${staging.meters.toFixed(1)}m 낙하`,
|
|
1135
|
-
));
|
|
1136
|
-
}
|
|
1137
|
-
const targetStillExists = appContext.live.characters.some((entry) => entry.id === targetCharacter.id);
|
|
1138
|
-
if (!targetStillExists) throw new Error(`Motion target ${targetCharacterId} no longer exists.`);
|
|
1139
|
-
const apply = () => {
|
|
1140
|
-
const bufferOwnsTarget = targetCharacter.id === appContext.shared.loadedLayerCharRef.current;
|
|
1141
|
-
beginPlaybackOn(rig);
|
|
1142
|
-
// THE INVARIANT: the take's travel assumes the character is scaled.
|
|
1143
|
-
// Extraction divided the root translation by the filmed person's
|
|
1144
|
-
// stature, so the clip and that stature have to be applied together
|
|
1145
|
-
// or every stride overshoots by the same factor and the feet skate.
|
|
1146
|
-
// The scale rides INSIDE the npz, so this one line covers every path
|
|
1147
|
-
// that loads a motion; an ARDY-generated take stores none and is
|
|
1148
|
-
// canonical, 1.
|
|
1149
|
-
const measuredScale = characterScaleFor(decoded);
|
|
1150
|
-
const scale = measuredScale === 1 && Number.isFinite(personScaleFallback) ? characterScaleFor(null, personScaleFallback) : measuredScale;
|
|
1151
|
-
const loaded = {
|
|
1152
|
-
// Identity calibration retains the legacy frame-zero anchorX: targetCharacter.x
|
|
1153
|
-
// and anchorZ: targetCharacter.z contract; calibrated takes use the scene anchor.
|
|
1154
|
-
// Capture the exact prompt this motion was generated from; the
|
|
1155
|
-
// timeline keeps showing it even if the input field is edited
|
|
1156
|
-
// afterwards.
|
|
1157
|
-
prompt: typeof prompt === "string" ? prompt : "",
|
|
1158
|
-
...decoded,
|
|
1159
|
-
url,
|
|
1160
|
-
anchorX: sceneAnchorX,
|
|
1161
|
-
anchorZ: sceneAnchorZ,
|
|
1162
|
-
anchorFrame: 0,
|
|
1163
|
-
rotationDeg: sceneRotationDeg,
|
|
1164
|
-
sceneCalibration: normalizedCalibration,
|
|
1165
|
-
editSegments: createMotionEdit(decoded.frames),
|
|
1166
|
-
};
|
|
1167
|
-
const owned = appContext.storeDomain('motion');
|
|
1168
|
-
if (preview) owned.preview(targetCharacter.id, url === owned.motionFor(targetCharacter.id)?.url ? null : loaded);
|
|
1169
|
-
else {
|
|
1170
|
-
commitLoadedTake(targetCharacter.id, loaded, { recipe, job, promptClips: targetPromptClips, scale });
|
|
1171
|
-
if (bufferOwnsTarget) { appContext.shared.setTlFrame(0); appContext.shared.setTlPlaying(false); }
|
|
1172
|
-
setMotionError('');
|
|
1173
|
-
}
|
|
1174
|
-
return scale;
|
|
1175
|
-
};
|
|
1176
|
-
return preview ? apply() : commandContext ? commandContext.commit(apply)
|
|
1177
|
-
: appContext.storeDomain('motion').runPrepared(targetCharacterId, apply);
|
|
1178
|
-
} catch (err) {
|
|
1179
|
-
if (tutorialEpoch !== null && tutorialEpoch !== appContext.shared.tutorialProjectEpochRef.current) return null;
|
|
1180
|
-
setMotionError(err?.message || String(err));
|
|
1181
|
-
throw err;
|
|
1182
|
-
} finally {
|
|
1183
|
-
setMotionBusy(false);
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
659
|
+
|
|
1186
660
|
|
|
1187
|
-
/** Drop the ACTIVE character's take
|
|
1188
|
-
*
|
|
1189
|
-
*
|
|
661
|
+
/** Drop the ACTIVE character's take and its IK corrections. One Ctrl+Z
|
|
662
|
+
* entry brings both back; nothing to clear records nothing, so the shortcut
|
|
663
|
+
* never becomes a dead press.
|
|
1190
664
|
*
|
|
1191
665
|
* The pose flows (studio Apply/Reset, pose tiles, photo pose) call this
|
|
1192
666
|
* first and then write the pose: the snapshot taken here predates both, so
|
|
@@ -1195,40 +669,8 @@ export function useMotion(appContext) {
|
|
|
1195
669
|
return appContext.storeDomain('motion').clear(appContext.shared.activeChar.id);
|
|
1196
670
|
}
|
|
1197
671
|
|
|
1198
|
-
/** Cut the ACTIVE character's take to [start, end] of the CURRENT view.
|
|
1199
|
-
* Offsets compose, so a second cut still slices the original take; a cut
|
|
1200
|
-
* take drops its bridge url — its frames no longer match the source npz,
|
|
1201
|
-
* and IK-edit regeneration must not pretend they do. Per-character layers
|
|
1202
|
-
* mean the cut lands on this layer only; nobody else's clip moves. */
|
|
1203
|
-
function applyMotionTrim(start, end) {
|
|
1204
|
-
return appContext.storeDomain('motion').edit('motion.trim', { characterId: appContext.shared.activeChar.id, start, end });
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1207
|
-
function resetMotionTrim() {
|
|
1208
|
-
return appContext.storeDomain('motion').run('motion.resetTrim', { characterId: appContext.shared.activeChar.id });
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
function cutMotionAtPlayhead() {
|
|
1212
|
-
return appContext.storeDomain('motion').run('motion.cut', { characterId: appContext.shared.activeChar.id, frame: appContext.shared.tlFrame });
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
function changeMotionSegmentSpeed(id, speed) {
|
|
1216
|
-
return appContext.storeDomain('motion').run('motion.setSegmentSpeed', { characterId: appContext.shared.activeChar.id, id, speed });
|
|
1217
|
-
}
|
|
1218
|
-
|
|
1219
|
-
/** Drop one Full-Body segment from the take. The removal composes like a
|
|
1220
|
-
* trim: the source frames stay untouched, so the trim-reset path (right-click
|
|
1221
|
-
* an outer handle) still restores the whole take. */
|
|
1222
|
-
function removeMotionSegmentById(id) {
|
|
1223
|
-
return appContext.storeDomain('motion').run('motion.removeSegment', { characterId: appContext.shared.activeChar.id, id });
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
672
|
// Everyone EXCEPT the active character, posed at an absolute frame from
|
|
1227
|
-
// their own session clips and their STORED IK corrections (#77).
|
|
1228
|
-
// out of the effect below because the whole-clip Fix Collisions pass needs
|
|
1229
|
-
// the same thing: the other bodies are static blockers, and a blocker
|
|
1230
|
-
// sampled from a rig still standing at the playhead would block the wrong
|
|
1231
|
-
// volume on every other frame of the walk.
|
|
673
|
+
// their own session clips and their STORED IK corrections (#77).
|
|
1232
674
|
const poseOtherCastMembers = (frame) => {
|
|
1233
675
|
for (const entry of appContext.shared.characters) {
|
|
1234
676
|
if (entry.id === appContext.shared.activeChar.id || isProxyFigure(entry)) continue;
|
|
@@ -1241,9 +683,8 @@ export function useMotion(appContext) {
|
|
|
1241
683
|
// Authoring modes are mutually exclusive: IK mode swaps the main pane to
|
|
1242
684
|
// the poser camera, which would silently invalidate any pulled path
|
|
1243
685
|
// anyway. Leaving the line mode explicitly says so instead.
|
|
1244
|
-
if (next && appContext.shared.lineEditMode) appContext.shared.exitLineEditMode();
|
|
1245
686
|
if (next) {
|
|
1246
|
-
// Always enter on the safe, detailed IK tool.
|
|
687
|
+
// Always enter on the safe, detailed IK tool. Range pins are an
|
|
1247
688
|
// explicit second tool and must never leave the regular handles
|
|
1248
689
|
// locked when the user re-enters IK mode.
|
|
1249
690
|
setIkEditTool("ik");
|
|
@@ -1291,9 +732,11 @@ export function useMotion(appContext) {
|
|
|
1291
732
|
// Exit: the keyed pose stays — the evaluate effect re-applies the
|
|
1292
733
|
// current frame's keyed rotations the moment ikMode flips, so nothing
|
|
1293
734
|
// the user authored is lost by toggling. Untracked/unkeyed parts keep
|
|
1294
|
-
// their current (FK) pose.
|
|
735
|
+
// their current (FK) pose. A correction still in preview is applied, as
|
|
736
|
+
// Apply range would: leaving the mode (the Stage tab, a mode key) is not
|
|
737
|
+
// a Cancel, and dropping it threw away what the user just posed.
|
|
738
|
+
if (!applyPendingIkEdit()) appContext.notify(ko("Pose mode off — keyed poses keep playing", "포즈 모드 꺼짐 — 키로 찍은 포즈는 계속 재생됩니다"));
|
|
1295
739
|
leaveIkMode();
|
|
1296
|
-
appContext.notify(ko("Pose mode off — keyed poses keep playing", "포즈 모드 꺼짐 — 키로 찍은 포즈는 계속 재생됩니다"));
|
|
1297
740
|
}
|
|
1298
741
|
|
|
1299
742
|
// Drag solve, routed by handle kind: chain targets solve the two-bone
|
|
@@ -1448,283 +891,6 @@ export function useMotion(appContext) {
|
|
|
1448
891
|
appContext.notify(isKo ? `${appContext.shared.tlFrame}프레임에 전신 IK 키를 추가했어요` : `Full-body IK key at frame ${appContext.shared.tlFrame}`);
|
|
1449
892
|
}
|
|
1450
893
|
|
|
1451
|
-
// Self-collision cleanup: push interpenetrating body parts apart with the
|
|
1452
|
-
// IK solver, then bake the fix as an ordinary IK correction key so it
|
|
1453
|
-
// survives scrubs, undo, and blends back into the clip outside its range.
|
|
1454
|
-
//
|
|
1455
|
-
// The result has THREE outcomes, not two, and each gets its own sentence.
|
|
1456
|
-
// "supported: false" means the rig has no capsule proxies to build at all
|
|
1457
|
-
// — a non-Mixamo skeleton — and reporting that as "no collisions" would
|
|
1458
|
-
// be a lie the user cannot act on: they would keep clicking a button that
|
|
1459
|
-
// silently does nothing. The buttons below are disabled in that case, so
|
|
1460
|
-
// this branch is the belt to that suspenders (a rig can be swapped under
|
|
1461
|
-
// a stale render).
|
|
1462
|
-
/**
|
|
1463
|
-
* Everything OUTSIDE the active character that a limb has to stay out of:
|
|
1464
|
-
* the other cast members' bodies (capsules, built from the rigs AS THEY ARE
|
|
1465
|
-
* POSED at the moment of the call) and every scene object (an upright box
|
|
1466
|
-
* on its footprint). Ids are namespaced `char:<id>:<capsule>` / `obj:<id>`,
|
|
1467
|
-
* so a penetration label names the thing that was hit.
|
|
1468
|
-
*
|
|
1469
|
-
* `frame` re-samples travel paths — a prop walking a route stands somewhere
|
|
1470
|
-
* else on every frame — but the CAST is read live from the scene graph, so
|
|
1471
|
-
* a caller walking a clip must pose the others at that frame first (see
|
|
1472
|
-
* runFixCollisionsRange's blockersAt).
|
|
1473
|
-
*/
|
|
1474
|
-
const externalBlockers = (frame = appContext.shared.tlFrame) => collisionBlockers({
|
|
1475
|
-
rigs: appContext.shared.rigs,
|
|
1476
|
-
activeId: appContext.shared.activeChar.id,
|
|
1477
|
-
// The CAST is the authority on who is on stage, not the rig map: undo can
|
|
1478
|
-
// put the cast list back to one subject while the rig mounted for the
|
|
1479
|
-
// removed one is still in `rigs`, and a ghost body would go on blocking
|
|
1480
|
-
// limbs that pass through empty space.
|
|
1481
|
-
characterIds: appContext.shared.characters,
|
|
1482
|
-
sceneObjects: appContext.shared.sceneObjects,
|
|
1483
|
-
library: OBJECT_LIBRARY,
|
|
1484
|
-
frame,
|
|
1485
|
-
take: { frameCount: appContext.shared.tlFrameCount, fps: appContext.shared.tlFps },
|
|
1486
|
-
});
|
|
1487
|
-
|
|
1488
|
-
function runFixCollisions() {
|
|
1489
|
-
if (appContext.storeDomain('motion')) return appContext.storeDomain('motion').run('motion.fixCollisions', { characterId: appContext.shared.activeChar.id, scope: 'frame' });
|
|
1490
|
-
if (!ikChains || !appContext.shared.activeRig) return;
|
|
1491
|
-
// A rig swap leaves one render where ikChains still describes the old
|
|
1492
|
-
// skeleton; solving the new rig with them would push the wrong bones.
|
|
1493
|
-
if (appContext.shared.ikStateRef.current.rig !== appContext.shared.activeRig) return;
|
|
1494
|
-
// The set as it stands right now: the other bodies at this frame's pose
|
|
1495
|
-
// and the props at this frame's placement.
|
|
1496
|
-
const result = fixCollisions(appContext.shared.activeRig, ikChains, { ikState: appContext.shared.ikStateRef.current, fkJoints: ikFkJoints, blockers: externalBlockers(appContext.shared.tlFrame) });
|
|
1497
|
-
if (!result.supported) {
|
|
1498
|
-
appContext.notify(ko("This rig doesn't support collision cleanup", "이 리그는 신체 관통 정리를 지원하지 않아요"));
|
|
1499
|
-
return;
|
|
1500
|
-
}
|
|
1501
|
-
if (!result.changed) {
|
|
1502
|
-
appContext.notify(ko("No body collisions at this frame", "이 프레임에는 신체 관통이 없어요"));
|
|
1503
|
-
return;
|
|
1504
|
-
}
|
|
1505
|
-
editIkKeys(() => ikBakeKeyframe(ikChains, appContext.shared.ikStateRef.current, appContext.shared.tlFrame, ikFkJoints, result.touched, null, result.baseQuats));
|
|
1506
|
-
setIkTick((n) => n + 1);
|
|
1507
|
-
appContext.notify(result.residual > 1e-4
|
|
1508
|
-
? ko(`Collisions reduced (residual ${(result.residual * 100).toFixed(1)} cm)`, `관통을 줄였어요 (잔여 ${(result.residual * 100).toFixed(1)} cm)`)
|
|
1509
|
-
: ko(`Collisions fixed at frame ${appContext.shared.tlFrame}`, `프레임 ${appContext.shared.tlFrame}의 관통을 정리했어요`));
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
// Whole-clip variant: walk the motion frame by frame, clean each pose and
|
|
1513
|
-
// key ONLY the frames that changed, so a clean clip stays keyless.
|
|
1514
|
-
function runFixCollisionsRange() {
|
|
1515
|
-
if (appContext.storeDomain('motion')) return appContext.storeDomain('motion').run('motion.fixCollisions', { characterId: appContext.shared.activeChar.id, scope: 'clip' });
|
|
1516
|
-
if (!ikChains || !appContext.shared.activeRig || !motion) return;
|
|
1517
|
-
if (appContext.shared.ikStateRef.current.rig !== appContext.shared.activeRig) return;
|
|
1518
|
-
// Screened before the undo entry: an unsupported rig would record an
|
|
1519
|
-
// undo step for a walk that keys nothing, leaving a no-op in history.
|
|
1520
|
-
if (!appContext.shared.collisionCleanupSupported) {
|
|
1521
|
-
appContext.notify(ko("This rig doesn't support collision cleanup", "이 리그는 신체 관통 정리를 지원하지 않아요"));
|
|
1522
|
-
return;
|
|
1523
|
-
}
|
|
1524
|
-
const currentFrame = appContext.shared.tlFrame;
|
|
1525
|
-
const applyFrame = (frame) => {
|
|
1526
|
-
applyMotionFrame(appContext.shared.activeRig, motion, frame);
|
|
1527
|
-
ikEvaluate(ikChains, appContext.shared.ikStateRef.current, frame, ikFkJoints, IK_CORRECTION_BLEND_FRAMES);
|
|
1528
|
-
};
|
|
1529
|
-
// The other bodies move too. blockersAt runs AFTER applyFrame(frame), so
|
|
1530
|
-
// it poses the rest of the cast at that same frame — their own clips and
|
|
1531
|
-
// their own stored IK layers, the very pass the viewport renders with —
|
|
1532
|
-
// and only then samples their capsules. Without this the blockers would
|
|
1533
|
-
// describe everyone frozen at the playhead, which is a wrong obstacle on
|
|
1534
|
-
// every frame but one.
|
|
1535
|
-
const blockersAt = (frame) => {
|
|
1536
|
-
poseOtherCastMembers(frame);
|
|
1537
|
-
return externalBlockers(frame);
|
|
1538
|
-
};
|
|
1539
|
-
let keyed = [];
|
|
1540
|
-
let unresolved = [];
|
|
1541
|
-
try {
|
|
1542
|
-
const walked = editIkKeys(() => fixCollisionsRange({
|
|
1543
|
-
rig: appContext.shared.activeRig,
|
|
1544
|
-
chains: ikChains,
|
|
1545
|
-
ikState: appContext.shared.ikStateRef.current,
|
|
1546
|
-
fkJoints: ikFkJoints,
|
|
1547
|
-
startFrame: 0,
|
|
1548
|
-
endFrame: motion.frames - 1,
|
|
1549
|
-
applyFrame,
|
|
1550
|
-
blockersAt,
|
|
1551
|
-
}));
|
|
1552
|
-
// The frames the walk keyed. `unresolved` — the frames whose residual
|
|
1553
|
-
// survived every pass — is ADDITIVE: read it defensively off either
|
|
1554
|
-
// shape so this keeps working before and after the driver grows it.
|
|
1555
|
-
keyed = Array.isArray(walked) ? walked : walked?.keyed ?? [];
|
|
1556
|
-
unresolved = (Array.isArray(walked) ? walked.unresolved : walked?.unresolved) ?? [];
|
|
1557
|
-
} finally {
|
|
1558
|
-
// The restore is the pass's CLEANUP, not its epilogue: a throw mid-walk
|
|
1559
|
-
// would otherwise leave the active rig and the rest of the cast frozen
|
|
1560
|
-
// at whatever frame it died on, which is a wrong-looking set the user
|
|
1561
|
-
// cannot scrub out of without touching the playhead.
|
|
1562
|
-
applyFrame(currentFrame);
|
|
1563
|
-
poseOtherCastMembers(currentFrame);
|
|
1564
|
-
setIkTick((n) => n + 1);
|
|
1565
|
-
}
|
|
1566
|
-
// Residual is worth saying out loud: a limb pinned between two blockers
|
|
1567
|
-
// (another body and a prop, say) can come out of the walk still touching,
|
|
1568
|
-
// and silence would read as "all clean".
|
|
1569
|
-
const stillPenetrating = unresolved.length
|
|
1570
|
-
? ko(` · ${unresolved.length} frame(s) still penetrate`, ` · ${unresolved.length}개 프레임은 남아 있어요`)
|
|
1571
|
-
: "";
|
|
1572
|
-
// "No body collisions" must never share a sentence with "still
|
|
1573
|
-
// penetrate": a converged pass over an unfixable clip has nothing more
|
|
1574
|
-
// to do, which is a different statement from the clip being clean.
|
|
1575
|
-
appContext.notify((keyed.length
|
|
1576
|
-
? ko(`Fixed collisions on ${keyed.length} frame(s)`, `${keyed.length}개 프레임의 관통을 정리했어요`)
|
|
1577
|
-
: unresolved.length
|
|
1578
|
-
? ko("Nothing more to fix", "더 고칠 수 있는 게 없어요")
|
|
1579
|
-
: ko("No body collisions in the clip", "클립에 신체 관통이 없어요")) + stillPenetrating);
|
|
1580
|
-
}
|
|
1581
|
-
|
|
1582
|
-
function changePhysicsOptions(next) {
|
|
1583
|
-
setPhysicsOptions(next); setPhysicsPreview(null); setIkTick((n) => n + 1);
|
|
1584
|
-
}
|
|
1585
|
-
|
|
1586
|
-
function showPhysicsPreview(show) { setPhysicsShow(show); setIkTick((n) => n + 1); }
|
|
1587
|
-
|
|
1588
|
-
function cancelPhysicsPreview() { setPhysicsPreview(null); setIkTick((n) => n + 1); }
|
|
1589
|
-
|
|
1590
|
-
function applyPhysicsPreview() {
|
|
1591
|
-
if (appContext.storeDomain('motion')) return appContext.storeDomain('motion').run('motion.applyPhysics', { characterId: appContext.shared.activeChar.id });
|
|
1592
|
-
if (!physicsPreview || physicsPreview.sourceStamp !== physicsKeyStamp(appContext.shared.ikStateRef.current.keys)) return;
|
|
1593
|
-
editIkKeys(() => { appContext.shared.ikStateRef.current.keys = copyPhysicsKeys(physicsPreview.candidate.keys); });
|
|
1594
|
-
appContext.shared.ikStateRef.current.tracked = new Set(physicsPreview.candidate.tracked);
|
|
1595
|
-
appContext.shared.autoPhysicsRunRef.current = { motion, rig: appContext.shared.activeRig, stamp: physicsKeyStamp(appContext.shared.ikStateRef.current.keys) };
|
|
1596
|
-
setPhysicsPreview(null); setIkTick((n) => n + 1);
|
|
1597
|
-
appContext.notify(ko("AutoPhysics applied · Undo restores the original", "오토피직스를 적용했어요 · 실행 취소로 원본 복구"));
|
|
1598
|
-
}
|
|
1599
|
-
|
|
1600
|
-
async function runAutoPhysics() {
|
|
1601
|
-
if (appContext.storeDomain('motion')) return appContext.storeDomain('motion').run('motion.autoPhysics', { characterId: appContext.shared.activeChar.id, ...physicsOptions, apply: false });
|
|
1602
|
-
if (autoPhysicsRunning || !ikChains || !appContext.shared.activeRig || !motion || appContext.shared.ikStateRef.current.rig !== appContext.shared.activeRig) return null;
|
|
1603
|
-
const previous = appContext.shared.autoPhysicsRunRef.current;
|
|
1604
|
-
if (previous?.motion === motion && previous.rig === appContext.shared.activeRig && previous.stamp === physicsKeyStamp(appContext.shared.ikStateRef.current.keys)) {
|
|
1605
|
-
appContext.notify(ko("Already applied. Undo to review this correction again.", "이미 적용했어요. 실행 취소 후 다시 비교할 수 있어요.")); return null;
|
|
1606
|
-
}
|
|
1607
|
-
const job = ++appContext.shared.physicsJobRef.current, frame = appContext.shared.tlFrame;
|
|
1608
|
-
const sourceKeys = copyPhysicsKeys(appContext.shared.ikStateRef.current.keys), stamp = physicsKeyStamp(sourceKeys);
|
|
1609
|
-
let lastYieldAt = Date.now(), yieldWaitMs = 0, yieldCount = 0;
|
|
1610
|
-
const restore = () => { appContext.shared.poseMemberAtFrame(appContext.shared.activeRig, motion, appContext.shared.ikStateRef.current, frame, IK_CORRECTION_BLEND_FRAMES); };
|
|
1611
|
-
appContext.shared.setTlPlaying(false); setAutoPhysicsRunning(true); setPhysicsProgress(0); setPhysicsPreview(null);
|
|
1612
|
-
try {
|
|
1613
|
-
const result = await reviewAutoPhysics({ rig: appContext.shared.activeRig, motion, chains: ikChains, fkJoints: ikFkJoints, sourceKeys,
|
|
1614
|
-
applyRaw: (f) => appContext.shared.poseMemberAtFrame(appContext.shared.activeRig, motion, null, f), sceneObjects: appContext.shared.sceneObjects, ...physicsOptions,
|
|
1615
|
-
cache: appContext.shared.physicsSourceCacheRef.current,
|
|
1616
|
-
onProgress: setPhysicsProgress,
|
|
1617
|
-
yieldFrame: async () => {
|
|
1618
|
-
if (appContext.shared.physicsJobRef.current !== job) throw new Error("Analysis cancelled after changing the character or motion");
|
|
1619
|
-
// A batch is a cancellation checkpoint, not necessarily a paint/
|
|
1620
|
-
// event-loop boundary. Yield on a time budget, not every 12 frames.
|
|
1621
|
-
if (Date.now() - lastYieldAt < 16) return;
|
|
1622
|
-
restore();
|
|
1623
|
-
const queuedAt = Date.now();
|
|
1624
|
-
// Yield CPU work without waiting for a paint. requestAnimationFrame
|
|
1625
|
-
// can be throttled/paused in an occluded tab, stretching a seconds-
|
|
1626
|
-
// long solve into minutes. MessageChannel also lets input run.
|
|
1627
|
-
await new Promise((resolve) => {
|
|
1628
|
-
const channel = new MessageChannel();
|
|
1629
|
-
channel.port1.onmessage = () => { channel.port1.close(); channel.port2.close(); resolve(); };
|
|
1630
|
-
channel.port2.postMessage(0);
|
|
1631
|
-
});
|
|
1632
|
-
lastYieldAt = Date.now(); yieldWaitMs += lastYieldAt - queuedAt; yieldCount += 1;
|
|
1633
|
-
if (appContext.shared.physicsJobRef.current !== job) throw new Error("Analysis cancelled after changing the character or motion");
|
|
1634
|
-
},
|
|
1635
|
-
});
|
|
1636
|
-
Object.assign(result.performance, { yieldWaitMs, yieldCount });
|
|
1637
|
-
if (appContext.shared.physicsJobRef.current !== job || physicsKeyStamp(appContext.shared.ikStateRef.current.keys) !== stamp) return null;
|
|
1638
|
-
setPhysicsPreview(result); setPhysicsShow(true);
|
|
1639
|
-
return { before: result.before, after: result.after, warnings: result.warnings, unresolved: result.unresolved, contacts: result.contacts.spans };
|
|
1640
|
-
} catch (error) {
|
|
1641
|
-
if (appContext.shared.physicsJobRef.current === job) appContext.notify(ko(`AutoPhysics: ${error.message}`, `오토피직스: ${error.message}`));
|
|
1642
|
-
return null;
|
|
1643
|
-
} finally {
|
|
1644
|
-
if (appContext.shared.physicsJobRef.current === job) { restore(); setAutoPhysicsRunning(false); setIkTick((n) => n + 1); }
|
|
1645
|
-
}
|
|
1646
|
-
}
|
|
1647
|
-
|
|
1648
|
-
async function platformFitRun(id, context, { removal = false, preview = false } = {}) {
|
|
1649
|
-
const rig = appContext.shared.rigs[id], resolved = rig && resolveIkRig(rig), take = domain.motionFor(id);
|
|
1650
|
-
if (!resolved || !take) throw new StudioProtocolError('TARGET_NOT_READY', 'Load a take and character rig first.');
|
|
1651
|
-
if (platformFitRunning) throw new StudioProtocolError('TARGET_BUSY', 'A platform fit is already running.');
|
|
1652
|
-
const job = ++platformFitJobRef.current;
|
|
1653
|
-
const sourceKeys = copyPhysicsKeys(decodeMotionKeys(domain.layer(id).ikKeys));
|
|
1654
|
-
const sourceTracked = new Set([...sourceKeys.values()].flatMap(entry => [...entry.keys()]));
|
|
1655
|
-
const stamp = physicsKeyStamp(sourceKeys);
|
|
1656
|
-
const prior = platformFitAppliedRef.current;
|
|
1657
|
-
const stacked = prior && prior.id === id && prior.stamp === stamp ? prior : null;
|
|
1658
|
-
const frame = appContext.shared.tlFrame;
|
|
1659
|
-
const restore = () => appContext.shared.poseMemberAtFrame(rig, take, appContext.shared.ikStatesRef.current.get(id), frame, 6);
|
|
1660
|
-
let lastYield = performance.now();
|
|
1661
|
-
const check = () => { context?.check?.(); if (platformFitJobRef.current !== job) throw new StudioProtocolError('STALE_TARGET', 'Platform fit was superseded.'); };
|
|
1662
|
-
setPlatformFitRunning(true); platformFitProgressSet(0); setPlatformFitLast(null);
|
|
1663
|
-
try {
|
|
1664
|
-
const result = await fitPlatforms({
|
|
1665
|
-
rig: rig, motion: take, chains: resolved.chains, fkJoints: resolved.fkJoints, sourceKeys,
|
|
1666
|
-
applyRaw: at => appContext.shared.poseMemberAtFrame(rig, take, null, at),
|
|
1667
|
-
sceneObjects: removal ? [] : (appContext.ports.read?.().objects ?? appContext.shared.sceneObjects),
|
|
1668
|
-
onProgress: platformFitProgressSet,
|
|
1669
|
-
yieldFrame: async () => {
|
|
1670
|
-
check(); if (performance.now() - lastYield < 16) return;
|
|
1671
|
-
restore();
|
|
1672
|
-
await new Promise(resolve => { const channel = new MessageChannel(); channel.port1.onmessage = () => { channel.port1.close(); channel.port2.close(); resolve(); }; channel.port2.postMessage(0); });
|
|
1673
|
-
lastYield = performance.now(); check();
|
|
1674
|
-
},
|
|
1675
|
-
});
|
|
1676
|
-
check();
|
|
1677
|
-
const last = { summary: result.summary, steps: result.steps, changedFrames: result.changedFrames };
|
|
1678
|
-
if (removal) {
|
|
1679
|
-
setPlatformFitLast(last);
|
|
1680
|
-
if (result.changedFrames.length) context.commit(() => domain.setKeys(id, result.candidate.keys));
|
|
1681
|
-
platformFitAppliedRef.current = null; setPlatformFitApplied(false); setPlatformFitLast(null);
|
|
1682
|
-
return { ...last, removed: result.changedFrames.length > 0 };
|
|
1683
|
-
}
|
|
1684
|
-
if (!preview) {
|
|
1685
|
-
if (result.changedFrames.length) context.commit(() => domain.setKeys(id, result.candidate.keys));
|
|
1686
|
-
platformFitAppliedRef.current = { id, before: stacked?.before ?? sourceKeys, tracked: stacked?.tracked ?? sourceTracked, stamp: physicsKeyStamp(decodeMotionKeys(domain.layer(id).ikKeys)) };
|
|
1687
|
-
setPlatformFitLast(last); setPlatformFitApplied(true);
|
|
1688
|
-
return last;
|
|
1689
|
-
}
|
|
1690
|
-
platformFitAppliedRef.current = { preview: { id, sourceStamp: stamp, sourceKeys, sourceTracked, candidate: result.candidate, last }, before: stacked?.before ?? sourceKeys, tracked: stacked?.tracked ?? sourceTracked, stamp };
|
|
1691
|
-
setPlatformFitLast({ ...last, candidate: result.candidate });
|
|
1692
|
-
setPlatformFitApplied(false);
|
|
1693
|
-
return last;
|
|
1694
|
-
} finally {
|
|
1695
|
-
if (platformFitJobRef.current === job) { restore(); setPlatformFitRunning(false); }
|
|
1696
|
-
}
|
|
1697
|
-
}
|
|
1698
|
-
|
|
1699
|
-
function platformFitApply(id, context) {
|
|
1700
|
-
const preview = platformFitAppliedRef.current?.preview;
|
|
1701
|
-
if (!preview || preview.id !== id || platformFitRunning) return { applied: false };
|
|
1702
|
-
const current = physicsKeyStamp(decodeMotionKeys(domain.layer(id).ikKeys));
|
|
1703
|
-
if (current !== preview.sourceStamp) throw new StudioProtocolError('STALE_TARGET', 'The platform preview is out of date. Analyse this take again before applying.');
|
|
1704
|
-
context.commit(() => domain.setKeys(id, copyPhysicsKeys(preview.candidate.keys)));
|
|
1705
|
-
platformFitAppliedRef.current = { id, before: preview.sourceKeys, tracked: preview.sourceTracked, stamp: physicsKeyStamp(decodeMotionKeys(domain.layer(id).ikKeys)) };
|
|
1706
|
-
setPlatformFitApplied(true);
|
|
1707
|
-
setPlatformFitLast({ ...preview.last, candidate: undefined });
|
|
1708
|
-
return { applied: true, changedFrames: preview.last.changedFrames };
|
|
1709
|
-
}
|
|
1710
|
-
|
|
1711
|
-
function platformFitRemove(id, context, { apply = false } = {}) {
|
|
1712
|
-
if (apply) return platformFitApply(id, context);
|
|
1713
|
-
const applied = platformFitAppliedRef.current;
|
|
1714
|
-
if (applied?.preview?.id === id) {
|
|
1715
|
-
platformFitAppliedRef.current = null; setPlatformFitLast(null); setPlatformFitApplied(false);
|
|
1716
|
-
return { removed: false, cancelled: true };
|
|
1717
|
-
}
|
|
1718
|
-
if (!applied || applied.id !== id || platformFitRunning) return { removed: false };
|
|
1719
|
-
const current = physicsKeyStamp(decodeMotionKeys(domain.layer(id).ikKeys));
|
|
1720
|
-
if (current === applied.stamp) {
|
|
1721
|
-
context.commit(() => domain.setKeys(id, copyPhysicsKeys(applied.before)));
|
|
1722
|
-
platformFitAppliedRef.current = null; setPlatformFitApplied(false); setPlatformFitLast(null);
|
|
1723
|
-
return { removed: true };
|
|
1724
|
-
}
|
|
1725
|
-
return platformFitRun(id, context, { removal: true });
|
|
1726
|
-
}
|
|
1727
|
-
|
|
1728
894
|
function ikDeleteKeyframe(frame) {
|
|
1729
895
|
if (!appContext.shared.ikStateRef.current.keys.has(frame)) return;
|
|
1730
896
|
appContext.shared.runStudioAction("character.removeIkKey", { characterId: appContext.shared.activeChar.id, frame });
|
|
@@ -1765,966 +931,22 @@ export function useMotion(appContext) {
|
|
|
1765
931
|
return true;
|
|
1766
932
|
}
|
|
1767
933
|
|
|
1768
|
-
|
|
1769
|
-
setBridgeChecking(true);
|
|
1770
|
-
return appContext.shared.bridgeRefreshRef.current().finally(() => setBridgeChecking(false));
|
|
1771
|
-
}
|
|
1772
|
-
|
|
1773
|
-
function requestMotionGeneration(surface, inputMode, body = {}) {
|
|
1774
|
-
const request = startMotionRequest({ surface, input_mode: inputMode });
|
|
1775
|
-
const options = { body, lineEditSupported: lineEditBackend };
|
|
1776
|
-
// Record known readiness refusals even if existing input validation returns
|
|
1777
|
-
// early. A pass waits for the fully packaged payload at the queue boundary.
|
|
1778
|
-
// The queue independently checks readiness; telemetry failure cannot
|
|
1779
|
-
// enable or disable generation.
|
|
1780
|
-
if (motionPreflightReason(bridge, options)) {
|
|
1781
|
-
request.preflight(bridge, options);
|
|
1782
|
-
const readiness = motionReadiness(bridge, options);
|
|
1783
|
-
appContext.notify(motionReadinessMessage(readiness), ({ loading: "Checking motion generation…", ready: "Ready for this motion request", not_configured: "No motion backend configured", unsupported_route: "This route cannot run the selected motion request" })[readiness] ?? "The motion backend is unavailable");
|
|
1784
|
-
}
|
|
1785
|
-
return request;
|
|
1786
|
-
}
|
|
1787
|
-
|
|
1788
|
-
// Optional native-ARDY seed. Empty = request omits seed; the raw string
|
|
1789
|
-
// is kept as typed (trimmed only). runArdy validates the bridge contract
|
|
1790
|
-
// (integer in 0..2**31-1) right before the request and toasts on a
|
|
1791
|
-
// violation, so an invalid seed can never reach the bridge silently.
|
|
1792
|
-
function changeArdySeed(value) {
|
|
1793
|
-
setArdySeed(value.trim());
|
|
1794
|
-
}
|
|
1795
|
-
|
|
1796
|
-
/** THE SEED RULE (contract C9), enforced in ONE place so no take-creating
|
|
1797
|
-
* call site can forget it: an empty field is rolled here, a typed one is
|
|
1798
|
-
* kept exactly as typed, and either way a concrete integer comes back to be
|
|
1799
|
-
* both SENT and RECORDED. A take whose seed was never written down cannot
|
|
1800
|
-
* be rebuilt from its recipe, which is the one promise the whole recipe
|
|
1801
|
-
* model rests on. Returns null after toasting when the typed value violates
|
|
1802
|
-
* the bridge contract — the caller must then abandon the request. */
|
|
1803
|
-
function takeSeed() {
|
|
1804
|
-
try {
|
|
1805
|
-
return resolveSeed(ardySeed, ARDY_SEED_MAX);
|
|
1806
|
-
} catch {
|
|
1807
|
-
appContext.notify((isKo, ko) => isKo ? `Seed는 0..${ARDY_SEED_MAX} 범위의 정수여야 해요. 비워 두면 자동으로 선택됩니다` : `Seed must be an integer in 0..${ARDY_SEED_MAX} — clear it to let the box pick one`);
|
|
1808
|
-
return null;
|
|
1809
|
-
}
|
|
1810
|
-
}
|
|
1811
|
-
|
|
1812
|
-
/** CONFIRM the pull: the full-quality run of exactly what the preview has
|
|
1813
|
-
* been showing. Its own run mode — the body carries lineEdit and NOTHING
|
|
1814
|
-
* else authored, because C6 makes it exclusive with preserve, waypoints,
|
|
1815
|
-
* segments, regenerateSegments and motionEdit — and it is the one path that
|
|
1816
|
-
* commits a recipe and a version. Same builder, same SESSION SEED and same
|
|
1817
|
-
* curve as the last preview; only `preview: true` is absent, which is what
|
|
1818
|
-
* buys the full step count. */
|
|
1819
|
-
function runLineEdit() {
|
|
1820
|
-
if (appContext.shared.generationPendingRef.current || appContext.shared.genRunningRef.current || ardyRunning) return;
|
|
1821
|
-
if (isProxyFigure(appContext.shared.activeChar)) {
|
|
1822
|
-
appContext.notify(kindRefusal("lineEdit", isKo));
|
|
1823
|
-
return;
|
|
1824
|
-
}
|
|
1825
|
-
const generationRequest = requestMotionGeneration("line_edit", "edit", { lineEdit: true });
|
|
1826
|
-
if (!appContext.shared.takeSourceUrl) {
|
|
1827
|
-
appContext.notify(ko("The current take has no bridge source — generate it once before editing a path", "현재 테이크에 브리지 원본이 없어요 — 궤적을 편집하기 전에 한 번 생성하세요"));
|
|
1828
|
-
return;
|
|
1829
|
-
}
|
|
1830
|
-
// No curve object means no edit — an untouched path is the take's own
|
|
1831
|
-
// trajectory, and sending it would ask the box to spend eight seconds
|
|
1832
|
-
// reproducing what is already there.
|
|
1833
|
-
if (!appContext.shared.lineEditPayload) {
|
|
1834
|
-
appContext.notify(ko(
|
|
1835
|
-
"Draw along the path, pull a dot, or pin a moment first",
|
|
1836
|
-
"먼저 궤적을 따라 그리거나, 점을 잡아당기거나, 순간을 찍어 주세요",
|
|
1837
|
-
));
|
|
1838
|
-
return;
|
|
1839
|
-
}
|
|
1840
|
-
if (!lineEditBackend) {
|
|
1841
|
-
appContext.notify(ko(
|
|
1842
|
-
"The line-editing backend is not connected yet",
|
|
1843
|
-
"라인 편집 백엔드가 아직 연결 전이에요",
|
|
1844
|
-
));
|
|
1845
|
-
return;
|
|
1846
|
-
}
|
|
1847
|
-
const request = appContext.shared.buildLineEditRequest(appContext.shared.lineEditPayload);
|
|
1848
|
-
if (!request.ok) {
|
|
1849
|
-
if (request.message) appContext.notify(request.message);
|
|
1850
|
-
return;
|
|
1851
|
-
}
|
|
1852
|
-
const { body, lineEdit, seed } = request;
|
|
1853
|
-
// The take being edited, not the draft that may be on screen: a preview
|
|
1854
|
-
// is a picture and must never become anyone's lineage.
|
|
1855
|
-
const source = appContext.shared.linePreviewSource;
|
|
1856
|
-
const queued = enqueueMotionJob({
|
|
1857
|
-
request: generationRequest,
|
|
1858
|
-
charId: source?.charId ?? appContext.shared.activeChar.id,
|
|
1859
|
-
charIndex: appContext.shared.activeCharIndex,
|
|
1860
|
-
prompt: body.prompt,
|
|
1861
|
-
body,
|
|
1862
|
-
hasBlockEdits: false,
|
|
1863
|
-
committedEditKeys: [],
|
|
1864
|
-
rootRotationDeg: source?.rotationDeg ?? motion.rotationDeg ?? appContext.shared.activeChar.rot,
|
|
1865
|
-
anchor: { x: motion.anchorX ?? appContext.shared.activeChar.x, z: motion.anchorZ ?? appContext.shared.activeChar.z },
|
|
1866
|
-
ikState: null,
|
|
1867
|
-
recipeIntent: "lineEdit",
|
|
1868
|
-
recipeSeed: seed,
|
|
1869
|
-
// sourceMotion is dropped on the way into the recipe: replay rebinds
|
|
1870
|
-
// it to whatever take it is re-applied to.
|
|
1871
|
-
// (stripSourceMotion keeps only C10's replay keys, so `preview` — when
|
|
1872
|
-
// the object came back from a draft build — cannot leak into a recipe.)
|
|
1873
|
-
recipeLineEdit: stripSourceMotion({ ...lineEdit, sourceMotion: undefined, seed }),
|
|
1874
|
-
recipeLabel: isKo ? `다듬기 · ${lineTrackLabel(appContext.shared.lineTrack)}` : `Refine · ${lineTrackLabel(appContext.shared.lineTrack)}`,
|
|
1875
|
-
});
|
|
1876
|
-
if (!queued) return;
|
|
1877
|
-
appContext.shared.generationPendingRef.current = true;
|
|
1878
|
-
// The pull has left the building. The curve stays (it is the reference
|
|
1879
|
-
// the next edit starts from) but its deformation is released, so the
|
|
1880
|
-
// Generate button goes back to needing a fresh pull instead of inviting
|
|
1881
|
-
// a second identical run while the first is still queued. The undo
|
|
1882
|
-
// stack goes with it: restoring a pull that is already generating would
|
|
1883
|
-
// invite the identical run this reset exists to prevent.
|
|
1884
|
-
//
|
|
1885
|
-
// This also ENDS THE PREVIEW SESSION: the draft comes off the viewport
|
|
1886
|
-
// (the real result will land on it in a couple of seconds, and until it
|
|
1887
|
-
// does the take on screen should be the take that exists) and the seed
|
|
1888
|
-
// is released, so the next pull is a new piece of work with a new roll.
|
|
1889
|
-
appContext.shared.clearLineEdit();
|
|
1890
|
-
}
|
|
1891
|
-
|
|
1892
|
-
function runAllPromptBlocks(commandContext = null) {
|
|
1893
|
-
const characterId = appContext.storeDomain('cast').activeId;
|
|
1894
|
-
return commandContext ? generateMotion({ characterId }, commandContext)
|
|
1895
|
-
: appContext.bus.run('motion.generate', { characterId });
|
|
1896
|
-
}
|
|
1897
|
-
|
|
1898
|
-
// The UI's uncommitted prompt/fresh choice is read synchronously at admission,
|
|
1899
|
-
// just like its seed and pose controls. It never survives into another run.
|
|
1900
|
-
let uiGenerationIntent = null;
|
|
1901
|
-
function runArdy({ promptOverride = ardyPrompt, durationOverride = ardyDuration, promptClipsOverride = [], fresh = false } = {}) {
|
|
1902
|
-
uiGenerationIntent = { prompt: promptOverride, blocks: promptClipsOverride, fresh };
|
|
1903
|
-
try { return appContext.bus.run('motion.generate', { characterId: appContext.storeDomain('cast').activeId, durationSeconds: Math.round(Number(durationOverride)) || ARDY_DURATION_MIN }); }
|
|
1904
|
-
finally { uiGenerationIntent = null; }
|
|
1905
|
-
}
|
|
1906
|
-
|
|
1907
|
-
async function generateMotion(args, commandContext) {
|
|
1908
|
-
if (appContext.shared.generationPendingRef.current || appContext.shared.genRunningRef.current || ardyRunning) throw generationRefusal('TARGET_BUSY', 'A motion generation is already running.');
|
|
1909
|
-
if (appContext.shared.linePreviewUrl) throw generationRefusal('TARGET_NOT_READY', previewBlockingReason(en => en), previewBlockingReason(ko));
|
|
1910
|
-
const character = appContext.storeDomain('cast').read().find(row => row.id === args.characterId);
|
|
1911
|
-
refuseRigOnly(character, 'motion');
|
|
1912
|
-
const active = character.id === appContext.shared.loadedLayerCharRef.current;
|
|
1913
|
-
const posing = active && appContext.shared.posing;
|
|
1914
|
-
const rig = posing ? appContext.shared.posedRig() : appContext.shared.rigs[character.id];
|
|
1915
|
-
if (!rig) throw generationRefusal('TARGET_NOT_READY', 'Character not loaded yet', ko('Character not loaded yet', '캐릭터가 아직 로드되지 않았어요'));
|
|
1916
|
-
const take = domain.motionFor(character.id), state = ikStateFor(character.id);
|
|
1917
|
-
const intent = commandContext.origin === 'ui' ? uiGenerationIntent : null;
|
|
1918
|
-
const blocks = (intent?.blocks ?? character.layer.promptClips).filter(clip => clip.text.trim()).sort((a, b) => a.startFrame - b.startFrame);
|
|
1919
|
-
const prompt = intent?.prompt ?? blocks[0]?.text ?? (active ? ardyPrompt : '');
|
|
1920
|
-
let seed;
|
|
1921
|
-
try { seed = resolveSeed(args.seed ?? (active ? ardySeed : ''), ARDY_SEED_MAX); }
|
|
1922
|
-
catch (error) { throw generationRefusal('INVALID_ARGUMENT', error.message); }
|
|
1923
|
-
const input = { character, prompt, blocks, seed, durationSeconds: args.durationSeconds ?? (blocks.length ? Math.max(ARDY_DURATION_MIN, Math.ceil(Math.max(...blocks.map(clip => clip.endFrame)) / TIMELINE_FPS)) : ardyDuration),
|
|
1924
|
-
waypoints: character.layer.waypoints, motion: take, ikKeys: state.keys,
|
|
1925
|
-
startFromPose: ardyStartFromPose, posePlacement: ardyPosePlacement, frame: appContext.shared.tlFrame,
|
|
1926
|
-
preserveStrength, recipe: domain.layer(character.id).takeRecipe, fresh: intent?.fresh ?? false };
|
|
1927
|
-
const plan = buildGenerationRequest(input), resolved = resolveIkRig(rig);
|
|
1928
|
-
const image = appContext.shared.snapshotExportRig(rig);
|
|
1929
|
-
let poses;
|
|
1930
|
-
try {
|
|
1931
|
-
poses = plan.constraintFrames.map(frame => {
|
|
1932
|
-
if (take) applyMotionFrame(rig, take, frame);
|
|
1933
|
-
if (resolved && state.keys.size) ikEvaluate(resolved.chains, state, frame, resolved.fkJoints, take ? IK_CORRECTION_BLEND_FRAMES : 0);
|
|
1934
|
-
return { frame, pose: buildArdyPose({ rig, camRef: appContext.shared.shotCamRef, look: appContext.shared.look,
|
|
1935
|
-
fovDeg: appContext.shared.fovDeg, slate: slateLine(appContext.shared.shot), rigName: posing ? appContext.shared.posingChar?.model ?? character.model : character.model, root: captureArdyRoot(rig) }) };
|
|
1936
|
-
});
|
|
1937
|
-
} finally { appContext.shared.restoreExportRig(image); }
|
|
1938
|
-
const built = buildGenerationRequest({ ...input, poses });
|
|
1939
|
-
for (const warning of built.warnings) appContext.notify(warning);
|
|
1940
|
-
const request = requestMotionGeneration('timeline', take?.url && state.keys.size ? 'edit' : ardyStartFromPose ? 'pose' : 'prompt');
|
|
1941
|
-
commandContext.check();
|
|
1942
|
-
return new Promise((resolve, reject) => {
|
|
1943
|
-
const job = { request, commandContext, commandCompletion: { resolve, reject }, charId: character.id,
|
|
1944
|
-
charIndex: appContext.live.characters.findIndex(row => row.id === character.id), prompt: built.body.prompt,
|
|
1945
|
-
body: built.body, hasBlockEdits: built.hasBlockEdits, committedEditKeys: built.committedEditKeys,
|
|
1946
|
-
rootRotationDeg: built.rootRotationDeg, anchor: { x: character.x, z: character.z }, ikState: built.hasBlockEdits ? state : null,
|
|
1947
|
-
recipeIntent: built.hasBlockEdits ? 'carry' : 'fresh', recipeSeed: seed,
|
|
1948
|
-
recipeLabel: built.hasBlockEdits ? ko('Block fix', '블록 수정') : built.hasPromptSchedule ? ko('Blocks', '블록 생성')
|
|
1949
|
-
: input.fresh ? ko('New', '새로 만들기') : take?.url ? ko('Again', '다시 뽑기') : ko('Generate', '생성') };
|
|
1950
|
-
appContext.shared.generationPendingRef.current = enqueueMotionJob(job) === true;
|
|
1951
|
-
if (!appContext.shared.generationPendingRef.current) reject(generationRefusal('TARGET_NOT_READY', 'The motion backend cannot run this request.'));
|
|
1952
|
-
});
|
|
1953
|
-
}
|
|
1954
|
-
|
|
1955
|
-
function onTrailDragStart({ track, grabFrame } = {}) {
|
|
1956
|
-
if (!motion) return;
|
|
1957
|
-
if (track) {
|
|
1958
|
-
setTrailTrackFocus(track);
|
|
1959
|
-
focusIkHandle(track);
|
|
1960
|
-
}
|
|
1961
|
-
const frame = Math.round(grabFrame ?? appContext.shared.tlFrame);
|
|
1962
|
-
appContext.shared.setTlFrame(frame);
|
|
1963
|
-
// The pre-drag take is both the deformation base (repeated moves re-derive
|
|
1964
|
-
// from it, so deltas never accumulate) and the undo snapshot.
|
|
1965
|
-
appContext.shared.trailBaseMotionRef.current = motion;
|
|
1966
|
-
appContext.storeDomain('motion').beginGesture();
|
|
1967
|
-
}
|
|
1968
|
-
|
|
1969
|
-
/** World drag delta -> clip delta, shedding the character's stature scale
|
|
1970
|
-
* (the trail is drawn scaled by it). */
|
|
1971
|
-
function trailClipDelta(base, delta, track = "hips", grabFrame = appContext.shared.tlFrame) {
|
|
1972
|
-
const statureScale = appContext.shared.activeChar.scale ?? 1;
|
|
1973
|
-
return worldDeltaToTrailClip(base, delta, { track, grabFrame, radiusFrames: trailFalloffFrames, rig: appContext.shared.activeRig, scale: statureScale });
|
|
1974
|
-
}
|
|
1975
|
-
|
|
1976
|
-
/** Per-rAF drag preview. Deliberately React-free: the deformed take lands in
|
|
1977
|
-
* a ref and on the rig directly, so a drag never re-renders the app. The
|
|
1978
|
-
* trail/highlight lines are rewritten in place by MotionTrails itself. */
|
|
1979
|
-
function onTrailDragPreview({ track, grabFrame, delta }) {
|
|
1980
|
-
const base = appContext.shared.trailBaseMotionRef.current;
|
|
1981
|
-
if (!base) return;
|
|
1982
|
-
const deformed = applyTrailFalloffDelta(base, {
|
|
1983
|
-
grabFrame,
|
|
1984
|
-
radiusFrames: trailFalloffFrames,
|
|
1985
|
-
track, clipDelta: trailClipDelta(base, delta, track, grabFrame),
|
|
1986
|
-
});
|
|
1987
|
-
appContext.shared.trailPreviewMotionRef.current = deformed;
|
|
1988
|
-
const rig = appContext.shared.activeRig;
|
|
1989
|
-
if (!rig) return;
|
|
1990
|
-
applyMotionFrame(rig, deformed, Math.round(grabFrame));
|
|
1991
|
-
if (ikChains && appContext.shared.ikStateRef.current.keys.size > 0) {
|
|
1992
|
-
ikEvaluate(ikChains, appContext.shared.ikStateRef.current, Math.round(grabFrame), ikFkJoints, IK_CORRECTION_BLEND_FRAMES);
|
|
1993
|
-
}
|
|
1994
|
-
}
|
|
1995
|
-
|
|
1996
|
-
function onTrailDragEnd({ track, grabFrame, delta }) {
|
|
1997
|
-
const base = appContext.shared.trailBaseMotionRef.current;
|
|
1998
|
-
const deformed = appContext.shared.trailPreviewMotionRef.current;
|
|
1999
|
-
appContext.shared.trailBaseMotionRef.current = null;
|
|
2000
|
-
appContext.shared.trailPreviewMotionRef.current = null;
|
|
2001
|
-
const size = delta ? Math.hypot(delta.x, delta.y, delta.z) : 0;
|
|
2002
|
-
if (!base || !deformed || size < 0.01) {
|
|
2003
|
-
// A sub-centimetre nudge is a mis-grab, not an authored edit: the
|
|
2004
|
-
// motion state never changed, so only the rig pose needs restoring.
|
|
2005
|
-
if (base && appContext.shared.activeRig) {
|
|
2006
|
-
applyMotionFrame(appContext.shared.activeRig, base, appContext.shared.tlFrame);
|
|
2007
|
-
if (ikChains && appContext.shared.ikStateRef.current.keys.size > 0) {
|
|
2008
|
-
ikEvaluate(ikChains, appContext.shared.ikStateRef.current, appContext.shared.tlFrame, ikFkJoints, IK_CORRECTION_BLEND_FRAMES);
|
|
2009
|
-
}
|
|
2010
|
-
}
|
|
2011
|
-
setTrailEdit(null);
|
|
2012
|
-
return;
|
|
2013
|
-
}
|
|
2014
|
-
const owned = appContext.storeDomain('motion');
|
|
2015
|
-
if (base !== owned.motionFor(appContext.shared.activeChar.id)) { owned.project(); throw new StudioProtocolError('STALE_TARGET', 'The take changed during the trail drag.'); }
|
|
2016
|
-
owned.run('motion.editTrail', { characterId: appContext.shared.activeChar.id, track, grabFrame, radiusFrames: trailFalloffFrames, delta });
|
|
2017
|
-
owned.finishGesture();
|
|
2018
|
-
setTrailEdit({ track, grabFrame, radiusFrames: trailFalloffFrames, clipDelta: trailClipDelta(base, delta, track, grabFrame) });
|
|
2019
|
-
const { startFrame, endFrame } = trailEditRange(base.frames, grabFrame, trailFalloffFrames);
|
|
2020
|
-
const conflictFrames = findAbsoluteIkKeyConflicts({ keys: appContext.shared.ikStateRef.current.keys, track, startFrame, endFrame });
|
|
2021
|
-
if (conflictFrames.length) appContext.notify(`${ko("Absolute IK keys in this range override the path edit — remove or re-key them", "이 구간의 절대 IK 키가 궤적 수정을 덮어써요 — 해당 키를 지우거나 다시 찍어주세요")} ${isKo ? `프레임 ${conflictFrames.join(", ")}` : `frames ${conflictFrames.join(", ")}`}`);
|
|
2022
|
-
}
|
|
2023
|
-
|
|
2024
|
-
/** Send the pending trail edit through the existing motionEdit pipeline:
|
|
2025
|
-
* the regen window is auto-derived from the grab + falloff, explicit IK
|
|
2026
|
-
* keys inside the window ride as hard constraints (their tracks), and the
|
|
2027
|
-
* deformed line contributes the grab-frame pose as a root guide. */
|
|
2028
|
-
function runTrailRegeneration() {
|
|
2029
|
-
if (appContext.shared.generationPendingRef.current || appContext.shared.genRunningRef.current || ardyRunning) return;
|
|
2030
|
-
if (isProxyFigure(appContext.shared.activeChar)) {
|
|
2031
|
-
appContext.notify(kindRefusal("trails", isKo));
|
|
2032
|
-
return;
|
|
2033
|
-
}
|
|
2034
|
-
const request = requestMotionGeneration("trail", "edit", { motionEdit: true });
|
|
2035
|
-
if (!trailEdit) return;
|
|
2036
|
-
// Same rule as runArdy: motionEdit rewrites a span of THE take, and a
|
|
2037
|
-
// draft on the viewport is not it.
|
|
2038
|
-
if (appContext.shared.linePreviewUrl) {
|
|
2039
|
-
appContext.notify(previewBlockingReason());
|
|
2040
|
-
return;
|
|
2041
|
-
}
|
|
2042
|
-
if (!motion?.url) {
|
|
2043
|
-
appContext.notify(ko("The current motion has no bridge source; generate the prompt blocks once before regenerating a trail edit", "현재 모션에 브리지 원본이 없어요. 궤적 수정을 재생성하려면 프롬프트 블록을 먼저 한 번 생성하세요"));
|
|
2044
|
-
return;
|
|
2045
|
-
}
|
|
2046
|
-
const rig = appContext.shared.activeRig;
|
|
2047
|
-
if (!rig) {
|
|
2048
|
-
appContext.notify(ko("Character not loaded yet", "캐릭터가 아직 로드되지 않았어요"));
|
|
2049
|
-
return;
|
|
2050
|
-
}
|
|
2051
|
-
const { startFrame, endFrame } = trailEditRange(motion.frames, trailEdit.grabFrame, trailEdit.radiusFrames);
|
|
2052
|
-
const frames = [...new Set([
|
|
2053
|
-
trailEdit.grabFrame,
|
|
2054
|
-
...appContext.shared.ikFrames.filter((frame) => frame >= startFrame && frame < endFrame),
|
|
2055
|
-
])].sort((a, b) => a - b);
|
|
2056
|
-
const currentFrame = appContext.shared.tlFrame;
|
|
2057
|
-
const entries = [];
|
|
2058
|
-
for (const frame of frames) {
|
|
2059
|
-
applyMotionFrame(rig, motion, frame);
|
|
2060
|
-
if (ikChains && appContext.shared.ikStateRef.current.keys.size > 0) {
|
|
2061
|
-
ikEvaluate(ikChains, appContext.shared.ikStateRef.current, frame, ikFkJoints, IK_CORRECTION_BLEND_FRAMES);
|
|
2062
|
-
}
|
|
2063
|
-
const pose = buildArdyPose({
|
|
2064
|
-
rig,
|
|
2065
|
-
camRef: appContext.shared.shotCamRef,
|
|
2066
|
-
look: appContext.shared.look,
|
|
2067
|
-
fovDeg: appContext.shared.fovDeg,
|
|
2068
|
-
slate: slateLine(appContext.shared.shot),
|
|
2069
|
-
rigName: appContext.shared.activeChar.model,
|
|
2070
|
-
root: captureArdyRoot(rig),
|
|
2071
|
-
});
|
|
2072
|
-
const wireFrame = toArdyFrame(frame);
|
|
2073
|
-
if (entries.length && wireFrame <= entries[entries.length - 1].frame) continue;
|
|
2074
|
-
const ikTracks = [...(appContext.shared.ikStateRef.current.keys.get(frame)?.keys() || [])];
|
|
2075
|
-
if (frame === trailEdit.grabFrame && trailEdit.track && !ikTracks.includes(trailEdit.track)) ikTracks.push(trailEdit.track);
|
|
2076
|
-
entries.push({ frame: wireFrame, timelineFrame: frame, tracks: ikTracks.length ? ikTracks : ["hips"], pose });
|
|
2077
|
-
}
|
|
2078
|
-
applyMotionFrame(rig, motion, currentFrame);
|
|
2079
|
-
if (ikChains && appContext.shared.ikStateRef.current.keys.size > 0) {
|
|
2080
|
-
ikEvaluate(ikChains, appContext.shared.ikStateRef.current, currentFrame, ikFkJoints, IK_CORRECTION_BLEND_FRAMES);
|
|
2081
|
-
}
|
|
2082
|
-
const prompt = (motion.prompt || "").trim() || "A person continues the motion naturally.";
|
|
2083
|
-
const body = {
|
|
2084
|
-
prompt,
|
|
2085
|
-
duration: motion.frames / TIMELINE_FPS,
|
|
2086
|
-
posePin: true,
|
|
2087
|
-
motionEdit: {
|
|
2088
|
-
sourceMotion: motion.url,
|
|
2089
|
-
startFrame: toArdyFrame(startFrame),
|
|
2090
|
-
endFrame: toArdyFrame(endFrame),
|
|
2091
|
-
contextBefore: 40,
|
|
2092
|
-
contextAfter: 20,
|
|
2093
|
-
edits: entries.map(({ frame, tracks, pose }) => ({ frame, tracks, pose })),
|
|
2094
|
-
},
|
|
2095
|
-
};
|
|
2096
|
-
// THE SEED RULE (C9) — a trail regeneration writes a new take too, so its
|
|
2097
|
-
// seed is rolled, sent and recorded like every other take-creating run.
|
|
2098
|
-
const seed = takeSeed();
|
|
2099
|
-
if (seed === null) return;
|
|
2100
|
-
body.seed = seed;
|
|
2101
|
-
const queued = enqueueMotionJob({
|
|
2102
|
-
request,
|
|
2103
|
-
charId: appContext.shared.activeChar.id,
|
|
2104
|
-
charIndex: appContext.shared.activeCharIndex,
|
|
2105
|
-
prompt,
|
|
2106
|
-
body,
|
|
2107
|
-
hasBlockEdits: true,
|
|
2108
|
-
committedEditKeys: entries.map(({ timelineFrame, tracks }) => ({ frame: timelineFrame, tracks })),
|
|
2109
|
-
rootRotationDeg: motion.rotationDeg ?? appContext.shared.activeChar.rot,
|
|
2110
|
-
anchor: { x: motion.anchorX ?? appContext.shared.activeChar.x, z: motion.anchorZ ?? appContext.shared.activeChar.z },
|
|
2111
|
-
ikState: appContext.shared.ikStateRef.current,
|
|
2112
|
-
// motionEdit rewrites a span of the loaded take; the recipe travels
|
|
2113
|
-
// forward unchanged because there is no recipe field that could
|
|
2114
|
-
// describe the splice (C10 excludes motionEdit from replay outright).
|
|
2115
|
-
recipeIntent: "carry",
|
|
2116
|
-
recipeSeed: seed,
|
|
2117
|
-
recipeLabel: ko("Trail fix", "궤적 수정"),
|
|
2118
|
-
});
|
|
2119
|
-
if (!queued) return;
|
|
2120
|
-
appContext.shared.generationPendingRef.current = true;
|
|
2121
|
-
setTrailEdit(null);
|
|
2122
|
-
}
|
|
2123
|
-
|
|
2124
|
-
/* One box, one job at a time: explicit entry points suppress duplicate
|
|
2125
|
-
* requests through queueing and execution. The
|
|
2126
|
-
* payload is frozen at enqueue time; completion delivers the clip to the
|
|
2127
|
-
* REQUESTING character's layer, not whoever happens to be selected then. */
|
|
2128
|
-
const [genQueue, setGenQueue] = useState([]);
|
|
2129
|
-
|
|
2130
|
-
function enqueueMotionJob(spec) {
|
|
2131
|
-
const options = { body: spec.body, lineEditSupported: lineEditBackend };
|
|
2132
|
-
spec.request.preflight(bridge, options);
|
|
2133
|
-
if (motionPreflightReason(bridge, options)) return;
|
|
2134
|
-
const id = `gen-${++appContext.shared.genJobSeq.current}`;
|
|
2135
|
-
setGenQueue((queue) => [...queue, { id, status: "queued", ...spec }]);
|
|
2136
|
-
appContext.notify((isKo, ko) => isKo ? `인물 ${spec.charIndex + 1} 모션 생성을 대기열에 넣었어요` : `Queued motion generation for Subject ${spec.charIndex + 1}`);
|
|
2137
|
-
return true;
|
|
2138
|
-
}
|
|
2139
|
-
|
|
2140
|
-
async function executeMotionJob(job) {
|
|
2141
|
-
job.commandContext?.check();
|
|
2142
|
-
const controller = new AbortController();
|
|
2143
|
-
const abort = () => controller.abort(job.commandContext.signal.reason);
|
|
2144
|
-
job.commandContext?.signal.addEventListener("abort", abort, { once: true });
|
|
2145
|
-
appContext.shared.ardyAbortRef.current = controller;
|
|
2146
|
-
setArdyRunning(true);
|
|
2147
|
-
reportArdyStatus(ko("connecting…", "연결 중…"));
|
|
2148
|
-
setArdyReport(null);
|
|
2149
|
-
setArdyOutcome(null);
|
|
2150
|
-
// Replay notices belong to ONE run; the next run re-earns them.
|
|
2151
|
-
setReplayNotices([]);
|
|
2152
|
-
const request = job.request;
|
|
2153
|
-
request.start();
|
|
2154
|
-
controller.signal.addEventListener("abort", () => request.fail(new DOMException("", "AbortError")), { once: true });
|
|
2155
|
-
let editCommitReport = null;
|
|
2156
|
-
try {
|
|
2157
|
-
const done = await ardyGenerate(
|
|
2158
|
-
job.body,
|
|
2159
|
-
(event) => {
|
|
2160
|
-
if (event.event === "status") reportArdyStatus(event.message);
|
|
2161
|
-
else if (event.event === "report") {
|
|
2162
|
-
setArdyReport(event.report);
|
|
2163
|
-
if (job.hasBlockEdits) editCommitReport = event.report;
|
|
2164
|
-
// C10's per-entry replay report. Only the entries worth
|
|
2165
|
-
// acting on are kept: a refinement that failed outright,
|
|
2166
|
-
// or one whose range straddles an internal block
|
|
2167
|
-
// boundary (where block N+1 was conditioned on N's
|
|
2168
|
-
// PRE-edit tail, so the replay is approximate rather
|
|
2169
|
-
// than exact). Both are non-blocking — the take exists.
|
|
2170
|
-
if (Array.isArray(event.report.replay)) {
|
|
2171
|
-
setReplayNotices(event.report.replay.filter((entry) => entry?.ok === false || entry?.boundaryWarning === true));
|
|
2172
|
-
}
|
|
2173
|
-
}
|
|
2174
|
-
},
|
|
2175
|
-
{ signal: controller.signal },
|
|
2176
|
-
);
|
|
2177
|
-
if (
|
|
2178
|
-
job.hasBlockEdits &&
|
|
2179
|
-
(
|
|
2180
|
-
editCommitReport?.commit_verified !== true ||
|
|
2181
|
-
!job.body.motionEdit.edits.every((entry) =>
|
|
2182
|
-
editCommitReport.committed_keys?.includes(entry.frame)
|
|
2183
|
-
)
|
|
2184
|
-
)
|
|
2185
|
-
) {
|
|
2186
|
-
throw new Error(ko("The generated motion came back without verified authored pose keys", "생성된 모션에 검증된 수동 포즈 키가 없어요"));
|
|
2187
|
-
}
|
|
2188
|
-
job.commandContext?.check();
|
|
2189
|
-
if (!done.motionUrl) throw generationRefusal('TARGET_NOT_READY', 'The generator finished without a motion artifact.');
|
|
2190
|
-
setArdyOutcome({ ok: true, output: done.output, bytes: done.bytes, motionUrl: done.motionUrl, rotationDeg: job.rootRotationDeg });
|
|
2191
|
-
request.succeed();
|
|
2192
|
-
trackActivation("motion");
|
|
2193
|
-
// Fetch and decode the real npz right away; decode errors are shown
|
|
2194
|
-
// in the card, playback is never faked. The clip lands on the
|
|
2195
|
-
// REQUESTING character, not whoever is selected now.
|
|
2196
|
-
if (done.motionUrl) {
|
|
2197
|
-
await deliverMotion(job, done.motionUrl);
|
|
2198
|
-
if (!controller.signal.aborted && appContext.live.characters.some((entry) => entry.id === job.charId)) request.apply();
|
|
2199
|
-
if (!appContext.storeDomain('motion')) commitTakeRecipe(job, done.motionUrl);
|
|
2200
|
-
}
|
|
2201
|
-
if (!appContext.storeDomain('motion') && job.hasBlockEdits && job.ikState) {
|
|
2202
|
-
// Timeline frames, not the wire frames in body.motionEdit.edits:
|
|
2203
|
-
// these light up the IK markers on the production clock.
|
|
2204
|
-
setCommittedIkEdits((current) => [...current, ...job.committedEditKeys]);
|
|
2205
|
-
job.ikState.keys.clear();
|
|
2206
|
-
job.ikState.tracked.clear();
|
|
2207
|
-
job.ikState.plants.clear();
|
|
2208
|
-
setIkTick((value) => value + 1);
|
|
2209
|
-
}
|
|
2210
|
-
appContext.notify((isKo, ko) => isKo ? `인물 ${job.charIndex + 1} 모션 생성됨` : `Motion generated for Subject ${job.charIndex + 1}`);
|
|
2211
|
-
} catch (err) {
|
|
2212
|
-
// Wave-2 gate, second line of defence. The capability preflight
|
|
2213
|
-
// normally stops a line edit before it is sent, but a bridge that
|
|
2214
|
-
// advertises the route and then 400s on the field (a half-landed
|
|
2215
|
-
// wave 2, an older sidecar behind the proxy) must read as "not
|
|
2216
|
-
// connected yet", not as a red generation failure the user could
|
|
2217
|
-
// act on.
|
|
2218
|
-
if (job.body.lineEdit && isLineEditUnsupported(err?.message)) {
|
|
2219
|
-
appContext.notify(ko(
|
|
2220
|
-
"The line-editing backend is not connected yet",
|
|
2221
|
-
"라인 편집 백엔드가 아직 연결 전이에요",
|
|
2222
|
-
));
|
|
2223
|
-
}
|
|
2224
|
-
setArdyOutcome({
|
|
2225
|
-
ok: false,
|
|
2226
|
-
message: err?.name === "AbortError" ? ko("Cancelled", "취소됨") : err?.message || String(err),
|
|
2227
|
-
});
|
|
2228
|
-
request.fail(err, job.body.lineEdit && isLineEditUnsupported(err?.message) ? "unsupported_route" : undefined);
|
|
2229
|
-
throw err;
|
|
2230
|
-
} finally {
|
|
2231
|
-
setArdyRunning(false);
|
|
2232
|
-
appContext.shared.ardyAbortRef.current = null;
|
|
2233
|
-
job.commandContext?.signal.removeEventListener("abort", abort);
|
|
2234
|
-
}
|
|
2235
|
-
}
|
|
2236
|
-
|
|
2237
|
-
/* ONE writer for both. Every take that reaches the app came out of a job,
|
|
2238
|
-
* so a job's completion is the only place where "what is this take made of"
|
|
2239
|
-
* can be answered honestly, and the answer is checkpointed next to the
|
|
2240
|
-
* motionUrl in the same breath. Nothing else may write takeRecipeRef except
|
|
2241
|
-
* loadTakeVersion, which restores a checkpoint rather than authoring one. */
|
|
2242
|
-
function commitTakeRecipe(job, motionUrl) {
|
|
2243
|
-
const owned = appContext.storeDomain('motion');
|
|
2244
|
-
const base = owned ? owned.layer(job.charId).takeRecipe : appContext.shared.takeRecipeRef.current;
|
|
2245
|
-
let next = base;
|
|
2246
|
-
if (job.recipeIntent === "fresh") {
|
|
2247
|
-
// A regeneration that carried `replay` produced a take that ALREADY
|
|
2248
|
-
// contains those edits, so they stay on the recipe. Resetting
|
|
2249
|
-
// lineEdits to [] here would make the second regeneration lose what
|
|
2250
|
-
// the first one preserved — the exact failure replay exists to fix.
|
|
2251
|
-
next = freshRecipe({
|
|
2252
|
-
seed: job.recipeSeed,
|
|
2253
|
-
blocks: blocksFromRequest(job.body, ARDY_FPS),
|
|
2254
|
-
lineEdits: job.body.replay ?? [],
|
|
2255
|
-
});
|
|
2256
|
-
} else if (job.recipeIntent === "lineEdit") {
|
|
2257
|
-
// A take imported by url (?motion=, a reload) has no recipe of its own.
|
|
2258
|
-
// The edit's body carries the take's prompt and length, so a
|
|
2259
|
-
// best-effort single block is recorded rather than dropping the
|
|
2260
|
-
// refinement on the floor; only the SEED is a guess, and it is the
|
|
2261
|
-
// one this edit actually ran with. The seedless placeholder recipe an
|
|
2262
|
-
// imported take is given (seedLoadedTake) counts as "no recipe" for
|
|
2263
|
-
// the seed specifically: adopting this edit's seed is what turns it
|
|
2264
|
-
// into something that can replay at all.
|
|
2265
|
-
const seeded = Number.isInteger(base?.seed)
|
|
2266
|
-
? base
|
|
2267
|
-
: freshRecipe({
|
|
2268
|
-
seed: job.recipeSeed,
|
|
2269
|
-
blocks: base?.blocks?.length ? base.blocks : blocksFromRequest(job.body, ARDY_FPS),
|
|
2270
|
-
lineEdits: base?.lineEdits ?? [],
|
|
2271
|
-
});
|
|
2272
|
-
next = withLineEdit(seeded, job.recipeLineEdit);
|
|
2273
|
-
}
|
|
2274
|
-
if (owned) {
|
|
2275
|
-
owned.writeLayer(job.charId, { takeRecipe: next, takeVersions: pushTakeVersion(owned.layer(job.charId).takeVersions, {
|
|
2276
|
-
motionUrl, recipe: next, savedAt: Date.now(), label: job.recipeLabel ?? '',
|
|
2277
|
-
}, TAKE_VERSIONS_MAX) });
|
|
2278
|
-
return;
|
|
2279
|
-
}
|
|
2280
|
-
appContext.shared.takeRecipeRef.current = next;
|
|
2281
|
-
setTakeRecipe(next);
|
|
2282
|
-
setTakeVersions((list) => pushTakeVersion(list, {
|
|
2283
|
-
motionUrl,
|
|
2284
|
-
recipe: next,
|
|
2285
|
-
savedAt: Date.now(),
|
|
2286
|
-
label: job.recipeLabel ?? "",
|
|
2287
|
-
}, TAKE_VERSIONS_MAX));
|
|
2288
|
-
}
|
|
2289
|
-
|
|
2290
|
-
/* A take can also arrive WITHOUT a job behind it — ?motion=<url>, the shipped
|
|
2291
|
-
* demo clip, a scene reload that re-fetches a stored motionRef. Those takes
|
|
2292
|
-
* used to leave the version strip empty, so the first refinement had nothing
|
|
2293
|
-
* to walk back to and the artist's only checkpoint was the thing they had
|
|
2294
|
-
* just overwritten. They get a v1 like everything else.
|
|
2295
|
-
*
|
|
2296
|
-
* WHAT IS HONESTLY KNOWN is the take's url, its prompt and its length —
|
|
2297
|
-
* NOT its seed, which was rolled on the box in a session nobody here
|
|
2298
|
-
* witnessed. So the placeholder recipe carries `seed: null` and every reader
|
|
2299
|
-
* treats that as "this take cannot be rebuilt": no request may attach it as
|
|
2300
|
-
* C10 `replay` (a replay whose base is a different random take re-applies
|
|
2301
|
-
* refinements to a stranger), and the first real edit adopts its own seed in
|
|
2302
|
-
* commitTakeRecipe. A checkpoint you can return to beats a recipe you can
|
|
2303
|
-
* replay, and this gets the first without pretending to the second. */
|
|
2304
|
-
function seedLoadedTake(url, prompt, frames) {
|
|
2305
|
-
if (appContext.storeDomain('motion')) return;
|
|
2306
|
-
if (!url || appContext.shared.takeRecipeRef.current) return;
|
|
2307
|
-
const recipe = Object.freeze({
|
|
2308
|
-
seed: null,
|
|
2309
|
-
blocks: Object.freeze([Object.freeze({
|
|
2310
|
-
prompt: typeof prompt === "string" ? prompt : "",
|
|
2311
|
-
duration: frames > 0 ? frames / TIMELINE_FPS : 0,
|
|
2312
|
-
})]),
|
|
2313
|
-
lineEdits: Object.freeze([]),
|
|
2314
|
-
});
|
|
2315
|
-
appContext.shared.takeRecipeRef.current = recipe;
|
|
2316
|
-
setTakeRecipe(recipe);
|
|
2317
|
-
setTakeVersions((list) => pushTakeVersion(list, {
|
|
2318
|
-
motionUrl: url,
|
|
2319
|
-
recipe,
|
|
2320
|
-
savedAt: Date.now(),
|
|
2321
|
-
label: ko("Loaded", "불러옴"),
|
|
2322
|
-
}, TAKE_VERSIONS_MAX));
|
|
2323
|
-
}
|
|
2324
|
-
|
|
2325
|
-
/** Click a chip: that motionUrl becomes the active take through the SAME
|
|
2326
|
-
* delivery path a fresh generation result travels, and the recipe saved
|
|
2327
|
-
* beside it becomes the current one. Nothing is truncated — editing from an
|
|
2328
|
-
* old version pushes a NEW version on top, so no click can destroy work. */
|
|
2329
|
-
async function loadTakeVersion(entry) {
|
|
2330
|
-
if (!entry?.motionUrl || motionBusy) return;
|
|
2331
|
-
if (appContext.storeDomain('motion')) return appContext.storeDomain('motion').run('motion.loadVersion', { characterId: appContext.shared.activeChar.id, motionUrl: entry.motionUrl });
|
|
2332
|
-
if (entry.motionUrl === appContext.shared.takeSourceUrl) return;
|
|
2333
|
-
// A pull in hand was authored against the take that is leaving. The
|
|
2334
|
-
// preview is dropped WITHOUT reverting: this call is already loading a
|
|
2335
|
-
// different take, and a revert would race it with a reload of the one
|
|
2336
|
-
// being left behind.
|
|
2337
|
-
appContext.shared.cancelLinePreview({ revert: false });
|
|
2338
|
-
if (appContext.shared.lineEditMode) appContext.shared.clearLineEdit();
|
|
2339
|
-
setReplayNotices([]);
|
|
2340
|
-
appContext.shared.takeRecipeRef.current = entry.recipe ?? null;
|
|
2341
|
-
setTakeRecipe(entry.recipe ?? null);
|
|
2342
|
-
try {
|
|
2343
|
-
await deliverMotion({
|
|
2344
|
-
charId: appContext.shared.activeChar.id,
|
|
2345
|
-
prompt: entry.recipe?.blocks?.[0]?.prompt ?? motion?.prompt ?? "",
|
|
2346
|
-
rootRotationDeg: motion?.rotationDeg ?? appContext.shared.activeChar.rot,
|
|
2347
|
-
anchor: { x: motion?.anchorX ?? appContext.shared.activeChar.x, z: motion?.anchorZ ?? appContext.shared.activeChar.z },
|
|
2348
|
-
calibration: motion?.sceneCalibration ?? null,
|
|
2349
|
-
}, entry.motionUrl);
|
|
2350
|
-
} catch {
|
|
2351
|
-
/* loadMotion already surfaced the decode failure in the panel */
|
|
2352
|
-
}
|
|
2353
|
-
}
|
|
2354
|
-
|
|
2355
|
-
/* Scene blocks the shot with Kimodo; Refine pulls one joint's path with
|
|
2356
|
-
* ProjFlow. Everything else the pipeline can do is one of those two said
|
|
2357
|
-
* more precisely, and both are reachable from the take itself instead of
|
|
2358
|
-
* from a foldout the artist has to remember to open.
|
|
2359
|
-
*
|
|
2360
|
-
* WHY THE REASONS ARE FUNCTIONS, not toasts. An action the artist cannot
|
|
2361
|
-
* take must say so BEFORE the click, in place, next to the button. A toast
|
|
2362
|
-
* fired after the click teaches nothing: it arrives once, scrolls away, and
|
|
2363
|
-
* leaves the button looking identical to the ones that work. Each reason
|
|
2364
|
-
* below is rendered as a line under its entry AND as data-disabled-reason,
|
|
2365
|
-
* which is also what the CDP surface gate reads. */
|
|
2366
|
-
function selectedMotionReadiness({ fresh = false, clips = appContext.shared.promptClips } = {}) {
|
|
2367
|
-
const authored = clips.filter((clip) => clip.text.trim()).sort((a, b) => a.startFrame - b.startFrame);
|
|
2368
|
-
const prompt = authored[0]?.text.trim() || ardyPrompt.trim();
|
|
2369
|
-
const duration = motion && appContext.shared.ikFrames.length > 0 ? motion.frames / motion.fps
|
|
2370
|
-
: authored.length ? Math.max(ARDY_DURATION_MIN, Math.ceil(Math.max(...authored.map((clip) => clip.endFrame)) / TIMELINE_FPS))
|
|
2371
|
-
: Math.round(Number(ardyDuration)) || ARDY_DURATION_MIN;
|
|
2372
|
-
const clipFrames = duration * TIMELINE_FPS;
|
|
2373
|
-
const segments = buildPromptSchedule(authored, clipFrames, prompt);
|
|
2374
|
-
const hasPromptSchedule = segments.length > 1;
|
|
2375
|
-
const editedSegments = motion?.url && hasPromptSchedule
|
|
2376
|
-
? segments.filter((segment) => appContext.shared.ikFrames.some((frame) => frame >= segment.startFrame && frame < segment.endFrame))
|
|
2377
|
-
: [];
|
|
2378
|
-
const hasBlockEdits = editedSegments.length > 0;
|
|
2379
|
-
const pinPlan = planPosePin({
|
|
2380
|
-
startFromPose: ardyStartFromPose,
|
|
2381
|
-
poseFrame: posePlacementFrame(ardyPosePlacement, clipFrames, appContext.shared.tlFrame),
|
|
2382
|
-
hasPromptSchedule, hasBlockEdits, waypointMode: appContext.shared.waypointMode, ikFrames: appContext.shared.ikFrames, clipFrames, segments, editedSegments,
|
|
2383
|
-
});
|
|
2384
|
-
// Only the capability-bearing fields are needed here; the queue still
|
|
2385
|
-
// preflights the actual frozen request before any generation HTTP call.
|
|
2386
|
-
const body = { prompt, duration, posePin: pinPlan.pin };
|
|
2387
|
-
if (hasBlockEdits) body.motionEdit = {};
|
|
2388
|
-
else if (hasPromptSchedule) body.segments = toArdySegments(segments);
|
|
2389
|
-
if (appContext.shared.waypointMode) body.waypoints = [{}];
|
|
2390
|
-
const recipe = appContext.shared.takeRecipeRef.current;
|
|
2391
|
-
if (!fresh && !hasBlockEdits && Number.isInteger(recipe?.seed)) body.replay = replayPayload(recipe);
|
|
2392
|
-
if (!fresh && !hasBlockEdits && !hasPromptSchedule && motion?.url && preserveStrength > 0
|
|
2393
|
-
&& Math.abs(motion.frames / TIMELINE_FPS - duration) <= 1 / ARDY_FPS + 1e-9) {
|
|
2394
|
-
const blocks = blocksFromRequest(body, ARDY_FPS);
|
|
2395
|
-
if (recipe?.blocks?.length === blocks.length
|
|
2396
|
-
&& recipe.blocks.every((block, index) => block.prompt.trim() === blocks[index].prompt.trim())) body.preserve = {};
|
|
2397
|
-
}
|
|
2398
|
-
return motionReadiness(bridge, { body, lineEditSupported: lineEditBackend });
|
|
2399
|
-
}
|
|
2400
|
-
|
|
2401
|
-
const generationBusy = ardyRunning || genQueue.some((job) => job.status === "queued" || job.status === "running");
|
|
2402
|
-
|
|
2403
|
-
function openMotionSetup(kind = "prompt") {
|
|
2404
|
-
setMotionSetupKind(kind);
|
|
2405
|
-
setMotionSetupReveal((value) => value + 1);
|
|
2406
|
-
}
|
|
2407
|
-
|
|
2408
|
-
function refineDisabledReason() {
|
|
2409
|
-
if (!characterCapabilities(appContext.shared.activeChar).lineEdit) return kindRefusal("lineEdit", isKo);
|
|
2410
|
-
if (!motion) return ko("No take yet — block a scene first", "아직 테이크가 없어요 — 먼저 장면을 만들어 주세요");
|
|
2411
|
-
if (!motion.url) return ko("This take has no bridge source — generate it once before refining", "이 테이크에는 브리지 원본이 없어요 — 한 번 생성해야 다듬을 수 있어요");
|
|
2412
|
-
return "";
|
|
2413
|
-
}
|
|
2414
|
-
|
|
2415
|
-
function sceneDisabledReason() {
|
|
2416
|
-
// A standing capability, so it outranks the bridge probe: a capsule figure
|
|
2417
|
-
// cannot generate whatever the backend says.
|
|
2418
|
-
if (!characterCapabilities(appContext.shared.activeChar).motion) return kindRefusal("motion", isKo);
|
|
2419
|
-
if (bridge === null || bridgeChecking) return motionReadinessMessage("loading");
|
|
2420
|
-
if (generationBusy) return ko("A generation is already running", "이미 생성이 돌고 있어요");
|
|
2421
|
-
// NOT a line-edit preview, deliberately. Every other reason here is a
|
|
2422
|
-
// standing capability the entry should be greyed for; a draft on the
|
|
2423
|
-
// viewport lasts a second and a half, and a reason line appearing and
|
|
2424
|
-
// vanishing under the Scene button RESIZES THE TAKE BAR — which shortens
|
|
2425
|
-
// the stage, which changes the camera aspect, which the drift watcher
|
|
2426
|
-
// reads as "the view moved".
|
|
2427
|
-
//
|
|
2428
|
-
// THE TEETH ARE OUT OF THAT TRAP: drift no longer discards anything, so a
|
|
2429
|
-
// take-bar resize now costs at most a flicker of the ghosted paint and the
|
|
2430
|
-
// hint while the aspect settles — it used to kill the pull ~400 ms after
|
|
2431
|
-
// every draft landed. The refusal still lives in runArdy /
|
|
2432
|
-
// runTrailRegeneration rather than here, because a reason line that
|
|
2433
|
-
// appears and vanishes under the pointer is its own small nuisance and
|
|
2434
|
-
// nothing is gained by moving it back.
|
|
2435
|
-
return "";
|
|
2436
|
-
}
|
|
2437
|
-
|
|
2438
|
-
/** The one sentence every take-consuming action says while a draft is up. */
|
|
2439
|
-
function previewBlockingReason(localize = ko) {
|
|
2440
|
-
return localize(
|
|
2441
|
-
"A line-edit preview is on the viewport — press Generate to keep it, or undo (Ctrl/Cmd+Z) to drop it",
|
|
2442
|
-
"라인 편집 미리보기가 떠 있어요 — 생성으로 확정하거나 Ctrl/Cmd+Z로 되돌린 뒤에 쓰세요",
|
|
2443
|
-
);
|
|
2444
|
-
}
|
|
2445
|
-
|
|
2446
|
-
function sceneGenerateDisabledReason() {
|
|
2447
|
-
return sceneDisabledReason()
|
|
2448
|
-
|| (ardyPrompt.trim() || appContext.shared.promptClips.some((clip) => clip.text.trim())
|
|
2449
|
-
? ""
|
|
2450
|
-
: ko("Describe the motion first — press 4 and add a prompt with + on the Prompt lane", "먼저 어떤 동작인지 적어 주세요 — 4를 누르고 프롬프트 레인의 +로 프롬프트를 추가하세요"));
|
|
2451
|
-
}
|
|
2452
|
-
|
|
2453
|
-
/** Taking it AGAIN needs no fresh wording: the loaded take already knows what
|
|
2454
|
-
* it was asked for, so its own prompt is the fallback (the same fallback
|
|
2455
|
-
* runLineEdit uses). What it does need is a take to re-take. */
|
|
2456
|
-
function sceneAgainPrompt() {
|
|
2457
|
-
return ardyPrompt.trim() || (motion?.prompt ?? "").trim();
|
|
2458
|
-
}
|
|
2459
|
-
|
|
2460
|
-
function sceneAgainDisabledReason() {
|
|
2461
|
-
return sceneDisabledReason()
|
|
2462
|
-
|| (motion?.url ? "" : ko("Nothing to redo yet — make a take first", "다시 뽑을 테이크가 없어요 — 먼저 한 번 만들어 주세요"))
|
|
2463
|
-
|| (sceneAgainPrompt() || appContext.shared.promptClips.some((clip) => clip.text.trim())
|
|
2464
|
-
? ""
|
|
2465
|
-
: ko("This take carries no prompt — add a block and describe it", "이 테이크에는 프롬프트가 없어요 — 블록을 추가하고 동작을 적어 주세요"));
|
|
2466
|
-
}
|
|
2467
|
-
|
|
2468
|
-
/** ONE CLICK from a loaded take into drag mode. The pull itself is authored
|
|
2469
|
-
* on the viewport, but its controls live under the character's Inspector, so
|
|
2470
|
-
* selecting that character and revealing the panel happen HERE rather than
|
|
2471
|
-
* being three clicks the artist has to find first. */
|
|
2472
|
-
function enterRefineMode() {
|
|
2473
|
-
const reason = refineDisabledReason();
|
|
2474
|
-
if (reason) {
|
|
2475
|
-
appContext.notify(reason);
|
|
2476
|
-
return;
|
|
2477
|
-
}
|
|
2478
|
-
setSceneMenuOpen(false);
|
|
2479
|
-
appContext.shared.selectActiveCharacterInHierarchy();
|
|
2480
|
-
appContext.shared.revealPromptBlocks();
|
|
2481
|
-
appContext.shared.toggleLineEditMode();
|
|
2482
|
-
}
|
|
2483
|
-
|
|
2484
|
-
/** Take it again — same blocks, same lineage, refinements replayed. Authored
|
|
2485
|
-
* blocks go through the batch path so the schedule survives; a single-prompt
|
|
2486
|
-
* take goes straight through runArdy. */
|
|
2487
|
-
function runSceneAgain() {
|
|
2488
|
-
if (appContext.shared.promptClips.some((clip) => clip.text.trim())) runAllPromptBlocks();
|
|
2489
|
-
else runArdy({ promptOverride: sceneAgainPrompt() });
|
|
2490
|
-
}
|
|
2491
|
-
|
|
2492
|
-
/** Add a block — the timeline's own add-block gesture, said as a button. */
|
|
2493
|
-
function addSceneBlock() {
|
|
2494
|
-
appContext.shared.addPromptClip(appContext.shared.tlFrame);
|
|
2495
|
-
appContext.shared.selectActiveCharacterInHierarchy();
|
|
2496
|
-
appContext.shared.revealPromptBlocks();
|
|
2497
|
-
}
|
|
2498
|
-
|
|
2499
|
-
/** Hand a finished clip to the layer that asked for it: the buffer when
|
|
2500
|
-
* the requester is still active, its stored session motion otherwise. A
|
|
2501
|
-
* lightweight motionRef is persisted with the entry either way, so the
|
|
2502
|
-
* clip can be re-fetched after a reload. */
|
|
2503
|
-
async function deliverMotion(job, motionUrl) {
|
|
2504
|
-
const calibration = job.calibration ?? job.sceneCalibration ?? null;
|
|
2505
|
-
const normalizedCalibration = normalizeMotionCalibration(calibration);
|
|
2506
|
-
const sceneAnchorX = job.anchor.x + normalizedCalibration.offsetX;
|
|
2507
|
-
const sceneAnchorZ = job.anchor.z + normalizedCalibration.offsetZ;
|
|
2508
|
-
const sceneRotationDeg = job.rootRotationDeg + normalizedCalibration.yawDeg;
|
|
2509
|
-
const motionRef = {
|
|
2510
|
-
url: motionUrl,
|
|
2511
|
-
prompt: job.prompt,
|
|
2512
|
-
rotationDeg: sceneRotationDeg,
|
|
2513
|
-
anchorX: sceneAnchorX,
|
|
2514
|
-
anchorZ: sceneAnchorZ,
|
|
2515
|
-
};
|
|
2516
|
-
if (calibration && typeof calibration === "object") motionRef.calibration = normalizedCalibration;
|
|
2517
|
-
if (!appContext.storeDomain('motion') && !job.commandContext) appContext.shared.publishStudioCharacters((list) => list.map((entry) => entry.id === job.charId ? { ...entry, motionRef } : entry));
|
|
2518
|
-
if (job.charId === appContext.shared.loadedLayerCharRef.current) {
|
|
2519
|
-
await loadMotion(motionUrl, job.prompt, job.rootRotationDeg, null, job.charId, null, { calibration, commandContext: job.commandContext, job });
|
|
2520
|
-
if (!appContext.storeDomain('motion') && job.commandContext) appContext.shared.publishStudioCharacters(appContext.live.characters.map(entry => entry.id === job.charId ? { ...entry, motionRef } : entry));
|
|
2521
|
-
return;
|
|
2522
|
-
}
|
|
2523
|
-
// Inbound boundary for a clip delivered to a non-active layer.
|
|
2524
|
-
const retimed = retimeMotion(await loadMotionFromUrl(motionUrl), TIMELINE_FPS);
|
|
2525
|
-
if (retimed.sourceBytes) retimed.motionId = await sha256Hex(retimed.sourceBytes);
|
|
2526
|
-
const decoded = applyMotionCalibration(retimed, { ...normalizedCalibration, yawDeg: 0, offsetX: 0, offsetZ: 0 }).motion;
|
|
2527
|
-
const clip = {
|
|
2528
|
-
...decoded,
|
|
2529
|
-
url: motionUrl,
|
|
2530
|
-
prompt: job.prompt,
|
|
2531
|
-
anchorX: sceneAnchorX,
|
|
2532
|
-
anchorZ: sceneAnchorZ,
|
|
2533
|
-
anchorFrame: 0,
|
|
2534
|
-
rotationDeg: sceneRotationDeg,
|
|
2535
|
-
sceneCalibration: normalizedCalibration,
|
|
2536
|
-
editSegments: createMotionEdit(decoded.frames),
|
|
2537
|
-
};
|
|
2538
|
-
if (calibration && typeof calibration === "object") clip.sceneCalibration = normalizedCalibration;
|
|
2539
|
-
// Same stature rule as loadMotion, on the layer that asked for the clip.
|
|
2540
|
-
const scale = characterScaleFor(decoded);
|
|
2541
|
-
const apply = () => {
|
|
2542
|
-
if (appContext.storeDomain('motion')) return commitLoadedTake(job.charId, clip, { job });
|
|
2543
|
-
appContext.shared.motionFullRef.current.set(job.charId, clip);
|
|
2544
|
-
const next = appContext.live.characters.map(entry => entry.id === job.charId ? { ...entry, scale, sessionMotion: clip, motionRef } : entry);
|
|
2545
|
-
appContext.shared.publishStudioCharacters(next);
|
|
2546
|
-
};
|
|
2547
|
-
if (job.commandContext) job.commandContext.commit(apply);
|
|
2548
|
-
else if (appContext.storeDomain('motion')) appContext.storeDomain('motion').runPrepared(job.charId, apply);
|
|
2549
|
-
else apply();
|
|
2550
|
-
}
|
|
2551
|
-
|
|
2552
|
-
/** After a scene (re)load, re-fetch every persisted clip reference and
|
|
2553
|
-
* rebuild the session motions. The bridge may be gone — failures just
|
|
2554
|
-
* leave the character posed, never an error the user must act on. */
|
|
2555
|
-
async function restoreMotionRefs(list) {
|
|
2556
|
-
const epoch = ++appContext.shared.restoreEpochRef.current;
|
|
2557
|
-
const motions = appContext.shared.projectMotionsRef.current;
|
|
2558
|
-
try { const db = await openMotionDb(); const ids = [...new Set(list.map((entry) => entry.motionRef?.motionId?.toLowerCase()).filter(Boolean))]; const cached = await Promise.all(ids.map((id) => getMotion(db, id))); cached.filter(Boolean).forEach((record) => motions.set(record.motionId.toLowerCase(), record)); db.close(); } catch (error) { console.warn("[cozyclay] could not restore motion cache", error); }
|
|
2559
|
-
for (const entry of list) {
|
|
2560
|
-
const source = resolveMotionSource(entry.motionRef, motions);
|
|
2561
|
-
if (!entry.motionRef) continue;
|
|
2562
|
-
if (source.kind === "missing") {
|
|
2563
|
-
appContext.notify(isKo ? `저장된 모션이 누락되었습니다 (${entry.subject || entry.id})` : `Saved motion is missing for ${entry.subject || entry.id}`);
|
|
2564
|
-
continue;
|
|
2565
|
-
}
|
|
2566
|
-
const load = source.kind === "embedded" ? decodeMotionResource(source.record) : loadMotionFromUrl(source.url);
|
|
2567
|
-
load.then((raw) => {
|
|
2568
|
-
if (epoch !== appContext.shared.restoreEpochRef.current) return;
|
|
2569
|
-
// Inbound boundary: a re-fetched clip is retimed exactly like a
|
|
2570
|
-
// freshly generated one, so a reload cannot resurrect 20 fps frames.
|
|
2571
|
-
const sourceUrl = source.kind === "url" ? source.url : entry.motionRef?.url;
|
|
2572
|
-
const retimed = retimeMotion(raw, TIMELINE_FPS);
|
|
2573
|
-
const normalizedCalibration = normalizeMotionCalibration(entry.motionRef.calibration);
|
|
2574
|
-
const decoded = applyMotionCalibration(retimed, { ...normalizedCalibration, yawDeg: 0, offsetX: 0, offsetZ: 0 }).motion;
|
|
2575
|
-
const clip = {
|
|
2576
|
-
...decoded,
|
|
2577
|
-
url: sourceUrl,
|
|
2578
|
-
sourceBytes: raw.sourceBytes,
|
|
2579
|
-
prompt: entry.motionRef.prompt,
|
|
2580
|
-
anchorX: entry.motionRef.anchorX,
|
|
2581
|
-
anchorZ: entry.motionRef.anchorZ,
|
|
2582
|
-
anchorFrame: 0,
|
|
2583
|
-
rotationDeg: entry.motionRef.rotationDeg,
|
|
2584
|
-
sceneCalibration: normalizedCalibration,
|
|
2585
|
-
editSegments: createMotionEdit(decoded.frames),
|
|
2586
|
-
};
|
|
2587
|
-
if (entry.motionRef.calibration) clip.sceneCalibration = entry.motionRef.calibration;
|
|
2588
|
-
if (entry.motionRef.trailEdits) Object.assign(clip, restoreTrailEdits(clip, entry.motionRef.trailEdits));
|
|
2589
|
-
if (entry.motionRef.studioTakeId) clip.studioTakeId = entry.motionRef.studioTakeId;
|
|
2590
|
-
appContext.storeDomain('motion').hydrate(entry.id, clip, entry.motionRef);
|
|
2591
|
-
}).catch((error) => {
|
|
2592
|
-
if (epoch !== appContext.shared.restoreEpochRef.current) return;
|
|
2593
|
-
appContext.shared.setProjectManifest((current) => {
|
|
2594
|
-
const id = entry.motionRef?.motionId?.toLowerCase();
|
|
2595
|
-
if (!id || !current?.items?.some((item) => item.kind === "motion" && item.id === id)) return current;
|
|
2596
|
-
const items = current.items.map((item) => item.kind === "motion" && item.id === id
|
|
2597
|
-
? { ...item, status: "missing", url: undefined }
|
|
2598
|
-
: item);
|
|
2599
|
-
const totals = { embedded: 0, external: 0, missing: 0, bytes: 0 };
|
|
2600
|
-
for (const item of items) {
|
|
2601
|
-
totals[item.status] += 1;
|
|
2602
|
-
if (Number.isFinite(item.bytes)) totals.bytes += item.bytes;
|
|
2603
|
-
}
|
|
2604
|
-
return { items, totals, missing: items.filter((item) => item.status === "missing") };
|
|
2605
|
-
});
|
|
2606
|
-
// A saved take that fails to refetch used to vanish silently — the
|
|
2607
|
-
// user would find a merely posed character and assume their motion
|
|
2608
|
-
// was lost. Name it and offer the reload path.
|
|
2609
|
-
const subject = entry.subject || entry.id;
|
|
2610
|
-
appContext.notify(isKo
|
|
2611
|
-
? `저장된 모션을 다시 불러오지 못했어요 (${subject}) [${error?.code || "decode"}]`
|
|
2612
|
-
: `Saved motion could not be restored for ${subject} [${error?.code || "decode"}]`);
|
|
2613
|
-
});
|
|
2614
|
-
}
|
|
2615
|
-
}
|
|
2616
|
-
|
|
2617
|
-
function cancelArdy() {
|
|
2618
|
-
appContext.shared.ardyAbortRef.current?.abort();
|
|
2619
|
-
}
|
|
2620
|
-
const {
|
|
2621
|
-
chooseMultiModelFile,
|
|
2622
|
-
pasteMultiModelUrl,
|
|
2623
|
-
useMultiModelUrl,
|
|
2624
|
-
ingestPlatformFootage,
|
|
2625
|
-
ingestFootage,
|
|
2626
|
-
extractMultiModelMotion,
|
|
2627
|
-
extractMultiModelMotionGpu,
|
|
2628
|
-
deliverExtraTakes,
|
|
2629
|
-
captureI2vStill,
|
|
2630
|
-
enterI2vFraming,
|
|
2631
|
-
markI2vPose,
|
|
2632
|
-
clearI2vPose,
|
|
2633
|
-
clearI2vMotion,
|
|
2634
|
-
restoreI2vCamera,
|
|
2635
|
-
framingDistance,
|
|
2636
|
-
showI2vMotionLock,
|
|
2637
|
-
generateI2vMotion,
|
|
2638
|
-
i2vMotionUnavailable,
|
|
2639
|
-
generateI2vMotionFromUi,
|
|
2640
|
-
updateI2vMotionQuota,
|
|
2641
|
-
} = createMotionVideoCapture({
|
|
2642
|
-
appContext,
|
|
2643
|
-
bridge,
|
|
2644
|
-
i2vMotion,
|
|
2645
|
-
setI2vMotion,
|
|
2646
|
-
multiModelUrl,
|
|
2647
|
-
multiModelSource,
|
|
2648
|
-
multiModelFootage,
|
|
2649
|
-
multiModelExtract,
|
|
2650
|
-
setMultiModelUrl,
|
|
2651
|
-
setMultiModelSource,
|
|
2652
|
-
setMultiModelStatus,
|
|
2653
|
-
setMultiModelStage,
|
|
2654
|
-
setMultiModelProgress,
|
|
2655
|
-
setMultiModelFootage,
|
|
2656
|
-
setMultiModelError,
|
|
2657
|
-
setMultiModelTake,
|
|
2658
|
-
setMultiModelExtract,
|
|
2659
|
-
setMultiModelExtractProgress,
|
|
2660
|
-
setMultiModelExtractError,
|
|
2661
|
-
loadMotion,
|
|
2662
|
-
authoredSupportDescriptors,
|
|
2663
|
-
applyAuthoredSupportRise,
|
|
2664
|
-
});
|
|
2665
|
-
domain.setVideoDraft = patch => setI2vMotion(current => ({ ...current, ...patch }));
|
|
2666
|
-
domain.requestLineEdit = runLineEdit;
|
|
2667
|
-
domain.requestTrailRegeneration = runTrailRegeneration;
|
|
2668
|
-
domain.generate = generateMotion;
|
|
2669
|
-
domain.isGenerating = () => appContext.shared.generationPendingRef.current || appContext.shared.genRunningRef.current || ardyRunning;
|
|
2670
|
-
domain.onPhysicsRunning = setAutoPhysicsRunning;
|
|
2671
|
-
domain.onPhysicsProgress = setPhysicsProgress;
|
|
2672
|
-
domain.onPhysicsPreview = result => { setPhysicsPreview(result); setPhysicsShow(true); setIkTick(value => value + 1); };
|
|
2673
|
-
domain.platformFitRun = platformFitRun;
|
|
2674
|
-
domain.platformFitRemove = platformFitRemove;
|
|
2675
|
-
domain.beginPlayback = id => { const rig = appContext.shared.rigs[id]; if (rig) beginPlaybackOn(rig); };
|
|
2676
|
-
domain.loadRemote = async (args, context) => {
|
|
2677
|
-
const entry = domain.layer(args.characterId).takeVersions.find(version => version.motionUrl === args.motionUrl);
|
|
2678
|
-
if (args.motionUrl && !entry) throw new StudioProtocolError('TARGET_NOT_READY', 'This take version is no longer available.');
|
|
2679
|
-
if (args.drop != null && !normalizeRootDrop(args.drop)) throw new StudioProtocolError('INVALID_ARGUMENT', 'Invalid drop.');
|
|
2680
|
-
const toTimeline = frame => Math.round(frame * TIMELINE_FPS / ARDY_FPS);
|
|
2681
|
-
const clips = args.blocks?.length ? args.blocks.map(block => {
|
|
2682
|
-
if (block.endFrame <= block.startFrame) throw new StudioProtocolError('INVALID_RANGE', 'Prompt blocks must have a positive range.');
|
|
2683
|
-
return { id: crypto.randomUUID(), startFrame: toTimeline(block.startFrame), endFrame: toTimeline(block.endFrame), text: block.prompt };
|
|
2684
|
-
}) : null;
|
|
2685
|
-
const character = appContext.storeDomain('cast').read().find(row => row.id === args.characterId);
|
|
2686
|
-
const commandContext = !clips ? context : { ...context, commit: apply => context.commit(() => {
|
|
2687
|
-
const result = apply();
|
|
2688
|
-
appContext.recordAction('cast', () => appContext.storeDomain('cast').extendTimeline(Math.max(...clips.map(clip => clip.endFrame))), null, true);
|
|
2689
|
-
return result;
|
|
2690
|
-
}) };
|
|
2691
|
-
await loadMotion(args.url ?? args.motionUrl, args.prompt ?? entry?.recipe?.blocks?.map(block => block.prompt).join(' ') ?? domain.motionFor(args.characterId)?.prompt ?? '', character.rot,
|
|
2692
|
-
args.drop ?? null, args.characterId, clips, { commandContext, recipe: entry?.recipe });
|
|
2693
|
-
};
|
|
934
|
+
|
|
2694
935
|
appContext.updateActionPorts({ clearMotionNative: clearMotion, setCharacterIkKey, removeCharacterIkKey, clearCharacterIkKeys });
|
|
2695
936
|
return {
|
|
2696
937
|
...domain,
|
|
2697
|
-
i2vMotion, setI2vMotion, captureI2vStill, enterI2vFraming, markI2vPose, clearI2vPose, clearI2vMotion, restoreI2vCamera, framingDistance, showI2vMotionLock, generateI2vMotion, i2vMotionUnavailable, generateI2vMotionFromUi, updateI2vMotionQuota,
|
|
2698
938
|
ikMode, ikChains, setIkChains, ikFkJoints, setIkFkJoints, ikFocus, setIkFocus, footSnap, setFootSnap,
|
|
2699
|
-
bodyContact, setBodyContact, IK_CORRECTION_BLEND_FRAMES,
|
|
2700
|
-
|
|
2701
|
-
ikTick, setIkTick, committedIkEdits, setCommittedIkEdits, trailFalloffS, setTrailFalloffS, showTrails,
|
|
2702
|
-
setShowTrails, ikEditTool, setIkEditTool, trailEdit, trailTrackFocus, selectTrailTrack, clearTrailTrackFocus, trailFalloffFrames, focusIkHandle, snapshotIkKeys,
|
|
939
|
+
bodyContact, setBodyContact, IK_CORRECTION_BLEND_FRAMES,
|
|
940
|
+
ikTick, setIkTick, committedIkEdits, setCommittedIkEdits, ikEditTool, setIkEditTool, focusIkHandle, snapshotIkKeys,
|
|
2703
941
|
pendingIkEdit, applyPendingIkEdit, cancelPendingIkEdit,
|
|
2704
942
|
rangePinApplySpec: (pin) => {
|
|
2705
943
|
domain.run('motion.rangePin.apply', { characterId: appContext.shared.activeChar.id, pin });
|
|
2706
944
|
return { residuals: decodeRangePinResiduals(domain.layer(appContext.shared.activeChar.id).ikPinResiduals).get(pin.id) };
|
|
2707
945
|
},
|
|
2708
946
|
rangePins, rangePinResiduals, rangePinSelection, setRangePinSelection, rangePinPartPick, setRangePinPartPick, rangePinPreview, previewRangePinDraft, applyRangePinDraft, deleteRangePin,
|
|
2709
|
-
setCharacterIkKey, removeCharacterIkKey, clearCharacterIkKeys,
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
multiModelUrl, setMultiModelUrl, multiModelSource, setMultiModelSource, multiModelStatus,
|
|
2714
|
-
multiModelStage, multiModelProgress, multiModelFootage, multiModelError, multiModelTake,
|
|
2715
|
-
multiModelExtract, multiModelExtractProgress, multiModelExtractError, advanceFrame, stepFrame,
|
|
2716
|
-
leaveIkMode, beginPlaybackOn, chooseMultiModelFile, pasteMultiModelUrl, useMultiModelUrl, ingestFootage,
|
|
2717
|
-
extractMultiModelMotion, deliverExtraTakes, loadMotion, clearMotion: () => appContext.bus.run('motion.clear', { characterId: appContext.shared.activeChar.id }), clearMotionNative: clearMotion, applyMotionTrim, resetMotionTrim, cutMotionAtPlayhead,
|
|
2718
|
-
changeMotionSegmentSpeed, removeMotionSegmentById, poseOtherCastMembers, toggleIkMode, ikSolve,
|
|
2719
|
-
ikDragEnd, ikAddKeyframe, externalBlockers, runFixCollisions, runFixCollisionsRange,
|
|
2720
|
-
changePhysicsOptions, showPhysicsPreview, cancelPhysicsPreview, applyPhysicsPreview, runAutoPhysics,
|
|
2721
|
-
platformFitRun, platformFitApply, platformFitRemove, platformFitRunning, platformFitLast, platformFitProgress, platformFitApplied,
|
|
2722
|
-
ikDeleteKeyframe, ikApplyPoseAsKey, recheckMotionHealth, changeArdySeed, takeSeed,
|
|
2723
|
-
runLineEdit: () => domain.run('motion.commitLineEdit', { characterId: appContext.shared.activeChar.id }),
|
|
2724
|
-
runAllPromptBlocks, runArdy, onTrailDragStart, onTrailDragPreview, onTrailDragEnd,
|
|
2725
|
-
runTrailRegeneration: () => domain.run('motion.regenerateTrail', { characterId: appContext.shared.activeChar.id }),
|
|
2726
|
-
genQueue, setGenQueue, executeMotionJob, seedLoadedTake, loadTakeVersion, selectedMotionReadiness,
|
|
2727
|
-
generationBusy, openMotionSetup, refineDisabledReason, sceneDisabledReason, sceneGenerateDisabledReason,
|
|
2728
|
-
sceneAgainDisabledReason, enterRefineMode, runSceneAgain, addSceneBlock, restoreMotionRefs, cancelArdy,
|
|
947
|
+
setCharacterIkKey, removeCharacterIkKey, clearCharacterIkKeys, motion,
|
|
948
|
+
advanceFrame, stepFrame,
|
|
949
|
+
leaveIkMode, clearMotion: () => appContext.bus.run('motion.clear', { characterId: appContext.shared.activeChar.id }), clearMotionNative: clearMotion,
|
|
950
|
+
poseOtherCastMembers, toggleIkMode, ikSolve, ikDragEnd, ikAddKeyframe, ikDeleteKeyframe, ikApplyPoseAsKey,
|
|
2729
951
|
};
|
|
2730
952
|
}
|