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