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/App.jsx
CHANGED
|
@@ -6,6 +6,7 @@ import { useScenes } from "./domains/scenes.js";
|
|
|
6
6
|
import { useStage } from "./domains/stage.js";
|
|
7
7
|
import StudioShell from "./shell/StudioShell.jsx";
|
|
8
8
|
import ViewportToolbar from "./shell/ViewportToolbar.jsx";
|
|
9
|
+
import CompositionGuideButton from "./shell/CompositionGuideButton.jsx";
|
|
9
10
|
import { StudioShellContext } from "./shell/studio-shell-context.js";
|
|
10
11
|
|
|
11
12
|
import {
|
|
@@ -24,24 +25,8 @@ import {
|
|
|
24
25
|
} from "@react-three/drei";
|
|
25
26
|
import * as THREE from "three";
|
|
26
27
|
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
loadMotionFromUrl,
|
|
30
|
-
} from "./ardy/npz.js";
|
|
31
|
-
|
|
32
|
-
import { motionUrlFromQuery } from "./ardy/motion-url.js";
|
|
33
|
-
|
|
34
|
-
import {
|
|
35
|
-
applyMotionFrame,
|
|
36
|
-
restorePlaybackBones,
|
|
37
|
-
snapshotPlaybackBones,
|
|
38
|
-
} from "./ardy/playback.js";
|
|
39
|
-
|
|
40
|
-
import {
|
|
41
|
-
TRAIL_EFFECTOR_JOINTS,
|
|
42
|
-
jointTrailPoints,
|
|
43
|
-
worldPointToClip,
|
|
44
|
-
} from "./motion-trail.js";
|
|
28
|
+
import { restorePlaybackBones, snapshotPlaybackBones } from "./motion/rig-snapshot.js";
|
|
29
|
+
import { applyTakeFrame } from "./motion3/playback.js";
|
|
45
30
|
|
|
46
31
|
import { FlyControls, aimAt, forwardFrom } from "./controls.jsx";
|
|
47
32
|
import { createLiveControl, loadLiveWorkspaceId, mintLiveWorkspaceId } from "./live-control.js";
|
|
@@ -60,7 +45,6 @@ import { useFirstSuccessPrompt } from "./use-first-success-prompt.js";
|
|
|
60
45
|
import { useCameraTutorial } from "./use-camera-tutorial.js";
|
|
61
46
|
import { useEmbedBridge } from "./use-embed-bridge.js";
|
|
62
47
|
import { useAssetShelf } from "./use-asset-shelf.js";
|
|
63
|
-
import { useI2vCaptureModal } from "./use-i2v-capture-modal.js";
|
|
64
48
|
import { StudioProtocolError } from "./studio-agent-protocol.js";
|
|
65
49
|
import { elementByPath } from "./studio-elements.js";
|
|
66
50
|
import {
|
|
@@ -78,59 +62,15 @@ import { drawPlanDiagram } from "./plan-diagram.js";
|
|
|
78
62
|
import { placementAt } from "./root-path.js";
|
|
79
63
|
import { proxyYaw } from "./proxy-figure.jsx";
|
|
80
64
|
import { autoColorHex, loadAutoColor } from "./auto-color.js";
|
|
81
|
-
import { DualRender,
|
|
65
|
+
import { DualRender, GIZMO_LAYER } from "./dualview.jsx";
|
|
82
66
|
import { GridFloor } from "./grid-floor.jsx";
|
|
83
|
-
import {
|
|
67
|
+
import { readStoredGridView, writeStoredGridView } from "./grid-view.js";
|
|
84
68
|
import { useUiTheme } from "./theme.js";
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
DRAG_WEIGHT_EPSILON,
|
|
91
|
-
DRAW_MIN_STROKE_POINTS,
|
|
92
|
-
DRAW_MIN_STROKE_PX,
|
|
93
|
-
MAX_LINE_POINTS,
|
|
94
|
-
MIN_LINE_POINTS,
|
|
95
|
-
cameraDrifted,
|
|
96
|
-
cameraToC6,
|
|
97
|
-
changedFrameRange,
|
|
98
|
-
curveToPoints2d,
|
|
99
|
-
curvesEqual,
|
|
100
|
-
sliceCurveToRange,
|
|
101
|
-
dragCurve,
|
|
102
|
-
dragWeight,
|
|
103
|
-
isCurveEndPinned,
|
|
104
|
-
isCurvePointOnScreen,
|
|
105
|
-
nearestCurvePoint,
|
|
106
|
-
projectTrailCurve,
|
|
107
|
-
projectPointC6,
|
|
108
|
-
reprojectCurveWorld,
|
|
109
|
-
drawStrokeEdit,
|
|
110
|
-
unprojectDeltaC6,
|
|
111
|
-
pinsFrameRange,
|
|
112
|
-
upsertPin,
|
|
113
|
-
validateLineEdit,
|
|
114
|
-
} from "./line-edit.js";
|
|
115
|
-
|
|
116
|
-
import { Room, StageLights } from "./room.jsx";
|
|
117
|
-
import {
|
|
118
|
-
createShotAuthoringDocument,
|
|
119
|
-
} from "./shot-authoring.js";
|
|
120
|
-
import {
|
|
121
|
-
buildFollowTrack,
|
|
122
|
-
buildRail,
|
|
123
|
-
buildRailFollowTrack,
|
|
124
|
-
simplifyStroke,
|
|
125
|
-
} from "./camera-follow.js";
|
|
126
|
-
import {
|
|
127
|
-
createCameraBlock,
|
|
128
|
-
} from "./camera-block.js";
|
|
129
|
-
import {
|
|
130
|
-
RAIL_SCHEDULE_LEGACY,
|
|
131
|
-
RAIL_SCHEDULE_RANGE,
|
|
132
|
-
resolveRailSchedule,
|
|
133
|
-
} from "./camera-rail-schedule.js";
|
|
69
|
+
|
|
70
|
+
import { Room, StageLights, StageAtmosphere } from "./room.jsx";
|
|
71
|
+
import { createDocumentMirrors } from "./document-mirrors.js";
|
|
72
|
+
import { buildRail, simplifyStroke } from "./camera-follow.js";
|
|
73
|
+
import { useShotCameraTracks } from "./use-shot-camera-tracks.js";
|
|
134
74
|
import { ObjectCarrier, SetProps } from "./props.jsx";
|
|
135
75
|
import {
|
|
136
76
|
CUTOUT_KIND,
|
|
@@ -171,7 +111,6 @@ import {
|
|
|
171
111
|
} from "./scenes.js";
|
|
172
112
|
import {
|
|
173
113
|
hasFileSystemAccess,
|
|
174
|
-
loadProjectSession,
|
|
175
114
|
loadStoredProjectHandle,
|
|
176
115
|
queryHandlePermission,
|
|
177
116
|
requestHandlePermission,
|
|
@@ -185,17 +124,14 @@ import { CameraTutorial } from "./camera-tutorial.jsx";
|
|
|
185
124
|
import { createTutorialAnalytics } from "./tutorial-analytics.js";
|
|
186
125
|
import { cameraTutorialSuppressed, createFirstShotHandoff, rememberCameraTutorialTerminal } from "./first-shot-handoff.js";
|
|
187
126
|
import ObjectGizmo from "./object-gizmo.jsx";
|
|
127
|
+
import { useHeldObjectEdit } from "./held-object-edit.jsx";
|
|
188
128
|
|
|
189
129
|
import {
|
|
190
130
|
SaveBlockedDialog,
|
|
191
131
|
} from "./resource-status.jsx";
|
|
192
132
|
|
|
193
133
|
import ResultModal from "./result-modal.jsx";
|
|
194
|
-
import {
|
|
195
|
-
I2vMotionModal,
|
|
196
|
-
} from "./i2v-motion-studio.jsx";
|
|
197
134
|
|
|
198
|
-
import { demoSeedGate, hasLineEditCapability, motionReadiness } from "./motion-readiness.js";
|
|
199
135
|
|
|
200
136
|
import { PWA_UPDATE_EVENT } from "./pwa.js";
|
|
201
137
|
import {
|
|
@@ -205,7 +141,7 @@ import {
|
|
|
205
141
|
pathMetrics,
|
|
206
142
|
strokeToPathPoints,
|
|
207
143
|
} from "./object-path.js";
|
|
208
|
-
import { carriedPointAt, sceneObjectsAt } from "./object-travel.js";
|
|
144
|
+
import { carriedPointAt, sceneObjectsAt } from "./object-travel.js"; import { objectTracksOf } from "./motion3/object-tracks.js"; import { tutorialWalkSpec } from "./tutorial-walk.js"; import { ViewGhosts } from "./view-ghosts.jsx"; import { DraftGhost } from "./draft-ghost.jsx";
|
|
209
145
|
import { routeOwnerFor } from "./route-owner.js";
|
|
210
146
|
import {
|
|
211
147
|
analyticsActive,
|
|
@@ -242,19 +178,7 @@ import {
|
|
|
242
178
|
ikSeedTargets,
|
|
243
179
|
resolveIkRig,
|
|
244
180
|
shareContactMeasurements,
|
|
245
|
-
} from "./
|
|
246
|
-
import {
|
|
247
|
-
buildCollisionCapsules,
|
|
248
|
-
detectPenetrations,
|
|
249
|
-
supportsCollisionCleanup,
|
|
250
|
-
} from "./ardy/fix-collisions.js";
|
|
251
|
-
import {
|
|
252
|
-
blockerSummary,
|
|
253
|
-
} from "./ardy/collision-blockers.js";
|
|
254
|
-
import { computeCenterOfMass, markerPositions } from "./ardy/auto-physics.js";
|
|
255
|
-
import {
|
|
256
|
-
physicsKeyStamp,
|
|
257
|
-
} from "./ardy/physics-review.js";
|
|
181
|
+
} from "./motion/ik.js";
|
|
258
182
|
|
|
259
183
|
import {
|
|
260
184
|
Toast,
|
|
@@ -275,9 +199,7 @@ import { exportOffscreenVideo } from "./offscreen-export.js";
|
|
|
275
199
|
import { parseRigNodeId } from "./hierarchy-model.js";
|
|
276
200
|
import { timelineContentExtent } from "./timeline-extent.js";
|
|
277
201
|
import {
|
|
278
|
-
GUIDE_LABELS,
|
|
279
202
|
guideGeometry,
|
|
280
|
-
nextGuideMode,
|
|
281
203
|
readStoredGuideMode,
|
|
282
204
|
writeStoredGuideMode,
|
|
283
205
|
} from "./shot-guides.js";
|
|
@@ -288,7 +210,6 @@ import { buildZip } from "./zip-store.js";
|
|
|
288
210
|
import { composeStoryboard } from "./storyboard.js";
|
|
289
211
|
import { DEPTH_RANGE_M, depthRangeFromFrames, passFileName, renderPass } from "./render-passes.js";
|
|
290
212
|
|
|
291
|
-
import { motionApiOrigin, I2V_MOTION_SHOT_ASPECT } from "./i2v-motion-client.js";
|
|
292
213
|
import { serializeOtio } from "./otio.js";
|
|
293
214
|
import "./shell/mode.css";
|
|
294
215
|
import { readPoseFadeOpacity, withPoseFadeLifted, writePoseFadeOpacity } from "./pose-fade.js";
|
|
@@ -297,7 +218,6 @@ import {
|
|
|
297
218
|
shotIndexAtFrame,
|
|
298
219
|
} from "./cuts.js";
|
|
299
220
|
import {
|
|
300
|
-
BRIDGE_RECHECK_MS,
|
|
301
221
|
CameraGlide,
|
|
302
222
|
CameraRailScenePreview,
|
|
303
223
|
CaptureRig,
|
|
@@ -306,20 +226,13 @@ import {
|
|
|
306
226
|
CraneHandles,
|
|
307
227
|
DEFAULT_CAMERA_POSITION,
|
|
308
228
|
DEFAULT_DURATION_S,
|
|
309
|
-
DEMO_MOTION_PROMPT,
|
|
310
|
-
DEMO_MOTION_URL,
|
|
311
229
|
EditorCamSeed,
|
|
312
230
|
FollowCamRig,
|
|
313
231
|
GIZMO_HOTKEYS,
|
|
314
232
|
KeyLightPuck,
|
|
315
|
-
LINE_CURVE_MARKER_STRIDE,
|
|
316
|
-
LINE_CURVE_REFUSALS,
|
|
317
|
-
LINE_EDIT_DEFAULT_TRACK,
|
|
318
|
-
LINE_EDIT_REFUSALS,
|
|
319
|
-
LINE_PREVIEW_DEBOUNCE_MS,
|
|
320
233
|
MCP_CAPTURE_H,
|
|
321
234
|
MCP_CAPTURE_W,
|
|
322
|
-
|
|
235
|
+
|
|
323
236
|
MoveRig,
|
|
324
237
|
OBJECT_DELETE_UNDO_MS,
|
|
325
238
|
ObjectPathHandles,
|
|
@@ -334,15 +247,12 @@ import {
|
|
|
334
247
|
ViewportLayoutInvalidator,
|
|
335
248
|
WORKSPACE_LAYOUT_KEY,
|
|
336
249
|
attachFrameMatrix,
|
|
337
|
-
buildPromptSchedule,
|
|
338
250
|
captureMcpFrame,
|
|
339
251
|
characterModelUrl,
|
|
340
252
|
defaultCharacterTint,
|
|
341
|
-
ikTracksInRange,
|
|
342
253
|
loadSceneStartup,
|
|
343
254
|
loadWorkspaceLayout,
|
|
344
255
|
moveSequenceSlateKo,
|
|
345
|
-
preserveTracksSummary,
|
|
346
256
|
slateLineKo,
|
|
347
257
|
useStageFilesDrop,
|
|
348
258
|
} from "./app-stage.jsx";
|
|
@@ -376,10 +286,6 @@ function ShotGuideOverlay({ mode, aspect, className = "" }) {
|
|
|
376
286
|
);
|
|
377
287
|
}
|
|
378
288
|
|
|
379
|
-
// Below this the mean landmark visibility is too low to claim the fit measured
|
|
380
|
-
// the photograph rather than guessed at it. Same number the fit diagnostics are
|
|
381
|
-
// scaled on (0..1 visibility), so it reads as "less than half seen".
|
|
382
|
-
const PHOTO_POSE_LOW_CONFIDENCE = 0.5;
|
|
383
289
|
const CHARACTER_POSITION_BOUNDS = elementByPath("character.position").gizmo;
|
|
384
290
|
const CHARACTER_SCALE_BOUNDS = elementByPath("character.scale");
|
|
385
291
|
|
|
@@ -411,42 +317,12 @@ function storyboardCaption(prompt) {
|
|
|
411
317
|
.join(" · "));
|
|
412
318
|
}
|
|
413
319
|
|
|
414
|
-
// cskel27 joint names are rig vocabulary — "RightForeArm" means nothing to
|
|
415
|
-
// someone holding a photograph. Every joint collapses into one of six groups a
|
|
416
|
-
// viewer can check against their own picture, ordered so the sentence always
|
|
417
|
-
// reads limbs before body.
|
|
418
|
-
const RELEASED_BONE_LABELS = [
|
|
419
|
-
["LeftArm", "left arm", "왼팔"],
|
|
420
|
-
["RightArm", "right arm", "오른팔"],
|
|
421
|
-
["LeftLeg", "left leg", "왼다리"],
|
|
422
|
-
["RightLeg", "right leg", "오른다리"],
|
|
423
|
-
["Torso", "torso", "몸통"],
|
|
424
|
-
["Head", "head", "머리"],
|
|
425
|
-
];
|
|
426
|
-
|
|
427
|
-
function releasedBoneGroup(name) {
|
|
428
|
-
const side = name.startsWith("Left") ? "Left" : name.startsWith("Right") ? "Right" : "";
|
|
429
|
-
const part = side ? name.slice(side.length) : name;
|
|
430
|
-
if (part === "UpLeg" || part === "Leg" || part === "Foot" || part === "ToeBase") return side + "Leg";
|
|
431
|
-
if (part === "Shoulder" || part === "Arm" || part === "ForeArm" || part === "Hand" || part === "HandEnd") return side + "Arm";
|
|
432
|
-
if (part === "Head" || part === "Neck") return "Head";
|
|
433
|
-
// Hips and the Spine chain are the only names left, and they are the torso.
|
|
434
|
-
return "Torso";
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
// 이/가 is fixed by the final syllable of the word it follows — 왼팔이 but
|
|
438
|
-
// 왼다리가 — so it cannot be baked into the sentence template.
|
|
439
|
-
function koSubjectParticle(word) {
|
|
440
|
-
const syllable = word.charCodeAt(word.length - 1) - 0xac00;
|
|
441
|
-
return syllable >= 0 && syllable < 11172 && syllable % 28 !== 0 ? "이" : "가";
|
|
442
|
-
}
|
|
443
|
-
|
|
444
320
|
/**
|
|
445
321
|
* Pose ONE cast member's rig at an absolute timeline frame: its own clip first,
|
|
446
322
|
* then its own IK correction layer on top. This is the single description of
|
|
447
323
|
* "where is this character at frame N" — the viewport effect, the offscreen
|
|
448
|
-
* recorder and the
|
|
449
|
-
*
|
|
324
|
+
* recorder and the range-pin bake all go through it, so a body stands exactly
|
|
325
|
+
* where the render would draw it.
|
|
450
326
|
*
|
|
451
327
|
* Pure in the sense that matters here: it takes the rig, the clip and the layer
|
|
452
328
|
* state and writes bones. No React, no refs, no knowledge of who is active — a
|
|
@@ -460,7 +336,7 @@ function poseMemberAtFrame(rig, clip, ikState, frame, blendFrames = 0, pose = nu
|
|
|
460
336
|
if (!rig) return;
|
|
461
337
|
if (clip) {
|
|
462
338
|
const sampled = sampleAt({ frameCount: clip.frames, motion: clip }, null, frame);
|
|
463
|
-
|
|
339
|
+
applyTakeFrame(rig, clip, sampled.motionFrame);
|
|
464
340
|
} else if (pose) {
|
|
465
341
|
applyPose(rig, pose);
|
|
466
342
|
applyHipsOffset(rig, pose.rootY ?? 0);
|
|
@@ -669,7 +545,7 @@ export default function App() {
|
|
|
669
545
|
get objects() { return storeRef.current.objects; },
|
|
670
546
|
}));
|
|
671
547
|
const {
|
|
672
|
-
preset, shotAspectKey, environmentImage, cameraPresetId, sensorId, keyLight, changeKeyLight,
|
|
548
|
+
preset, shotAspectKey, environmentImage, cameraPresetId, sensorId, keyLight, lighting, changeKeyLight,
|
|
673
549
|
resetKeyLight, changeEnvironmentImage, hasEnvSheet, environment, style, publishStudioStage,
|
|
674
550
|
} = stageDomain;
|
|
675
551
|
const shotsDomain = useShots(appContext.forRender({
|
|
@@ -685,7 +561,7 @@ export default function App() {
|
|
|
685
561
|
get markSemanticEdit() { return markSemanticEdit; },
|
|
686
562
|
get motionPos() { return motionPos; },
|
|
687
563
|
get playMode() { return playMode; },
|
|
688
|
-
|
|
564
|
+
|
|
689
565
|
|
|
690
566
|
get runStudioAction() { return runStudioAction; },
|
|
691
567
|
get selectWorkflowMode() { return selectWorkflowMode; },
|
|
@@ -705,9 +581,8 @@ export default function App() {
|
|
|
705
581
|
get executeExportRequest() { return executeExportRequest; },
|
|
706
582
|
get exportRequest() { return exportRequest; },
|
|
707
583
|
get motion() { return motion; },
|
|
708
|
-
get multiModelFootage() { return multiModelFootage; },
|
|
709
584
|
get playgroundMode() { return playgroundMode; },
|
|
710
|
-
|
|
585
|
+
|
|
711
586
|
get recRef() { return recRef; },
|
|
712
587
|
get runShotExport() { return runShotExport; },
|
|
713
588
|
get setCameraPos() { return setCameraPos; },
|
|
@@ -1105,25 +980,21 @@ export default function App() {
|
|
|
1105
980
|
get committedIkEdits() { return committedIkEdits; },
|
|
1106
981
|
get environment() { return environment; },
|
|
1107
982
|
get environmentImage() { return environmentImage; },
|
|
1108
|
-
get exitLineEditMode() { return exitLineEditMode; },
|
|
1109
983
|
get frameCountRef() { return frameCountRef; },
|
|
1110
984
|
get gestureUndoRef() { return gestureUndoRef; },
|
|
1111
985
|
get hasEnvSheet() { return hasEnvSheet; },
|
|
1112
986
|
get ikStateRef() { return ikStateRef; },
|
|
1113
987
|
get ikStatesRef() { return ikStatesRef; },
|
|
1114
988
|
get keyLight() { return keyLight; },
|
|
1115
|
-
get lineEditMode() { return lineEditMode; },
|
|
1116
989
|
get loadedLayerCharRef() { return loadedLayerCharRef; },
|
|
1117
990
|
get markSemanticEdit() { return markSemanticEdit; },
|
|
1118
991
|
get motion() { return motion; },
|
|
1119
992
|
get motionFullRef() { return motionFullRef; },
|
|
1120
|
-
|
|
993
|
+
|
|
1121
994
|
get publishStudioCharacters() { return publishStudioCharacters; },
|
|
1122
995
|
get rigReportersRef() { return rigReportersRef; },
|
|
1123
996
|
get rigWaitersRef() { return rigWaitersRef; },
|
|
1124
997
|
get runStudioAction() { return runStudioAction; },
|
|
1125
|
-
get setArdyDuration() { return setArdyDuration; },
|
|
1126
|
-
get setArdyPrompt() { return setArdyPrompt; },
|
|
1127
998
|
get setCommittedIkEdits() { return setCommittedIkEdits; },
|
|
1128
999
|
get setIkTick() { return setIkTick; },
|
|
1129
1000
|
get setSelectedHierarchyId() { return setSelectedHierarchyId; },
|
|
@@ -1160,25 +1031,22 @@ export default function App() {
|
|
|
1160
1031
|
get snapshotExportRig() { return snapshotExportRig; },
|
|
1161
1032
|
get studioBindingRef() { return studioBindingRef; },
|
|
1162
1033
|
get objectsDomain() { return objectsDomain; },
|
|
1163
|
-
|
|
1034
|
+
|
|
1164
1035
|
get shotsDomain() { return shotsDomain; },
|
|
1165
1036
|
}));
|
|
1166
1037
|
const {
|
|
1167
|
-
characters, customPoses, posing,
|
|
1038
|
+
characters, customPoses, posing, posingClosing, studioPick, setStudioPick,
|
|
1168
1039
|
rigs, rigMountEpoch, setRigMountEpoch, setPoseTick, charA, charB, showB, poseA, poseB, subject, subject2,
|
|
1169
1040
|
updateCharacterAt, setShowB, moveCharacter, removeCharacter, reportRig, spawnCharacter,
|
|
1170
1041
|
charKeyToHierarchyId, charIdFromHierarchyId, activeCharacterId, setActiveCharacterId, rowIdForCharIndex,
|
|
1171
1042
|
activeChar, selectActiveCharacterInHierarchy, activeCharIndex, activeRig, waitForRig, ghostLayers,
|
|
1172
|
-
snapshotCast, changeInspectorCharacter, hasCharSheet, setHasCharSheet,
|
|
1173
|
-
|
|
1174
|
-
activeWaypointId, setActiveWaypointId, pendingWaypointFrame, setPendingWaypointFrame,
|
|
1175
|
-
setPromptClips, editPromptClips, selectedPromptId, setSelectedPromptId, photoPoseState, photoPoseError,
|
|
1176
|
-
setPhotoPoseError, allPoses, selectablePoses, posingIndex, posingChar, posedRig, setPosed, rootStart, commitPosedPose,
|
|
1043
|
+
snapshotCast, changeInspectorCharacter, hasCharSheet, setHasCharSheet,
|
|
1044
|
+
waypointMode, setWaypointMode, waypoints, setWaypoints,
|
|
1045
|
+
activeWaypointId, setActiveWaypointId, pendingWaypointFrame, setPendingWaypointFrame, allPoses, selectablePoses, posingIndex, posingChar, posedRig, setPosed, rootStart, commitPosedPose,
|
|
1177
1046
|
queueRootWaypointFrame, castMemberOf, addCharacterWaypoint, moveCharacterWaypoint,
|
|
1178
1047
|
removeCharacterWaypoint, clearCharacterWaypoints, addFloorWaypoint, moveWaypoint, removeWaypoint,
|
|
1179
|
-
toggleWaypointMode, openStudio, closeStudio, saveCurrentPose, savePose,
|
|
1180
|
-
|
|
1181
|
-
removePromptClip,
|
|
1048
|
+
toggleWaypointMode, openStudio, closeStudio, saveCurrentPose, savePose, removePose,
|
|
1049
|
+
|
|
1182
1050
|
} = castDomain;
|
|
1183
1051
|
const { publishStudioCharacters, syncStudioLayerBuffer, undoScene, redoScene, snapshotStudioDomain } = castDomain;
|
|
1184
1052
|
// The cast as of this render, for async handlers: an extraction that
|
|
@@ -1186,28 +1054,6 @@ export default function App() {
|
|
|
1186
1054
|
// not the one its closure captured.
|
|
1187
1055
|
appContext.publishCharacters(characters);
|
|
1188
1056
|
|
|
1189
|
-
const {
|
|
1190
|
-
i2vMotionEnabled,
|
|
1191
|
-
setI2vMotionEnabled,
|
|
1192
|
-
i2vMotionMode,
|
|
1193
|
-
setI2vMotionMode,
|
|
1194
|
-
i2vMotionCameraUnlocked,
|
|
1195
|
-
setI2vMotionCameraUnlocked,
|
|
1196
|
-
} = useI2vCaptureModal();
|
|
1197
|
-
|
|
1198
|
-
useEffect(() => {
|
|
1199
|
-
let cancelled = false;
|
|
1200
|
-
fetch(`${motionApiOrigin()}/v1/motion/me`, { credentials: "include" })
|
|
1201
|
-
.then((response) => response.ok ? response.json() : null)
|
|
1202
|
-
.then((payload) => {
|
|
1203
|
-
if (cancelled || !payload) return;
|
|
1204
|
-
setI2vMotionEnabled(payload.enabled === true);
|
|
1205
|
-
if (Number.isFinite(payload.dailyRemaining)) motionDomain.updateI2vMotionQuota(payload.dailyRemaining);
|
|
1206
|
-
})
|
|
1207
|
-
.catch(() => { if (!cancelled) setI2vMotionEnabled(false); });
|
|
1208
|
-
return () => { cancelled = true; };
|
|
1209
|
-
}, []);
|
|
1210
|
-
|
|
1211
1057
|
// Per-character rig report: stable callback identity per character so
|
|
1212
1058
|
// the Character effect does not re-fire on every App render.
|
|
1213
1059
|
const rigReportersRef = useRef(new Map());
|
|
@@ -1266,66 +1112,46 @@ export default function App() {
|
|
|
1266
1112
|
|
|
1267
1113
|
const motionDomain = useMotion(appContext.forRender({
|
|
1268
1114
|
get attachFrameRef() { return attachFrameRef; },
|
|
1269
|
-
|
|
1115
|
+
|
|
1270
1116
|
get activeChar() { return activeChar; },
|
|
1271
1117
|
get activeCharIndex() { return activeCharIndex; },
|
|
1272
1118
|
get activeRig() { return activeRig; },
|
|
1273
|
-
|
|
1274
|
-
get ardyAbortRef() { return ardyAbortRef; },
|
|
1275
|
-
get autoPhysicsRunRef() { return autoPhysicsRunRef; },
|
|
1276
|
-
get bridgeRefreshRef() { return bridgeRefreshRef; },
|
|
1119
|
+
|
|
1277
1120
|
get bufferRef() { return bufferRef; },
|
|
1278
|
-
get buildLineEditRequest() { return buildLineEditRequest; },
|
|
1279
1121
|
get cameraPreviewEndRef() { return cameraPreviewEndRef; },
|
|
1280
|
-
get cancelLinePreview() { return cancelLinePreview; },
|
|
1281
1122
|
get castDomain() { return castDomain; },
|
|
1282
1123
|
get castMemberOf() { return castMemberOf; },
|
|
1283
1124
|
get charA() { return charA; },
|
|
1284
1125
|
get characters() { return characters; },
|
|
1285
|
-
get clearLineEdit() { return clearLineEdit; },
|
|
1286
|
-
get collisionCleanupSupported() { return collisionCleanupSupported; },
|
|
1287
|
-
get exitLineEditMode() { return exitLineEditMode; },
|
|
1288
1126
|
get fovDeg() { return fovDeg; },
|
|
1289
1127
|
get frameCountRef() { return frameCountRef; },
|
|
1290
|
-
get genJobSeq() { return genJobSeq; },
|
|
1291
|
-
get genRunningRef() { return genRunningRef; },
|
|
1292
|
-
get generationPendingRef() { return generationPendingRef; },
|
|
1293
1128
|
get ikBodyDragRef() { return ikBodyDragRef; },
|
|
1294
1129
|
get ikFrames() { return ikFrames; },
|
|
1295
1130
|
get ikStateRef() { return ikStateRef; },
|
|
1296
1131
|
get ikStatesRef() { return ikStatesRef; },
|
|
1297
|
-
get lineEditMode() { return lineEditMode; },
|
|
1298
|
-
get lineEditPayload() { return lineEditPayload; },
|
|
1299
|
-
get linePreviewSource() { return linePreviewSource; },
|
|
1300
|
-
get linePreviewUrl() { return linePreviewUrl; },
|
|
1301
|
-
get lineTrack() { return lineTrack; },
|
|
1302
1132
|
get loadedLayerCharRef() { return loadedLayerCharRef; },
|
|
1303
1133
|
get look() { return look; },
|
|
1304
1134
|
get markSemanticEdit() { return markSemanticEdit; },
|
|
1305
1135
|
get motionFullRef() { return motionFullRef; },
|
|
1306
|
-
get multiModelObjectUrlRef() { return multiModelObjectUrlRef; },
|
|
1307
|
-
get multiModelRunRef() { return multiModelRunRef; },
|
|
1308
|
-
get physicsJobRef() { return physicsJobRef; },
|
|
1309
|
-
get physicsSourceCacheRef() { return physicsSourceCacheRef; },
|
|
1310
1136
|
get poseMemberAtFrame() { return poseMemberAtFrame; },
|
|
1311
1137
|
get posedRig() { return posedRig; },
|
|
1312
1138
|
get poserCamRef() { return poserCamRef; },
|
|
1313
1139
|
get poserLook() { return poserLook; },
|
|
1314
1140
|
get posing() { return posing; },
|
|
1315
1141
|
get posingChar() { return posingChar; },
|
|
1316
|
-
|
|
1317
|
-
|
|
1142
|
+
|
|
1143
|
+
|
|
1318
1144
|
get publishStudioCharacters() { return publishStudioCharacters; },
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1145
|
+
|
|
1146
|
+
|
|
1147
|
+
|
|
1322
1148
|
get rigs() { return rigs; },
|
|
1323
1149
|
get rowIdForCharIndex() { return rowIdForCharIndex; },
|
|
1324
1150
|
get runStudioAction() { return runStudioAction; },
|
|
1325
1151
|
get sceneObjects() { return sceneObjects; },
|
|
1326
1152
|
get selectActiveCharacterInHierarchy() { return selectActiveCharacterInHierarchy; },
|
|
1327
1153
|
get setProjectManifest() { return setProjectManifest; },
|
|
1328
|
-
|
|
1154
|
+
|
|
1329
1155
|
get setSelectedHierarchyId() { return setSelectedHierarchyId; },
|
|
1330
1156
|
get setTlFps() { return setTlFps; },
|
|
1331
1157
|
get setTlFrame() { return setTlFrame; },
|
|
@@ -1333,31 +1159,24 @@ export default function App() {
|
|
|
1333
1159
|
get setTlPlaying() { return setTlPlaying; },
|
|
1334
1160
|
get shot() { return shot; },
|
|
1335
1161
|
get shotCamRef() { return shotCamRef; },
|
|
1336
|
-
get takeRecipeRef() { return takeRecipeRef; },
|
|
1337
|
-
get takeSourceUrl() { return takeSourceUrl; },
|
|
1338
1162
|
get tlFps() { return tlFps; },
|
|
1339
1163
|
get tlFrame() { return tlFrame; },
|
|
1340
1164
|
get tlFrameCount() { return tlFrameCount; },
|
|
1341
1165
|
get tlFrameRef() { return tlFrameRef; },
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
get trailPreviewMotionRef() { return trailPreviewMotionRef; },
|
|
1166
|
+
|
|
1167
|
+
|
|
1345
1168
|
get tutorialProjectEpochRef() { return tutorialProjectEpochRef; },
|
|
1346
1169
|
get waitForRig() { return waitForRig; },
|
|
1347
1170
|
get waypointMode() { return waypointMode; },
|
|
1348
1171
|
get waypoints() { return waypoints; },
|
|
1349
1172
|
get captureCurrentFraming() { return captureCurrentFraming; },
|
|
1350
1173
|
get enterShotLook() { return enterShotLook; },
|
|
1351
|
-
get i2vMotionEnabled() { return i2vMotionEnabled; },
|
|
1352
|
-
get i2vMotionSegmentationReady() { return i2vMotionSegmentationReady; },
|
|
1353
1174
|
get captureLiveFraming() { return liveQueries.capture_framing_png; },
|
|
1354
1175
|
get lookThroughShot() { return lookThroughShot; },
|
|
1355
|
-
|
|
1176
|
+
|
|
1356
1177
|
get readStudioState() { return readStudioState; },
|
|
1357
1178
|
get restoreExportRig() { return restoreExportRig; },
|
|
1358
1179
|
get setCameraPos() { return setCameraPos; },
|
|
1359
|
-
get setI2vMotionCameraUnlocked() { return setI2vMotionCameraUnlocked; },
|
|
1360
|
-
get setI2vMotionStudioOpen() { return setI2vMotionStudioOpen; },
|
|
1361
1180
|
get setFovDeg() { return setFovDeg; },
|
|
1362
1181
|
get setResult() { return setResult; },
|
|
1363
1182
|
get setResultOpen() { return setResultOpen; },
|
|
@@ -1367,32 +1186,17 @@ export default function App() {
|
|
|
1367
1186
|
}));
|
|
1368
1187
|
const {
|
|
1369
1188
|
ikMode, ikChains, setIkChains, ikFkJoints, setIkFkJoints, ikFocus, setIkFocus, footSnap, setFootSnap,
|
|
1370
|
-
bodyContact, setBodyContact, IK_CORRECTION_BLEND_FRAMES,
|
|
1371
|
-
|
|
1372
|
-
ikTick, setIkTick, committedIkEdits, setCommittedIkEdits, trailFalloffS, setTrailFalloffS, showTrails,
|
|
1373
|
-
setShowTrails, ikEditTool, setIkEditTool, trailEdit, trailTrackFocus, selectTrailTrack, clearTrailTrackFocus, trailFalloffFrames, focusIkHandle, snapshotIkKeys,
|
|
1189
|
+
bodyContact, setBodyContact, IK_CORRECTION_BLEND_FRAMES,
|
|
1190
|
+
ikTick, setIkTick, committedIkEdits, setCommittedIkEdits, ikEditTool, setIkEditTool, focusIkHandle, snapshotIkKeys,
|
|
1374
1191
|
pendingIkEdit, applyPendingIkEdit, cancelPendingIkEdit,
|
|
1375
1192
|
rangePins, rangePinResiduals, rangePinSelection, setRangePinSelection, rangePinPartPick, setRangePinPartPick, rangePinPreview, previewRangePinDraft, applyRangePinDraft, deleteRangePin,
|
|
1376
|
-
setCharacterIkKey, removeCharacterIkKey, clearCharacterIkKeys,
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
multiModelExtract, multiModelExtractProgress, multiModelExtractError, advanceFrame, stepFrame,
|
|
1383
|
-
leaveIkMode, beginPlaybackOn, chooseMultiModelFile, pasteMultiModelUrl, useMultiModelUrl, ingestFootage,
|
|
1384
|
-
extractMultiModelMotion, loadMotion, clearMotion, applyMotionTrim, resetMotionTrim, cutMotionAtPlayhead,
|
|
1385
|
-
changeMotionSegmentSpeed, removeMotionSegmentById, poseOtherCastMembers, toggleIkMode, ikSolve,
|
|
1386
|
-
ikDragEnd, ikAddKeyframe, externalBlockers, runFixCollisions, runFixCollisionsRange,
|
|
1387
|
-
changePhysicsOptions, showPhysicsPreview, cancelPhysicsPreview, applyPhysicsPreview, runAutoPhysics,
|
|
1388
|
-
platformFitRunning, platformFitLast, platformFitProgress, platformFitApplied,
|
|
1389
|
-
ikDeleteKeyframe, ikApplyPoseAsKey, recheckMotionHealth, changeArdySeed, takeSeed, runLineEdit,
|
|
1390
|
-
runAllPromptBlocks, runArdy, onTrailDragStart, onTrailDragPreview, onTrailDragEnd, runTrailRegeneration,
|
|
1391
|
-
genQueue, setGenQueue, executeMotionJob, seedLoadedTake, loadTakeVersion, selectedMotionReadiness,
|
|
1392
|
-
generationBusy, openMotionSetup, refineDisabledReason, sceneDisabledReason, sceneGenerateDisabledReason,
|
|
1393
|
-
sceneAgainDisabledReason, enterRefineMode, runSceneAgain, addSceneBlock, restoreMotionRefs, cancelArdy,
|
|
1193
|
+
setCharacterIkKey, removeCharacterIkKey, clearCharacterIkKeys,
|
|
1194
|
+
motion,
|
|
1195
|
+
advanceFrame, stepFrame,
|
|
1196
|
+
leaveIkMode, clearMotion, poseOtherCastMembers, toggleIkMode, ikSolve,
|
|
1197
|
+
ikDragEnd, ikAddKeyframe,
|
|
1198
|
+
ikDeleteKeyframe, ikApplyPoseAsKey,
|
|
1394
1199
|
} = motionDomain;
|
|
1395
|
-
const { i2vMotion, captureI2vStill, enterI2vFraming, markI2vPose, clearI2vPose, clearI2vMotion, restoreI2vCamera, framingDistance, showI2vMotionLock, generateI2vMotion, i2vMotionUnavailable, generateI2vMotionFromUi } = motionDomain;
|
|
1396
1200
|
const [partColoursEnabled, setPartColoursEnabled] = useState(false);
|
|
1397
1201
|
const [partColoursMode, setPartColoursMode] = useState("shaded");
|
|
1398
1202
|
|
|
@@ -1430,9 +1234,7 @@ export default function App() {
|
|
|
1430
1234
|
// renders for a specific cast member, so selecting the group used to
|
|
1431
1235
|
// drop the operator into Motion with nothing to drag.
|
|
1432
1236
|
setSelectedHierarchyId(rowIdForCharIndex(activeCharIndex));
|
|
1433
|
-
|
|
1434
|
-
// leaving the operator to hunt through a long inspector column.
|
|
1435
|
-
setPromptBlocksReveal((signal) => signal + 1);
|
|
1237
|
+
|
|
1436
1238
|
}
|
|
1437
1239
|
// Pose lands on the active character's rig: its Rig Control panel holds
|
|
1438
1240
|
// the IK tools that W/E/R switch between.
|
|
@@ -1599,14 +1401,14 @@ export default function App() {
|
|
|
1599
1401
|
get loadedLayerCharRef() { return loadedLayerCharRef; },
|
|
1600
1402
|
get manualCameraOverrideRef() { return manualCameraOverrideRef; },
|
|
1601
1403
|
get markSemanticEdit() { return markSemanticEdit; },
|
|
1602
|
-
|
|
1404
|
+
|
|
1603
1405
|
get motionFullRef() { return motionFullRef; },
|
|
1604
1406
|
get playgroundMode() { return playgroundMode; },
|
|
1605
1407
|
get projectHandleRef() { return projectHandleRef; },
|
|
1606
|
-
|
|
1408
|
+
|
|
1607
1409
|
get projectSnapshotRef() { return projectSnapshotRef; },
|
|
1608
1410
|
get projectStateRef() { return projectStateRef; },
|
|
1609
|
-
|
|
1411
|
+
|
|
1610
1412
|
get runStudioAction() { return runStudioAction; },
|
|
1611
1413
|
get saveBlockedRef() { return saveBlockedRef; },
|
|
1612
1414
|
get saveFailureToastRef() { return saveFailureToastRef; },
|
|
@@ -1618,11 +1420,11 @@ export default function App() {
|
|
|
1618
1420
|
get setHasCharSheet() { return setHasCharSheet; },
|
|
1619
1421
|
get setMovePlaying() { return setMovePlaying; },
|
|
1620
1422
|
get setPendingWaypointFrame() { return setPendingWaypointFrame; },
|
|
1621
|
-
|
|
1423
|
+
|
|
1622
1424
|
get setRailDraw() { return setRailDraw; },
|
|
1623
1425
|
get setRigMountEpoch() { return setRigMountEpoch; },
|
|
1624
1426
|
get setSelectedHierarchyId() { return setSelectedHierarchyId; },
|
|
1625
|
-
|
|
1427
|
+
|
|
1626
1428
|
get setShots() { return setShots; },
|
|
1627
1429
|
get setTlFrame() { return setTlFrame; },
|
|
1628
1430
|
get setTlFrameCount() { return setTlFrameCount; },
|
|
@@ -1677,10 +1479,6 @@ export default function App() {
|
|
|
1677
1479
|
const ikBodyDragRef = useRef(false); // true while a body drag is active
|
|
1678
1480
|
|
|
1679
1481
|
const ikStateRef = useRef(createIkState());
|
|
1680
|
-
const autoPhysicsRunRef = useRef(null);
|
|
1681
|
-
|
|
1682
|
-
const physicsJobRef = useRef(0);
|
|
1683
|
-
const physicsSourceCacheRef = useRef({ value: null });
|
|
1684
1482
|
|
|
1685
1483
|
// Sorted full-body key frames for the timeline markers. Derived from the
|
|
1686
1484
|
// ref state; ikTick re-derives after every key add/remove.
|
|
@@ -1688,9 +1486,7 @@ export default function App() {
|
|
|
1688
1486
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1689
1487
|
[ikTick]);
|
|
1690
1488
|
|
|
1691
|
-
|
|
1692
|
-
const trailBaseMotionRef = useRef(null);
|
|
1693
|
-
const trailPreviewMotionRef = useRef(null);
|
|
1489
|
+
|
|
1694
1490
|
|
|
1695
1491
|
function selectHierarchy(id) {
|
|
1696
1492
|
// A selection switch is the user starting something else: settle any open
|
|
@@ -1835,10 +1631,7 @@ export default function App() {
|
|
|
1835
1631
|
camera.rotation.set(angles.pitch, angles.yaw, 0);
|
|
1836
1632
|
}
|
|
1837
1633
|
|
|
1838
|
-
//
|
|
1839
|
-
// changePromptClip) and viewport/plan camera framing are the two streams
|
|
1840
|
-
// that would otherwise push an entry per keystroke / per pointermove.
|
|
1841
|
-
const promptTextSessionRef = useRef(null);
|
|
1634
|
+
// Viewport/plan camera framing would otherwise push an entry per pointermove.
|
|
1842
1635
|
const framingSessionRef = useRef(null);
|
|
1843
1636
|
// A colour picker streams values for as long as its dialog is open.
|
|
1844
1637
|
const tintSessionRef = useRef(null);
|
|
@@ -1925,21 +1718,15 @@ export default function App() {
|
|
|
1925
1718
|
requestSculptView({ KeyW: "front", KeyE: "side", KeyR: "back" }[event.code]);
|
|
1926
1719
|
return;
|
|
1927
1720
|
}
|
|
1928
|
-
// Pose mode's W/
|
|
1929
|
-
// buttons: the
|
|
1721
|
+
// Pose mode's W/R are the IK tools, gated like their Rig Control
|
|
1722
|
+
// buttons: the range pin needs a take.
|
|
1930
1723
|
if (workflowMode === "pose" && GIZMO_HOTKEYS[event.code]) {
|
|
1931
1724
|
event.preventDefault();
|
|
1932
1725
|
if (event.code === "KeyW") setIkEditTool("ik");
|
|
1933
|
-
|
|
1726
|
+
|
|
1934
1727
|
else if (event.code === "KeyR" && motion) { setIkEditTool("pin"); setRangePinPartPick(null); }
|
|
1935
1728
|
return;
|
|
1936
1729
|
}
|
|
1937
|
-
// Motion mode's E is Refine: drag the take's path to re-route it.
|
|
1938
|
-
if (workflowMode === "motion" && event.code === "KeyE") {
|
|
1939
|
-
event.preventDefault();
|
|
1940
|
-
enterRefineMode();
|
|
1941
|
-
return;
|
|
1942
|
-
}
|
|
1943
1730
|
if (GIZMO_HOTKEYS[event.code]) {
|
|
1944
1731
|
event.preventDefault();
|
|
1945
1732
|
setGizmoMode(GIZMO_HOTKEYS[event.code]);
|
|
@@ -2068,7 +1855,6 @@ export default function App() {
|
|
|
2068
1855
|
const liveWorkspaceIdRef = useRef("");
|
|
2069
1856
|
if (!liveWorkspaceIdRef.current) liveWorkspaceIdRef.current = loadLiveWorkspaceId();
|
|
2070
1857
|
const [result, setResult] = useState(null);
|
|
2071
|
-
const [i2vMotionStudioOpen, setI2vMotionStudioOpen] = useState(false);
|
|
2072
1858
|
const [resultOpen, setResultOpen] = useState(false);
|
|
2073
1859
|
const [copied, setCopied] = useState(false);
|
|
2074
1860
|
const [recordedVideoName, setRecordedVideoName] = useState(null);
|
|
@@ -2101,13 +1887,10 @@ export default function App() {
|
|
|
2101
1887
|
}, []);
|
|
2102
1888
|
const [glContextLost, setGlContextLost] = useState(false);
|
|
2103
1889
|
|
|
2104
|
-
const bridgeRefreshRef = useRef(null);
|
|
2105
1890
|
|
|
2106
|
-
const generationPendingRef = useRef(false);
|
|
2107
1891
|
|
|
2108
1892
|
// default clip length in seconds; aligned with the recommended 3-5 s block range
|
|
2109
1893
|
|
|
2110
|
-
const takeRecipeRef = useRef(null);
|
|
2111
1894
|
|
|
2112
1895
|
const [bottomTab, setBottomTab] = useState("timeline");
|
|
2113
1896
|
const { shelfImageIds, setShelfImageIds, shelfMeshIds, setShelfMeshIds, manageAssetStorage, setManageAssetStorage, unusedAssetIds, setUnusedAssetIds, usedAssetIds, setUsedAssetIds, usageCounts, setUsageCounts, assetShelfScanTokenRef, legacyDerivedIdsRef, assetTrash, setAssetTrash, assetUndoOffered, setAssetUndoOffered, deletingAssetId, setDeletingAssetId, cutoutLineage, meshLineage, projectCutoutLineage, projectAssetGraphSignature } = useAssetShelf({ scenes, activeSceneId, sceneObjects });
|
|
@@ -2274,7 +2057,6 @@ export default function App() {
|
|
|
2274
2057
|
}
|
|
2275
2058
|
}
|
|
2276
2059
|
|
|
2277
|
-
const ardyAbortRef = useRef(null);
|
|
2278
2060
|
|
|
2279
2061
|
const renderActive = useRenderActivity(tlPlaying || movePlaying);
|
|
2280
2062
|
// the clip length on the production clock
|
|
@@ -2283,10 +2065,12 @@ export default function App() {
|
|
|
2283
2065
|
// the export overwrites it per captured frame and restores nothing, which
|
|
2284
2066
|
// is correct — the next render puts it back.
|
|
2285
2067
|
propFrameRef.current = tlFrame;
|
|
2068
|
+
// A held object's gizmo drag edits its motion; its live preview rides the take tracks (held-object-edit.jsx).
|
|
2069
|
+
const heldEdit = useHeldObjectEdit({ selected: selectedSceneObject, objects: sceneObjects, tracks: objectTracksOf(appContext.storeDomain("motion"), appContext.live.characters), frame: tlFrame, frameCount: tlFrameCount, fps: tlFps, motion: appContext.storeDomain("motion"), run: runStudioAction });
|
|
2286
2070
|
// Routed records and everything grouped under them (object-travel.js).
|
|
2287
|
-
const animatedSceneObjects = useMemo(
|
|
2288
|
-
() => sceneObjectsAt(sceneObjects, tlFrame, { frameCount: tlFrameCount, fps: tlFps }),
|
|
2289
|
-
[sceneObjects, tlFrame, tlFrameCount, tlFps],
|
|
2071
|
+
const objectTracks = heldEdit.tracks, animatedSceneObjects = useMemo(
|
|
2072
|
+
() => sceneObjectsAt(sceneObjects, tlFrame, { frameCount: tlFrameCount, fps: tlFps, objectTracks }),
|
|
2073
|
+
[sceneObjects, tlFrame, tlFrameCount, tlFps, objectTracks],
|
|
2290
2074
|
);
|
|
2291
2075
|
|
|
2292
2076
|
// The selection as it is DRAWN at this frame, for the transform gizmo to sit
|
|
@@ -2294,10 +2078,8 @@ export default function App() {
|
|
|
2294
2078
|
// routed descendant all stand away from their authored numbers while the
|
|
2295
2079
|
// playhead is off frame 0. Drags still read the authored records (the gizmo
|
|
2296
2080
|
// is handed those separately), so they stay delta-based on authored numbers.
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
[selectedSceneObject, animatedSceneObjects],
|
|
2300
|
-
);
|
|
2081
|
+
// A take-drawn object also gets `heldEdit.gizmo`: the exact drawn centre and rotation.
|
|
2082
|
+
const gizmoSceneObject = selectedSceneObject ? animatedSceneObjects.find((object) => object.id === selectedSceneObject.id) ?? selectedSceneObject : null;
|
|
2301
2083
|
|
|
2302
2084
|
// Auto color: Blender's viewport "Random" mode. A DISPLAY-ONLY marker rides
|
|
2303
2085
|
// each non-cutout object into the renderers; the authored `color`, the scene
|
|
@@ -2377,167 +2159,6 @@ export default function App() {
|
|
|
2377
2159
|
const frameCountRef = useRef(DEFAULT_DURATION_S * TIMELINE_FPS);
|
|
2378
2160
|
frameCountRef.current = tlFrameCount;
|
|
2379
2161
|
|
|
2380
|
-
/* ------------------------------ line editing ---------------------------
|
|
2381
|
-
* Contract C6. A modal editing surface exactly like waypointMode above,
|
|
2382
|
-
* but it works in SCREEN space on a 2D overlay canvas rather than raycast
|
|
2383
|
-
* onto the floor — depth is the model's problem, not the UI's.
|
|
2384
|
-
*
|
|
2385
|
-
* TWO GESTURES, ONE CURVE. The primary interaction is TRAJECTORY DRAGGING:
|
|
2386
|
-
* the joint's existing path is projected onto the viewport and the user
|
|
2387
|
-
* grabs a point on it and pulls, with a Gaussian falloff carrying the
|
|
2388
|
-
* neighbours along. A press that lands nowhere near the path instead DRAWS
|
|
2389
|
-
* one freehand (drawStrokeEdit), because a trail can project into a few
|
|
2390
|
-
* screen pixels — a person backing up and falling barely moves the hand
|
|
2391
|
-
* across the image — and with nothing grabbable the whole mode reads as
|
|
2392
|
-
* dead. Drawing is not the old freehand tool coming back, though: that one
|
|
2393
|
-
* popped the take 8x its own frame delta at the range edges because the
|
|
2394
|
-
* joint teleported to wherever the stroke began (gate GP2). A stroke here is
|
|
2395
|
-
* MATCHED BACK ONTO THE TRAIL — its endpoints pick the frames it was drawn
|
|
2396
|
-
* over, and those frames become the edit's range — so it reroutes the stretch
|
|
2397
|
-
* of path it covers, replays it with that stretch's own velocity profile, and
|
|
2398
|
-
* eases out of the original trajectory at both seams. See the block comment
|
|
2399
|
-
* above matchStrokeWindow in line-edit.js for why all three are one decision.
|
|
2400
|
-
* A drag pins its ends instead (its Gaussian already eases), and both
|
|
2401
|
-
* gestures produce the SAME dense frame-indexed curve, so everything
|
|
2402
|
-
* downstream (preview, undo, camera drift, the wire payload) cannot tell them
|
|
2403
|
-
* apart — except that a drawn curve also carries the `frameRange` it chose.
|
|
2404
|
-
*
|
|
2405
|
-
* `lineCurve` is non-null EXACTLY WHEN THERE IS AN EDIT, and that invariant
|
|
2406
|
-
* is the whole camera policy in one sentence. Null means the curve is
|
|
2407
|
-
* re-projected from the LIVE camera on every repaint, so orbiting the view
|
|
2408
|
-
* carries the path along and navigation never fights the mode. Non-null is
|
|
2409
|
-
* `{ camera, original, edited }` — one object, because a pull is a 2D offset
|
|
2410
|
-
* that only means something through the lens it was authored with, so the
|
|
2411
|
-
* camera is frozen beside the points and travels with them onto the wire.
|
|
2412
|
-
* Moving the view therefore does NOT drop the pull; it only stops the line
|
|
2413
|
-
* being drawable here (see lineDrifted just below, and the watcher further
|
|
2414
|
-
* down).
|
|
2415
|
-
*
|
|
2416
|
-
* The four refs never re-render: `lineDragRef` is the in-flight grab and
|
|
2417
|
-
* carries the live deformed curve, `lineDrawRef` is the in-flight freehand
|
|
2418
|
-
* stroke, `lineLiveRef` caches the last projection the painter made so the
|
|
2419
|
-
* hit test does not redo it, and `lineHoverRef` is the marker under the
|
|
2420
|
-
* pointer. A pointermove must repaint the overlay
|
|
2421
|
-
* without re-rendering an 11k-line component 200 times a second, so the
|
|
2422
|
-
* painter reads the refs and only pointerup commits to state. */
|
|
2423
|
-
const [lineEditMode, setLineEditMode] = useState(false);
|
|
2424
|
-
const [lineTrack, setLineTrack] = useState(LINE_EDIT_DEFAULT_TRACK);
|
|
2425
|
-
// null means "the whole clip" — resolved against the loaded take at use
|
|
2426
|
-
// time so loading a different clip cannot leave a stale range behind.
|
|
2427
|
-
const [lineRange, setLineRange] = useState(null);
|
|
2428
|
-
const [lineCurve, setLineCurve] = useState(null);
|
|
2429
|
-
/* THE VIEW HAS MOVED, AND THE EDIT IS STILL HERE.
|
|
2430
|
-
*
|
|
2431
|
-
* A committed curve carries its OWN camera (the `{ camera, original, edited }`
|
|
2432
|
-
* above), and buildLineEditRequest sends that snapshot — so a later orbit,
|
|
2433
|
-
* fly or shot switch cannot invalidate the pending edit, its preview or the
|
|
2434
|
-
* confirming run. What it invalidates is only the ALIGNMENT of the painted
|
|
2435
|
-
* overlay: the uv were authored through one lens, there is no depth to
|
|
2436
|
-
* reproject them with, and drawing them over a different view would put the
|
|
2437
|
-
* line somewhere the artist never aimed. So drift is a PRESENTATION state,
|
|
2438
|
-
* not a destruction trigger — the curve is painted detached (ghosted, no
|
|
2439
|
-
* grab handles), the panel says so, and a NEW gesture is refused because it
|
|
2440
|
-
* would mix two cameras into one curve. Come back toward the snapshot and the
|
|
2441
|
-
* line paints normally again.
|
|
2442
|
-
*
|
|
2443
|
-
* The ref is what the pointer handlers and the painter read (a poll is 250 ms
|
|
2444
|
-
* stale and a press must not be); the state is what re-renders the panel. */
|
|
2445
|
-
const [lineDrifted, setLineDrifted] = useState(false);
|
|
2446
|
-
const lineDriftRef = useRef(false);
|
|
2447
|
-
/* ------------------------------ 3D pins --------------------------------
|
|
2448
|
-
* The THIRD gesture: scrub to a moment, grab the joint where it is, put it
|
|
2449
|
-
* where it should be. Entries are `{ frame, position: [x, y, z] }` in the
|
|
2450
|
-
* TAKE's own clip space (worldPointToClip converts on commit), ascending by
|
|
2451
|
-
* frame, capped at LINE_EDIT_PINS_MAX — exactly the C6 `pins3d` payload, so
|
|
2452
|
-
* the panel state and the wire object are the same value.
|
|
2453
|
-
*
|
|
2454
|
-
* ONE EDIT IS ONE GESTURE (v1). A pin clears the curve and a stroke or drag
|
|
2455
|
-
* clears the pins, because a 2D path and a set of 3D points are two answers
|
|
2456
|
-
* to "where does this joint go" and the box would be asked to average them.
|
|
2457
|
-
* Combining them is a real feature (pin the extremes, draw the arc between)
|
|
2458
|
-
* and it needs a story about which one owns a frame they both name — that
|
|
2459
|
-
* story is not written, so the modes are exclusive and say so.
|
|
2460
|
-
*
|
|
2461
|
-
* `linePinMode` is the stage's gesture selector rather than a modifier key:
|
|
2462
|
-
* pressing on the joint's own marker is ALSO how a curve drag starts, so the
|
|
2463
|
-
* two cannot share a press, and a modal toggle beside the joint picker is
|
|
2464
|
-
* discoverable in a way a chord is not. */
|
|
2465
|
-
const [linePins, setLinePins] = useState([]);
|
|
2466
|
-
const [linePinMode, setLinePinMode] = useState(false);
|
|
2467
|
-
// The in-flight pin drag. Same no-re-render discipline as lineDragRef: a
|
|
2468
|
-
// pointermove writes the ref and repaints the overlay, only pointerup
|
|
2469
|
-
// commits to state.
|
|
2470
|
-
const linePinDragRef = useRef(null);
|
|
2471
|
-
const [lineRadius, setLineRadius] = useState(DRAG_RADIUS_DEFAULT);
|
|
2472
|
-
const lineDragRef = useRef(null);
|
|
2473
|
-
// The in-flight freehand STROKE, when the press missed the curve. Same
|
|
2474
|
-
// no-re-render discipline as lineDragRef: pointermove appends a uv sample and
|
|
2475
|
-
// repaints the overlay, and only pointerup turns the stroke into a curve (via
|
|
2476
|
-
// strokeToCurve) and commits it through the drag's own pipeline.
|
|
2477
|
-
const lineDrawRef = useRef(null);
|
|
2478
|
-
// The range a DRAW just auto-matched into the panel. "Switching the range
|
|
2479
|
-
// drops the pull in hand" is the right rule for a range the ARTIST typed —
|
|
2480
|
-
// the pull was authored against a trajectory that is no longer on screen —
|
|
2481
|
-
// but a drawn stroke authors its range and its curve in the same gesture,
|
|
2482
|
-
// and letting that rule fire on the range the draw itself installed would
|
|
2483
|
-
// wipe the drawing on commit and cancel its preview. One-shot: the effect
|
|
2484
|
-
// consumes it.
|
|
2485
|
-
const lineAutoRangeRef = useRef(null);
|
|
2486
|
-
const lineLiveRef = useRef(null);
|
|
2487
|
-
const lineHoverRef = useRef(null);
|
|
2488
|
-
// Undo stack for committed pulls: each entry is the WHOLE lineCurve value
|
|
2489
|
-
// that a commit replaced (null = "no edit yet"), so Ctrl/Cmd+Z is a plain
|
|
2490
|
-
// pop-and-restore. A camera move no longer touches it: an edit survives the
|
|
2491
|
-
// view moving, so the pulls behind it are still meaningful too, and undo is
|
|
2492
|
-
// one of the three things (with reset and Generate) that must keep working
|
|
2493
|
-
// while the view has drifted away from the line.
|
|
2494
|
-
const lineUndoRef = useRef([]);
|
|
2495
|
-
const lineOverlayRef = useRef(null);
|
|
2496
|
-
|
|
2497
|
-
/* ------------------ live preview of a pull (contracts C10/C11) ------------
|
|
2498
|
-
* Releasing the drag fires a FULL-QUALITY run of the same edit (same steps
|
|
2499
|
-
* and session seed as Generate, ~2 s on the warm resident, so the draft and
|
|
2500
|
-
* the confirmed take are bit-identical) and swaps the VIEWPORT to it, so
|
|
2501
|
-
* the artist judges the correction by watching it move instead of by
|
|
2502
|
-
* reading a curve. Three rules make that honest:
|
|
2503
|
-
*
|
|
2504
|
-
* 1. A PREVIEW IS A PICTURE, NOT A TAKE. It never pushes a version, never
|
|
2505
|
-
* touches the recipe and never becomes anyone's sourceMotion. The take
|
|
2506
|
-
* being edited is `takeSourceUrl` — remembered here the moment a
|
|
2507
|
-
* preview starts — and `motion.url` is merely what is on screen.
|
|
2508
|
-
* 2. ONE SEED PER EDITING SESSION. A draft rendered with a different seed
|
|
2509
|
-
* predicts nothing, so the seed is rolled once (first preview or the
|
|
2510
|
-
* confirming run, whichever comes first), reused by every preview, SENT
|
|
2511
|
-
* by the full-quality run, and only then re-rolled. A typed seed is
|
|
2512
|
-
* already constant, so the rule costs nothing there.
|
|
2513
|
-
* 3. SUPERSEDE, NEVER STACK. At most one request is in flight; a drag
|
|
2514
|
-
* that lands while one is out replaces the pending curve, and the older
|
|
2515
|
-
* answer is dropped on arrival. Queueing them would make the viewport
|
|
2516
|
-
* replay a history the artist has already moved past.
|
|
2517
|
-
*
|
|
2518
|
-
* Undo, reset and leaving the mode all revert the viewport to the source take
|
|
2519
|
-
* and discard whatever is in flight. A CAMERA MOVE does not: the draft is a
|
|
2520
|
-
* picture of an edit that survives the view moving, so it stays on screen and
|
|
2521
|
-
* stays confirmable. */
|
|
2522
|
-
const [linePreviewSource, setLinePreviewSource] = useState(null);
|
|
2523
|
-
const linePreviewSourceRef = useRef(null);
|
|
2524
|
-
// The preview motionUrl currently ON SCREEN — null means the source take is.
|
|
2525
|
-
const [linePreviewUrl, setLinePreviewUrl] = useState(null);
|
|
2526
|
-
const [linePreviewBusy, setLinePreviewBusy] = useState(false);
|
|
2527
|
-
const [linePreviewError, setLinePreviewError] = useState("");
|
|
2528
|
-
// Round trip of the last preview, in ms. Surfaced because "is this loop
|
|
2529
|
-
// actually live?" is the question the number answers in one glance.
|
|
2530
|
-
const [linePreviewMs, setLinePreviewMs] = useState(0);
|
|
2531
|
-
const linePreviewSeedRef = useRef(null);
|
|
2532
|
-
// Monotonic: a result whose token is stale was superseded or cancelled, and
|
|
2533
|
-
// is discarded without ever reaching the viewport.
|
|
2534
|
-
const linePreviewTokenRef = useRef(0);
|
|
2535
|
-
const linePreviewAbortRef = useRef(null);
|
|
2536
|
-
const linePreviewPendingRef = useRef(null);
|
|
2537
|
-
const linePreviewTimerRef = useRef(0);
|
|
2538
|
-
// The draft that actually REACHED the viewport, so a cancel knows whether
|
|
2539
|
-
// there is anything to put back.
|
|
2540
|
-
const linePreviewShownRef = useRef(null);
|
|
2541
2162
|
useEffect(castDomain.removeLegacyRootWaypoint, []);
|
|
2542
2163
|
|
|
2543
2164
|
// Shot trims also trim their local Rail Follow card once. Growing a shot
|
|
@@ -2549,31 +2170,22 @@ export default function App() {
|
|
|
2549
2170
|
// its latest objects and camera department envelope before React opens the
|
|
2550
2171
|
// destination room.
|
|
2551
2172
|
const activeSceneIdRef = useRef(activeSceneId);
|
|
2552
|
-
|
|
2553
|
-
|
|
2173
|
+
// The shot document and the stage's cast are read from the stores when used,
|
|
2174
|
+
// not rebuilt here: a render-time copy lagged a synchronous command by one
|
|
2175
|
+
// render and moved the scene revision after its receipt (document-mirrors.js).
|
|
2176
|
+
const [{ shotDocumentRef, actorStageRef }] = useState(() => createDocumentMirrors(appContext));
|
|
2554
2177
|
appContext.publishScenes(scenes);
|
|
2555
2178
|
activeSceneIdRef.current = activeSceneId;
|
|
2556
|
-
|
|
2557
|
-
//
|
|
2558
|
-
// into the saved stage below with each character, and a change to them is
|
|
2559
|
-
// a reason to autosave.
|
|
2179
|
+
// The motion layers (IK edits ride into the saved stage with each
|
|
2180
|
+
// character): a change to them is a reason to autosave.
|
|
2560
2181
|
const motionLayers = appContext.storeDomain("motion")?.read() ?? null;
|
|
2561
|
-
const ikEditsOf = (id) => {
|
|
2562
|
-
const row = motionLayers?.find((layer) => layer.id === id);
|
|
2563
|
-
if (!row || (!row.ikKeys?.length && !row.ikPins?.length)) return null;
|
|
2564
|
-
return { keys: row.ikKeys, pins: row.ikPins ?? [], pinResiduals: row.ikPinResiduals ?? [] };
|
|
2565
|
-
};
|
|
2566
|
-
// sessionMotion is stripped: generated clips are session-only and far too
|
|
2567
|
-
// heavy for the stage envelope; paths, prompt blocks and IK edits persist.
|
|
2568
|
-
const stageCharacters = characters.map(({ sessionMotion, ...entry }) => ({ ...entry, ikEdits: ikEditsOf(entry.id) }));
|
|
2569
2182
|
actorStageRef.current = {
|
|
2570
|
-
characters: stageCharacters,
|
|
2571
2183
|
hasCharSheet,
|
|
2572
2184
|
environmentImage,
|
|
2573
2185
|
shotAspect: shotAspectKey,
|
|
2574
2186
|
cameraPresetId,
|
|
2575
2187
|
sensorId,
|
|
2576
|
-
keyLight,
|
|
2188
|
+
keyLight, lighting,
|
|
2577
2189
|
// What this location IS and how it should look. Session state until #345:
|
|
2578
2190
|
// a reopened scene came back with another room's description.
|
|
2579
2191
|
environment,
|
|
@@ -2717,11 +2329,7 @@ export default function App() {
|
|
|
2717
2329
|
return () => { window.removeEventListener("cozyclay:agent-panel-toggle", toggle); window.removeEventListener("keydown", key); };
|
|
2718
2330
|
}, [embedMode]);
|
|
2719
2331
|
const projectHandleRef = useRef(null);
|
|
2720
|
-
|
|
2721
|
-
// Loaded clips keep the same Uint8Array identity while they remain active.
|
|
2722
|
-
// Reuse the expensive encoded record until a new byte buffer is supplied.
|
|
2723
|
-
const motionEncodingCacheRef = useRef(new WeakMap());
|
|
2724
|
-
const restoreEpochRef = useRef(0);
|
|
2332
|
+
|
|
2725
2333
|
|
|
2726
2334
|
const projectSnapshotRef = useRef("");
|
|
2727
2335
|
const projectStateRef = useRef(null);
|
|
@@ -2799,7 +2407,6 @@ export default function App() {
|
|
|
2799
2407
|
&& tutorialInitialSnapshotRef.current === collectProjectSnapshot("Tutorial");
|
|
2800
2408
|
if (seed) {
|
|
2801
2409
|
tutorialLoadingRef.current = true;
|
|
2802
|
-
demoSeeded.current = true;
|
|
2803
2410
|
try {
|
|
2804
2411
|
const opened = await openStarterScene("city-block", "tutorial");
|
|
2805
2412
|
if (opened) {
|
|
@@ -2894,7 +2501,7 @@ export default function App() {
|
|
|
2894
2501
|
filmback,
|
|
2895
2502
|
// keyLight rides the live stage envelope: it is authored, undoable and
|
|
2896
2503
|
// patchable state, so every reader sees the body the save path writes.
|
|
2897
|
-
stage: { shotAspect: shotAspectKey, cameraPresetId, sensorId, hasCharSheet, environmentImage, environment, style, hasEnvSheet, keyLight },
|
|
2504
|
+
stage: { shotAspect: shotAspectKey, cameraPresetId, sensorId, hasCharSheet, environmentImage, environment, style, hasEnvSheet, keyLight, lighting },
|
|
2898
2505
|
timeline: { currentFrame: tlFrame, frameCount: tlFrameCount, fps: tlFps },
|
|
2899
2506
|
activeCharacterId,
|
|
2900
2507
|
partColours: partColoursEnabled ? PART_COLOURS : null,
|
|
@@ -2906,7 +2513,6 @@ export default function App() {
|
|
|
2906
2513
|
removeCharacter,
|
|
2907
2514
|
persistScenes,
|
|
2908
2515
|
openScene,
|
|
2909
|
-
loadMotion,
|
|
2910
2516
|
// The framing-capture pair the agent commands call. Both are per-render
|
|
2911
2517
|
// closures (captureFramingPng sizes its canvas off the render's own
|
|
2912
2518
|
// shotOutput), so the ref must always hold THIS render's instance — a
|
|
@@ -3096,10 +2702,10 @@ export default function App() {
|
|
|
3096
2702
|
|
|
3097
2703
|
// These hooks are declared after the liveStateRef assignment above runs, so
|
|
3098
2704
|
// they join the live read model here — same render, no TDZ.
|
|
3099
|
-
appContext.patchLive({
|
|
2705
|
+
appContext.patchLive({ setTlFrameCount });
|
|
3100
2706
|
|
|
3101
2707
|
// Dirty tracking: any divergence from the last saved file lights the dot.
|
|
3102
|
-
useEffect(scenesDomain.refreshProjectDirty, [scenes, activeSceneId, workspaceLayout, customPoses, characters, shots, waypoints,
|
|
2708
|
+
useEffect(scenesDomain.refreshProjectDirty, [scenes, activeSceneId, workspaceLayout, customPoses, characters, shots, waypoints, projectName, keyLight, sceneObjects, shotAspectKey, environmentImage, environment, style, hasEnvSheet, sensorId, tlFrameCount, workflowRevision]);
|
|
3103
2709
|
|
|
3104
2710
|
// The untrimmed take per CHARACTER. Trims are non-destructive views of the
|
|
3105
2711
|
// full take, so re-trimming and "restore full" always cut from the
|
|
@@ -3107,35 +2713,8 @@ export default function App() {
|
|
|
3107
2713
|
// ref would hand Subject 2's take to Subject 1 on the next switch.
|
|
3108
2714
|
const motionFullRef = useRef(new Map()); // charId -> untrimmed take
|
|
3109
2715
|
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
// contain authored IK keys, the tracks keyed inside those blocks. The blocks
|
|
3113
|
-
// TILE 0..clipFrames, so the union over the edited ones is just the union
|
|
3114
|
-
// over the whole clip — blocks without keys contribute nothing either way.
|
|
3115
|
-
// Empty means the request carries no `tracks` at all and the panel says
|
|
3116
|
-
// nothing extra. Regenerating a take is the only clock that matters here, so
|
|
3117
|
-
// clipFrames comes from the loaded take exactly as runArdy takes it.
|
|
3118
|
-
const preserveEditedTracks = useMemo(() => {
|
|
3119
|
-
if (!motion?.url || ikFrames.length === 0) return [];
|
|
3120
|
-
const sourcePromptClips = promptClips
|
|
3121
|
-
.filter((clip) => clip.text.trim())
|
|
3122
|
-
.sort((a, b) => a.startFrame - b.startFrame);
|
|
3123
|
-
if (sourcePromptClips.length === 0) return [];
|
|
3124
|
-
const clipFrames = (motion.frames / motion.fps) * TIMELINE_FPS;
|
|
3125
|
-
// A single block spanning the whole take is not a schedule, and without a
|
|
3126
|
-
// schedule there are no edited blocks to attribute (runArdy's own rule).
|
|
3127
|
-
if (buildPromptSchedule(sourcePromptClips, clipFrames, sourcePromptClips[0].text).length < 2) return [];
|
|
3128
|
-
return ikTracksInRange(ikStateRef.current, ikFrames, 0, clipFrames);
|
|
3129
|
-
}, [motion, promptClips, ikFrames]);
|
|
3130
|
-
const preserveTracksLine = preserveTracksSummary(preserveEditedTracks);
|
|
3131
|
-
|
|
3132
|
-
const multiModelFileRef = useRef(null);
|
|
3133
|
-
const multiModelRunRef = useRef(0);
|
|
3134
|
-
const multiModelObjectUrlRef = useRef(null);
|
|
3135
|
-
|
|
3136
|
-
// idle | running | done | error
|
|
3137
|
-
|
|
3138
|
-
const photoPoseFileRef = useRef(null);
|
|
2716
|
+
|
|
2717
|
+
|
|
3139
2718
|
|
|
3140
2719
|
// Cast render props, memoized with Character itself (React.memo): during
|
|
3141
2720
|
// playback the playhead ticks 24 times a second, and a character whose
|
|
@@ -3204,20 +2783,18 @@ export default function App() {
|
|
|
3204
2783
|
}, [gizmoView, characters, activeChar.id]);
|
|
3205
2784
|
|
|
3206
2785
|
/* --------------------- per-character layer buffers ---------------------
|
|
3207
|
-
* waypoints /
|
|
2786
|
+
* waypoints / motion above are the EDITING BUFFER of the
|
|
3208
2787
|
* active character's animation layer. On a character switch the buffer is
|
|
3209
2788
|
* committed back into the previous character's entry and the new one's
|
|
3210
2789
|
* layer is loaded, so each cast member keeps its own schedule. The
|
|
3211
2790
|
* generated clip is session-only; paths and prompt blocks persist in the
|
|
3212
2791
|
* stage envelope via the characters array. */
|
|
3213
|
-
const bufferRef = useRef({ waypoints: [],
|
|
3214
|
-
bufferRef.current = { waypoints,
|
|
2792
|
+
const bufferRef = useRef({ waypoints: [], motion: null, ik: null });
|
|
2793
|
+
bufferRef.current = { waypoints, motion, ik: ikStateRef.current };
|
|
3215
2794
|
const loadedLayerCharRef = useRef(activeChar.id);
|
|
3216
2795
|
const ikStatesRef = useRef(new Map()); // charId -> ikState, one per layer
|
|
3217
2796
|
useEffect(castDomain.switchActiveCharacterLayer, [activeChar.id]);
|
|
3218
|
-
|
|
3219
|
-
// has re-applied poseA) puts the rig back exactly where it was.
|
|
3220
|
-
const restoreRef = useRef(null);
|
|
2797
|
+
|
|
3221
2798
|
|
|
3222
2799
|
const shotCamRef = useRef(null);
|
|
3223
2800
|
const captureRef = useRef(null);
|
|
@@ -3570,11 +3147,10 @@ export default function App() {
|
|
|
3570
3147
|
characters,
|
|
3571
3148
|
activeChar.id,
|
|
3572
3149
|
motion,
|
|
3573
|
-
|
|
3574
|
-
multiModelFootage?.frames,
|
|
3150
|
+
null,
|
|
3575
3151
|
);
|
|
3576
3152
|
// Camera-only scenes still use their authored production duration. Once
|
|
3577
|
-
// motion
|
|
3153
|
+
// motion exists, content is the authoritative record
|
|
3578
3154
|
// range even if an older timeline count was left behind.
|
|
3579
3155
|
return contentExtent > 0 ? contentExtent : tlFrameCount;
|
|
3580
3156
|
}
|
|
@@ -4125,10 +3701,6 @@ export default function App() {
|
|
|
4125
3701
|
return captureFraming({ pos: { x: pos.x, y: pos.y, z: pos.z }, yaw: look.current.yaw, pitch: look.current.pitch, fovDeg });
|
|
4126
3702
|
}
|
|
4127
3703
|
|
|
4128
|
-
/** The agent panel's Generate motion chip: the motion.generateFromVideo
|
|
4129
|
-
* action. Locked, the action is unavailable and never runs, so the chip shows
|
|
4130
|
-
* the AI motion card's lock line itself, as it always did. */
|
|
4131
|
-
|
|
4132
3704
|
// What a framing capture says about the shot it came from: lens, delivery
|
|
4133
3705
|
// aspect, cast, cut range and the video model the shot is aimed at. Both
|
|
4134
3706
|
// capture paths (the live command and the embed message) attach this, so a
|
|
@@ -4172,7 +3744,6 @@ export default function App() {
|
|
|
4172
3744
|
// sidebar tabs. Every panel belongs to the thing that owns it: the scene
|
|
4173
3745
|
// owns what gets generated, the camera owns the lens, and a character owns
|
|
4174
3746
|
// its pose and its motion.
|
|
4175
|
-
const isSceneSelection = selectedHierarchyId === "shot";
|
|
4176
3747
|
const isCameraSelection = selectedHierarchyId === "camera";
|
|
4177
3748
|
const isCharacterSelection = selectedHierarchyId === "characters"
|
|
4178
3749
|
|| selectedHierarchyId === "characterA"
|
|
@@ -4182,64 +3753,18 @@ export default function App() {
|
|
|
4182
3753
|
// for the part colours, and a dot on the trigger whenever the viewport is
|
|
4183
3754
|
// showing something other than the plain stage.
|
|
4184
3755
|
const partColoursChoice = partColoursEnabled ? partColoursMode : "off";
|
|
4185
|
-
|
|
4186
|
-
// surface lighting H3 uses to infer the character's volume and pose.
|
|
4187
|
-
const i2vMotionSegmentationReady = partColoursEnabled && partColoursMode === "shaded";
|
|
4188
|
-
const i2vMotionHasA = Boolean(i2vMotion.a);
|
|
4189
|
-
const i2vMotionHasB = Boolean(i2vMotion.b);
|
|
4190
|
-
const i2vMotionCameraMatch = i2vMotionHasA && i2vMotionHasB && framingDistance(i2vMotion.a.framing, i2vMotion.b.framing) <= 0.001;
|
|
4191
|
-
const i2vMotionCameraLocked = i2vMotionHasA && !i2vMotionCameraUnlocked;
|
|
4192
|
-
const i2vMotionCurrentCameraMatch = !i2vMotionHasA || framingDistance(i2vMotion.a.framing, captureCurrentFraming()) <= 0.001;
|
|
4193
|
-
const i2vMotionStep = !i2vMotionSegmentationReady ? 1 : !i2vMotionHasA ? 2 : i2vMotionMode === "interpolate" && !i2vMotionHasB ? 3 : 4;
|
|
4194
|
-
// The AI motion workflow is split into a viewport-side capture card and a wide
|
|
4195
|
-
// authoring modal (i2v-motion-studio.jsx); both render from this one model
|
|
4196
|
-
// and action set so they cannot drift.
|
|
4197
|
-
const i2vMotionModel = {
|
|
4198
|
-
i2vMotion,
|
|
4199
|
-
mode: i2vMotionMode,
|
|
4200
|
-
enabled: i2vMotionEnabled,
|
|
4201
|
-
segmentationReady: i2vMotionSegmentationReady,
|
|
4202
|
-
step: i2vMotionStep,
|
|
4203
|
-
hasA: i2vMotionHasA,
|
|
4204
|
-
hasB: i2vMotionHasB,
|
|
4205
|
-
cameraMatch: i2vMotionCameraMatch,
|
|
4206
|
-
cameraUnlocked: i2vMotionCameraUnlocked,
|
|
4207
|
-
currentCameraMatch: i2vMotionCurrentCameraMatch,
|
|
4208
|
-
framingActive: lookThroughShot && shotAspectKey === I2V_MOTION_SHOT_ASPECT,
|
|
4209
|
-
};
|
|
4210
|
-
const i2vMotionActions = {
|
|
4211
|
-
setI2vMotion: motionDomain.setI2vMotion,
|
|
4212
|
-
setMode: setI2vMotionMode,
|
|
4213
|
-
enterFraming: enterI2vFraming,
|
|
4214
|
-
markPose: markI2vPose,
|
|
4215
|
-
clearPose: clearI2vPose,
|
|
4216
|
-
clear: clearI2vMotion,
|
|
4217
|
-
toggleCameraLock: () => setI2vMotionCameraUnlocked((value) => !value),
|
|
4218
|
-
restoreCamera: restoreI2vCamera,
|
|
4219
|
-
generate: (kind) => void generateI2vMotion(kind),
|
|
4220
|
-
enableShaded: () => runStudioAction("view.setPartColours", { mode: "shaded" }),
|
|
4221
|
-
};
|
|
4222
|
-
const viewLooksActive = gridView || autoColor || partColoursEnabled;
|
|
3756
|
+
const viewLooksActive = gridView || autoColor || partColoursEnabled || guideMode !== "off";
|
|
4223
3757
|
const rigSelection = parseRigNodeId(selectedHierarchyId);
|
|
4224
3758
|
const isRigSelection = rigSelection !== null;
|
|
4225
|
-
|
|
3759
|
+
// The scene row owns no settings of its own, so it leaves the empty-panel hint up rather than a blank column.
|
|
3760
|
+
const inspectorHasContent = isCameraSelection || isCharacterSelection || isRigSelection
|
|
4226
3761
|
|| selectedHierarchyId === "environment" || selectedHierarchyId === "props" || selectedHierarchyId === "light" || Boolean(selectedSceneObject) || sculptMode;
|
|
4227
3762
|
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
}, []);
|
|
4231
|
-
|
|
4232
|
-
// Hosted-demo seed. A build served as static files has no ARDY sidecar, so
|
|
4233
|
-
// a first-time visitor would otherwise land on a character standing still
|
|
4234
|
-
// with no way to see generated motion. The clip below ships with the build
|
|
4235
|
-
// and is loaded once, only when the bridge is absent and nothing has been
|
|
4236
|
-
// loaded or generated yet. A local session with the bridge running is
|
|
4237
|
-
// untouched.
|
|
4238
|
-
const motionRefsRestored = useRef(false);
|
|
3763
|
+
// Startup idle work, once.
|
|
3764
|
+
const startupWarmed = useRef(false);
|
|
4239
3765
|
useEffect(() => {
|
|
4240
|
-
if (
|
|
4241
|
-
|
|
4242
|
-
restoreMotionRefs(startupStage.characters);
|
|
3766
|
+
if (startupWarmed.current) return;
|
|
3767
|
+
startupWarmed.current = true;
|
|
4243
3768
|
// Parse the thumbnail rigs during startup idle so the first pose-studio
|
|
4244
3769
|
// open starts rendering immediately instead of paying a 100ms+ FBX parse.
|
|
4245
3770
|
warmPoseThumbnails();
|
|
@@ -4247,64 +3772,21 @@ export default function App() {
|
|
|
4247
3772
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
4248
3773
|
}, []);
|
|
4249
3774
|
|
|
4250
|
-
|
|
4251
|
-
//
|
|
4252
|
-
//
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
const demoSeed = demoSeedGate(bridge, bridgeSeenOk.current);
|
|
4256
|
-
bridgeSeenOk.current = demoSeed.bridgeSeenOk;
|
|
4257
|
-
if (demoSeeded.current) return;
|
|
4258
|
-
if (!activeRig || motion || motionBusy) return;
|
|
4259
|
-
// A hosted-demo result link opens the app with ?motion=<url>. The value
|
|
4260
|
-
// is gated by motionUrlFromQuery (same-origin or allowlisted https host
|
|
4261
|
-
// only) and, unlike the shipped seed below, loads regardless of bridge
|
|
4262
|
-
// state — the visitor followed a link whose whole point is this clip.
|
|
4263
|
-
const queryMotion = motionUrlFromQuery(window.location.search, window.location.origin);
|
|
4264
|
-
if (queryMotion) {
|
|
4265
|
-
demoSeeded.current = true;
|
|
4266
|
-
loadMotion(queryMotion, "").catch(() => {
|
|
4267
|
-
/* a dead link degrades to the normal empty stage, not an error */
|
|
4268
|
-
});
|
|
4269
|
-
return;
|
|
4270
|
-
}
|
|
4271
|
-
// The seed dresses a visitor's first, unnamed stage and the starter set a
|
|
4272
|
-
// `?scene=` launch always re-opens ("one character mid-walk"). A named
|
|
4273
|
-
// project is the author's document: reloading it must never put the demo
|
|
4274
|
-
// walk back on a character they deliberately left without a take.
|
|
4275
|
-
if (loadProjectSession()?.name && !new URLSearchParams(window.location.search).get("scene")) { demoSeeded.current = true; return; }
|
|
4276
|
-
if (!demoSeed.seed) return;
|
|
4277
|
-
demoSeeded.current = true;
|
|
4278
|
-
// Loaded, not played: the clip walks the subject out of the default
|
|
4279
|
-
// framing, so autoplay would greet a first-time visitor with an empty
|
|
4280
|
-
// room. Frame 0 is composed; PLAYBACK is one click away.
|
|
4281
|
-
loadMotion(DEMO_MOTION_URL, DEMO_MOTION_PROMPT).catch(() => {
|
|
4282
|
-
/* the seed is a nicety, never a failure the visitor must act on */
|
|
4283
|
-
});
|
|
4284
|
-
}, [bridge, activeRig, motion, motionBusy]);
|
|
4285
|
-
|
|
4286
|
-
// The camera tutorial's seed (#209). The hosted-demo seed above is gated on
|
|
4287
|
-
// a missing bridge, which is why the tutorial used to open on an empty room
|
|
4288
|
-
// in a local session. startCameraTutorial arms tutorialSeedPending once the
|
|
4289
|
-
// starter scene is applied, and the same clip goes on regardless of bridge
|
|
4290
|
-
// state as soon as that scene's character has a parsed rig — a state
|
|
4291
|
-
// condition, not a timer, exactly like the seed above.
|
|
3775
|
+
// The camera tutorial's seed (#209). startCameraTutorial arms
|
|
3776
|
+
// tutorialSeedPending once the starter scene is applied, and a compiled walk
|
|
3777
|
+
// (take v3) goes on as soon as that scene's character has a parsed rig: a
|
|
3778
|
+
// state condition, not a timer. The compile runs in the worker; an edit that lands
|
|
3779
|
+
// before it answers makes the job's fence refuse the install (set dressing, no error).
|
|
4292
3780
|
useEffect(() => {
|
|
4293
|
-
if (!tutorialSeedPending || !activeRig
|
|
3781
|
+
if (!tutorialSeedPending || !activeRig) return;
|
|
4294
3782
|
const seedEpoch = tutorialSeedEpochRef.current;
|
|
4295
|
-
if (seedEpoch === null || seedEpoch !== tutorialProjectEpochRef.current) {
|
|
4296
|
-
tutorialSeedEpochRef.current = null;
|
|
4297
|
-
setTutorialSeedPending(false);
|
|
4298
|
-
return;
|
|
4299
|
-
}
|
|
4300
3783
|
tutorialSeedEpochRef.current = null;
|
|
4301
3784
|
setTutorialSeedPending(false);
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
});
|
|
3785
|
+
if (seedEpoch === null || seedEpoch !== tutorialProjectEpochRef.current) return;
|
|
3786
|
+
const spec = tutorialWalkSpec(activeChar); // the take is set dressing, never an error to act on
|
|
3787
|
+
if (spec && runStudioAction("motion.compile", { characterId: activeChar.id, spec })) { setTlFrame(0); setTlPlaying(false); }
|
|
4306
3788
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
4307
|
-
}, [tutorialSeedPending, activeRig
|
|
3789
|
+
}, [tutorialSeedPending, activeRig]);
|
|
4308
3790
|
|
|
4309
3791
|
// Drive every cast member from ITS OWN clip on the shared playhead. The
|
|
4310
3792
|
// active character's buffer motion and the stored session motions of the
|
|
@@ -4323,7 +3805,7 @@ export default function App() {
|
|
|
4323
3805
|
// clock. Recomputing both directions matters: deleting Subject 2 or
|
|
4324
3806
|
// shortening its take must pull the end back in instead of leaving the
|
|
4325
3807
|
// recorder with frozen tail frames (#80).
|
|
4326
|
-
useEffect(shotsDomain.syncTimelineExtent, [characters, activeChar.id, motion,
|
|
3808
|
+
useEffect(shotsDomain.syncTimelineExtent, [characters, activeChar.id, motion, shots]);
|
|
4327
3809
|
|
|
4328
3810
|
/* ------------------------------ IK logic ------------------------------ */
|
|
4329
3811
|
|
|
@@ -4343,32 +3825,6 @@ export default function App() {
|
|
|
4343
3825
|
if (!chains) leaveIkMode();
|
|
4344
3826
|
}, [activeRig, activeCapabilities.ik]);
|
|
4345
3827
|
|
|
4346
|
-
// Whether the collision capsules can be built for this rig at all. Bone
|
|
4347
|
-
// lookups only — no mesh measurement — so it is cheap enough to hang off
|
|
4348
|
-
// the rig identity and read straight in the render. A rig that resolves
|
|
4349
|
-
// for IK can still miss the toes/spine the capsule table needs, so this is
|
|
4350
|
-
// a SEPARATE question from `ikChains`: the two collision buttons disable
|
|
4351
|
-
// on it rather than offering a click whose only possible answer is "not
|
|
4352
|
-
// supported".
|
|
4353
|
-
const collisionCleanupSupported = useMemo(() => activeCapabilities.physics && supportsCollisionCleanup(activeRig), [activeRig, activeCapabilities.physics]);
|
|
4354
|
-
|
|
4355
|
-
//
|
|
4356
|
-
|
|
4357
|
-
useEffect(() => {
|
|
4358
|
-
physicsJobRef.current += 1;
|
|
4359
|
-
physicsSourceCacheRef.current.value = null;
|
|
4360
|
-
setPhysicsPreview(null);
|
|
4361
|
-
setPhysicsOptions({ overrides: [], protectedFrames: [], strength: 1 });
|
|
4362
|
-
setAutoPhysicsRunning(false);
|
|
4363
|
-
autoPhysicsRunRef.current = null;
|
|
4364
|
-
return () => { physicsJobRef.current += 1; };
|
|
4365
|
-
}, [activeRig, motion, activeChar.x, activeChar.y, activeChar.z, activeChar.rot, activeChar.scale]);
|
|
4366
|
-
useEffect(() => {
|
|
4367
|
-
if (physicsPreview && physicsPreview.sourceStamp !== physicsKeyStamp(ikStateRef.current.keys)) {
|
|
4368
|
-
setPhysicsPreview(null);
|
|
4369
|
-
}
|
|
4370
|
-
}, [ikTick, physicsPreview]);
|
|
4371
|
-
|
|
4372
3828
|
// Keyed-pose playback: the IK layer's keyed bone rotations apply at the
|
|
4373
3829
|
// current frame whether or not IK edit mode is on — IK-authored keys are
|
|
4374
3830
|
// the source of truth (the user designs first/end keys and ARDY in-
|
|
@@ -4387,12 +3843,11 @@ export default function App() {
|
|
|
4387
3843
|
// Preview changes may re-run this effect without a playhead change.
|
|
4388
3844
|
// Always re-establish the motion base before adding a correction.
|
|
4389
3845
|
if (motion) poseMemberAtFrame(activeRig, motion, null, tlFrame);
|
|
4390
|
-
const layer = pendingIkEdit?.characterId === activeChar.id ? pendingIkEdit.candidate
|
|
4391
|
-
: physicsPreview && physicsShow ? physicsPreview.candidate : ikStateRef.current;
|
|
3846
|
+
const layer = pendingIkEdit?.characterId === activeChar.id ? pendingIkEdit.candidate : ikStateRef.current;
|
|
4392
3847
|
if (ikMode || layer.keys.size > 0) {
|
|
4393
3848
|
ikEvaluate(ikChains, layer, tlFrame, ikFkJoints, motion ? IK_CORRECTION_BLEND_FRAMES : 0);
|
|
4394
3849
|
}
|
|
4395
|
-
}, [ikMode, ikChains, activeRig, motion, posing, tlFrame, ikTick, ikFkJoints,
|
|
3850
|
+
}, [ikMode, ikChains, activeRig, motion, posing, tlFrame, ikTick, ikFkJoints, pendingIkEdit, activeChar.id]);
|
|
4396
3851
|
|
|
4397
3852
|
// Re-seat the handles on the keyed pose when the FRAME changes with IK
|
|
4398
3853
|
// on — scrubbing to frame 39 shows that frame's interpolated pose AND
|
|
@@ -4410,11 +3865,10 @@ export default function App() {
|
|
|
4410
3865
|
}, [ikMode, ikChains, activeRig, motion, posing, tlFrame, ikTick, ikFkJoints]);
|
|
4411
3866
|
|
|
4412
3867
|
// QA hook: lets headless visual checks read the live rig/motion state
|
|
4413
|
-
// (
|
|
3868
|
+
// (test/motion3/qa-motion3-*-browser.mjs). Harmless in normal use.
|
|
4414
3869
|
useEffect(() => {
|
|
4415
3870
|
window.__cozyclay = {
|
|
4416
|
-
runStudioAction,
|
|
4417
|
-
runArdy: (options) => appContext.live.state.runArdy(options),
|
|
3871
|
+
runStudioAction, setFrame: setTlFrame,
|
|
4418
3872
|
rigA: activeRig, motion, tlFrame, frameCount: tlFrameCount, playing: tlPlaying, ikMode, ikChains, ikFocus, contactRadii: ikChains?.values().next().value?.contactRadii ?? null, ik: ikStateRef.current,
|
|
4419
3873
|
rangePins, rangePinResiduals,
|
|
4420
3874
|
rangePinApplySpec: motionDomain.rangePinApplySpec,
|
|
@@ -4527,16 +3981,7 @@ export default function App() {
|
|
|
4527
3981
|
addSceneObject: (kind, placement = {}) => addSceneObject(kind, placement),
|
|
4528
3982
|
scrub: (frame) => setTlFrame(Math.max(0, Math.min(tlFrameCount - 1, Math.round(frame)))),
|
|
4529
3983
|
pause: () => setTlPlaying(false),
|
|
4530
|
-
|
|
4531
|
-
// same drag -> preview -> pending-edit path headless checks cannot reach
|
|
4532
|
-
// through synthetic pointers reliably.
|
|
4533
|
-
trail: { falloffFrames: trailFalloffFrames, falloffS: trailFalloffS, edit: trailEdit, tool: ikEditTool, visible: showTrails, activeTrackId: trailTrackFocus, visibleTracks: trailTrackFocus ? [trailTrackFocus] : null },
|
|
4534
|
-
trailPoints: (jointName = "Hips") => jointTrailPoints(motion, jointName, { baseY: activeChar.y ?? 0, scale: activeChar.scale ?? 1 }),
|
|
4535
|
-
trailEditApply: (grabFrame, delta) => {
|
|
4536
|
-
onTrailDragStart({ track: "hips", grabFrame });
|
|
4537
|
-
onTrailDragPreview({ track: "hips", grabFrame, delta });
|
|
4538
|
-
onTrailDragEnd({ track: "hips", grabFrame, delta });
|
|
4539
|
-
},
|
|
3984
|
+
|
|
4540
3985
|
// QA-only IK drag seam: resolves the same production solve/end path as
|
|
4541
3986
|
// the viewport manipulator, with a deterministic world-space offset.
|
|
4542
3987
|
ikPreviewApply: (track = "leftHand", offset = { x: 0.02, y: 0, z: 0 }) => {
|
|
@@ -4547,53 +3992,11 @@ export default function App() {
|
|
|
4547
3992
|
ikDragEnd();
|
|
4548
3993
|
return true;
|
|
4549
3994
|
},
|
|
4550
|
-
trailRegenerate: runTrailRegeneration,
|
|
4551
|
-
// Fix-collisions QA surface: live penetration readout for headless
|
|
4552
|
-
// checks; the fix itself runs through the buttons / runFixCollisions.
|
|
4553
|
-
// null (not []) on an unsupported rig, so a check can tell "the tool
|
|
4554
|
-
// cannot describe this skeleton" from "this pose is clean".
|
|
4555
|
-
// EXTERNAL blockers ride this readout too, so a headless check can see
|
|
4556
|
-
// `leftHand×obj:chair` / `leftHand×char:subject-2:torso` and not just
|
|
4557
|
-
// self-collisions. A blocker hit has no `def` on its side of the pair
|
|
4558
|
-
// — it is not a capsule of THIS rig — so the label falls back to the
|
|
4559
|
-
// blocker's own namespaced id.
|
|
4560
|
-
fcDetect: () => {
|
|
4561
|
-
const capsules = activeRig ? buildCollisionCapsules(activeRig) : null;
|
|
4562
|
-
if (!capsules) return null;
|
|
4563
|
-
const label = (side) => side?.def?.id ?? side?.id ?? "?";
|
|
4564
|
-
return detectPenetrations(capsules, { blockers: externalBlockers(tlFrame) })
|
|
4565
|
-
.map((p) => ({ pair: `${label(p.a)}×${label(p.b)}`, depth: p.depth }));
|
|
4566
|
-
},
|
|
4567
|
-
// What those `obj:` / `char:` names stand for, as plain numbers: the
|
|
4568
|
-
// boxes and capsules the fixer is being asked to keep the body out of.
|
|
4569
|
-
fcBlockers: () => blockerSummary(externalBlockers(tlFrame)),
|
|
4570
|
-
// AutoPhysics QA surface: the centre of mass of the CURRENT pose, so
|
|
4571
|
-
// a headless check can sample the arc before/after the button press.
|
|
4572
|
-
apCom: () => {
|
|
4573
|
-
const com = activeRig ? computeCenterOfMass(activeRig) : null;
|
|
4574
|
-
return com ? { x: com.x, y: com.y, z: com.z } : null;
|
|
4575
|
-
},
|
|
4576
|
-
apFeet: () => {
|
|
4577
|
-
const points = activeRig ? markerPositions(activeRig) : null;
|
|
4578
|
-
return points ? Object.fromEntries(Object.entries(points).map(([name, point]) => [name, { x: point.x, y: point.y, z: point.z }])) : null;
|
|
4579
|
-
},
|
|
4580
|
-
apRun: runAutoPhysics,
|
|
4581
|
-
physics: { preview: physicsPreview, show: physicsShow, running: autoPhysicsRunning, options: physicsOptions },
|
|
4582
|
-
platformFit: {
|
|
4583
|
-
running: platformFitRunning, applied: platformFitApplied, last: platformFitLast,
|
|
4584
|
-
run: () => Promise.resolve(motionDomain.run('motion.platformFit.run', { characterId: activeChar?.id })).then(receipt => receipt.output),
|
|
4585
|
-
remove: () => Promise.resolve(motionDomain.run('motion.platformFit.remove', { characterId: activeChar?.id })).then(receipt => receipt.output),
|
|
4586
|
-
},
|
|
4587
|
-
apOptions: changePhysicsOptions,
|
|
4588
3995
|
preview,
|
|
4589
3996
|
pathDraw,
|
|
4590
3997
|
};
|
|
4591
3998
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
4592
|
-
|
|
4593
|
-
// close over them: a stale closure would report the set as it was two
|
|
4594
|
-
// edits ago — and, after an undo that removes a subject, would keep
|
|
4595
|
-
// reporting the ghost's capsules.
|
|
4596
|
-
}, [activeRig, motion, tlFrame, tlFrameCount, ikMode, ikChains, ikFocus, ikTick, charA, rangePins, rangePinResiduals, committedIkEdits, waypoints, lookThroughShot, selectedSceneObject, sceneObjects, rigs, characters, pathPointIndex, preview, posing, playMode, pathDraw, trailEdit, trailTrackFocus, pendingIkEdit, trailFalloffFrames, trailFalloffS, ikEditTool, showTrails, physicsPreview, physicsShow, physicsOptions, autoPhysicsRunning, platformFitRunning, platformFitLast, platformFitApplied]);
|
|
3999
|
+
}, [activeRig, motion, tlFrame, tlFrameCount, ikMode, ikChains, ikFocus, ikTick, charA, rangePins, rangePinResiduals, committedIkEdits, waypoints, lookThroughShot, selectedSceneObject, sceneObjects, rigs, characters, pathPointIndex, preview, posing, playMode, pathDraw, pendingIkEdit, ikEditTool]);
|
|
4597
4000
|
// QA hook (plan §6.5): exposes history depth and the present === objects
|
|
4598
4001
|
// invariant so the browser suite can assert undo entry counts directly.
|
|
4599
4002
|
// Reads live store state at call time; re-registered after every render.
|
|
@@ -4622,16 +4025,7 @@ export default function App() {
|
|
|
4622
4025
|
};
|
|
4623
4026
|
});
|
|
4624
4027
|
|
|
4625
|
-
|
|
4626
|
-
// the parent AFTER Character's pose effect (children flush first), so even
|
|
4627
|
-
// a pose change made during playback cannot leak into the restored rig.
|
|
4628
|
-
useEffect(() => {
|
|
4629
|
-
if (motion) return;
|
|
4630
|
-
const saved = restoreRef.current;
|
|
4631
|
-
if (!saved) return;
|
|
4632
|
-
restoreRef.current = null;
|
|
4633
|
-
restorePlaybackBones(saved.rig, saved.bones);
|
|
4634
|
-
}, [motion]);
|
|
4028
|
+
|
|
4635
4029
|
|
|
4636
4030
|
const playbackSceneBase = useMemo(() => ({
|
|
4637
4031
|
frameCount: tlFrameCount,
|
|
@@ -4678,32 +4072,8 @@ export default function App() {
|
|
|
4678
4072
|
// the follow controller and the Top-View display.
|
|
4679
4073
|
const railCurve = useMemo(() => (cameraRail ? buildRail(cameraRail) : null), [cameraRail]);
|
|
4680
4074
|
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
const yaw = (charA.rot * Math.PI) / 180;
|
|
4684
|
-
const combined = new Array(subjectTrack.length).fill(null);
|
|
4685
|
-
for (let index = 0; index < shots.length; index += 1) {
|
|
4686
|
-
const shot = shots[index];
|
|
4687
|
-
const camera = createCameraBlock(shot.camera);
|
|
4688
|
-
if (camera.mode === "keys") continue;
|
|
4689
|
-
const start = shot.startFrame;
|
|
4690
|
-
const end = Math.min(subjectTrack.length, shot.endFrame + 1);
|
|
4691
|
-
const subjectSlice = subjectTrack.slice(start, end);
|
|
4692
|
-
const params = { ...camera.followCam, craneHeight: camera.craneHeight, dollyTiming: camera.dollyTiming, initialDir: { x: Math.sin(yaw), z: Math.cos(yaw) } };
|
|
4693
|
-
const rail = camera.mode === "rail" ? buildRail(camera.cameraRail) : null;
|
|
4694
|
-
if (rail) {
|
|
4695
|
-
const schedule = resolveRailSchedule({ railFollow: camera.railFollow, cameraRail: camera.cameraRail, frameCount: subjectSlice.length });
|
|
4696
|
-
if (schedule.kind !== RAIL_SCHEDULE_RANGE && schedule.kind !== RAIL_SCHEDULE_LEGACY) continue;
|
|
4697
|
-
const railSlice = subjectSlice.slice(schedule.startFrame, schedule.endFrame + 1);
|
|
4698
|
-
const track = buildRailFollowTrack(railSlice, tlFps, rail, params);
|
|
4699
|
-
track.forEach((sample, offset) => { combined[start + schedule.startFrame + offset] = sample; });
|
|
4700
|
-
} else {
|
|
4701
|
-
const track = buildFollowTrack(subjectSlice, tlFps, params);
|
|
4702
|
-
track.forEach((sample, offset) => { combined[start + offset] = sample; });
|
|
4703
|
-
}
|
|
4704
|
-
}
|
|
4705
|
-
return combined;
|
|
4706
|
-
}, [shots, subjectTrack, tlFps, charA.rot]);
|
|
4075
|
+
// A rail shot with an aim holds its cast (or a world point) instead of the subject.
|
|
4076
|
+
const followTrack = useShotCameraTracks({ shots, subjectTrack, subjectId: charA.id, fps: tlFps, yawDeg: charA.rot, filmback, characters, objects: sceneObjects, frameCount: tlFrameCount, activeId: activeChar.id, motion });
|
|
4707
4077
|
|
|
4708
4078
|
const playbackScene = useMemo(() => ({
|
|
4709
4079
|
...playbackSceneBase,
|
|
@@ -4775,13 +4145,8 @@ export default function App() {
|
|
|
4775
4145
|
return { min, max, warn: min < 0.5 || max > 3 };
|
|
4776
4146
|
}, [activeChar.x, activeChar.z, tlFps, waypoints]);
|
|
4777
4147
|
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
: motion
|
|
4781
|
-
? { label: ko("PLAYBACK", "재생"), kind: "playback" }
|
|
4782
|
-
: waypointMode
|
|
4783
|
-
? { label: ko("ROOT PATH", "루트 경로"), kind: "root" }
|
|
4784
|
-
: null;
|
|
4148
|
+
// A loaded take needs no badge: its clip on the Body lane already says so.
|
|
4149
|
+
const stateBadge = !motion && waypointMode ? { label: ko("ROOT PATH", "루트 경로"), kind: "root" } : null;
|
|
4785
4150
|
|
|
4786
4151
|
function bufferToPng(buffer, output = shotOutput, decorate = null) {
|
|
4787
4152
|
const canvas = document.createElement("canvas");
|
|
@@ -5051,1783 +4416,6 @@ export default function App() {
|
|
|
5051
4416
|
return executeExportRequest(frameJob.request);
|
|
5052
4417
|
}
|
|
5053
4418
|
|
|
5054
|
-
// Keep the optional sidecar live instead of freezing its startup state.
|
|
5055
|
-
// Developers commonly open the studio first and start `npm run bridge`
|
|
5056
|
-
// second; a one-shot failed probe left Generate disabled until reload.
|
|
5057
|
-
useEffect(() => {
|
|
5058
|
-
let alive = true;
|
|
5059
|
-
let inflight = null;
|
|
5060
|
-
// The poll answer is almost always identical to the last one; keeping the
|
|
5061
|
-
// previous object identity skips a full App re-render per poll. Each of
|
|
5062
|
-
// those renders costs ~80ms of main thread on this tree, which read as a
|
|
5063
|
-
// periodic hitch while orbiting/flying the camera.
|
|
5064
|
-
const refreshBridge = () => {
|
|
5065
|
-
if (inflight) return inflight;
|
|
5066
|
-
inflight = checkBridge().then((state) => {
|
|
5067
|
-
if (!alive) return;
|
|
5068
|
-
if (state.ok) trackFeature("mcp_connected");
|
|
5069
|
-
setBridge((previous) => JSON.stringify(previous) === JSON.stringify(state) ? previous : state);
|
|
5070
|
-
setLineEditBackend(hasLineEditCapability(state));
|
|
5071
|
-
}).finally(() => { inflight = null; });
|
|
5072
|
-
return inflight;
|
|
5073
|
-
};
|
|
5074
|
-
bridgeRefreshRef.current = refreshBridge;
|
|
5075
|
-
refreshBridge();
|
|
5076
|
-
const id = window.setInterval(refreshBridge, BRIDGE_RECHECK_MS);
|
|
5077
|
-
window.addEventListener("focus", refreshBridge);
|
|
5078
|
-
return () => {
|
|
5079
|
-
alive = false;
|
|
5080
|
-
window.clearInterval(id);
|
|
5081
|
-
window.removeEventListener("focus", refreshBridge);
|
|
5082
|
-
};
|
|
5083
|
-
}, []);
|
|
5084
|
-
|
|
5085
|
-
// QA/programmatic requests must use the current render's same generation path.
|
|
5086
|
-
appContext.patchLive({ runArdy: runArdy });
|
|
5087
|
-
|
|
5088
|
-
/* ======================= line editing (contract C6) =======================
|
|
5089
|
-
* Grab the joint's own motion path on the viewport and pull it; the joint
|
|
5090
|
-
* then follows the pulled path exactly.
|
|
5091
|
-
*
|
|
5092
|
-
* Four invariants hold this together and every function below serves one
|
|
5093
|
-
* of them:
|
|
5094
|
-
* 1. THE CURVE AND THE CAMERA ARE ONE OBJECT. uv only mean something
|
|
5095
|
-
* through the lens they were projected with, so the camera is captured
|
|
5096
|
-
* when the curve is built, stored next to it, and the pair is rebuilt
|
|
5097
|
-
* together when the view moves. Nothing re-reads old uv through a new
|
|
5098
|
-
* lens.
|
|
5099
|
-
* 2. THE EDIT IS A DEFORMATION OF THE TAKE, NOT A REPLACEMENT OF IT. The
|
|
5100
|
-
* curve starts as the joint's current trajectory and the falloff pins
|
|
5101
|
-
* its ends there, so the first and last constrained frames always
|
|
5102
|
-
* agree with the take. That is the seam fix GP2 measured: 8.0x median
|
|
5103
|
-
* frame delta for an arbitrary line, 1.09x when the ends sit on the
|
|
5104
|
-
* joint's own path.
|
|
5105
|
-
* 3. THE OVERLAY NEVER TOUCHES THE SCENE GRAPH. It is a plain 2D canvas
|
|
5106
|
-
* stacked on the stage, so an edit cannot disturb playback, picking
|
|
5107
|
-
* or the render loop, and it works identically in every view mode.
|
|
5108
|
-
* 4. THE REQUEST IS ITS OWN RUN. C6 makes lineEdit exclusive with
|
|
5109
|
-
* preserve/waypoints/segments/motionEdit, so runLineEdit builds a
|
|
5110
|
-
* dedicated body instead of decorating runArdy's. The WIRE SHAPE is
|
|
5111
|
-
* untouched by the interaction change: points2d is still a <=64-point
|
|
5112
|
-
* viewport-normalized polyline. */
|
|
5113
|
-
|
|
5114
|
-
/** The loaded take's length on the TIMELINE clock — C6's frameRange is in
|
|
5115
|
-
* app clip frames and, unlike waypoints or motionEdit, is never converted
|
|
5116
|
-
* to the bridge clock. */
|
|
5117
|
-
const lineClipFrames = motion?.frames ?? 0;
|
|
5118
|
-
/** THE TAKE, as opposed to what is on screen. While a line-edit preview is
|
|
5119
|
-
* showing, `motion` is the draft and this is still the take the draft was
|
|
5120
|
-
* drafted FROM — so an edit sources the take, the version strip keeps
|
|
5121
|
-
* highlighting the take's own chip, and a preview can never quietly become
|
|
5122
|
-
* the thing everything else is built on. With no preview the two are the
|
|
5123
|
-
* same url, which is why every call site can read this one unconditionally. */
|
|
5124
|
-
const takeSourceUrl = linePreviewSource?.url ?? motion?.url ?? null;
|
|
5125
|
-
/** The authored range, or the whole clip when the user has not narrowed it.
|
|
5126
|
-
* Re-derived rather than stored so loading a different take cannot leave a
|
|
5127
|
-
* range pointing past the end of the new one. */
|
|
5128
|
-
const lineEditRange = useMemo(() => {
|
|
5129
|
-
if (!(lineClipFrames > 1)) return null;
|
|
5130
|
-
const start = Math.max(0, Math.min(lineRange?.startFrame ?? 0, lineClipFrames - 2));
|
|
5131
|
-
const end = Math.max(start + 2, Math.min(lineRange?.endFrame ?? lineClipFrames, lineClipFrames));
|
|
5132
|
-
return { startFrame: start, endFrame: end };
|
|
5133
|
-
}, [lineRange, lineClipFrames]);
|
|
5134
|
-
|
|
5135
|
-
/* ---------------------------- the pins edit -----------------------------
|
|
5136
|
-
* A pins edit is derived, not stored: the pins ARE the payload and the range
|
|
5137
|
-
* follows from them (pinsFrameRange), so there is no second copy of either
|
|
5138
|
-
* to keep in sync. `lineEditPayload` is what every downstream call site
|
|
5139
|
-
* takes — Generate, the preview, the request builder — and it is the one
|
|
5140
|
-
* place the two gestures meet. */
|
|
5141
|
-
const linePinRange = useMemo(
|
|
5142
|
-
() => (linePins.length ? pinsFrameRange(linePins, lineClipFrames) : null),
|
|
5143
|
-
[linePins, lineClipFrames],
|
|
5144
|
-
);
|
|
5145
|
-
const linePinEdit = useMemo(
|
|
5146
|
-
() => (linePins.length && linePinRange ? { pins: linePins, frameRange: linePinRange } : null),
|
|
5147
|
-
[linePins, linePinRange],
|
|
5148
|
-
);
|
|
5149
|
-
/** The edit in hand, whichever gesture made it. Null means "nothing to
|
|
5150
|
-
* send", which is exactly what the Generate gate asks. */
|
|
5151
|
-
const lineEditPayload = lineCurve ?? linePinEdit;
|
|
5152
|
-
|
|
5153
|
-
/** The joint's WORLD position at one frame, from the same trail sampler the
|
|
5154
|
-
* curve is projected from — so a pin and the curve can never disagree about
|
|
5155
|
-
* where the joint is. Null when there is no take, no trail or no such
|
|
5156
|
-
* frame. */
|
|
5157
|
-
function lineJointWorldAt(frame) {
|
|
5158
|
-
const jointName = TRAIL_EFFECTOR_JOINTS[lineTrack];
|
|
5159
|
-
if (!motion || !jointName) return null;
|
|
5160
|
-
const trail = jointTrailPoints(motion, jointName, { baseY: activeChar.y ?? 0, scale: activeChar.scale ?? 1 });
|
|
5161
|
-
const index = Math.max(0, Math.min(Math.trunc(frame) || 0, motion.frames - 1));
|
|
5162
|
-
if (!trail || index * 3 + 2 >= trail.length) return null;
|
|
5163
|
-
return [trail[index * 3], trail[index * 3 + 1], trail[index * 3 + 2]];
|
|
5164
|
-
}
|
|
5165
|
-
|
|
5166
|
-
/** Every pin as a WORLD point, for the overlay. Pins are stored in clip
|
|
5167
|
-
* space (that is what the wire wants); drawing them means undoing the
|
|
5168
|
-
* conversion, which is jointTrailPoints' own transform applied to one
|
|
5169
|
-
* point. Kept here rather than storing both spellings: two stored copies of
|
|
5170
|
-
* a coordinate is how a pin ends up drawn somewhere it was not placed. */
|
|
5171
|
-
function linePinWorld(pin) {
|
|
5172
|
-
if (!motion) return null;
|
|
5173
|
-
const basis = { baseY: activeChar.y ?? 0, scale: activeChar.scale ?? 1 };
|
|
5174
|
-
// Forward transform, mirroring motion-trail's jointTrailPoints.
|
|
5175
|
-
const clipToWorld = (p) => {
|
|
5176
|
-
const radians = ((Number.isFinite(motion.rotationDeg) ? motion.rotationDeg : 0) * Math.PI) / 180;
|
|
5177
|
-
const cos = Math.cos(radians);
|
|
5178
|
-
const sin = Math.sin(radians);
|
|
5179
|
-
const anchorFrame = Math.max(0, Math.min(motion.anchorFrame || 0, Math.max(0, motion.frames - 1)));
|
|
5180
|
-
const rootX = motion.posedJoints?.[anchorFrame * 27 * 3] ?? 0;
|
|
5181
|
-
const rootZ = motion.posedJoints?.[anchorFrame * 27 * 3 + 2] ?? 0;
|
|
5182
|
-
const anchorX = Number.isFinite(motion.anchorX) ? motion.anchorX : 0;
|
|
5183
|
-
const anchorZ = Number.isFinite(motion.anchorZ) ? motion.anchorZ : 0;
|
|
5184
|
-
const dx = p[0] - rootX;
|
|
5185
|
-
const dz = p[2] - rootZ;
|
|
5186
|
-
return [
|
|
5187
|
-
anchorX + (dx * cos + dz * sin) * basis.scale,
|
|
5188
|
-
basis.baseY + p[1] * basis.scale,
|
|
5189
|
-
anchorZ + (-dx * sin + dz * cos) * basis.scale,
|
|
5190
|
-
];
|
|
5191
|
-
};
|
|
5192
|
-
return clipToWorld(pin.position);
|
|
5193
|
-
}
|
|
5194
|
-
|
|
5195
|
-
/** Drop the pins, and say so. Called when a stroke or a drag commits — the
|
|
5196
|
-
* two gestures are exclusive, and a silently discarded pin is worse than a
|
|
5197
|
-
* refused one. */
|
|
5198
|
-
function clearLinePins({ toast = true } = {}) {
|
|
5199
|
-
if (!linePins.length) return;
|
|
5200
|
-
setLinePins([]);
|
|
5201
|
-
if (toast) {
|
|
5202
|
-
setToast(ko(
|
|
5203
|
-
"Pins cleared — one edit is one gesture, and this one is now the path",
|
|
5204
|
-
"찍은 순간을 지웠어요 — 한 번의 편집은 한 가지 방식이라, 지금은 궤적 편집이에요",
|
|
5205
|
-
));
|
|
5206
|
-
}
|
|
5207
|
-
}
|
|
5208
|
-
|
|
5209
|
-
/** Has the user pulled anything yet? The Generate gate, and the reason the
|
|
5210
|
-
* panel can say "pull first" instead of shipping a no-op edit. It is just
|
|
5211
|
-
* the state's existence: pointerup installs a curve only when the pull
|
|
5212
|
-
* actually moved something and clears it otherwise, so "there is a curve
|
|
5213
|
-
* object" and "there is an edit" are the same fact. */
|
|
5214
|
-
const lineCurveDirty = !!lineEditPayload;
|
|
5215
|
-
/** Frames of the range whose joint has no image (behind the lens or out of
|
|
5216
|
-
* frame). Those points cannot be grabbed and are dropped from the payload,
|
|
5217
|
-
* so the count is worth showing rather than leaving as a mystery gap. */
|
|
5218
|
-
const lineCurveHidden = useMemo(
|
|
5219
|
-
() => (lineCurve ? lineCurve.original.reduce((count, point) => count + (isCurvePointOnScreen(point) ? 0 : 1), 0) : 0),
|
|
5220
|
-
[lineCurve],
|
|
5221
|
-
);
|
|
5222
|
-
/** How many points the box will actually receive — the curve is downsampled
|
|
5223
|
-
* to MAX_LINE_POINTS, and seeing the number keeps "64" from being a
|
|
5224
|
-
* surprise buried in the contract. */
|
|
5225
|
-
const lineCurvePointCount = useMemo(
|
|
5226
|
-
() => (lineCurve ? Math.min(MAX_LINE_POINTS, lineCurve.original.length - lineCurveHidden) : 0),
|
|
5227
|
-
[lineCurve, lineCurveHidden],
|
|
5228
|
-
);
|
|
5229
|
-
/** The frames the edit in hand actually covers. A DRAWN curve carries the
|
|
5230
|
-
* window its stroke matched (which is also what goes on the wire); a dragged
|
|
5231
|
-
* one has always covered the panel's range. Shown rather than left implicit
|
|
5232
|
-
* because auto-ranging means the artist did not choose these numbers and
|
|
5233
|
-
* deserves to see what the stroke was read as. endFrame is exclusive on the
|
|
5234
|
-
* wire and inclusive to a human, hence the -1. */
|
|
5235
|
-
const lineEditFrom = lineCurve?.frameRange?.startFrame ?? lineEditRange?.startFrame ?? 0;
|
|
5236
|
-
const lineEditTo = (lineCurve?.frameRange?.endFrame ?? lineEditRange?.endFrame ?? 0) - 1;
|
|
5237
|
-
|
|
5238
|
-
/** Change the influence radius. A live drag is re-derived from its snapshot
|
|
5239
|
-
* rather than left showing the old falloff, which is what makes the slider
|
|
5240
|
-
* legible: drag, then widen, and the same pull spreads under the finger. */
|
|
5241
|
-
function changeLineRadius(next) {
|
|
5242
|
-
const radius = Math.max(DRAG_RADIUS_MIN, Math.min(DRAG_RADIUS_MAX, Math.round(Number(next) || 0)));
|
|
5243
|
-
setLineRadius(radius);
|
|
5244
|
-
const drag = lineDragRef.current;
|
|
5245
|
-
if (!drag) return;
|
|
5246
|
-
// The drag carries its own radius so the window-level pointermove (which
|
|
5247
|
-
// closed over the value at grab time) keeps agreeing with what is drawn.
|
|
5248
|
-
drag.radius = radius;
|
|
5249
|
-
drag.live = dragCurve(drag.snapshot, drag.index, drag.du, drag.dv, radius);
|
|
5250
|
-
}
|
|
5251
|
-
|
|
5252
|
-
/** Which camera actually draws the MAIN pane right now, and the exact
|
|
5253
|
-
* rectangle it draws into.
|
|
5254
|
-
*
|
|
5255
|
-
* This mirrors DualRender's own branch order on purpose: the pane holds
|
|
5256
|
-
* different cameras in different modes, and the letterboxed shot views draw
|
|
5257
|
-
* into a fitAspect sub-rect rather than the whole pane. Measuring the curve
|
|
5258
|
-
* against the pane instead of the IMAGE would shift every point by the
|
|
5259
|
-
* width of the black bars. Plan and IK views return null — the plan camera
|
|
5260
|
-
* is orthographic (no pinhole intrinsics to send) and IK mode is mutually
|
|
5261
|
-
* exclusive with this one anyway. Rects are in CLIENT coordinates, which is
|
|
5262
|
-
* what pointer events speak. */
|
|
5263
|
-
function lineEditPane() {
|
|
5264
|
-
const pane = mainPaneRef.current;
|
|
5265
|
-
if (!pane) return null;
|
|
5266
|
-
const box = pane.getBoundingClientRect();
|
|
5267
|
-
if (!(box.width >= 2 && box.height >= 2)) return null;
|
|
5268
|
-
const rect = { x: box.left, y: box.top, w: box.width, h: box.height };
|
|
5269
|
-
if (planIsMain || ikMode) return null;
|
|
5270
|
-
if (playMode || lookThroughShot) {
|
|
5271
|
-
const camera = shotCamRef.current;
|
|
5272
|
-
return camera ? { camera, rect: fitAspect(rect, shotOutput.aspect) } : null;
|
|
5273
|
-
}
|
|
5274
|
-
const camera = editorCamRef.current;
|
|
5275
|
-
return camera ? { camera, rect } : null;
|
|
5276
|
-
}
|
|
5277
|
-
|
|
5278
|
-
/** Freeze the pane's camera into the C6 block. Returns null rather than
|
|
5279
|
-
* throwing when the camera is not a settled perspective camera — a refusal
|
|
5280
|
-
* here means "do not send", never "send something approximate". */
|
|
5281
|
-
function captureLineCamera(pane) {
|
|
5282
|
-
const camera = pane?.camera;
|
|
5283
|
-
if (!camera?.isPerspectiveCamera) return null;
|
|
5284
|
-
camera.updateMatrixWorld();
|
|
5285
|
-
const inverse = new THREE.Matrix4().copy(camera.matrixWorld).invert();
|
|
5286
|
-
try {
|
|
5287
|
-
return cameraToC6({
|
|
5288
|
-
fovDeg: camera.fov,
|
|
5289
|
-
aspect: camera.aspect,
|
|
5290
|
-
matrixWorldInverse: [...inverse.elements],
|
|
5291
|
-
width: pane.rect.w,
|
|
5292
|
-
height: pane.rect.h,
|
|
5293
|
-
});
|
|
5294
|
-
} catch (err) {
|
|
5295
|
-
// The only way here is a pane whose aspect disagrees with the
|
|
5296
|
-
// projection matrix, i.e. a frame drawn before DualRender settled.
|
|
5297
|
-
// Refusing is correct; the next paint or pointerup retries.
|
|
5298
|
-
console.warn("line edit: camera capture refused —", err.message);
|
|
5299
|
-
return null;
|
|
5300
|
-
}
|
|
5301
|
-
}
|
|
5302
|
-
|
|
5303
|
-
/** Has the live view moved out from under a COMMITTED edit?
|
|
5304
|
-
*
|
|
5305
|
-
* The comparison is the same cameraDrifted the old watcher used; what changed
|
|
5306
|
-
* is what a `true` MEANS. It is not "this edit is invalid" — the edit carries
|
|
5307
|
-
* the lens it was authored through and goes on the wire with it, so it stays
|
|
5308
|
-
* exactly as applicable as it was. It is "the painted line can no longer be
|
|
5309
|
-
* drawn where it belongs", because reprojecting the edited uv through the new
|
|
5310
|
-
* camera would need a depth that a 2D edit does not have.
|
|
5311
|
-
*
|
|
5312
|
-
* Called both from the 4 Hz watcher (which owns the panel's state) and
|
|
5313
|
-
* synchronously at pointerdown (which cannot afford to be a quarter second
|
|
5314
|
-
* behind) — one spelling, so the paint, the hint and the refusal can never
|
|
5315
|
-
* disagree about which state the mode is in. */
|
|
5316
|
-
function lineCurveDrifted(pane, edit = lineCurve) {
|
|
5317
|
-
if (!edit) return false;
|
|
5318
|
-
const live = captureLineCamera(pane);
|
|
5319
|
-
// An unmeasurable pane is not a moved view — a frame drawn before
|
|
5320
|
-
// DualRender settled must not flicker the line out or refuse a press.
|
|
5321
|
-
if (!live) return lineDriftRef.current;
|
|
5322
|
-
return cameraDrifted(live, edit.camera);
|
|
5323
|
-
}
|
|
5324
|
-
|
|
5325
|
-
/** The one sentence the drifted state says — in the panel, and in the toast
|
|
5326
|
-
* that refuses a new gesture. ONE spelling, because a hint that disagreed
|
|
5327
|
-
* with the refusal would read as two different problems. */
|
|
5328
|
-
function lineDriftHint() {
|
|
5329
|
-
return ko(
|
|
5330
|
-
"The view moved — the pending edit still applies; the dashed line is that same edit seen from here. Return toward the original view to grab it again, or Generate/undo from here.",
|
|
5331
|
-
"시점이 움직였어요 — 편집한 궤적은 그대로 적용되며, 점선은 같은 궤적을 지금 시점에서 본 모습입니다. 다시 잡으려면 원래 시점 쪽으로 돌아가고, 지금 이 상태에서 생성하거나 되돌려도 됩니다.",
|
|
5332
|
-
);
|
|
5333
|
-
}
|
|
5334
|
-
|
|
5335
|
-
/** The drifted ghost, RE-ANCHORED: lift the committed edit into world space
|
|
5336
|
-
* with the trail's own per-frame depth and see it through the LIVE lens, so
|
|
5337
|
-
* the dashed line hugs the trajectory instead of floating wherever the old
|
|
5338
|
-
* uv happen to land in the new view. Returns null when the trip cannot be
|
|
5339
|
-
* made (no live lens yet, no motion, a pins-only edit with no curves) — the
|
|
5340
|
-
* caller then paints the authored uv as before, which is at least honest
|
|
5341
|
-
* about being stale. Runs per frame like the rest of the painter; it is the
|
|
5342
|
-
* same few hundred pinhole projections projectLineCurve already pays. */
|
|
5343
|
-
function reprojectDriftedEdit(pane, edit) {
|
|
5344
|
-
const live = captureLineCamera(pane);
|
|
5345
|
-
const jointName = TRAIL_EFFECTOR_JOINTS[lineTrack];
|
|
5346
|
-
if (!live || !jointName || !motion) return null;
|
|
5347
|
-
const trail = jointTrailPoints(motion, jointName, { baseY: activeChar.y ?? 0, scale: activeChar.scale ?? 1 });
|
|
5348
|
-
if (!trail) return null;
|
|
5349
|
-
const original = reprojectCurveWorld(edit.original, trail, edit.camera, live);
|
|
5350
|
-
const edited = reprojectCurveWorld(edit.edited, trail, edit.camera, live);
|
|
5351
|
-
if (!original || !edited) return null;
|
|
5352
|
-
return { original, edited };
|
|
5353
|
-
}
|
|
5354
|
-
|
|
5355
|
-
/** Project the joint's trail for the current track and range through the
|
|
5356
|
-
* pane's LIVE camera. `{ camera, curve }`, or null when the view cannot
|
|
5357
|
-
* supply a usable lens right now — a refusal means "no curve", never "a
|
|
5358
|
-
* curve through some other camera".
|
|
5359
|
-
*
|
|
5360
|
-
* Deliberately the same sampler and the same arguments the old ghost line
|
|
5361
|
-
* used (jointTrailPoints + TRAIL_EFFECTOR_JOINTS), so the curve the user
|
|
5362
|
-
* grabs is the trajectory they were previously told to trace by hand.
|
|
5363
|
-
*
|
|
5364
|
-
* Cheap ON PURPOSE: while nothing has been pulled this runs once per
|
|
5365
|
-
* animation frame, and that is exactly what lets the path FOLLOW the camera
|
|
5366
|
-
* instead of fighting it. A few hundred pinhole projections is nothing
|
|
5367
|
-
* beside the WebGL frame it is drawn over. */
|
|
5368
|
-
function projectLineCurve(pane, { camera: reuseCamera = null, frameRange = null } = {}) {
|
|
5369
|
-
if (!pane || !motion) return null;
|
|
5370
|
-
const range = frameRange ?? lineEditRange;
|
|
5371
|
-
if (!range) return null;
|
|
5372
|
-
const camera = reuseCamera ?? captureLineCamera(pane);
|
|
5373
|
-
const jointName = TRAIL_EFFECTOR_JOINTS[lineTrack];
|
|
5374
|
-
if (!camera || !jointName) return null;
|
|
5375
|
-
const trail = jointTrailPoints(motion, jointName, { baseY: activeChar.y ?? 0, scale: activeChar.scale ?? 1 });
|
|
5376
|
-
if (!trail) return null;
|
|
5377
|
-
const curve = projectTrailCurve({ trail, frameRange: range, camera });
|
|
5378
|
-
if (!curve || curve.length < MIN_LINE_POINTS) return null;
|
|
5379
|
-
return { camera, curve };
|
|
5380
|
-
}
|
|
5381
|
-
|
|
5382
|
-
/** Repaint the whole overlay from scratch: the drawable frame, the joint's
|
|
5383
|
-
* ORIGINAL trajectory as a faint reference once there is something to
|
|
5384
|
-
* compare it against, the EDITED (or live) curve as the hero with its
|
|
5385
|
-
* draggable markers, and — while a grab is live — the span the falloff is
|
|
5386
|
-
* actually moving. Cheap enough to run per pointermove and stateless, so
|
|
5387
|
-
* there is no partial-repaint bug class to have.
|
|
5388
|
-
*
|
|
5389
|
-
* This is also where the "follow the camera" half of the camera policy
|
|
5390
|
-
* lives: with no edit in hand the curve is re-projected from the CURRENT
|
|
5391
|
-
* lens every frame, so orbiting drags the path around with the render. The
|
|
5392
|
-
* projection is cached in lineLiveRef for the hit test, which therefore
|
|
5393
|
-
* never has to re-derive what was just drawn. */
|
|
5394
|
-
function paintLineOverlay() {
|
|
5395
|
-
const canvas = lineOverlayRef.current;
|
|
5396
|
-
const stage = stageRef.current;
|
|
5397
|
-
if (!canvas || !stage) return;
|
|
5398
|
-
const stageBox = stage.getBoundingClientRect();
|
|
5399
|
-
const dpr = Math.min(2, window.devicePixelRatio || 1);
|
|
5400
|
-
const width = Math.max(1, Math.round(stageBox.width));
|
|
5401
|
-
const height = Math.max(1, Math.round(stageBox.height));
|
|
5402
|
-
if (canvas.width !== Math.round(width * dpr) || canvas.height !== Math.round(height * dpr)) {
|
|
5403
|
-
canvas.width = Math.round(width * dpr);
|
|
5404
|
-
canvas.height = Math.round(height * dpr);
|
|
5405
|
-
}
|
|
5406
|
-
const ctx = canvas.getContext("2d");
|
|
5407
|
-
if (!ctx) return;
|
|
5408
|
-
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
5409
|
-
ctx.clearRect(0, 0, width, height);
|
|
5410
|
-
const pane = lineEditPane();
|
|
5411
|
-
if (!pane) {
|
|
5412
|
-
lineLiveRef.current = null;
|
|
5413
|
-
return;
|
|
5414
|
-
}
|
|
5415
|
-
// Stage-relative, because the canvas is stretched over the stage while
|
|
5416
|
-
// the pane rect is measured in client coordinates.
|
|
5417
|
-
const ox = pane.rect.x - stageBox.left;
|
|
5418
|
-
const oy = pane.rect.y - stageBox.top;
|
|
5419
|
-
|
|
5420
|
-
// The drawable frame: the letterboxed shot views draw into less than the
|
|
5421
|
-
// full pane, and a curve point outside this box is outside 0..1 and
|
|
5422
|
-
// cannot be sent. Saying so with a border beats clamping silently.
|
|
5423
|
-
ctx.save();
|
|
5424
|
-
ctx.setLineDash([6, 5]);
|
|
5425
|
-
ctx.strokeStyle = "rgba(255, 255, 255, .35)";
|
|
5426
|
-
ctx.lineWidth = 1;
|
|
5427
|
-
ctx.strokeRect(ox + .5, oy + .5, pane.rect.w - 1, pane.rect.h - 1);
|
|
5428
|
-
ctx.restore();
|
|
5429
|
-
|
|
5430
|
-
// Which curve is on screen right now, in priority order: the one under
|
|
5431
|
-
// the finger, the committed edit, or a fresh projection through the live
|
|
5432
|
-
// camera. The live deformation has to win over the committed one because
|
|
5433
|
-
// a pointermove writes only lineDragRef — reading state here would
|
|
5434
|
-
// freeze the curve under the finger until pointerup.
|
|
5435
|
-
const drag = lineDragRef.current;
|
|
5436
|
-
const edit = drag ? { camera: drag.camera, original: drag.snapshot, edited: drag.live } : lineCurve;
|
|
5437
|
-
let original = null;
|
|
5438
|
-
let edited = null;
|
|
5439
|
-
// DETACHED. The committed edit was authored through its own lens; once the
|
|
5440
|
-
// live view has left that lens the same uv name different rays, so the
|
|
5441
|
-
// line is painted as a ghost — dashed, dim, no halo and no grab handles —
|
|
5442
|
-
// rather than drawn confidently in the wrong place or silently reprojected
|
|
5443
|
-
// (there is no depth to reproject it with). Nothing is discarded: the edit,
|
|
5444
|
-
// its preview and the Generate button all still run off the snapshot.
|
|
5445
|
-
// Computed HERE, per frame, rather than read off the 4 Hz watcher's state,
|
|
5446
|
-
// so the line detaches the instant the view moves instead of a quarter
|
|
5447
|
-
// second later. Never while a gesture is live — a drag paints its own
|
|
5448
|
-
// snapshot under the finger and mid-gesture drift is a different rule.
|
|
5449
|
-
let ghost = false;
|
|
5450
|
-
if (edit) {
|
|
5451
|
-
// An edit pins the curve to the lens it was authored through; the
|
|
5452
|
-
// cached live projection would disagree with it, so it is dropped
|
|
5453
|
-
// rather than left to go stale.
|
|
5454
|
-
lineLiveRef.current = null;
|
|
5455
|
-
original = edit.original;
|
|
5456
|
-
edited = edit.edited;
|
|
5457
|
-
ghost = !drag && lineCurveDrifted(pane, edit);
|
|
5458
|
-
// A drifted line is still WORLD-anchored through the trail's depth, so
|
|
5459
|
-
// draw it where the edit actually sits under the live lens rather than
|
|
5460
|
-
// at its stale authored uv. Falls back to the authored uv when the
|
|
5461
|
-
// re-anchoring cannot run (see reprojectDriftedEdit).
|
|
5462
|
-
if (ghost) {
|
|
5463
|
-
const anchored = reprojectDriftedEdit(pane, edit);
|
|
5464
|
-
if (anchored) ({ original, edited } = anchored);
|
|
5465
|
-
}
|
|
5466
|
-
} else {
|
|
5467
|
-
const live = projectLineCurve(pane);
|
|
5468
|
-
lineLiveRef.current = live;
|
|
5469
|
-
if (!live) { delete stage.dataset.lineDrift; return; }
|
|
5470
|
-
edited = live.curve;
|
|
5471
|
-
}
|
|
5472
|
-
// The CDP/e2e-visible handle for "the line is detached from this view".
|
|
5473
|
-
// On the stage rather than in React state because it has to be true on
|
|
5474
|
-
// the frame it becomes true, and this painter is the only thing that runs
|
|
5475
|
-
// on every frame.
|
|
5476
|
-
if (ghost) stage.dataset.lineDrift = "true";
|
|
5477
|
-
else delete stage.dataset.lineDrift;
|
|
5478
|
-
const pointPx = (point) => [ox + point.u * pane.rect.w, oy + point.v * pane.rect.h];
|
|
5479
|
-
/** Trace a frame-indexed curve, breaking the path at every null — a
|
|
5480
|
-
* frame whose joint is behind the lens has no image, and joining across
|
|
5481
|
-
* it would draw a straight line through the middle of the screen. */
|
|
5482
|
-
const traceCurve = (curve, from = 0, to = curve.length - 1) => {
|
|
5483
|
-
ctx.beginPath();
|
|
5484
|
-
let started = false;
|
|
5485
|
-
for (let index = Math.max(0, from); index <= Math.min(curve.length - 1, to); index += 1) {
|
|
5486
|
-
const point = curve[index];
|
|
5487
|
-
if (!point) { started = false; continue; }
|
|
5488
|
-
const [px, py] = pointPx(point);
|
|
5489
|
-
if (started) ctx.lineTo(px, py);
|
|
5490
|
-
else ctx.moveTo(px, py);
|
|
5491
|
-
started = true;
|
|
5492
|
-
}
|
|
5493
|
-
ctx.stroke();
|
|
5494
|
-
};
|
|
5495
|
-
|
|
5496
|
-
ctx.save();
|
|
5497
|
-
ctx.lineJoin = "round";
|
|
5498
|
-
ctx.lineCap = "round";
|
|
5499
|
-
// Everything from here down to the pins is the CURVE, and the curve is the
|
|
5500
|
-
// one thing a moved view makes unplaceable, so the whole block fades
|
|
5501
|
-
// together. The pins below deliberately do not: they are world-space and
|
|
5502
|
-
// are re-projected through the live lens on every repaint.
|
|
5503
|
-
if (ghost) ctx.globalAlpha = .3;
|
|
5504
|
-
|
|
5505
|
-
// (1) The ORIGINAL trajectory, faint, and ONLY while there is an edit to
|
|
5506
|
-
// compare it with. It used to be the instruction ("draw something like
|
|
5507
|
-
// this"); now it is a reference — how far the pull has taken the joint
|
|
5508
|
-
// from where the take put it. Unedited it would sit exactly under the
|
|
5509
|
-
// hero and just fatten the line.
|
|
5510
|
-
if (original) {
|
|
5511
|
-
ctx.setLineDash([7, 6]);
|
|
5512
|
-
ctx.strokeStyle = "rgba(0, 20, 40, .45)";
|
|
5513
|
-
ctx.lineWidth = 3;
|
|
5514
|
-
traceCurve(original);
|
|
5515
|
-
ctx.strokeStyle = "rgba(120, 205, 255, .5)";
|
|
5516
|
-
ctx.lineWidth = 1.5;
|
|
5517
|
-
traceCurve(original);
|
|
5518
|
-
ctx.setLineDash([]);
|
|
5519
|
-
}
|
|
5520
|
-
|
|
5521
|
-
// (2) The curve in hand — the hero, and literally what gets sent. Two
|
|
5522
|
-
// passes: a dark halo so it reads on bright floors and skin tones alike,
|
|
5523
|
-
// then the glowing yellow the eye follows. GHOSTED the halo and the glow
|
|
5524
|
-
// both go: a detached line must not look like something you can grab, and
|
|
5525
|
-
// the dash says "this is where it was, not where it is".
|
|
5526
|
-
if (!ghost) {
|
|
5527
|
-
ctx.strokeStyle = "rgba(40, 24, 0, .8)";
|
|
5528
|
-
ctx.lineWidth = 9;
|
|
5529
|
-
traceCurve(edited);
|
|
5530
|
-
ctx.shadowColor = "rgba(255, 210, 61, .9)";
|
|
5531
|
-
ctx.shadowBlur = 10;
|
|
5532
|
-
} else {
|
|
5533
|
-
ctx.setLineDash([3, 7]);
|
|
5534
|
-
}
|
|
5535
|
-
ctx.strokeStyle = "#ffd23d";
|
|
5536
|
-
ctx.lineWidth = ghost ? 2 : 4.5;
|
|
5537
|
-
traceCurve(edited);
|
|
5538
|
-
ctx.shadowBlur = 0;
|
|
5539
|
-
ctx.setLineDash([]);
|
|
5540
|
-
|
|
5541
|
-
// (4) The influenced span, while a grab is live: the stretch the falloff
|
|
5542
|
-
// is actually moving, drawn thicker and hotter. It is the only honest
|
|
5543
|
-
// picture of what the influence slider does, and it walks out with the
|
|
5544
|
-
// SAME dragWeight the deformation used rather than a redrawn guess.
|
|
5545
|
-
if (drag) {
|
|
5546
|
-
let from = drag.index;
|
|
5547
|
-
let to = drag.index;
|
|
5548
|
-
while (from > 0 && dragWeight(from - 1 - drag.index, drag.radius) > DRAG_WEIGHT_EPSILON) from -= 1;
|
|
5549
|
-
while (to < edited.length - 1 && dragWeight(to + 1 - drag.index, drag.radius) > DRAG_WEIGHT_EPSILON) to += 1;
|
|
5550
|
-
ctx.strokeStyle = "rgba(255, 245, 200, .95)";
|
|
5551
|
-
ctx.lineWidth = 6;
|
|
5552
|
-
traceCurve(edited, from, to);
|
|
5553
|
-
}
|
|
5554
|
-
|
|
5555
|
-
// (3) The grab handles. Every 4th frame keeps a 200-frame range from
|
|
5556
|
-
// turning into a solid bead of dots while still leaving a target within
|
|
5557
|
-
// a couple of frames of wherever the pointer lands (the hit test
|
|
5558
|
-
// tolerates 14 px anyway, so the dots are an affordance, not the
|
|
5559
|
-
// geometry). Pinned ends and offscreen frames are deliberately NOT drawn
|
|
5560
|
-
// as handles: nearestCurvePoint refuses them, and a dot that cannot be
|
|
5561
|
-
// grabbed is worse than no dot. The hovered one swells, because the
|
|
5562
|
-
// scene's own cursor is already `grab` and a cursor alone cannot say
|
|
5563
|
-
// "this pointer would pick up the path rather than orbit the view".
|
|
5564
|
-
// GHOSTED THERE ARE NO HANDLES AT ALL. A dot is a promise that pressing it
|
|
5565
|
-
// picks the path up, and while the view has drifted that press is refused
|
|
5566
|
-
// — and would be aiming at a place the point is not, anyway.
|
|
5567
|
-
const hovered = ghost ? null : (drag ? drag.index : lineHoverRef.current);
|
|
5568
|
-
for (let index = 0; !ghost && index < edited.length; index += 1) {
|
|
5569
|
-
const point = edited[index];
|
|
5570
|
-
if (!isCurvePointOnScreen(point)) continue;
|
|
5571
|
-
if (isCurveEndPinned(index, edited.length)) continue;
|
|
5572
|
-
const active = hovered === index;
|
|
5573
|
-
if (!active && index % LINE_CURVE_MARKER_STRIDE !== 0) continue;
|
|
5574
|
-
const [px, py] = pointPx(point);
|
|
5575
|
-
const radius = active ? (drag ? 7 : 5.5) : 3.2;
|
|
5576
|
-
ctx.fillStyle = "rgba(40, 24, 0, .85)";
|
|
5577
|
-
ctx.beginPath();
|
|
5578
|
-
ctx.arc(px, py, radius + 1.6, 0, Math.PI * 2);
|
|
5579
|
-
ctx.fill();
|
|
5580
|
-
ctx.fillStyle = active ? "#fff6d0" : "#ffe27a";
|
|
5581
|
-
ctx.beginPath();
|
|
5582
|
-
ctx.arc(px, py, radius, 0, Math.PI * 2);
|
|
5583
|
-
ctx.fill();
|
|
5584
|
-
}
|
|
5585
|
-
|
|
5586
|
-
// The pinned ends, drawn in the REFERENCE colour rather than the edit
|
|
5587
|
-
// colour, because that is exactly what they are: the frames at each edge
|
|
5588
|
-
// that stay on the original trajectory no matter how hard the middle is
|
|
5589
|
-
// pulled. Seeing them anchored is the whole explanation of why this edit
|
|
5590
|
-
// does not pop at the seams.
|
|
5591
|
-
for (const index of ghost ? [] : [0, edited.length - 1]) {
|
|
5592
|
-
const point = edited[index];
|
|
5593
|
-
if (!isCurvePointOnScreen(point)) continue;
|
|
5594
|
-
const [px, py] = pointPx(point);
|
|
5595
|
-
ctx.fillStyle = "rgba(0, 20, 40, .8)";
|
|
5596
|
-
ctx.beginPath();
|
|
5597
|
-
ctx.arc(px, py, 5.5, 0, Math.PI * 2);
|
|
5598
|
-
ctx.fill();
|
|
5599
|
-
ctx.fillStyle = "#78cdff";
|
|
5600
|
-
ctx.beginPath();
|
|
5601
|
-
ctx.arc(px, py, 3.2, 0, Math.PI * 2);
|
|
5602
|
-
ctx.fill();
|
|
5603
|
-
}
|
|
5604
|
-
// The curve's fade ends here; the pins below are world-space and paint at
|
|
5605
|
-
// full strength through the live lens whatever the curve's lens is doing.
|
|
5606
|
-
ctx.globalAlpha = 1;
|
|
5607
|
-
|
|
5608
|
-
// (4b) THE PINS, and — in pin mode — the joint's own handle at the
|
|
5609
|
-
// playhead. Drawn in the pose-studio's effector green rather than the
|
|
5610
|
-
// path's yellow, because a pin is a different KIND of statement: the path
|
|
5611
|
-
// says "go this way", a pin says "be exactly here at this instant". Each
|
|
5612
|
-
// pin carries a leader line back to where the take put the joint at that
|
|
5613
|
-
// frame, which is the only honest picture of what was asked for; without
|
|
5614
|
-
// it a pin is just a dot floating in space.
|
|
5615
|
-
if (linePinMode || linePins.length) {
|
|
5616
|
-
const pinDrag = linePinDragRef.current;
|
|
5617
|
-
// ONE capture for the whole block: pins are world-space, so unlike the
|
|
5618
|
-
// curve they are re-projected through the LIVE lens on every repaint
|
|
5619
|
-
// and an orbit carries them along, at full strength. The curve above
|
|
5620
|
-
// can only ghost — that is the camera-policy difference between the two
|
|
5621
|
-
// gestures, and it is a difference in what can be DRAWN, not in what
|
|
5622
|
-
// survives: both edits outlive the move.
|
|
5623
|
-
const pinCam = pinDrag?.camera ?? captureLineCamera(pane);
|
|
5624
|
-
const marks = [];
|
|
5625
|
-
for (const pin of linePins) {
|
|
5626
|
-
const world = pinDrag && pinDrag.frame === pin.frame ? pinDrag.world : linePinWorld(pin);
|
|
5627
|
-
if (world) marks.push({ frame: pin.frame, world, placed: true });
|
|
5628
|
-
}
|
|
5629
|
-
if (pinDrag && !linePins.some((pin) => pin.frame === pinDrag.frame)) {
|
|
5630
|
-
marks.push({ frame: pinDrag.frame, world: pinDrag.world, placed: false });
|
|
5631
|
-
}
|
|
5632
|
-
// The grabbable handle: the joint where the take currently puts it at
|
|
5633
|
-
// the playhead. Only in pin mode, and only when that frame is not
|
|
5634
|
-
// already pinned — otherwise the pin IS the handle.
|
|
5635
|
-
const playhead = Math.max(0, Math.min(Math.trunc(tlFrame) || 0, Math.max(0, lineClipFrames - 1)));
|
|
5636
|
-
if (linePinMode && !pinDrag && !linePins.some((pin) => pin.frame === playhead)) {
|
|
5637
|
-
const here = lineJointWorldAt(playhead);
|
|
5638
|
-
if (here) marks.push({ frame: playhead, world: here, placed: false, handle: true });
|
|
5639
|
-
}
|
|
5640
|
-
for (const mark of pinCam ? marks : []) {
|
|
5641
|
-
const uv = projectPointC6(pinCam, mark.world[0], mark.world[1], mark.world[2]);
|
|
5642
|
-
if (!uv) continue;
|
|
5643
|
-
const px = ox + uv[0] * pane.rect.w;
|
|
5644
|
-
const py = oy + uv[1] * pane.rect.h;
|
|
5645
|
-
if (mark.placed) {
|
|
5646
|
-
const origin = lineJointWorldAt(mark.frame);
|
|
5647
|
-
const originUv = origin ? projectPointC6(pinCam, origin[0], origin[1], origin[2]) : null;
|
|
5648
|
-
if (originUv) {
|
|
5649
|
-
ctx.save();
|
|
5650
|
-
ctx.setLineDash([4, 4]);
|
|
5651
|
-
ctx.strokeStyle = "rgba(120, 255, 190, .55)";
|
|
5652
|
-
ctx.lineWidth = 1.5;
|
|
5653
|
-
ctx.beginPath();
|
|
5654
|
-
ctx.moveTo(ox + originUv[0] * pane.rect.w, oy + originUv[1] * pane.rect.h);
|
|
5655
|
-
ctx.lineTo(px, py);
|
|
5656
|
-
ctx.stroke();
|
|
5657
|
-
ctx.restore();
|
|
5658
|
-
}
|
|
5659
|
-
}
|
|
5660
|
-
const radius = mark.handle ? 6 : 7;
|
|
5661
|
-
ctx.fillStyle = "rgba(0, 30, 20, .85)";
|
|
5662
|
-
ctx.beginPath();
|
|
5663
|
-
ctx.arc(px, py, radius + 2, 0, Math.PI * 2);
|
|
5664
|
-
ctx.fill();
|
|
5665
|
-
ctx.fillStyle = mark.placed ? "#5cffb0" : "rgba(92, 255, 176, .5)";
|
|
5666
|
-
ctx.beginPath();
|
|
5667
|
-
ctx.arc(px, py, radius, 0, Math.PI * 2);
|
|
5668
|
-
ctx.fill();
|
|
5669
|
-
if (mark.placed) {
|
|
5670
|
-
ctx.fillStyle = "rgba(0, 30, 20, .9)";
|
|
5671
|
-
ctx.font = "600 10px system-ui, sans-serif";
|
|
5672
|
-
ctx.textAlign = "center";
|
|
5673
|
-
ctx.textBaseline = "middle";
|
|
5674
|
-
ctx.fillText(String(mark.frame), px, py);
|
|
5675
|
-
}
|
|
5676
|
-
}
|
|
5677
|
-
}
|
|
5678
|
-
|
|
5679
|
-
// (5) The stroke under the finger, while one is being drawn. Same family
|
|
5680
|
-
// as the hero curve — dark halo, warm core — but PALE and dashed rather
|
|
5681
|
-
// than the solid yellow, because it is not the curve yet: it is a shape
|
|
5682
|
-
// that becomes the curve's interior on release, and the two ends it will
|
|
5683
|
-
// be pinned to are the blue dots already drawn above. Painted last so it
|
|
5684
|
-
// sits over everything, with a dot at the head so a stroke that is only
|
|
5685
|
-
// beginning is still visible.
|
|
5686
|
-
const draw = lineDrawRef.current;
|
|
5687
|
-
if (draw && draw.points.length) {
|
|
5688
|
-
ctx.save();
|
|
5689
|
-
ctx.lineJoin = "round";
|
|
5690
|
-
ctx.lineCap = "round";
|
|
5691
|
-
ctx.beginPath();
|
|
5692
|
-
draw.points.forEach(([su, sv], index) => {
|
|
5693
|
-
const px = ox + su * pane.rect.w;
|
|
5694
|
-
const py = oy + sv * pane.rect.h;
|
|
5695
|
-
if (index === 0) ctx.moveTo(px, py);
|
|
5696
|
-
else ctx.lineTo(px, py);
|
|
5697
|
-
});
|
|
5698
|
-
ctx.strokeStyle = "rgba(40, 24, 0, .75)";
|
|
5699
|
-
ctx.lineWidth = 8;
|
|
5700
|
-
ctx.stroke();
|
|
5701
|
-
ctx.setLineDash([9, 6]);
|
|
5702
|
-
ctx.shadowColor = "rgba(255, 246, 208, .9)";
|
|
5703
|
-
ctx.shadowBlur = 10;
|
|
5704
|
-
ctx.strokeStyle = "#fff6d0";
|
|
5705
|
-
ctx.lineWidth = 3.5;
|
|
5706
|
-
ctx.stroke();
|
|
5707
|
-
ctx.shadowBlur = 0;
|
|
5708
|
-
ctx.setLineDash([]);
|
|
5709
|
-
const head = draw.points[draw.points.length - 1];
|
|
5710
|
-
const hx = ox + head[0] * pane.rect.w;
|
|
5711
|
-
const hy = oy + head[1] * pane.rect.h;
|
|
5712
|
-
ctx.fillStyle = "rgba(40, 24, 0, .85)";
|
|
5713
|
-
ctx.beginPath();
|
|
5714
|
-
ctx.arc(hx, hy, 6, 0, Math.PI * 2);
|
|
5715
|
-
ctx.fill();
|
|
5716
|
-
ctx.fillStyle = "#fff6d0";
|
|
5717
|
-
ctx.beginPath();
|
|
5718
|
-
ctx.arc(hx, hy, 4, 0, Math.PI * 2);
|
|
5719
|
-
ctx.fill();
|
|
5720
|
-
ctx.restore();
|
|
5721
|
-
}
|
|
5722
|
-
ctx.restore();
|
|
5723
|
-
}
|
|
5724
|
-
|
|
5725
|
-
/** Client pointer -> viewport-normalized uv. Unlike the old freehand path
|
|
5726
|
-
* this does NOT reject coordinates outside the image: a pull that leaves the
|
|
5727
|
-
* frame is a real gesture and the deformation it implies is real too. It is
|
|
5728
|
-
* refused later, at curveToPoints2d, with a message that says which problem
|
|
5729
|
-
* it is — the bridge rejects points2d outside 0..1. */
|
|
5730
|
-
function lineUvFromPointer(pane, event) {
|
|
5731
|
-
return [
|
|
5732
|
-
(event.clientX - pane.rect.x) / pane.rect.w,
|
|
5733
|
-
(event.clientY - pane.rect.y) / pane.rect.h,
|
|
5734
|
-
];
|
|
5735
|
-
}
|
|
5736
|
-
|
|
5737
|
-
/** The curve a pointer could grab this instant, WITHOUT side effects — the
|
|
5738
|
-
* committed edit if there is one, otherwise whatever the last repaint
|
|
5739
|
-
* projected. Used by the hover cursor, which must never change state. */
|
|
5740
|
-
function lineHoverCurve() {
|
|
5741
|
-
// A drifted curve is painted detached and has no handles, so it has no
|
|
5742
|
-
// hover targets either — swelling a marker the press would refuse is the
|
|
5743
|
-
// cursor telling a lie. The ref (rather than a fresh capture) because
|
|
5744
|
-
// hover fires on every pointermove and 250 ms of staleness on a cursor
|
|
5745
|
-
// shape is not worth a matrix inversion per move.
|
|
5746
|
-
if (lineCurve) return lineDriftRef.current ? null : lineCurve.edited;
|
|
5747
|
-
return lineLiveRef.current?.curve ?? null;
|
|
5748
|
-
}
|
|
5749
|
-
|
|
5750
|
-
/** Side-effect-free "does this press belong to line editing?" — handed to
|
|
5751
|
-
* ObjectGizmo as `claimPointer` so its WINDOW-capture selection handler
|
|
5752
|
-
* yields the press. Without the yield the gizmo's stopPropagation kills the
|
|
5753
|
-
* event above the stage and the gesture never reaches the stage listener at
|
|
5754
|
-
* all; that swallowing is the entire reason claimPointer exists.
|
|
5755
|
-
*
|
|
5756
|
-
* IT CLAIMS EVERY PLAIN-LEFT PRESS ON THE IMAGE, not only the ones that hit
|
|
5757
|
-
* the curve. It has to: a press on EMPTY space is now a freehand stroke, so
|
|
5758
|
-
* "missed the curve" is no longer "not ours" — it is the other half of the
|
|
5759
|
-
* mode, and a probe that answered false there would let the gizmo eat exactly
|
|
5760
|
-
* the presses drawing depends on. The cost is explicit and deliberate: while
|
|
5761
|
-
* the mode is ON, plain-left no longer picks a cast member on the deck. What
|
|
5762
|
-
* is NOT claimed is anything the camera owns — alt+left orbits, middle pans,
|
|
5763
|
-
* right flies (see controls.jsx), so navigation is untouched — nor a press
|
|
5764
|
-
* outside the drawn image, nor a press on the HUD chrome stacked over the
|
|
5765
|
-
* stage, nor anything at all outside line-edit mode: App only passes this
|
|
5766
|
-
* probe to the gizmos while lineEditMode is true.
|
|
5767
|
-
*
|
|
5768
|
-
* Must never mutate state, and must agree exactly with what
|
|
5769
|
-
* onLineStagePointerDown will do with the same event — the two conditions
|
|
5770
|
-
* below are the same ones that handler opens with. */
|
|
5771
|
-
function lineGrabProbe(event) {
|
|
5772
|
-
if (event.button !== 0 || event.altKey) return false;
|
|
5773
|
-
if (!(event.target instanceof HTMLCanvasElement)) return false;
|
|
5774
|
-
const pane = lineEditPane();
|
|
5775
|
-
if (!pane) return false;
|
|
5776
|
-
// No curve to grab AND nothing to draw onto: without a projected
|
|
5777
|
-
// trajectory there is no base curve for a stroke to replace the interior
|
|
5778
|
-
// of, so the press is not ours and the gizmo keeps it.
|
|
5779
|
-
//
|
|
5780
|
-
// A COMMITTED EDIT CLAIMS THE PRESS EVEN WHILE THE VIEW HAS DRIFTED, which
|
|
5781
|
-
// is why the test is on lineCurve rather than on lineHoverCurve alone
|
|
5782
|
-
// (that one goes null under drift, deliberately — no handles to hover).
|
|
5783
|
-
// The stage handler refuses that press with the drift hint; yielding it
|
|
5784
|
-
// instead would hand it to the gizmo, which would quietly select a cast
|
|
5785
|
-
// member and never say why the gesture did nothing.
|
|
5786
|
-
if (!lineCurve && !lineHoverCurve()) return false;
|
|
5787
|
-
const [u, v] = lineUvFromPointer(pane, event);
|
|
5788
|
-
// Outside the drawable image (the letterbox bars of a shot view) a stroke
|
|
5789
|
-
// could only author points the bridge refuses, so it is not a stroke.
|
|
5790
|
-
return u >= 0 && u <= 1 && v >= 0 && v <= 1;
|
|
5791
|
-
}
|
|
5792
|
-
|
|
5793
|
-
/** What a grab picks up, `{ camera, curve }`, and the ONE place the camera
|
|
5794
|
-
* is snapshotted: the pair returned here is what the drag deforms and what
|
|
5795
|
-
* the eventual C6 request is built against, so the uv and the lens can never
|
|
5796
|
-
* come from different moments.
|
|
5797
|
-
*
|
|
5798
|
-
* A committed edit is handed back with ITS OWN lens, and a drifted one is
|
|
5799
|
-
* never reached at all — onLineStagePointerDown refuses the press above,
|
|
5800
|
-
* synchronously, before this function is called. That refusal (rather than
|
|
5801
|
-
* the old discard) is the whole policy change: a second gesture through a
|
|
5802
|
-
* different lens would mix two cameras into one curve, which is a real
|
|
5803
|
-
* problem; the edit already in hand is not. */
|
|
5804
|
-
function lineGrabSource(pane) {
|
|
5805
|
-
if (lineCurve) return { camera: lineCurve.camera, curve: lineCurve.edited };
|
|
5806
|
-
const cached = lineLiveRef.current;
|
|
5807
|
-
if (cached) return cached;
|
|
5808
|
-
const fresh = projectLineCurve(pane);
|
|
5809
|
-
lineLiveRef.current = fresh;
|
|
5810
|
-
return fresh;
|
|
5811
|
-
}
|
|
5812
|
-
|
|
5813
|
-
/** The stage's CAPTURE-phase pointerdown: which of the two gestures this is.
|
|
5814
|
-
*
|
|
5815
|
-
* The overlay canvas is `pointer-events: none` and only paints; this listener
|
|
5816
|
-
* sits on the stage container instead, ahead of r3f and the fly controls, and
|
|
5817
|
-
* asks one question: did the pointer land within CURVE_GRAB_RADIUS_PX of a
|
|
5818
|
-
* draggable point?
|
|
5819
|
-
*
|
|
5820
|
-
* YES -> a PULL. The curve is snapshotted and deformed under the finger.
|
|
5821
|
-
* NO -> a DRAW. The press starts a freehand stroke on empty space, which
|
|
5822
|
-
* on release picks its own frame window and becomes the same kind of
|
|
5823
|
-
* curve (drawStrokeEdit), committed through the identical pipeline.
|
|
5824
|
-
* This used to be the
|
|
5825
|
-
* do-nothing path, and on a take whose trail projects into a few
|
|
5826
|
-
* pixels that made the mode feel broken: nothing to grab, nothing
|
|
5827
|
-
* happens, no way in.
|
|
5828
|
-
*
|
|
5829
|
-
* Either way the event is consumed exclusively (preventDefault +
|
|
5830
|
-
* stopPropagation) so one gesture cannot also select or orbit. The presses
|
|
5831
|
-
* this handler deliberately never takes are the camera's — alt+left orbits,
|
|
5832
|
-
* middle pans, right flies — so judging a correction from another angle
|
|
5833
|
-
* still works and the mode never takes the viewport hostage. Plain-left
|
|
5834
|
-
* click-to-select IS given up for as long as the mode is on; that is the
|
|
5835
|
-
* price of an empty-space gesture, and the mode is modal and escapable.
|
|
5836
|
-
*
|
|
5837
|
-
* Capture phase rather than bubble because the decision has to be made before
|
|
5838
|
-
* the controls start an orbit; a bubble listener would arrive after they had
|
|
5839
|
-
* already latched on (FlyControls binds pointerdown on gl.domElement, which
|
|
5840
|
-
* is a descendant of the stage, so stopping here is enough — nothing binds
|
|
5841
|
-
* this gesture at the window). Nothing is re-dispatched. */
|
|
5842
|
-
function onLineStagePointerDown(event) {
|
|
5843
|
-
// Alt+left is the orbit gesture and belongs to the camera, never to a
|
|
5844
|
-
// stroke — the one plain-left press this mode must still let through.
|
|
5845
|
-
if (event.button !== 0 || event.altKey) return;
|
|
5846
|
-
// Only the render surface itself. The stage also carries HUD chrome — the
|
|
5847
|
-
// inset's drag chip, the plan pane, overlay buttons — and a control that
|
|
5848
|
-
// happens to sit within 14 px of the curve must keep its own click. The
|
|
5849
|
-
// overlay canvas cannot be the target (pointer-events: none), so a canvas
|
|
5850
|
-
// target is always the WebGL one.
|
|
5851
|
-
if (!(event.target instanceof HTMLCanvasElement)) return;
|
|
5852
|
-
const pane = lineEditPane();
|
|
5853
|
-
if (!pane) return;
|
|
5854
|
-
// THE VIEW HAS DRIFTED AWAY FROM THE EDIT IN HAND — refuse the gesture,
|
|
5855
|
-
// keep the edit. A pull, a stroke or a pin started now would be authored
|
|
5856
|
-
// through THIS lens and committed onto a curve authored through another
|
|
5857
|
-
// one, and there is no honest way to merge two cameras into a single set
|
|
5858
|
-
// of uv. So the press is consumed (so it cannot orbit or select behind the
|
|
5859
|
-
// refusal) and answered with the same sentence the panel is showing. Undo,
|
|
5860
|
-
// reset, Generate and Esc all still work — none of them authors anything.
|
|
5861
|
-
//
|
|
5862
|
-
// Checked synchronously rather than off the 4 Hz watcher's state: the
|
|
5863
|
-
// window between polls is exactly where a fly-then-press lands.
|
|
5864
|
-
if (lineCurve && lineCurveDrifted(pane)) {
|
|
5865
|
-
event.preventDefault();
|
|
5866
|
-
event.stopPropagation();
|
|
5867
|
-
setToast(lineDriftHint());
|
|
5868
|
-
return;
|
|
5869
|
-
}
|
|
5870
|
-
// PIN MODE takes the press before the curve does: in this mode the marker
|
|
5871
|
-
// under the pointer is the joint AT THE PLAYHEAD, not a point of a path,
|
|
5872
|
-
// and the two gestures start on the same pixels.
|
|
5873
|
-
if (linePinMode) {
|
|
5874
|
-
if (beginLinePinDrag(event, pane)) return;
|
|
5875
|
-
// A press that missed the handle in pin mode does nothing rather than
|
|
5876
|
-
// falling through to a stroke: the artist asked for pins, and a
|
|
5877
|
-
// surprise reroute is exactly the "I did not mean that" the redesign
|
|
5878
|
-
// is about.
|
|
5879
|
-
return;
|
|
5880
|
-
}
|
|
5881
|
-
const source = lineGrabSource(pane);
|
|
5882
|
-
if (!source) return;
|
|
5883
|
-
const [u, v] = lineUvFromPointer(pane, event);
|
|
5884
|
-
const hit = nearestCurvePoint(source.curve, u, v, CURVE_GRAB_RADIUS_PX, pane.rect.w, pane.rect.h);
|
|
5885
|
-
// MISSED THE CURVE — draw a new path instead of doing nothing.
|
|
5886
|
-
if (!hit) {
|
|
5887
|
-
if (!(u >= 0 && u <= 1 && v >= 0 && v <= 1)) return;
|
|
5888
|
-
beginLineDraw(event, pane, source, u, v);
|
|
5889
|
-
return;
|
|
5890
|
-
}
|
|
5891
|
-
event.preventDefault();
|
|
5892
|
-
event.stopPropagation();
|
|
5893
|
-
const stage = stageRef.current;
|
|
5894
|
-
if (stage) stage.dataset.lineGrab = "drag";
|
|
5895
|
-
lineHoverRef.current = null;
|
|
5896
|
-
// SNAPSHOT. Every pointermove re-derives the deformation from this exact
|
|
5897
|
-
// curve, so one drag is idempotent (return the pointer to the grab point
|
|
5898
|
-
// and the curve is restored) and changing the radius mid-drag re-deforms
|
|
5899
|
-
// rather than compounding. Successive drags stack because each new grab
|
|
5900
|
-
// snapshots what the previous one committed.
|
|
5901
|
-
lineDragRef.current = {
|
|
5902
|
-
index: hit.index,
|
|
5903
|
-
prev: lineCurve,
|
|
5904
|
-
u0: u,
|
|
5905
|
-
v0: v,
|
|
5906
|
-
du: 0,
|
|
5907
|
-
dv: 0,
|
|
5908
|
-
radius: lineRadius,
|
|
5909
|
-
camera: source.camera,
|
|
5910
|
-
snapshot: source.curve,
|
|
5911
|
-
live: source.curve,
|
|
5912
|
-
};
|
|
5913
|
-
// Window-level move/up, the same idiom beginInsetDrag uses above: a pull
|
|
5914
|
-
// that leaves the stage still tracks, and a pointerup anywhere ends it.
|
|
5915
|
-
const onMove = (moveEvent) => {
|
|
5916
|
-
const drag = lineDragRef.current;
|
|
5917
|
-
const livePane = lineEditPane();
|
|
5918
|
-
if (!drag || !livePane) return;
|
|
5919
|
-
const [mu, mv] = lineUvFromPointer(livePane, moveEvent);
|
|
5920
|
-
if (!Number.isFinite(mu) || !Number.isFinite(mv)) return;
|
|
5921
|
-
drag.du = mu - drag.u0;
|
|
5922
|
-
drag.dv = mv - drag.v0;
|
|
5923
|
-
drag.live = dragCurve(drag.snapshot, drag.index, drag.du, drag.dv, drag.radius);
|
|
5924
|
-
paintLineOverlay();
|
|
5925
|
-
};
|
|
5926
|
-
const onUp = () => {
|
|
5927
|
-
window.removeEventListener("pointermove", onMove);
|
|
5928
|
-
window.removeEventListener("pointerup", onUp);
|
|
5929
|
-
window.removeEventListener("pointercancel", onUp);
|
|
5930
|
-
const drag = lineDragRef.current;
|
|
5931
|
-
lineDragRef.current = null;
|
|
5932
|
-
const endStage = stageRef.current;
|
|
5933
|
-
if (endStage) delete endStage.dataset.lineGrab;
|
|
5934
|
-
if (!drag) return;
|
|
5935
|
-
// A pull that went nowhere changes NOTHING — including an edit that
|
|
5936
|
-
// was already there (wiping it on a stray click was a bug). A real
|
|
5937
|
-
// pull commits with the lens it was authored through and records
|
|
5938
|
-
// what it replaced, which is what Ctrl/Cmd+Z restores.
|
|
5939
|
-
const changed = !curvesEqual(drag.live, drag.snapshot);
|
|
5940
|
-
// ONE EDIT IS ONE GESTURE: a real pull makes this a path edit, so any
|
|
5941
|
-
// pins in hand are dropped (with a toast, never silently).
|
|
5942
|
-
if (changed) { clearLinePins(); lineUndoRef.current.push(drag.prev ?? null); }
|
|
5943
|
-
// A FRESH PULL EDITS ONLY THE FRAMES THE FALLOFF TOUCHED. Inheriting
|
|
5944
|
-
// the panel's whole-clip default meant zero preserve rows on the box,
|
|
5945
|
-
// and the sampler re-rolled the entire body of the entire clip — a
|
|
5946
|
-
// 25 px nudge measured 4.6 m of drift eight seconds away. A pull that
|
|
5947
|
-
// refines an existing edit keeps that edit's window instead: shrinking
|
|
5948
|
-
// it would drop the drawn reroute outside the newly touched frames.
|
|
5949
|
-
const inherited = drag.prev?.frameRange ?? null;
|
|
5950
|
-
const pulledRange = changed && !inherited
|
|
5951
|
-
? changedFrameRange(drag.snapshot, drag.live, { clipFrames: lineClipFrames })
|
|
5952
|
-
: null;
|
|
5953
|
-
const committed = changed
|
|
5954
|
-
? {
|
|
5955
|
-
camera: drag.camera,
|
|
5956
|
-
original: drag.snapshot,
|
|
5957
|
-
edited: drag.live,
|
|
5958
|
-
frameRange: inherited ?? pulledRange ?? lineEditRange,
|
|
5959
|
-
}
|
|
5960
|
-
: (drag.prev ?? null);
|
|
5961
|
-
// Same panel feedback as a drawn stroke: the numbers the artist never
|
|
5962
|
-
// typed show up filled in.
|
|
5963
|
-
if (changed && pulledRange && (
|
|
5964
|
-
pulledRange.startFrame !== lineEditRange?.startFrame || pulledRange.endFrame !== lineEditRange?.endFrame
|
|
5965
|
-
)) {
|
|
5966
|
-
lineAutoRangeRef.current = pulledRange;
|
|
5967
|
-
setLineRange(pulledRange);
|
|
5968
|
-
}
|
|
5969
|
-
setLineCurve(committed);
|
|
5970
|
-
// RELEASE FIRES A DRAFT. A pull that changed nothing asks for nothing:
|
|
5971
|
-
// the viewport already shows what it would show.
|
|
5972
|
-
if (changed) scheduleLinePreview(committed);
|
|
5973
|
-
paintLineOverlay();
|
|
5974
|
-
};
|
|
5975
|
-
window.addEventListener("pointermove", onMove);
|
|
5976
|
-
window.addEventListener("pointerup", onUp);
|
|
5977
|
-
window.addEventListener("pointercancel", onUp);
|
|
5978
|
-
paintLineOverlay();
|
|
5979
|
-
}
|
|
5980
|
-
|
|
5981
|
-
/** The DRAW half: a press on empty space collects a freehand stroke and, on
|
|
5982
|
-
* release, hands it to drawStrokeEdit, which decides WHICH FRAMES the stroke
|
|
5983
|
-
* was drawn over, replays it with those frames' own timing, and eases it into
|
|
5984
|
-
* the original trajectory at both ends.
|
|
5985
|
-
*
|
|
5986
|
-
* The commit below therefore does one thing the drag's does not: it writes
|
|
5987
|
-
* the matched window back into the panel's range inputs, so "frames 30-66"
|
|
5988
|
-
* appears filled in without anyone having typed a number. That is the entire
|
|
5989
|
-
* fix for the complaint that a short hook became an eight-second crawl — the
|
|
5990
|
-
* range used to default to the whole clip and nobody ever narrowed it.
|
|
5991
|
-
*
|
|
5992
|
-
* Why drawing exists beside dragging at all: the grab is a hit test against
|
|
5993
|
-
* the joint's PROJECTED trail, and on a take where the joint barely moves
|
|
5994
|
-
* across the image — the person who backs up and falls, whose hand travels a
|
|
5995
|
-
* few screen pixels over 192 frames — that trail is a dot. There is nothing
|
|
5996
|
-
* to grab, so the mode did nothing, so the mode was broken. Drawing needs no
|
|
5997
|
-
* target.
|
|
5998
|
-
*
|
|
5999
|
-
* The commit below is deliberately the SAME six lines the drag's pointerup
|
|
6000
|
-
* runs: compare against the snapshot, push the replaced curve on the undo
|
|
6001
|
-
* stack, install `{ camera, original, edited }`, schedule a preview. Every
|
|
6002
|
-
* downstream behaviour — Ctrl/Cmd+Z, the reset button, the detached paint a
|
|
6003
|
-
* moved view puts it in, the wire payload — therefore treats a drawn curve as
|
|
6004
|
-
* an ordinary one, because it IS one. */
|
|
6005
|
-
function beginLineDraw(event, pane, source, u, v) {
|
|
6006
|
-
event.preventDefault();
|
|
6007
|
-
event.stopPropagation();
|
|
6008
|
-
const stage = stageRef.current;
|
|
6009
|
-
if (stage) stage.dataset.lineGrab = "draw";
|
|
6010
|
-
lineHoverRef.current = null;
|
|
6011
|
-
lineDrawRef.current = {
|
|
6012
|
-
prev: lineCurve,
|
|
6013
|
-
camera: source.camera,
|
|
6014
|
-
snapshot: source.curve,
|
|
6015
|
-
// THE WHOLE CLIP'S trail, through the SAME lens the stroke is being
|
|
6016
|
-
// drawn with, captured once at press. This is what the stroke's
|
|
6017
|
-
// endpoints are matched against on release (redesign A), and it has to
|
|
6018
|
-
// be the ORIGINAL trail rather than whatever is currently edited so
|
|
6019
|
-
// that redrawing re-matches against a fixed reference instead of
|
|
6020
|
-
// walking away from it one stroke at a time. Capturing at press rather
|
|
6021
|
-
// than at release means a camera that drifts mid-stroke cannot leave
|
|
6022
|
-
// the match and the stroke measured through different lenses.
|
|
6023
|
-
fullCurve: lineClipFrames > 1
|
|
6024
|
-
? projectLineCurve(pane, { camera: source.camera, frameRange: { startFrame: 0, endFrame: lineClipFrames } })?.curve ?? null
|
|
6025
|
-
: null,
|
|
6026
|
-
// Pixel size of the pane the stroke is being drawn on, captured once:
|
|
6027
|
-
// the "is this a stroke or a click?" threshold is in CSS pixels, where
|
|
6028
|
-
// the hand drew it, and uv distance is anisotropic so it cannot be
|
|
6029
|
-
// judged in uv without this pair.
|
|
6030
|
-
paneW: pane.rect.w,
|
|
6031
|
-
paneH: pane.rect.h,
|
|
6032
|
-
points: [[u, v]],
|
|
6033
|
-
// Running length in PIXELS, accumulated as the stroke is drawn rather
|
|
6034
|
-
// than re-measured at release.
|
|
6035
|
-
lengthPx: 0,
|
|
6036
|
-
};
|
|
6037
|
-
const onMove = (moveEvent) => {
|
|
6038
|
-
const draw = lineDrawRef.current;
|
|
6039
|
-
const livePane = lineEditPane();
|
|
6040
|
-
if (!draw || !livePane) return;
|
|
6041
|
-
const [mu, mv] = lineUvFromPointer(livePane, moveEvent);
|
|
6042
|
-
if (!Number.isFinite(mu) || !Number.isFinite(mv)) return;
|
|
6043
|
-
const last = draw.points[draw.points.length - 1];
|
|
6044
|
-
const stepPx = Math.hypot((mu - last[0]) * draw.paneW, (mv - last[1]) * draw.paneH);
|
|
6045
|
-
// Sub-pixel jitter carries no shape and only costs arc-length samples.
|
|
6046
|
-
if (!(stepPx > 0.5)) return;
|
|
6047
|
-
draw.points.push([mu, mv]);
|
|
6048
|
-
draw.lengthPx += stepPx;
|
|
6049
|
-
paintLineOverlay();
|
|
6050
|
-
};
|
|
6051
|
-
const onUp = () => {
|
|
6052
|
-
window.removeEventListener("pointermove", onMove);
|
|
6053
|
-
window.removeEventListener("pointerup", onUp);
|
|
6054
|
-
window.removeEventListener("pointercancel", onUp);
|
|
6055
|
-
const draw = lineDrawRef.current;
|
|
6056
|
-
lineDrawRef.current = null;
|
|
6057
|
-
const endStage = stageRef.current;
|
|
6058
|
-
if (endStage) delete endStage.dataset.lineGrab;
|
|
6059
|
-
if (!draw) return;
|
|
6060
|
-
// A CLICK IS NOT A STROKE, and a click must change nothing — including
|
|
6061
|
-
// an edit that is already there. Same rule the drag's commit enforces
|
|
6062
|
-
// with curvesEqual; here the test is the length of what was drawn.
|
|
6063
|
-
const edit = draw.points.length >= DRAW_MIN_STROKE_POINTS && draw.lengthPx >= DRAW_MIN_STROKE_PX
|
|
6064
|
-
? drawStrokeEdit(draw.points, {
|
|
6065
|
-
fullCurve: draw.fullCurve,
|
|
6066
|
-
fallbackCurve: draw.snapshot,
|
|
6067
|
-
fallbackRange: lineEditRange,
|
|
6068
|
-
paneW: draw.paneW,
|
|
6069
|
-
paneH: draw.paneH,
|
|
6070
|
-
clipFrames: lineClipFrames,
|
|
6071
|
-
})
|
|
6072
|
-
: null;
|
|
6073
|
-
if (!edit) {
|
|
6074
|
-
paintLineOverlay();
|
|
6075
|
-
return;
|
|
6076
|
-
}
|
|
6077
|
-
const drawn = edit.curve;
|
|
6078
|
-
const changed = !curvesEqual(drawn, edit.base);
|
|
6079
|
-
if (changed) { clearLinePins(); lineUndoRef.current.push(draw.prev ?? null); }
|
|
6080
|
-
// The matched window rides ON the committed curve, not only in
|
|
6081
|
-
// `lineRange` state. setLineRange is asynchronous and the preview below
|
|
6082
|
-
// fires from a timeout that closed over THIS render's lineEditRange, so
|
|
6083
|
-
// a request built from state alone would ship the old range with the
|
|
6084
|
-
// new curve — the exact mismatch that would splice a re-routed stretch
|
|
6085
|
-
// into the wrong frames. buildLineEditRequest reads curve.frameRange.
|
|
6086
|
-
const committed = changed
|
|
6087
|
-
? { camera: draw.camera, original: edit.base, edited: drawn, frameRange: edit.frameRange ?? lineEditRange }
|
|
6088
|
-
: (draw.prev ?? null);
|
|
6089
|
-
// PANEL FEEDBACK (redesign D): the numbers the artist never typed show
|
|
6090
|
-
// up filled in, so "frames 30-66" is visible confirmation of what the
|
|
6091
|
-
// stroke was read as. Only on a real match — a fallback draw is still
|
|
6092
|
-
// in whatever range the panel already showed.
|
|
6093
|
-
if (changed && edit.matched && edit.frameRange && (
|
|
6094
|
-
edit.frameRange.startFrame !== lineEditRange?.startFrame || edit.frameRange.endFrame !== lineEditRange?.endFrame
|
|
6095
|
-
)) {
|
|
6096
|
-
lineAutoRangeRef.current = edit.frameRange;
|
|
6097
|
-
setLineRange(edit.frameRange);
|
|
6098
|
-
}
|
|
6099
|
-
setLineCurve(committed);
|
|
6100
|
-
if (changed) scheduleLinePreview(committed);
|
|
6101
|
-
paintLineOverlay();
|
|
6102
|
-
};
|
|
6103
|
-
window.addEventListener("pointermove", onMove);
|
|
6104
|
-
window.addEventListener("pointerup", onUp);
|
|
6105
|
-
window.addEventListener("pointercancel", onUp);
|
|
6106
|
-
paintLineOverlay();
|
|
6107
|
-
}
|
|
6108
|
-
|
|
6109
|
-
/** THE PIN GESTURE (순간 찍기): grab the joint at the playhead and put it
|
|
6110
|
-
* where it should be.
|
|
6111
|
-
*
|
|
6112
|
-
* Returns true when the press was taken. The handle is the joint's own
|
|
6113
|
-
* position at the CURRENT FRAME — the artist scrubbed there, so that is the
|
|
6114
|
-
* moment they are talking about — plus any pin already placed, which can be
|
|
6115
|
-
* re-grabbed to adjust it (grabbing one moves the playhead to its frame, so
|
|
6116
|
-
* the viewport shows the pose being pinned).
|
|
6117
|
-
*
|
|
6118
|
-
* The 2D-to-3D step is unprojectDeltaC6: the joint slides in the image plane
|
|
6119
|
-
* at its own depth, which is the effector-drag mechanic the pose studio's
|
|
6120
|
-
* gizmo uses and the only reading of a screen drag that does not invent a
|
|
6121
|
-
* depth the artist could not see. THE DEVIATION IS DELIBERATE and worth
|
|
6122
|
-
* naming: this is not posestudio's TransformControls gizmo, because that one
|
|
6123
|
-
* requires ikMode, which is mutually exclusive with this mode. What is
|
|
6124
|
-
* reused is the MECHANIC, not the widget.
|
|
6125
|
-
*
|
|
6126
|
-
* On release the world point is converted to the take's own clip space
|
|
6127
|
-
* (worldPointToClip) and stored — pins go on the wire in the space the npz
|
|
6128
|
-
* is written in, which is the only space a "within 2 cm of the target" claim
|
|
6129
|
-
* can be checked in. */
|
|
6130
|
-
function beginLinePinDrag(event, pane) {
|
|
6131
|
-
const camera = captureLineCamera(pane);
|
|
6132
|
-
if (!camera) return false;
|
|
6133
|
-
const frame = Math.max(0, Math.min(Math.trunc(tlFrame) || 0, Math.max(0, lineClipFrames - 1)));
|
|
6134
|
-
const [u, v] = lineUvFromPointer(pane, event);
|
|
6135
|
-
const reach = (candidate) => {
|
|
6136
|
-
const uv = projectPointC6(camera, candidate.world[0], candidate.world[1], candidate.world[2]);
|
|
6137
|
-
if (!uv) return null;
|
|
6138
|
-
const dist = Math.hypot((uv[0] - u) * pane.rect.w, (uv[1] - v) * pane.rect.h);
|
|
6139
|
-
return dist > CURVE_GRAB_RADIUS_PX * 2 ? null : { ...candidate, dist };
|
|
6140
|
-
};
|
|
6141
|
-
// THE PLAYHEAD WINS TIES, and not by accident. The obvious rule — nearest
|
|
6142
|
-
// candidate in pixels — was measured wrong on the take this mode exists
|
|
6143
|
-
// for: the head's whole trail is ~100 px, so a pin already placed at frame
|
|
6144
|
-
// 90 sits within the grab radius of the joint at frame 112, and scrubbing
|
|
6145
|
-
// forward to add a second pin instead re-grabbed the first one and dragged
|
|
6146
|
-
// the playhead back to it. The artist scrubbed somewhere on purpose; that
|
|
6147
|
-
// moment is what the press means. Existing pins are only considered when
|
|
6148
|
-
// the playhead's own handle is out of reach.
|
|
6149
|
-
const here = lineJointWorldAt(frame);
|
|
6150
|
-
let best = here && !linePins.some((pin) => pin.frame === frame)
|
|
6151
|
-
? reach({ frame, world: here })
|
|
6152
|
-
: null;
|
|
6153
|
-
if (!best) {
|
|
6154
|
-
for (const pin of linePins) {
|
|
6155
|
-
const world = linePinWorld(pin);
|
|
6156
|
-
if (!world) continue;
|
|
6157
|
-
const hit = reach({ frame: pin.frame, world });
|
|
6158
|
-
if (hit && (!best || hit.dist < best.dist)) best = hit;
|
|
6159
|
-
}
|
|
6160
|
-
}
|
|
6161
|
-
// A press on the joint at a frame that is ALREADY pinned re-places that
|
|
6162
|
-
// pin, which is the same "the second gesture wins" rule upsertPin applies.
|
|
6163
|
-
if (!best && here) best = reach({ frame, world: here });
|
|
6164
|
-
if (!best) return false;
|
|
6165
|
-
event.preventDefault();
|
|
6166
|
-
event.stopPropagation();
|
|
6167
|
-
const stage = stageRef.current;
|
|
6168
|
-
if (stage) stage.dataset.lineGrab = "pin";
|
|
6169
|
-
// Re-grabbing an existing pin moves the playhead to it, so the body on
|
|
6170
|
-
// screen is the pose the pin belongs to rather than whatever frame the
|
|
6171
|
-
// timeline happened to be parked on.
|
|
6172
|
-
if (best.frame !== frame) setTlFrame(best.frame);
|
|
6173
|
-
linePinDragRef.current = {
|
|
6174
|
-
frame: best.frame,
|
|
6175
|
-
camera,
|
|
6176
|
-
origin: best.world,
|
|
6177
|
-
world: best.world,
|
|
6178
|
-
u0: u,
|
|
6179
|
-
v0: v,
|
|
6180
|
-
moved: false,
|
|
6181
|
-
};
|
|
6182
|
-
const onMove = (moveEvent) => {
|
|
6183
|
-
const drag = linePinDragRef.current;
|
|
6184
|
-
const livePane = lineEditPane();
|
|
6185
|
-
if (!drag || !livePane) return;
|
|
6186
|
-
const [mu, mv] = lineUvFromPointer(livePane, moveEvent);
|
|
6187
|
-
if (!Number.isFinite(mu) || !Number.isFinite(mv)) return;
|
|
6188
|
-
const delta = unprojectDeltaC6(drag.camera, drag.origin, mu - drag.u0, mv - drag.v0);
|
|
6189
|
-
if (!delta) return;
|
|
6190
|
-
drag.world = [drag.origin[0] + delta[0], drag.origin[1] + delta[1], drag.origin[2] + delta[2]];
|
|
6191
|
-
// Sub-pixel tremor is not a placement, exactly as a few pixels of
|
|
6192
|
-
// wobble is not a stroke (DRAW_MIN_STROKE_PX).
|
|
6193
|
-
drag.moved = Math.hypot((mu - drag.u0) * livePane.rect.w, (mv - drag.v0) * livePane.rect.h) >= DRAW_MIN_STROKE_PX;
|
|
6194
|
-
paintLineOverlay();
|
|
6195
|
-
};
|
|
6196
|
-
const onUp = () => {
|
|
6197
|
-
window.removeEventListener("pointermove", onMove);
|
|
6198
|
-
window.removeEventListener("pointerup", onUp);
|
|
6199
|
-
window.removeEventListener("pointercancel", onUp);
|
|
6200
|
-
const drag = linePinDragRef.current;
|
|
6201
|
-
linePinDragRef.current = null;
|
|
6202
|
-
const endStage = stageRef.current;
|
|
6203
|
-
if (endStage) delete endStage.dataset.lineGrab;
|
|
6204
|
-
if (!drag || !drag.moved) { paintLineOverlay(); return; }
|
|
6205
|
-
const clip = worldPointToClip(motion, { x: drag.world[0], y: drag.world[1], z: drag.world[2] }, {
|
|
6206
|
-
baseY: activeChar.y ?? 0,
|
|
6207
|
-
scale: activeChar.scale ?? 1,
|
|
6208
|
-
});
|
|
6209
|
-
const next = upsertPin(linePins, { frame: drag.frame, position: [clip.x, clip.y, clip.z] });
|
|
6210
|
-
if (next === linePins) { paintLineOverlay(); return; }
|
|
6211
|
-
// A pin and a curve cannot coexist (see the linePins comment). The
|
|
6212
|
-
// curve goes on the undo stack first, so Ctrl/Cmd+Z brings it back.
|
|
6213
|
-
if (lineCurve) {
|
|
6214
|
-
lineUndoRef.current.push(lineCurve);
|
|
6215
|
-
setLineCurve(null);
|
|
6216
|
-
}
|
|
6217
|
-
const range = pinsFrameRange(next, lineClipFrames);
|
|
6218
|
-
if (range) {
|
|
6219
|
-
lineAutoRangeRef.current = range;
|
|
6220
|
-
setLineRange(range);
|
|
6221
|
-
}
|
|
6222
|
-
setLinePins(next);
|
|
6223
|
-
// Same debounce, same session seed, same supersede rule as a stroke:
|
|
6224
|
-
// the preview loop does not care which gesture asked for it.
|
|
6225
|
-
if (range) scheduleLinePreview({ pins: next, frameRange: range });
|
|
6226
|
-
paintLineOverlay();
|
|
6227
|
-
};
|
|
6228
|
-
window.addEventListener("pointermove", onMove);
|
|
6229
|
-
window.addEventListener("pointerup", onUp);
|
|
6230
|
-
window.addEventListener("pointercancel", onUp);
|
|
6231
|
-
paintLineOverlay();
|
|
6232
|
-
return true;
|
|
6233
|
-
}
|
|
6234
|
-
|
|
6235
|
-
/** Bubble-phase hover: the cursor and the swollen marker, nothing else. It
|
|
6236
|
-
* never stops the event and never writes state, so it cannot interfere with
|
|
6237
|
-
* the orbit the same pointermove is probably driving. */
|
|
6238
|
-
function onLineStageHover(event) {
|
|
6239
|
-
const stage = stageRef.current;
|
|
6240
|
-
if (!stage || lineDragRef.current || lineDrawRef.current || linePinDragRef.current) return;
|
|
6241
|
-
const pane = event.target instanceof HTMLCanvasElement ? lineEditPane() : null;
|
|
6242
|
-
const curve = pane ? lineHoverCurve() : null;
|
|
6243
|
-
let hit = null;
|
|
6244
|
-
if (curve) {
|
|
6245
|
-
const [u, v] = lineUvFromPointer(pane, event);
|
|
6246
|
-
hit = nearestCurvePoint(curve, u, v, CURVE_GRAB_RADIUS_PX, pane.rect.w, pane.rect.h);
|
|
6247
|
-
}
|
|
6248
|
-
lineHoverRef.current = hit ? hit.index : null;
|
|
6249
|
-
if (hit) stage.dataset.lineGrab = "hover";
|
|
6250
|
-
else delete stage.dataset.lineGrab;
|
|
6251
|
-
}
|
|
6252
|
-
|
|
6253
|
-
function onLineStageHoverEnd() {
|
|
6254
|
-
const stage = stageRef.current;
|
|
6255
|
-
if (lineDragRef.current || lineDrawRef.current || linePinDragRef.current) return;
|
|
6256
|
-
lineHoverRef.current = null;
|
|
6257
|
-
if (stage) delete stage.dataset.lineGrab;
|
|
6258
|
-
}
|
|
6259
|
-
|
|
6260
|
-
/** Back to the take's own trajectory — and, because an unedited curve
|
|
6261
|
-
* follows the live camera, back to a path that tracks the view. */
|
|
6262
|
-
function resetLineCurve() {
|
|
6263
|
-
lineDragRef.current = null;
|
|
6264
|
-
lineDrawRef.current = null;
|
|
6265
|
-
linePinDragRef.current = null;
|
|
6266
|
-
setLinePins([]);
|
|
6267
|
-
// The button is itself undoable: resetting a curve you spent five pulls
|
|
6268
|
-
// on should not be a cliff.
|
|
6269
|
-
if (lineCurve) lineUndoRef.current.push(lineCurve);
|
|
6270
|
-
// The draft on screen was a picture of the pull that is being thrown
|
|
6271
|
-
// away, so it goes with it — but the SESSION does not end, and the seed
|
|
6272
|
-
// survives, because the artist is still editing the same take.
|
|
6273
|
-
cancelLinePreview();
|
|
6274
|
-
setLineCurve(null);
|
|
6275
|
-
}
|
|
6276
|
-
|
|
6277
|
-
/** Internal clear — mode entry/exit and enqueue. (A camera move used to come
|
|
6278
|
-
* through here too; it no longer clears anything.) Unlike the
|
|
6279
|
-
* reset BUTTON this also empties the undo stack: those transitions change
|
|
6280
|
-
* what the stack's entries were authored against. It is also where a
|
|
6281
|
-
* preview SESSION ends, which is what re-rolls the seed: every one of these
|
|
6282
|
-
* transitions means the next pull is a different piece of work. */
|
|
6283
|
-
function clearLineEdit() {
|
|
6284
|
-
lineDragRef.current = null;
|
|
6285
|
-
lineDrawRef.current = null;
|
|
6286
|
-
linePinDragRef.current = null;
|
|
6287
|
-
setLinePins([]);
|
|
6288
|
-
setLinePinMode(false);
|
|
6289
|
-
lineUndoRef.current = [];
|
|
6290
|
-
cancelLinePreview();
|
|
6291
|
-
linePreviewSeedRef.current = null;
|
|
6292
|
-
setLineCurve(null);
|
|
6293
|
-
}
|
|
6294
|
-
|
|
6295
|
-
function undoLineCurve() {
|
|
6296
|
-
const previous = lineUndoRef.current.pop();
|
|
6297
|
-
if (previous === undefined) return false;
|
|
6298
|
-
lineDragRef.current = null;
|
|
6299
|
-
lineDrawRef.current = null;
|
|
6300
|
-
linePinDragRef.current = null;
|
|
6301
|
-
// Undo restores a CURVE, so it also un-does whatever pins replaced it —
|
|
6302
|
-
// the two are exclusive and the stack only ever stores curves.
|
|
6303
|
-
setLinePins([]);
|
|
6304
|
-
cancelLinePreview();
|
|
6305
|
-
setLineCurve(previous);
|
|
6306
|
-
return true;
|
|
6307
|
-
}
|
|
6308
|
-
|
|
6309
|
-
/* --------------------- the live preview loop (C10/C11) --------------------
|
|
6310
|
-
* Everything below is the machinery behind "release the drag, watch it move".
|
|
6311
|
-
* It is deliberately kept apart from enqueueMotionJob: that queue exists to
|
|
6312
|
-
* produce TAKES — it delivers to a character layer, commits a recipe and
|
|
6313
|
-
* pushes a version — and a preview must do none of those things. So a
|
|
6314
|
-
* preview is its own bare request, its own AbortController, and one
|
|
6315
|
-
* viewport swap that anything can undo. */
|
|
6316
|
-
|
|
6317
|
-
/** Pin (or release) the take a preview is drafted from. Mirrored into a ref
|
|
6318
|
-
* because the async completion below reads it after several awaits, where a
|
|
6319
|
-
* render-time closure would be pointing at the previous take. */
|
|
6320
|
-
function setPreviewSource(next) {
|
|
6321
|
-
linePreviewSourceRef.current = next;
|
|
6322
|
-
setLinePreviewSource(next);
|
|
6323
|
-
}
|
|
6324
|
-
|
|
6325
|
-
/** THE SESSION SEED (rule 2 above). Rolled at most once per editing session
|
|
6326
|
-
* and handed to every preview AND to the confirming full-quality run, so the
|
|
6327
|
-
* draft the artist accepted is the draft they get. Returns null when the
|
|
6328
|
-
* typed seed is invalid — takeSeed has already said so. */
|
|
6329
|
-
function lineSessionSeed() {
|
|
6330
|
-
if (Number.isInteger(linePreviewSeedRef.current)) return linePreviewSeedRef.current;
|
|
6331
|
-
const seed = takeSeed();
|
|
6332
|
-
if (seed === null) return null;
|
|
6333
|
-
linePreviewSeedRef.current = seed;
|
|
6334
|
-
return seed;
|
|
6335
|
-
}
|
|
6336
|
-
|
|
6337
|
-
/** The C6 body for a curve — ONE builder for the preview and the confirm, so
|
|
6338
|
-
* the only difference between what the artist watched and what they get is
|
|
6339
|
-
* the step count. Returns `{ ok: false, message }` with copy already
|
|
6340
|
-
* localized, or `{ ok: true, body, lineEdit, seed }`. */
|
|
6341
|
-
function buildLineEditRequest(curve, { preview = false } = {}) {
|
|
6342
|
-
const refuse = (copy) => ({ ok: false, message: ko(copy[0], copy[1]) });
|
|
6343
|
-
const sourceUrl = takeSourceUrl;
|
|
6344
|
-
if (!sourceUrl) return refuse(LINE_EDIT_REFUSALS.sourceMotion);
|
|
6345
|
-
if (!curve) {
|
|
6346
|
-
return refuse(["Pull the path first — grab a dot on it and drag", "커브를 먼저 잡아당겨 주세요"]);
|
|
6347
|
-
}
|
|
6348
|
-
// PINS take the other branch of C6 entirely: no points2d, no camera. The
|
|
6349
|
-
// prompt and the seed rule below are shared, because those belong to the
|
|
6350
|
-
// RUN, not to the gesture.
|
|
6351
|
-
if (curve.pins) {
|
|
6352
|
-
const prompt = ((linePreviewSource?.prompt ?? motion?.prompt) || "").trim() || "A person continues the motion naturally.";
|
|
6353
|
-
const lineEdit = {
|
|
6354
|
-
sourceMotion: sourceUrl,
|
|
6355
|
-
track: lineTrack,
|
|
6356
|
-
frameRange: curve.frameRange,
|
|
6357
|
-
pins3d: curve.pins.map((pin) => ({ frame: pin.frame, position: [...pin.position] })),
|
|
6358
|
-
prompt,
|
|
6359
|
-
};
|
|
6360
|
-
const refusal = validateLineEdit(lineEdit, { clipFrames: lineClipFrames });
|
|
6361
|
-
if (refusal) return refuse(LINE_EDIT_REFUSALS[refusal.code] ?? LINE_EDIT_REFUSALS.shape);
|
|
6362
|
-
const body = { prompt, duration: lineClipFrames / TIMELINE_FPS, posePin: false, lineEdit };
|
|
6363
|
-
const seed = lineSessionSeed();
|
|
6364
|
-
if (seed === null) return { ok: false, message: "" };
|
|
6365
|
-
body.seed = seed;
|
|
6366
|
-
if (preview) lineEdit.preview = true;
|
|
6367
|
-
return { ok: true, body, lineEdit, seed };
|
|
6368
|
-
}
|
|
6369
|
-
// The wire pairing is positional (driver.py spreads points2d across
|
|
6370
|
-
// frameRange by time), so the points sent are exactly the range's own
|
|
6371
|
-
// slice of the curve — a whole-clip dragged curve paired with a touched-
|
|
6372
|
-
// frames range would compress the full trail into the window.
|
|
6373
|
-
const requestRange = curve.frameRange ?? lineEditRange;
|
|
6374
|
-
const built = curveToPoints2d(sliceCurveToRange(curve.edited, requestRange));
|
|
6375
|
-
if (built.error) return refuse(LINE_CURVE_REFUSALS[built.error] ?? LINE_EDIT_REFUSALS.shape);
|
|
6376
|
-
// The take's own prompt is the text condition: a line edit changes WHERE
|
|
6377
|
-
// a joint goes, not what the shot is about. The fallback keeps the
|
|
6378
|
-
// bridge's non-empty-prompt contract satisfiable for takes imported
|
|
6379
|
-
// without one.
|
|
6380
|
-
const prompt = ((linePreviewSource?.prompt ?? motion?.prompt) || "").trim() || "A person continues the motion naturally.";
|
|
6381
|
-
const lineEdit = {
|
|
6382
|
-
sourceMotion: sourceUrl,
|
|
6383
|
-
track: lineTrack,
|
|
6384
|
-
// THE CURVE'S OWN RANGE FIRST. A drawn stroke picks its window from
|
|
6385
|
-
// where it was drawn (drawStrokeEdit) and a fresh pull from the frames
|
|
6386
|
-
// its falloff touched; the window is stamped on the committed curve
|
|
6387
|
-
// precisely so a request can never pair one gesture's points with
|
|
6388
|
-
// another render's range — `lineEditRange` is re-derived from state a
|
|
6389
|
-
// just-committed gesture has not landed in yet.
|
|
6390
|
-
frameRange: requestRange,
|
|
6391
|
-
points2d: built.points2d,
|
|
6392
|
-
camera: curve.camera,
|
|
6393
|
-
prompt,
|
|
6394
|
-
};
|
|
6395
|
-
const refusal = validateLineEdit(lineEdit, { clipFrames: lineClipFrames });
|
|
6396
|
-
if (refusal) return refuse(LINE_EDIT_REFUSALS[refusal.code] ?? LINE_EDIT_REFUSALS.shape);
|
|
6397
|
-
// posePin:false is mandatory, not decorative: the bridge demands a
|
|
6398
|
-
// `poses` array whenever posePin is not explicitly false, and a line
|
|
6399
|
-
// edit authors no poses at all.
|
|
6400
|
-
const body = { prompt, duration: lineClipFrames / TIMELINE_FPS, posePin: false, lineEdit };
|
|
6401
|
-
// THE SEED RULE (C9). An edit creates a take, so it may not leave the
|
|
6402
|
-
// seed to chance. The seed rides on the BODY, never inside `lineEdit` —
|
|
6403
|
-
// C6 validates that object field by field and an unknown key there is a
|
|
6404
|
-
// 400. `preview` is the one exception the bridge added for this loop.
|
|
6405
|
-
const seed = lineSessionSeed();
|
|
6406
|
-
if (seed === null) return { ok: false, message: "" };
|
|
6407
|
-
body.seed = seed;
|
|
6408
|
-
if (preview) lineEdit.preview = true;
|
|
6409
|
-
return { ok: true, body, lineEdit, seed };
|
|
6410
|
-
}
|
|
6411
|
-
|
|
6412
|
-
/** Put a draft on screen without letting it become the take. */
|
|
6413
|
-
async function showLinePreview(url) {
|
|
6414
|
-
const source = linePreviewSourceRef.current;
|
|
6415
|
-
if (!source) return;
|
|
6416
|
-
try {
|
|
6417
|
-
await loadMotion(url, source.prompt, source.rotationDeg, null, source.charId, null, { preview: true });
|
|
6418
|
-
linePreviewShownRef.current = url;
|
|
6419
|
-
setLinePreviewUrl(url);
|
|
6420
|
-
} catch {
|
|
6421
|
-
setLinePreviewError(ko("The preview could not be read back", "미리보기를 읽지 못했어요"));
|
|
6422
|
-
await revertLinePreview();
|
|
6423
|
-
}
|
|
6424
|
-
}
|
|
6425
|
-
|
|
6426
|
-
/** Back to the take itself. The source stays pinned until the reload lands,
|
|
6427
|
-
* so there is never an instant where `takeSourceUrl` points at the draft.
|
|
6428
|
-
* Nothing is reloaded when no draft ever reached the viewport — cancelling
|
|
6429
|
-
* an in-flight preview (Esc, undo with an empty stack, a joint switch) must
|
|
6430
|
-
* not cost a re-fetch and re-decode of a take that is already on screen. */
|
|
6431
|
-
async function revertLinePreview() {
|
|
6432
|
-
const source = linePreviewSourceRef.current;
|
|
6433
|
-
const shown = linePreviewShownRef.current;
|
|
6434
|
-
linePreviewShownRef.current = null;
|
|
6435
|
-
setLinePreviewUrl(null);
|
|
6436
|
-
if (!source || !shown) {
|
|
6437
|
-
setPreviewSource(null);
|
|
6438
|
-
return;
|
|
6439
|
-
}
|
|
6440
|
-
try {
|
|
6441
|
-
await loadMotion(source.url, source.prompt, source.rotationDeg, null, source.charId, null, { preview: true });
|
|
6442
|
-
} catch {
|
|
6443
|
-
/* loadMotion already surfaced the decode failure in the panel */
|
|
6444
|
-
}
|
|
6445
|
-
if (linePreviewSourceRef.current === source) setPreviewSource(null);
|
|
6446
|
-
}
|
|
6447
|
-
|
|
6448
|
-
/** Stop the loop. Bumping the token is what makes an in-flight answer
|
|
6449
|
-
* harmless: it arrives, finds itself stale, and is dropped without ever
|
|
6450
|
-
* reaching the viewport. `revert:false` is for the callers that are ALREADY
|
|
6451
|
-
* loading a different take (a version chip) and must not race a reload of
|
|
6452
|
-
* the take they are leaving. */
|
|
6453
|
-
function cancelLinePreview({ revert = true } = {}) {
|
|
6454
|
-
if (linePreviewTimerRef.current) {
|
|
6455
|
-
window.clearTimeout(linePreviewTimerRef.current);
|
|
6456
|
-
linePreviewTimerRef.current = 0;
|
|
6457
|
-
}
|
|
6458
|
-
linePreviewPendingRef.current = null;
|
|
6459
|
-
linePreviewTokenRef.current += 1;
|
|
6460
|
-
const controller = linePreviewAbortRef.current;
|
|
6461
|
-
linePreviewAbortRef.current = null;
|
|
6462
|
-
if (controller) controller.abort();
|
|
6463
|
-
setLinePreviewBusy(false);
|
|
6464
|
-
setLinePreviewError("");
|
|
6465
|
-
setLinePreviewMs(0);
|
|
6466
|
-
if (revert) {
|
|
6467
|
-
revertLinePreview();
|
|
6468
|
-
} else {
|
|
6469
|
-
linePreviewShownRef.current = null;
|
|
6470
|
-
setLinePreviewUrl(null);
|
|
6471
|
-
setPreviewSource(null);
|
|
6472
|
-
}
|
|
6473
|
-
}
|
|
6474
|
-
|
|
6475
|
-
/** A released drag asks for a draft — after a beat, so drag-drag-drag costs
|
|
6476
|
-
* one request rather than three. */
|
|
6477
|
-
function scheduleLinePreview(curve) {
|
|
6478
|
-
if (linePreviewTimerRef.current) window.clearTimeout(linePreviewTimerRef.current);
|
|
6479
|
-
linePreviewTimerRef.current = window.setTimeout(() => {
|
|
6480
|
-
linePreviewTimerRef.current = 0;
|
|
6481
|
-
runLinePreview(curve);
|
|
6482
|
-
}, LINE_PREVIEW_DEBOUNCE_MS);
|
|
6483
|
-
}
|
|
6484
|
-
|
|
6485
|
-
/** One preview round trip. Never more than one at a time (rule 3): a curve
|
|
6486
|
-
* that arrives while a request is out becomes THE pending curve, replacing
|
|
6487
|
-
* any earlier pending one, and is fired the moment the outstanding answer
|
|
6488
|
-
* lands — whose result is then thrown away, because it describes a pull the
|
|
6489
|
-
* artist has already moved past. */
|
|
6490
|
-
async function runLinePreview(curve) {
|
|
6491
|
-
if (!lineEditMode || !curve) return;
|
|
6492
|
-
// A preview is a courtesy, never a blocker: no bridge, no route, or the
|
|
6493
|
-
// box already busy with the real thing means the panel simply keeps the
|
|
6494
|
-
// curve and waits for the artist to press 생성.
|
|
6495
|
-
if (!bridge?.ok || !lineEditBackend || ardyRunning) return;
|
|
6496
|
-
if (linePreviewAbortRef.current) {
|
|
6497
|
-
linePreviewPendingRef.current = curve;
|
|
6498
|
-
return;
|
|
6499
|
-
}
|
|
6500
|
-
const source = linePreviewSourceRef.current ?? (motion?.url
|
|
6501
|
-
? {
|
|
6502
|
-
url: motion.url,
|
|
6503
|
-
prompt: motion.prompt ?? "",
|
|
6504
|
-
rotationDeg: motion.rotationDeg ?? activeChar.rot,
|
|
6505
|
-
charId: activeChar.id,
|
|
6506
|
-
}
|
|
6507
|
-
: null);
|
|
6508
|
-
if (!source) return;
|
|
6509
|
-
// Full quality on release, by request: the warm resident makes the 100-step
|
|
6510
|
-
// run ~2 s, and since the draft shares the session seed the confirm below
|
|
6511
|
-
// reproduces it bit for bit — so what the artist sees IS the final take,
|
|
6512
|
-
// and Generate is a commit, not a second opinion.
|
|
6513
|
-
const request = buildLineEditRequest(curve);
|
|
6514
|
-
if (!request.ok) {
|
|
6515
|
-
if (request.message) setLinePreviewError(request.message);
|
|
6516
|
-
return;
|
|
6517
|
-
}
|
|
6518
|
-
const token = ++linePreviewTokenRef.current;
|
|
6519
|
-
const controller = new AbortController();
|
|
6520
|
-
linePreviewAbortRef.current = controller;
|
|
6521
|
-
linePreviewPendingRef.current = null;
|
|
6522
|
-
setPreviewSource(source);
|
|
6523
|
-
setLinePreviewBusy(true);
|
|
6524
|
-
setLinePreviewError("");
|
|
6525
|
-
const startedAt = Date.now();
|
|
6526
|
-
let result = null;
|
|
6527
|
-
let failure = "";
|
|
6528
|
-
try {
|
|
6529
|
-
// No onEvent work: a preview's status lines belong to nobody. The
|
|
6530
|
-
// console stays the full-quality run's log.
|
|
6531
|
-
result = await ardyGenerate(request.body, () => {}, { signal: controller.signal });
|
|
6532
|
-
} catch (err) {
|
|
6533
|
-
// An abort is this loop's own doing and says nothing to the artist.
|
|
6534
|
-
failure = err?.name === "AbortError" ? "" : (err?.message || String(err));
|
|
6535
|
-
}
|
|
6536
|
-
// Stale: superseded by a newer drag or cancelled outright. Whoever bumped
|
|
6537
|
-
// the token owns the state now.
|
|
6538
|
-
if (linePreviewTokenRef.current !== token) return;
|
|
6539
|
-
linePreviewAbortRef.current = null;
|
|
6540
|
-
const pending = linePreviewPendingRef.current;
|
|
6541
|
-
if (pending) {
|
|
6542
|
-
linePreviewPendingRef.current = null;
|
|
6543
|
-
runLinePreview(pending);
|
|
6544
|
-
return;
|
|
6545
|
-
}
|
|
6546
|
-
setLinePreviewBusy(false);
|
|
6547
|
-
if (failure) {
|
|
6548
|
-
// Non-fatal by design: the curve survives, and 생성 still works.
|
|
6549
|
-
setLinePreviewError(failure);
|
|
6550
|
-
return;
|
|
6551
|
-
}
|
|
6552
|
-
if (!result?.motionUrl) return;
|
|
6553
|
-
setLinePreviewMs(Date.now() - startedAt);
|
|
6554
|
-
await showLinePreview(result.motionUrl);
|
|
6555
|
-
}
|
|
6556
|
-
|
|
6557
|
-
function exitLineEditMode() {
|
|
6558
|
-
clearLineEdit();
|
|
6559
|
-
lineHoverRef.current = null;
|
|
6560
|
-
lineLiveRef.current = null;
|
|
6561
|
-
const stage = stageRef.current;
|
|
6562
|
-
if (stage) delete stage.dataset.lineGrab;
|
|
6563
|
-
setLineEditMode(false);
|
|
6564
|
-
}
|
|
6565
|
-
|
|
6566
|
-
/** Entering is mutually exclusive with every other AUTHORING mode, the same
|
|
6567
|
-
* way waypoint/IK/pose already are with each other: they all claim the same
|
|
6568
|
-
* viewport pointer and the same playhead. Camera navigation is deliberately
|
|
6569
|
-
* NOT in that list — the pointer is shared with it, not taken from it. */
|
|
6570
|
-
function toggleLineEditMode() {
|
|
6571
|
-
if (lineEditMode) {
|
|
6572
|
-
exitLineEditMode();
|
|
6573
|
-
setToast(ko("Line editing off", "라인 편집 꺼짐"));
|
|
6574
|
-
return;
|
|
6575
|
-
}
|
|
6576
|
-
if (!motion?.url) {
|
|
6577
|
-
setToast(ko("The current take has no bridge source — generate it once before editing a path", "현재 테이크에 브리지 원본이 없어요 — 궤적을 편집하기 전에 한 번 생성하세요"));
|
|
6578
|
-
return;
|
|
6579
|
-
}
|
|
6580
|
-
if (waypointMode) setWaypointMode(false);
|
|
6581
|
-
if (ikMode) leaveIkMode();
|
|
6582
|
-
if (posing) setPosing(null);
|
|
6583
|
-
clearLineEdit();
|
|
6584
|
-
setLineEditMode(true);
|
|
6585
|
-
setToast(ko(
|
|
6586
|
-
"Path editing on — draw along the path to reroute that section, or grab a dot and pull; the view still orbits normally",
|
|
6587
|
-
"궤적 편집 켜짐 — 궤적을 따라 그리면 그 구간만 새로 지나가고, 점을 잡아 끌 수도 있어요. 시점은 평소처럼 돌릴 수 있어요",
|
|
6588
|
-
));
|
|
6589
|
-
}
|
|
6590
|
-
|
|
6591
|
-
/* Switching joint, range, take or character drops any pull in hand: it was
|
|
6592
|
-
* authored against a trajectory that is no longer the one on screen. The
|
|
6593
|
-
* curve itself needs no rebuilding — with no edit it is re-projected every
|
|
6594
|
-
* repaint from the live camera.
|
|
6595
|
-
*
|
|
6596
|
-
* The one thing worth SAYING here is framing. Part of the range may be
|
|
6597
|
-
* behind the lens or out of shot; the visible part stays draggable and the
|
|
6598
|
-
* hit test ignores the rest, but a path that stops halfway looks like a bug,
|
|
6599
|
-
* and hidden frames cannot be constrained. The retry exists because mode
|
|
6600
|
-
* entry and a measurable pane are not the same instant — the overlay mounts
|
|
6601
|
-
* in the commit that turns the mode on and DualRender may not have settled
|
|
6602
|
-
* the pane's aspect yet, so the projection legitimately refuses for a frame
|
|
6603
|
-
* or two. Failing silently after that is deliberate: the plan and IK views
|
|
6604
|
-
* have no pinhole camera at all and are not worth nagging about. */
|
|
6605
|
-
useEffect(() => {
|
|
6606
|
-
if (!lineEditMode || !motion) return undefined;
|
|
6607
|
-
// ...unless the RANGE CHANGED BECAUSE A STROKE SAID SO. A drawn stroke
|
|
6608
|
-
// picks its own window (drawStrokeEdit) and commits the curve for that
|
|
6609
|
-
// window in the same gesture, so this run is reacting to the draw's own
|
|
6610
|
-
// bookkeeping rather than to the artist moving the goalposts. Consumed
|
|
6611
|
-
// once, so a later hand-typed range still drops the edit as before.
|
|
6612
|
-
const auto = lineAutoRangeRef.current;
|
|
6613
|
-
lineAutoRangeRef.current = null;
|
|
6614
|
-
if (auto && auto.startFrame === lineEditRange?.startFrame && auto.endFrame === lineEditRange?.endFrame) return undefined;
|
|
6615
|
-
// Whatever draft is on screen was drafted for the joint/range/take that
|
|
6616
|
-
// just changed, so it goes back to the source take with the pull. Pins go
|
|
6617
|
-
// with it for the same reason and one more: a pin names a JOINT, and the
|
|
6618
|
-
// joint just changed under it.
|
|
6619
|
-
cancelLinePreview();
|
|
6620
|
-
setLineCurve(null);
|
|
6621
|
-
setLinePins([]);
|
|
6622
|
-
let attempts = 0;
|
|
6623
|
-
let timer = 0;
|
|
6624
|
-
const check = () => {
|
|
6625
|
-
const live = projectLineCurve(lineEditPane());
|
|
6626
|
-
if (!live) {
|
|
6627
|
-
if (attempts >= 12) return;
|
|
6628
|
-
attempts += 1;
|
|
6629
|
-
timer = window.setTimeout(check, 120);
|
|
6630
|
-
return;
|
|
6631
|
-
}
|
|
6632
|
-
const hidden = live.curve.reduce((count, point) => count + (isCurvePointOnScreen(point) ? 0 : 1), 0);
|
|
6633
|
-
if (hidden > 0) {
|
|
6634
|
-
setToast(isKo
|
|
6635
|
-
? `이 구간의 ${hidden}프레임이 화면 밖이에요 — 구간 전체가 보이도록 시점을 잡아 주세요`
|
|
6636
|
-
: `${hidden} frame(s) of this range are outside the frame — orbit until the whole range is in view`);
|
|
6637
|
-
}
|
|
6638
|
-
};
|
|
6639
|
-
check();
|
|
6640
|
-
return () => window.clearTimeout(timer);
|
|
6641
|
-
// The range is depended on by VALUE: lineEditRange is a fresh object on
|
|
6642
|
-
// every render that touches lineRange, and depending on its identity
|
|
6643
|
-
// would fire this constantly. activeChar's ground offset and scale are in
|
|
6644
|
-
// here because jointTrailPoints places the trail with them — move or
|
|
6645
|
-
// resize the character and the path really is somewhere else.
|
|
6646
|
-
//
|
|
6647
|
-
// THE TAKE is depended on as takeSourceUrl, not as `motion`: a preview
|
|
6648
|
-
// swaps `motion` for a draft of the same take several times a minute,
|
|
6649
|
-
// and depending on the object would make every landing draft wipe the
|
|
6650
|
-
// very curve it is a picture of. takeSourceUrl only moves when the take
|
|
6651
|
-
// really does.
|
|
6652
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
6653
|
-
}, [lineEditMode, lineTrack, takeSourceUrl, lineEditRange?.startFrame, lineEditRange?.endFrame, activeChar.y, activeChar.scale]);
|
|
6654
|
-
|
|
6655
|
-
// Repaint on anything that changes what the overlay should show. The window
|
|
6656
|
-
// resize listener is separate from React state because the pane rect can
|
|
6657
|
-
// change without any of it changing.
|
|
6658
|
-
useEffect(() => {
|
|
6659
|
-
if (!lineEditMode) return undefined;
|
|
6660
|
-
paintLineOverlay();
|
|
6661
|
-
const repaint = () => paintLineOverlay();
|
|
6662
|
-
window.addEventListener("resize", repaint);
|
|
6663
|
-
// A gentle rAF loop while the mode is on, and it earns its keep three
|
|
6664
|
-
// times over: the unedited curve is re-projected here so it follows the
|
|
6665
|
-
// camera, a pointermove writes only lineDragRef so this is what shows the
|
|
6666
|
-
// live pull without re-rendering App, and the render moves underneath
|
|
6667
|
-
// regardless (playback, a follow camera). A few hundred 2d-canvas points
|
|
6668
|
-
// per frame is noise next to the WebGL scene already animating behind it.
|
|
6669
|
-
let raf = requestAnimationFrame(function tick() {
|
|
6670
|
-
paintLineOverlay();
|
|
6671
|
-
raf = requestAnimationFrame(tick);
|
|
6672
|
-
});
|
|
6673
|
-
return () => {
|
|
6674
|
-
window.removeEventListener("resize", repaint);
|
|
6675
|
-
cancelAnimationFrame(raf);
|
|
6676
|
-
};
|
|
6677
|
-
});
|
|
6678
|
-
|
|
6679
|
-
// Pointer plumbing. The overlay canvas is `pointer-events: none` — it only
|
|
6680
|
-
// PAINTS — and the listeners live on the stage container instead, so a
|
|
6681
|
-
// pointer that misses the curve reaches the WebGL canvas untouched. See
|
|
6682
|
-
// onLineStagePointerDown for why the down listener is in the capture phase
|
|
6683
|
-
// and what "untouched" buys. No dependency array on purpose: the handlers
|
|
6684
|
-
// close over lineCurve, lineRadius and the live refs, and must always be
|
|
6685
|
-
// this render's.
|
|
6686
|
-
useEffect(() => {
|
|
6687
|
-
if (!lineEditMode) return undefined;
|
|
6688
|
-
const stage = stageRef.current;
|
|
6689
|
-
if (!stage) return undefined;
|
|
6690
|
-
stage.addEventListener("pointerdown", onLineStagePointerDown, true);
|
|
6691
|
-
stage.addEventListener("pointermove", onLineStageHover);
|
|
6692
|
-
stage.addEventListener("pointerleave", onLineStageHoverEnd);
|
|
6693
|
-
return () => {
|
|
6694
|
-
stage.removeEventListener("pointerdown", onLineStagePointerDown, true);
|
|
6695
|
-
stage.removeEventListener("pointermove", onLineStageHover);
|
|
6696
|
-
stage.removeEventListener("pointerleave", onLineStageHoverEnd);
|
|
6697
|
-
};
|
|
6698
|
-
});
|
|
6699
|
-
|
|
6700
|
-
// ESC leaves the mode, matching the shot look-through and the scene-object
|
|
6701
|
-
// selection above.
|
|
6702
|
-
useEffect(() => {
|
|
6703
|
-
if (!lineEditMode) return undefined;
|
|
6704
|
-
const onKey = (event) => {
|
|
6705
|
-
if (event.key === "Escape") {
|
|
6706
|
-
exitLineEditMode();
|
|
6707
|
-
return;
|
|
6708
|
-
}
|
|
6709
|
-
// Ctrl/Cmd+Z inside the mode undoes the last PULL, not the scene.
|
|
6710
|
-
// Capture phase, because the app's scene undo listens on the window
|
|
6711
|
-
// bubble: consuming here keeps one keystroke from doing both. An
|
|
6712
|
-
// empty stack still swallows the key — falling through to a scene
|
|
6713
|
-
// undo the user cannot see happening behind the mode would be worse
|
|
6714
|
-
// than a no-op. Text fields keep the browser's own undo. Shift+Z
|
|
6715
|
-
// (redo) is left alone: there is no line-redo yet, and silently
|
|
6716
|
-
// eating it would just feel broken in a different way.
|
|
6717
|
-
if (event.code === "KeyZ" && (event.ctrlKey || event.metaKey) && !event.shiftKey) {
|
|
6718
|
-
if (/INPUT|TEXTAREA/.test(document.activeElement?.tagName ?? "")) return;
|
|
6719
|
-
event.preventDefault();
|
|
6720
|
-
event.stopPropagation();
|
|
6721
|
-
undoLineCurve();
|
|
6722
|
-
}
|
|
6723
|
-
};
|
|
6724
|
-
window.addEventListener("keydown", onKey, true);
|
|
6725
|
-
return () => window.removeEventListener("keydown", onKey, true);
|
|
6726
|
-
}, [lineEditMode, lineCurve]);
|
|
6727
|
-
|
|
6728
|
-
// Camera-drift watcher — and note what it does NOT do, which is now the
|
|
6729
|
-
// interesting half: it never blocks navigation, and IT NEVER DESTROYS
|
|
6730
|
-
// ANYTHING. It has no job at all while the curve is unedited, because an
|
|
6731
|
-
// unedited curve is re-projected every repaint and simply follows the view.
|
|
6732
|
-
//
|
|
6733
|
-
// Once something has been pulled, that pull is a 2D offset authored through
|
|
6734
|
-
// one specific lens — and the committed curve KEEPS that lens beside the
|
|
6735
|
-
// points, which is what buildLineEditRequest puts on the wire. So moving the
|
|
6736
|
-
// view invalidates the drawing of the line and nothing else: the edit, its
|
|
6737
|
-
// preview and the confirming run are all still exactly what the artist
|
|
6738
|
-
// authored. This watcher therefore only reports, in both directions — come
|
|
6739
|
-
// back toward the snapshot and the line re-attaches, with no state to restore
|
|
6740
|
-
// because none was thrown away. (Before this, it called clearLineEdit(): a
|
|
6741
|
-
// right-drag fly, the app's own navigation gesture, silently wiped a finished
|
|
6742
|
-
// edit. That was the bug.)
|
|
6743
|
-
//
|
|
6744
|
-
// Polling beats hooking every camera mutation: the cameras are moved from a
|
|
6745
|
-
// dozen places (fly controls, shot presets, live control, follow tracks) and
|
|
6746
|
-
// none of them reports to React. The painter re-derives the same answer every
|
|
6747
|
-
// frame for the ghost, and onLineStagePointerDown re-derives it synchronously
|
|
6748
|
-
// to close the 250 ms window between polls; this poll exists to drive the
|
|
6749
|
-
// PANEL, which only re-renders on state.
|
|
6750
|
-
useEffect(() => {
|
|
6751
|
-
if (!lineEditMode || !lineCurve) {
|
|
6752
|
-
// No edit, no drift: whatever the last curve's lens was, the live one
|
|
6753
|
-
// is now the baseline again and gestures are re-enabled. This is what
|
|
6754
|
-
// makes undo/reset back to no-curve hand the mode straight back.
|
|
6755
|
-
lineDriftRef.current = false;
|
|
6756
|
-
setLineDrifted(false);
|
|
6757
|
-
return undefined;
|
|
6758
|
-
}
|
|
6759
|
-
const poll = () => {
|
|
6760
|
-
// Never re-classify under a finger that is holding the curve or drawing
|
|
6761
|
-
// a new one: a live gesture owns its own snapshot, and mid-gesture
|
|
6762
|
-
// drift is the one case that is still a corruption rather than a
|
|
6763
|
-
// misalignment (see the gesture handlers — that path is unchanged).
|
|
6764
|
-
if (lineDragRef.current || lineDrawRef.current || linePinDragRef.current) return;
|
|
6765
|
-
const drifted = lineCurveDrifted(lineEditPane(), lineCurve);
|
|
6766
|
-
if (drifted === lineDriftRef.current) return;
|
|
6767
|
-
lineDriftRef.current = drifted;
|
|
6768
|
-
setLineDrifted(drifted);
|
|
6769
|
-
};
|
|
6770
|
-
// Immediately, then at 4 Hz: a view switch or a fly that finishes just as
|
|
6771
|
-
// this effect re-runs should not leave the panel a quarter second behind
|
|
6772
|
-
// the line it is describing.
|
|
6773
|
-
poll();
|
|
6774
|
-
const id = window.setInterval(poll, 250);
|
|
6775
|
-
return () => window.clearInterval(id);
|
|
6776
|
-
// lookThroughShot / preview / ikMode are dependencies even though the
|
|
6777
|
-
// body never reads them directly: they are what lineEditPane branches on,
|
|
6778
|
-
// so a stale closure would keep measuring the camera the pane used to
|
|
6779
|
-
// hold and a view SWITCH — the most obvious way to invalidate a curve —
|
|
6780
|
-
// would go undetected. Lens and aspect changes need no dependency: they
|
|
6781
|
-
// move fx/fy, which the comparison sees on its own.
|
|
6782
|
-
}, [lineEditMode, lineCurve, lookThroughShot, preview, ikMode]);
|
|
6783
|
-
|
|
6784
|
-
// Use the same bounded, coalesced probe as generation. Capabilities can
|
|
6785
|
-
// disappear under a live bridge too; every health result refreshes them.
|
|
6786
|
-
useEffect(() => {
|
|
6787
|
-
if (lineEditMode) bridgeRefreshRef.current();
|
|
6788
|
-
}, [lineEditMode]);
|
|
6789
|
-
|
|
6790
|
-
const genRunningRef = useRef(false);
|
|
6791
|
-
const genJobSeq = useRef(0);
|
|
6792
|
-
|
|
6793
|
-
useEffect(() => {
|
|
6794
|
-
if (genRunningRef.current) return;
|
|
6795
|
-
const next = genQueue.find((job) => job.status === "queued");
|
|
6796
|
-
if (!next) return;
|
|
6797
|
-
genRunningRef.current = true;
|
|
6798
|
-
setGenQueue((queue) => queue.map((job) => (job.id === next.id ? { ...job, status: "running" } : job)));
|
|
6799
|
-
(async () => {
|
|
6800
|
-
try {
|
|
6801
|
-
await executeMotionJob(next);
|
|
6802
|
-
next.commandCompletion?.resolve();
|
|
6803
|
-
setGenQueue((queue) => queue.map((job) => (job.id === next.id ? { ...job, status: "done" } : job)));
|
|
6804
|
-
} catch (err) {
|
|
6805
|
-
next.commandCompletion?.reject(err);
|
|
6806
|
-
const message = err?.name === "AbortError" ? ko("Cancelled", "취소됨") : err?.message || String(err);
|
|
6807
|
-
setGenQueue((queue) => queue.map((job) => (job.id === next.id ? { ...job, status: "error", error: message } : job)));
|
|
6808
|
-
} finally {
|
|
6809
|
-
genRunningRef.current = false;
|
|
6810
|
-
generationPendingRef.current = false;
|
|
6811
|
-
}
|
|
6812
|
-
})();
|
|
6813
|
-
}, [genQueue]);
|
|
6814
|
-
|
|
6815
|
-
useEffect(() => {
|
|
6816
|
-
// Never for a draft: a preview is not a take and must not mint a chip.
|
|
6817
|
-
if (!motion?.url || linePreviewUrl) return;
|
|
6818
|
-
// Never for a generated take either — its own job is about to commit a
|
|
6819
|
-
// real recipe with a real seed, and this placeholder would beat it to the
|
|
6820
|
-
// strip and label it "불러옴".
|
|
6821
|
-
if (takeRecipeRef.current || ardyRunning || genQueue.length > 0) return;
|
|
6822
|
-
if (takeVersions.some((entry) => entry.motionUrl === motion.url)) return;
|
|
6823
|
-
seedLoadedTake(motion.url, motion.prompt, motion.frames);
|
|
6824
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
6825
|
-
}, [motion?.url, motion?.frames, linePreviewUrl, ardyRunning, genQueue.length, takeVersions]);
|
|
6826
|
-
|
|
6827
|
-
const readinessState = selectedMotionReadiness();
|
|
6828
|
-
const lineReadinessState = motionReadiness(bridge, { body: { lineEdit: true }, lineEditSupported: lineEditBackend });
|
|
6829
|
-
const trailReadinessState = motionReadiness(bridge, { body: { motionEdit: true } });
|
|
6830
|
-
|
|
6831
4419
|
// Studio native ports. Kept together so the binding test executes these exact
|
|
6832
4420
|
// publication/history functions, not a substitute editor or parallel journal.
|
|
6833
4421
|
function readStudioCamera() {
|
|
@@ -6842,11 +4430,11 @@ export default function App() {
|
|
|
6842
4430
|
function readStudioState() {
|
|
6843
4431
|
const live = appContext.live.state;
|
|
6844
4432
|
const list = appContext.live.characters.map(c => c.id === loadedLayerCharRef.current ? {
|
|
6845
|
-
...c, layer: { ...c.layer, waypoints: bufferRef.current.waypoints
|
|
4433
|
+
...c, layer: { ...c.layer, waypoints: bufferRef.current.waypoints }, sessionMotion: bufferRef.current.motion,
|
|
6846
4434
|
} : c);
|
|
6847
4435
|
const targets = new Map(list.map(c => [c.id, { rig: live.rigs[c.id], motion: c.sessionMotion ?? null,
|
|
6848
4436
|
ikState: c.id === loadedLayerCharRef.current ? ikStateRef.current : ikStatesRef.current.get(c.id),
|
|
6849
|
-
calibration: c.sessionMotion?.sceneCalibration, protectedFrames:
|
|
4437
|
+
calibration: c.sessionMotion?.sceneCalibration, protectedFrames: [],
|
|
6850
4438
|
preserveAuthoredMotion: Boolean(c.layer?.waypoints?.length) }]));
|
|
6851
4439
|
return { host: { workspaceId: liveWorkspaceIdRef.current, documentEpoch: studioDocumentEpochRef.current,
|
|
6852
4440
|
sceneId: activeSceneIdRef.current, sceneEpoch: studioSceneEpochRef.current }, workspaceHandle: liveWorkspaceHandleRef.current,
|
|
@@ -6854,8 +4442,27 @@ export default function App() {
|
|
|
6854
4442
|
objects: storeRef.current.objects, characters: list, targets, shots: live.shots, frameCount: live.timeline.frameCount,
|
|
6855
4443
|
selection: live.studioSelection, activeCharacterId: live.activeCharacterId, selectedShotId: live.studioShotId,
|
|
6856
4444
|
view: live.studioView, camera: live.studioCamera ?? readStudioCamera(), filmback: live.filmback, manual: manualCameraOverrideRef.current,
|
|
6857
|
-
bridgeReady:
|
|
6858
|
-
ikBodyDragRef.current ||
|
|
4445
|
+
bridgeReady: false, busy: storeRef.current.present() !== storeRef.current.objects || Boolean(studioGestureRef.current ||
|
|
4446
|
+
ikBodyDragRef.current || recRef.current) };
|
|
4447
|
+
}
|
|
4448
|
+
function sampleStudioStateAt(frame, current = readStudioState()) {
|
|
4449
|
+
const shot = shotAtFrame(current.shots ?? current.shotDocument?.shots ?? [], frame);
|
|
4450
|
+
const sampledPlayback = sampleAt(playbackScene, shot, frame);
|
|
4451
|
+
const sampledSubject = sampledPlayback.subject;
|
|
4452
|
+
const primaryId = current.activeCharacterId ?? current.characters[0]?.id;
|
|
4453
|
+
const characters = sampledSubject && primaryId
|
|
4454
|
+
? current.characters.map(character => character.id === primaryId ? { ...character, x: sampledSubject.x, z: sampledSubject.z } : character)
|
|
4455
|
+
: current.characters;
|
|
4456
|
+
const sampled = sampledPlayback.camera;
|
|
4457
|
+
if (!sampled) return { ...current, frame, characters, view: { ...current.view, frame } };
|
|
4458
|
+
const position = { x: sampled.pos.x, y: sampled.pos.y, z: sampled.pos.z };
|
|
4459
|
+
const direction = forwardFrom(sampled.yaw, sampled.pitch);
|
|
4460
|
+
return { ...current, frame, characters, view: { ...current.view, frame }, camera: {
|
|
4461
|
+
position,
|
|
4462
|
+
lookAt: { x: position.x + direction.x, y: position.y + direction.y, z: position.z + direction.z },
|
|
4463
|
+
focalMm: fovToFocalMm(sampled.fovDeg * Math.PI / 180, current.filmback.sensorId, current.filmback.aspectRatio),
|
|
4464
|
+
sensorId: current.filmback.sensorId,
|
|
4465
|
+
} };
|
|
6859
4466
|
}
|
|
6860
4467
|
|
|
6861
4468
|
/** The authored stage envelope (key light, environment, filmback) published
|
|
@@ -6942,7 +4549,7 @@ export default function App() {
|
|
|
6942
4549
|
}));
|
|
6943
4550
|
parent.matrixAutoUpdate = false; parent.matrix.copy(target.rig.parent?.matrixWorld ?? new THREE.Matrix4()); parent.add(rig);
|
|
6944
4551
|
try {
|
|
6945
|
-
if (target.motion)
|
|
4552
|
+
if (target.motion) applyTakeFrame(rig, target.motion, sampleAt({ frameCount: target.motion.frames, motion: target.motion }, null, frame).motionFrame);
|
|
6946
4553
|
// The clone shares the source rig's bind pose, so its contact radii
|
|
6947
4554
|
// and heights are the same numbers: measure the source once and
|
|
6948
4555
|
// hand them over instead of re-scanning every vertex per query (#413).
|
|
@@ -6986,7 +4593,6 @@ export default function App() {
|
|
|
6986
4593
|
studioSelection: selectedSceneObjectId ? { kind: "object", id: selectedSceneObjectId, hierarchyId: selectedHierarchyId } :
|
|
6987
4594
|
selectedStudioChar ? { kind: "character", id: selectedStudioChar, hierarchyId: selectedHierarchyId } : selectedHierarchyId === "camera" ? { kind: "camera", id: "camera" } : { kind: "scene", id: activeSceneId },
|
|
6988
4595
|
studioShotId: activeShot?.id ?? null,
|
|
6989
|
-
studioPhysicsRunning: autoPhysicsRunning,
|
|
6990
4596
|
// The agent's mode vocabulary is four (studio-agent-protocol.js): the
|
|
6991
4597
|
// sculpt turntable is the Stage's scene seen in isolation.
|
|
6992
4598
|
studioView: { mode: sculptMode ? "scene" : workflowMode, frame: tlFrame, playing: tlPlaying, lookThrough: lookThroughShot, grid: gridView, autoColor },
|
|
@@ -6994,24 +4600,22 @@ export default function App() {
|
|
|
6994
4600
|
appContext.updatePorts({
|
|
6995
4601
|
read: readStudioState, revision: sceneRevisionRef, bounds: studioBounds, commit: commitStudioDraft,
|
|
6996
4602
|
operate: operateStudio, undo: () => stepStudioHistory(false), redo: () => stepStudioHistory(true),
|
|
4603
|
+
sampleStateAt: sampleStudioStateAt,
|
|
6997
4604
|
history: redo => appContext.historyEntry(redo), finishHistoryGesture: finishStudioHistoryGesture,
|
|
6998
4605
|
stepHistory: stepStudioHistory, capture: () => liveQueries.capture_framing_png({}), capturePlan: () => liveQueries.capture_plan_png({}),
|
|
6999
4606
|
// One shot frame as raw read-back pixels (rows bottom-up), from the export
|
|
7000
4607
|
// path captureShotFramePng uses; an export in flight renders at its output.
|
|
7001
|
-
|
|
7002
|
-
|
|
4608
|
+
// With a view (verify_result's own cameras) the posed frame is seen by that
|
|
4609
|
+
// camera instead, at view.output; the shot camera is left as it is.
|
|
4610
|
+
renderFrameBuffer: (frame, view = null) => {
|
|
4611
|
+
const output = view?.output ?? (recRef.current?.capture ? recRef.current.request.context.output : shotOutput), data = withExportFrame(frame, view && (() => captureRef.current.renderView(view)));
|
|
7003
4612
|
if (!data) throw new Error("The shot renderer is not ready");
|
|
7004
4613
|
return { data, width: output.width, height: output.height };
|
|
7005
4614
|
},
|
|
7006
4615
|
encodePng: (buffer, output) => bufferToPng(buffer, output),
|
|
7007
4616
|
// The pose library a character.pose patch resolves its id against.
|
|
7008
4617
|
poses: () => [DEFAULT_POSE, ...customPoses],
|
|
7009
|
-
|
|
7010
|
-
// Keep the server-pinned URL. Stripping the origin would silently fetch
|
|
7011
|
-
// from a different bridge after a reconnect. The bridge owner must allow
|
|
7012
|
-
// CORS or supply a pinned same-origin artifact proxy; never use load_motion.
|
|
7013
|
-
return loadMotionFromUrl(artifact.url, options);
|
|
7014
|
-
},
|
|
4618
|
+
|
|
7015
4619
|
ikRevision: (id, stamp) => {
|
|
7016
4620
|
const prior = studioIkStampsRef.current.get(id);
|
|
7017
4621
|
if (!prior || prior.stamp !== stamp) studioIkStampsRef.current.set(id, { stamp, revision: (prior?.revision ?? 0) + 1 });
|
|
@@ -7030,9 +4634,9 @@ export default function App() {
|
|
|
7030
4634
|
state: () => ({
|
|
7031
4635
|
shots: appContext.live.state.shots, objects: storeRef.current.objects, characters: appContext.live.characters, frame: tlFrame, frameCount: tlFrameCount,
|
|
7032
4636
|
selectedObjectId: selectedSceneObjectId, activeCharacterId: activeChar?.id ?? null,
|
|
7033
|
-
|
|
7034
|
-
generating:
|
|
7035
|
-
motionReady:
|
|
4637
|
+
|
|
4638
|
+
generating: false,
|
|
4639
|
+
motionReady: false,
|
|
7036
4640
|
// The Export menu offers Video (mp4) on exactly these conditions.
|
|
7037
4641
|
exporting: Boolean(recRef.current), canExportVideo: shots.length > 0 || hasCameraKeys || Boolean(motion),
|
|
7038
4642
|
// The scene refs move synchronously with every scene handler.
|
|
@@ -7045,10 +4649,8 @@ export default function App() {
|
|
|
7045
4649
|
aiShot: { mode, imageModel },
|
|
7046
4650
|
// AI-video motion: the account's Fal access, the card's job state and
|
|
7047
4651
|
// the daily generations left (null until the server says).
|
|
7048
|
-
i2vMotion: { enabled: i2vMotionEnabled, status: i2vMotion.status, dailyRemaining: i2vMotion.dailyRemaining ?? null },
|
|
7049
4652
|
}),
|
|
7050
4653
|
addTimelineShot, splitTimelineShot, duplicateTimelineShot, removeTimelineShot, setTimelineShotRange, moveTimelineShot,
|
|
7051
|
-
runAllPromptBlocks,
|
|
7052
4654
|
duplicateSelectedSceneObject,
|
|
7053
4655
|
addCharacterWaypoint, moveCharacterWaypoint, removeCharacterWaypoint, clearCharacterWaypoints, setWaypointMode,
|
|
7054
4656
|
setCharacterIkKey, removeCharacterIkKey, clearCharacterIkKeys, attachSceneObject, setShotCameraRail, clearShotCameraRail,
|
|
@@ -7058,7 +4660,7 @@ export default function App() {
|
|
|
7058
4660
|
afterRender: () => new Promise(resolve => renderWaitersRef.current.push(resolve)),
|
|
7059
4661
|
saveProject, projectFileGranted: async () => (await queryHandlePermission(projectHandleRef.current)) === "granted",
|
|
7060
4662
|
importAsset: (args, context) => objectsDomain.importAsset(args, context), fetchImportSource,
|
|
7061
|
-
setAiShotMode: setMode, setAiImageModel: setImageModel, generate,
|
|
4663
|
+
setAiShotMode: setMode, setAiImageModel: setImageModel, generate,
|
|
7062
4664
|
});
|
|
7063
4665
|
if (!studioActionsRef.current) studioActionsRef.current = createStudioAppActions(appContext.actionPorts);
|
|
7064
4666
|
/** UI door into the shared registry. Refusal messages are written for the
|
|
@@ -7086,7 +4688,7 @@ export default function App() {
|
|
|
7086
4688
|
studioBindingRef.current.publishSemantic = (domain, after) => {
|
|
7087
4689
|
if (domain === "characters" && Array.isArray(after)) { appContext.publishCharacters(after); appContext.patchLive({ characters: after }); }
|
|
7088
4690
|
if (domain === "shots") appContext.patchLive({ shots: after });
|
|
7089
|
-
|
|
4691
|
+
|
|
7090
4692
|
};
|
|
7091
4693
|
}
|
|
7092
4694
|
useEffect(() => () => studioBindingRef.current?.dispose(), []);
|
|
@@ -7161,41 +4763,25 @@ export default function App() {
|
|
|
7161
4763
|
deleteSceneDocumentFromUi, addSceneObject, renameSceneObject, deleteSceneObject, frameSelection, groupObjectUnderNewEmpty, renameRequest,
|
|
7162
4764
|
toggleHierarchyHidden, propsDrop, hierarchyReparent, agentTouchedRows, workflowMode,
|
|
7163
4765
|
selectWorkflowMode, gizmoMode, setGizmoMode, snapEnabled, setSnapEnabled, sculptView, requestSculptView,
|
|
7164
|
-
preset, applyPreset, cameraPresetId,
|
|
4766
|
+
preset, applyPreset, cameraPresetId, shotAspectKey,
|
|
7165
4767
|
fovDeg, shotsDomain, shot, setNonce, workspaceLayout,
|
|
7166
4768
|
viewMenuTriggerRef, viewMenuOpen, setViewMenuAnchor, setViewMenuOpen, viewLooksActive,
|
|
7167
4769
|
viewMenuAnchor, gridView, setGridView, autoColor, setAutoColor,
|
|
4770
|
+
guideMode,
|
|
7168
4771
|
isCharacterSelection, partColoursChoice, embedMode, agentCollapsed, sceneSaveError,
|
|
7169
4772
|
studioAgentError, studioAgentMode, setStudioAgentMode, AgentPanel, setAgentCollapsed,
|
|
7170
|
-
buildStudioAgentContext, highlightAgentTargets,
|
|
4773
|
+
buildStudioAgentContext, highlightAgentTargets, selectedSceneObject, rigSelection,
|
|
7171
4774
|
inspectorActionsOpen, setInspectorActionsOpen, deleteSelectedSceneObject, inspectorHasContent, keyLightSelected,
|
|
7172
|
-
keyLight, changeKeyLight, resetKeyLight, isCameraSelection, moveSequence,
|
|
4775
|
+
keyLight, lighting, changeKeyLight, resetKeyLight, isCameraSelection, moveSequence,
|
|
7173
4776
|
cameraKeys, activeShot, changeShotTargetModel, updateCharacterAt, openStudio,
|
|
7174
4777
|
posing, removeCharacter, setShowB, activeChar, changeInspectorCharacter,
|
|
7175
|
-
|
|
7176
|
-
setStudioPick, appContext, removePose,
|
|
7177
|
-
|
|
7178
|
-
multiModelStage, multiModelFileRef, chooseMultiModelFile, multiModelSource, multiModelUrl,
|
|
7179
|
-
setMultiModelUrl, useMultiModelUrl, pasteMultiModelUrl, multiModelProgress, multiModelError,
|
|
7180
|
-
multiModelFootage, extractMultiModelMotion, multiModelExtract, multiModelTake, multiModelExtractProgress,
|
|
7181
|
-
multiModelExtractError, bridge, promptBlocksReveal, promptClips, selectedPromptId,
|
|
7182
|
-
setSelectedPromptId, setArdyPrompt, setTlFrame, tlFrameCount, changePromptClip,
|
|
7183
|
-
ardySeed, changeArdySeed, lineEditMode, toggleLineEditMode, linePreviewUrl,
|
|
7184
|
-
lineCurve, lineDrifted, lineTrack, setLineTrack, linePinMode,
|
|
7185
|
-
setLinePinMode, linePins, lineClipFrames, lineEditRange, setLineRange,
|
|
7186
|
-
lineRadius, changeLineRadius, lineCurveDirty, lineEditFrom, lineEditTo,
|
|
7187
|
-
lineCurvePointCount, lineDriftHint, lineCurveHidden, linePreviewBusy, linePreviewMs,
|
|
7188
|
-
linePreviewError, generationBusy, bridgeChecking, lineReadinessState, runLineEdit,
|
|
7189
|
-
openMotionSetup, recheckMotionHealth, resetLineCurve, exitLineEditMode, readinessState,
|
|
7190
|
-
ardyRunning, cancelArdy, ardyStatus, ardyOutcome, addPromptClip,
|
|
4778
|
+
selectablePoses, ikApplyPoseAsKey,
|
|
4779
|
+
setStudioPick, appContext, removePose, activeRig, saveCurrentPose,
|
|
4780
|
+
setTlFrame, tlFrameCount,
|
|
7191
4781
|
tlFrame, isRigSelection, ikChains, ikFocus, footSnap, poseObjectOpacity, setPoseObjectOpacity, capturePoseImage,
|
|
7192
|
-
|
|
7193
|
-
physicsPreview, physicsShow, physicsOptions, platformFitRunning, platformFitProgress,
|
|
7194
|
-
platformFitLast, platformFitApplied, changePhysicsOptions, runAutoPhysics, showPhysicsPreview,
|
|
7195
|
-
applyPhysicsPreview, cancelPhysicsPreview, ikEditTool, setIkEditTool, showTrails,
|
|
7196
|
-
setShowTrails, trailFalloffS, setTrailFalloffS, trailEdit, trailTrackFocus, selectTrailTrack, clearTrailTrackFocus, runTrailRegeneration,
|
|
4782
|
+
ikEditTool, setIkEditTool,
|
|
7197
4783
|
pendingIkEdit, applyPendingIkEdit, cancelPendingIkEdit,
|
|
7198
|
-
|
|
4784
|
+
rangePins, rangePinResiduals, rangePinSelection, rangePinPartPick,
|
|
7199
4785
|
rangePinPreview, setRangePinSelection, setRangePinPartPick, previewRangePinDraft, applyRangePinDraft,
|
|
7200
4786
|
deleteRangePin, hasEnvSheet, environment, style, environmentImage,
|
|
7201
4787
|
inspectorDrop, cutoutInputRef, meshInputRef, importCutout, importMesh,
|
|
@@ -7203,31 +4789,27 @@ export default function App() {
|
|
|
7203
4789
|
matteCanvasRef, matteStats, matteMode, setMatteMode, matteEditorRef,
|
|
7204
4790
|
matteTolerance, setMatteTolerance, matteBrush, setMatteBrush, matteShrink,
|
|
7205
4791
|
setMatteShrink, matteFeather, setMatteFeather, matteBusy, applyMatte,
|
|
7206
|
-
recentObjectColors, rememberSceneObjectColor, objectColorDraft, setObjectColorDraft,
|
|
4792
|
+
recentObjectColors, rememberSceneObjectColor, objectColorDraft, setObjectColorDraft,
|
|
7207
4793
|
posingIndex, posingChar, charA, allPoses, studioPick,
|
|
7208
4794
|
posingClosing, closeStudio, castDomain, setToast, savePose,
|
|
7209
4795
|
bottomTab, setBottomTab, beginAssetDrag, shelfImageIds, shelfMeshIds,
|
|
7210
4796
|
manageAssetStorage, setManageAssetStorage, unusedAssetIds, usedAssetIds, usageCounts,
|
|
7211
4797
|
projectAssetGraphSignature, assetTrash, deleteUnusedAsset, undoDeletedAsset, deletingAssetId,
|
|
7212
|
-
|
|
7213
|
-
|
|
7214
|
-
addSceneBlock, preserveStrength, setPreserveStrength, waypointMode, preserveTracksLine,
|
|
7215
|
-
takeRecipe, takeVersions, loadTakeVersion, replayNotices, craneSelectedIndex,
|
|
4798
|
+
waypointMode,
|
|
4799
|
+
craneSelectedIndex,
|
|
7216
4800
|
addActiveCranePoint, deleteSelectedCranePoint, setCraneSelectedIndex, tlFps, ghostLayers,
|
|
7217
|
-
pathSpeed, tlPlaying, pendingWaypointFrame, stateBadge,
|
|
7218
|
-
resetMotionTrim, cutMotionAtPlayhead, changeMotionSegmentSpeed, removeMotionSegmentById, bodyContact,
|
|
4801
|
+
pathSpeed, tlPlaying, pendingWaypointFrame, stateBadge, bodyContact,
|
|
7219
4802
|
activeShotIdx, railDraw, pathDraw, pathTarget, setPathDraw, setRailDraw,
|
|
7220
4803
|
setWorkspaceLayout, timingTokenRef, railCurve, ikAddKeyframe, ikDeleteKeyframe, pathPointIndex, setPathPointIndex,
|
|
7221
4804
|
setBodyContact, setFootSnap, advanceFrame, stepFrame, cameraPreviewEndRef,
|
|
7222
4805
|
manualCameraOverrideRef, setTlPlaying, setWaypointMode, selectActiveCharacterInHierarchy,
|
|
7223
4806
|
toggleWaypointMode: storyboardProject ? undefined : toggleWaypointMode,
|
|
7224
|
-
setActiveWaypointId, setPendingWaypointFrame, removeWaypoint, queueRootWaypointFrame,
|
|
7225
|
-
resizePromptClip, movePromptClip, removePromptClip, setSelectedHierarchyId, addCameraKeyframe,
|
|
4807
|
+
setActiveWaypointId, setPendingWaypointFrame, removeWaypoint, queueRootWaypointFrame, setSelectedHierarchyId, addCameraKeyframe,
|
|
7226
4808
|
moveCameraKeyframe, removeCameraKeyframe, syncActiveCameraFraming, activeCamera, activeShotDuration,
|
|
7227
4809
|
changeActiveCamera, cameraRail, previewCameraShot, toggleCameraRailDraw, deleteCameraRail,
|
|
7228
4810
|
selectTimelineShot, clearMotion,
|
|
7229
4811
|
spawnCharacter, exportShotVideo, stopShotRecording, retryExport, poseRefusal,
|
|
7230
|
-
beginWorkspaceResize,
|
|
4812
|
+
beginWorkspaceResize,
|
|
7231
4813
|
preferencesOpen, setPreferencesOpen, statusText, setStatusText, previewRanges, setPreviewRanges,
|
|
7232
4814
|
flySpeed, liveHubStatus: liveWorkspaceHandle ? "connected" : "disconnected",
|
|
7233
4815
|
agentOpen: !agentCollapsed,
|
|
@@ -7253,6 +4835,7 @@ export default function App() {
|
|
|
7253
4835
|
}
|
|
7254
4836
|
}}>
|
|
7255
4837
|
<ViewportToolbar />
|
|
4838
|
+
{(embedMode || playgroundMode) && <CompositionGuideButton guideMode={guideMode} onCycle={(mode) => runStudioAction("view.setGuideMode", { mode })} />}
|
|
7256
4839
|
{sculptMode && !sculptTarget && (
|
|
7257
4840
|
<div className="vp-sculpt-hint" role="status" data-testid="sculpt-empty">
|
|
7258
4841
|
<strong>{ko("New sculpt", "새 조형물")}</strong>
|
|
@@ -7322,16 +4905,9 @@ export default function App() {
|
|
|
7322
4905
|
timelineHeight={workspaceLayout.timelineHeight}
|
|
7323
4906
|
planZoom={workspaceLayout.planZoom}
|
|
7324
4907
|
/>
|
|
7325
|
-
|
|
7326
|
-
{
|
|
7327
|
-
|
|
7328
|
-
viewport answer is a clip distance that lets the neutral void
|
|
7329
|
-
show through; the fog below is the seamless version of the
|
|
7330
|
-
same idea — it fades the floor INTO the background colour, so
|
|
7331
|
-
past ~120 m the deck simply ceases to exist with no horizon
|
|
7332
|
-
line, no clip edge and no tone break. */}
|
|
7333
|
-
<fog attach="fog" args={gridView ? [GRID_FOG.color, GRID_FOG.near, GRID_FOG.far] : [stageBackground(uiTheme), 18, 54]} />
|
|
7334
|
-
<StageLights keyLight={keyLight} neutral={gridView} light={uiTheme === "light"} />
|
|
4908
|
+
{/* Sky, fog and exposure of the stage look (stage-sky.jsx), for every pane and export. */}
|
|
4909
|
+
<StageAtmosphere lighting={lighting} keyLight={keyLight} theme={uiTheme} grid={gridView} />
|
|
4910
|
+
<StageLights keyLight={keyLight} lighting={lighting} neutral={gridView} light={uiTheme === "light"} />
|
|
7335
4911
|
<KeyLightPuck
|
|
7336
4912
|
keyLight={keyLight}
|
|
7337
4913
|
selected={keyLightSelected}
|
|
@@ -7344,13 +4920,13 @@ export default function App() {
|
|
|
7344
4920
|
onChange={(patch) => changeKeyLight("puck", patch)}
|
|
7345
4921
|
onDragEnd={endGestureUndo}
|
|
7346
4922
|
/>
|
|
7347
|
-
{gridView ? <GridFloor layer={GIZMO_LAYER} /> : <Room light={uiTheme === "light"} />}
|
|
4923
|
+
{gridView ? <GridFloor layer={GIZMO_LAYER} /> : <Room light={uiTheme === "light" && !lighting.sky && !(lighting.hdr && lighting.hdrBackground)} />}
|
|
7348
4924
|
<SetProps
|
|
7349
4925
|
objects={stageSceneObjects}
|
|
7350
4926
|
authoredObjects={sceneObjects}
|
|
7351
4927
|
selectedId={selectedSceneObjectId}
|
|
7352
4928
|
frameRef={propFrameRef}
|
|
7353
|
-
take={{ frameCount: tlFrameCount, fps: tlFps }}
|
|
4929
|
+
take={{ frameCount: tlFrameCount, fps: tlFps, objectTracks }}
|
|
7354
4930
|
attachFrameRef={attachFrameRef}
|
|
7355
4931
|
syncRef={propSyncRef}
|
|
7356
4932
|
worldRef={propWorldRef}
|
|
@@ -7419,7 +4995,7 @@ export default function App() {
|
|
|
7419
4995
|
objectId={view.parent}
|
|
7420
4996
|
objectsRef={sceneObjectLookupRef}
|
|
7421
4997
|
frameRef={propFrameRef}
|
|
7422
|
-
take={{ frameCount: tlFrameCount, fps: tlFps }}
|
|
4998
|
+
take={{ frameCount: tlFrameCount, fps: tlFps, objectTracks }}
|
|
7423
4999
|
registryRef={carrierRegistryRef}
|
|
7424
5000
|
>
|
|
7425
5001
|
<Character
|
|
@@ -7432,18 +5008,17 @@ export default function App() {
|
|
|
7432
5008
|
partColoursMode={view.partColoursMode}
|
|
7433
5009
|
pose={view.pose}
|
|
7434
5010
|
scale={view.scale}
|
|
7435
|
-
onRig={view.onRig}
|
|
7436
|
-
pickId={view.pickId}
|
|
5011
|
+
onRig={view.onRig} pickId={view.pickId}
|
|
7437
5012
|
/>
|
|
7438
5013
|
</ObjectCarrier>
|
|
7439
5014
|
))}
|
|
5015
|
+
<ViewGhosts rig={rigs[activeChar.id]} take={motion} frame={tlFrame} objects={sceneObjects} propTake={{ frameCount: tlFrameCount, fps: tlFps, objectTracks }} /><DraftGhost rigs={rigs} objects={sceneObjects} />
|
|
7440
5016
|
|
|
7441
5017
|
{/* Selection marker: XYZ tripod + ring on the picked cast
|
|
7442
5018
|
member; the X/Z arrows drag it across the deck. */}
|
|
7443
5019
|
{gizmoView && !playMode && !posing && !ikMode && (
|
|
7444
5020
|
<ObjectGizmo
|
|
7445
5021
|
pickOnly
|
|
7446
|
-
claimPointer={lineEditMode ? lineGrabProbe : undefined}
|
|
7447
5022
|
object={characterGizmoObject}
|
|
7448
5023
|
objects={characterGizmoObject ? [characterGizmoObject] : []}
|
|
7449
5024
|
mode={gizmoMode}
|
|
@@ -7533,7 +5108,6 @@ export default function App() {
|
|
|
7533
5108
|
and the wheel dollies without wrecking the framing. */}
|
|
7534
5109
|
<FlyControls
|
|
7535
5110
|
enabled={!posing && !playMode && !sculptMode}
|
|
7536
|
-
cameraLocked={lookThroughShot && i2vMotionCameraLocked}
|
|
7537
5111
|
camRef={ikMode ? poserCamRef : lookThroughShot ? shotCamRef : editorCamRef}
|
|
7538
5112
|
look={ikMode ? poserLook : lookThroughShot ? look : editorLook}
|
|
7539
5113
|
getPivot={() => {
|
|
@@ -7675,6 +5249,7 @@ export default function App() {
|
|
|
7675
5249
|
<ObjectGizmo
|
|
7676
5250
|
object={cameraGizmoObject ?? lightGizmoObject ?? (selectedSceneObject && !isEffectivelyHidden(selectedSceneObject, sceneObjects, characters) ? gizmoSceneObject : null)}
|
|
7677
5251
|
objects={sceneObjects}
|
|
5252
|
+
held={cameraGizmoObject || lightGizmoObject ? null : heldEdit.gizmo}
|
|
7678
5253
|
mode={lightGizmoObject ? "move" : cameraGizmoObject ? (gizmoMode === "scale" ? "move" : gizmoMode) : gizmoMode}
|
|
7679
5254
|
snap={snapEnabled}
|
|
7680
5255
|
enabled={!planIsMain && !posing && !ikMode && !playMode && !sculptMode}
|
|
@@ -7696,7 +5271,6 @@ export default function App() {
|
|
|
7696
5271
|
)
|
|
7697
5272
|
}
|
|
7698
5273
|
onGroundClick={waypointMode && !planIsMain ? addFloorWaypoint : undefined}
|
|
7699
|
-
claimPointer={lineEditMode ? lineGrabProbe : undefined}
|
|
7700
5274
|
/>
|
|
7701
5275
|
{!preview && railCurve && (
|
|
7702
5276
|
<CameraRailScenePreview
|
|
@@ -7761,26 +5335,7 @@ export default function App() {
|
|
|
7761
5335
|
height={shotOutput.height}
|
|
7762
5336
|
/>
|
|
7763
5337
|
<CaptureRig apiRef={mcpCaptureRef} camRef={shotCamRef} width={MCP_CAPTURE_W} height={MCP_CAPTURE_H} />
|
|
7764
|
-
|
|
7765
|
-
trajectory, grabbable to deform the take with falloff. */}
|
|
7766
|
-
{ikMode && motion && (
|
|
7767
|
-
<MotionTrails
|
|
7768
|
-
motion={motion}
|
|
7769
|
-
rig={activeRig}
|
|
7770
|
-
baseY={activeChar.y ?? 0}
|
|
7771
|
-
charScale={activeChar.scale ?? 1}
|
|
7772
|
-
ikFocus={ikFocus}
|
|
7773
|
-
activeTrackId={trailTrackFocus}
|
|
7774
|
-
falloffFrames={trailFalloffFrames}
|
|
7775
|
-
playheadFrame={tlFrame}
|
|
7776
|
-
pendingEdit={trailEdit}
|
|
7777
|
-
enabled={ikMode && ikEditTool === "trail" && showTrails && !posing && !playMode}
|
|
7778
|
-
visible={showTrails}
|
|
7779
|
-
onDragStart={onTrailDragStart}
|
|
7780
|
-
onDragPreview={onTrailDragPreview}
|
|
7781
|
-
onDragEnd={onTrailDragEnd}
|
|
7782
|
-
/>
|
|
7783
|
-
)}
|
|
5338
|
+
|
|
7784
5339
|
<DualRender
|
|
7785
5340
|
stageRef={stageRef}
|
|
7786
5341
|
mainRef={mainPaneRef}
|
|
@@ -7818,13 +5373,6 @@ export default function App() {
|
|
|
7818
5373
|
pointer only when it actually grabs the curve. Everything else
|
|
7819
5374
|
— orbit, pan, click-to-select — reaches the WebGL canvas
|
|
7820
5375
|
untouched while the mode is on. */}
|
|
7821
|
-
{lineEditMode && (
|
|
7822
|
-
<canvas
|
|
7823
|
-
ref={lineOverlayRef}
|
|
7824
|
-
className="line-edit-overlay"
|
|
7825
|
-
aria-hidden="true"
|
|
7826
|
-
/>
|
|
7827
|
-
)}
|
|
7828
5376
|
|
|
7829
5377
|
{!(globalThis.__cozyclayWebGLAvailable ??= (() => {
|
|
7830
5378
|
if (typeof document === "undefined") return true;
|
|
@@ -7907,18 +5455,6 @@ export default function App() {
|
|
|
7907
5455
|
<span className="vp-inset-tag vp-shot-preview-tag">
|
|
7908
5456
|
<span className="vp-rec-dot" aria-hidden="true" />
|
|
7909
5457
|
{ko("Shot", "샷")}
|
|
7910
|
-
<button
|
|
7911
|
-
type="button"
|
|
7912
|
-
className={"vp-guide-cycle" + (guideMode === "off" ? "" : " on")}
|
|
7913
|
-
aria-label={ko("Cycle composition guides", "구도 가이드 전환")}
|
|
7914
|
-
title={ko(GUIDE_LABELS[guideMode].en, GUIDE_LABELS[guideMode].ko) + ko(" · click to cycle", " · 클릭으로 전환")}
|
|
7915
|
-
onClick={() => runStudioAction("view.setGuideMode", { mode: nextGuideMode(guideMode) })}
|
|
7916
|
-
>
|
|
7917
|
-
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
|
|
7918
|
-
<path d="M3 3h18v18H3z" />
|
|
7919
|
-
<path d="M9 3v18M15 3v18M3 9h18M3 15h18" />
|
|
7920
|
-
</svg>
|
|
7921
|
-
</button>
|
|
7922
5458
|
<button
|
|
7923
5459
|
type="button"
|
|
7924
5460
|
className="vp-look-through"
|
|
@@ -7973,7 +5509,6 @@ export default function App() {
|
|
|
7973
5509
|
}
|
|
7974
5510
|
>
|
|
7975
5511
|
|
|
7976
|
-
{i2vMotionStudioOpen && <I2vMotionModal model={i2vMotionModel} actions={i2vMotionActions} onClose={() => setI2vMotionStudioOpen(false)} />}
|
|
7977
5512
|
{result && resultOpen && (
|
|
7978
5513
|
<ResultModal
|
|
7979
5514
|
result={result}
|