cozyclay 2.0.2 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -0
- package/README.md +17 -2
- package/THIRD_PARTY_NOTICES.md +8 -0
- package/bin/agent/agent-routes.mjs +211 -36
- package/bin/agent/agent-runner.mjs +98 -4
- package/bin/agent/codex-client.mjs +2 -1
- package/bin/agent/fal-contract.mjs +0 -17
- package/bin/agent/h3-preservation.mjs +6 -8
- package/bin/agent/provider-keys.mjs +1 -1
- package/bin/agent/providers.mjs +124 -3
- package/bin/agent/script-tool.mjs +74 -0
- package/bin/agent/session-store.mjs +5 -5
- package/bin/agent/studio-prompt.mjs +2 -1
- package/bin/agent/studio-rebase.mjs +314 -0
- package/bin/agent/studio-tools.mjs +21 -14
- package/bin/agent/supervisor.mjs +311 -0
- package/bin/agent/video-adapters.mjs +0 -7
- package/bin/codex-auth.mjs +4 -4
- package/bin/live/cli.mjs +23 -8
- package/bin/live/client.mjs +1 -1
- package/bin/live/studio.mjs +5 -2
- package/bin/mcp-runtime.mjs +2 -2
- package/bin/telemetry-state.mjs +3 -3
- package/dist/app/index.html +6 -6
- package/dist/assets/analytics-C9pMpW7s.js +1 -0
- package/dist/assets/app-B_2XB9rw.css +1 -0
- package/dist/assets/app-DAuvKPP8.js +4920 -0
- package/dist/assets/{demo-ByeYp-dY.js → demo-CvKuBgy5.js} +1 -1
- package/dist/assets/{first-shot-handoff-DgT28ZiV.js → first-shot-handoff-msLI71g1.js} +1 -1
- package/dist/assets/{landing-BaXkpFip.js → landing-ChUvB_zL.js} +1 -1
- package/dist/assets/scene-asset-sync-DKkpjKBV.js +19 -0
- package/dist/assets/{shot-prompt-CbqWcWYi.css → scene-asset-sync-DjJ_cJ2R.css} +1 -1
- package/dist/assets/{ticket-BcmsnHn5.js → ticket-BPk2Ni95.js} +1 -1
- package/dist/assets/workflow-jJRBR94Z.js +179 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/index.html +328 -222
- package/dist/llms.txt +14 -0
- package/dist/sitemap.xml +1 -1
- package/dist/workflow/index.html +4 -4
- package/mcp/LIVE-PROTOCOL.md +23 -2
- package/mcp/README.md +37 -2
- package/mcp/ardy-prompts.mjs +1 -1
- package/mcp/image-file.mjs +132 -0
- package/mcp/live-hub.mjs +2 -2
- package/mcp/package.json +1 -0
- package/mcp/runtime/package-lock.json +7 -0
- package/mcp/runtime/package.json +1 -0
- package/mcp/server.mjs +0 -6
- package/mcp/tool-handlers.mjs +289 -87
- package/package.json +11 -2
- package/src/App.jsx +813 -278
- package/src/agent-image-client.js +94 -0
- package/src/analytics.js +47 -3
- package/src/app-stage.jsx +129 -2745
- package/src/ardy/auto-physics.js +6 -16
- package/src/ardy/collision-blockers.js +3 -7
- package/src/ardy/fix-collisions.js +5 -35
- package/src/ardy/ik-drag.js +86 -5
- package/src/ardy/ik-key-json.js +3 -1
- package/src/ardy/ik.js +15 -13
- package/src/ardy/motion-calibration.js +1 -1
- package/src/ardy/npz.js +0 -8
- package/src/ardy/physics-panel.jsx +3 -2
- package/src/ardy/physics-review.js +3 -2
- package/src/ardy/physics-support.js +1 -1
- package/src/ardy/physics-surface.js +1 -1
- package/src/ardy/platform-fit-panel.jsx +2 -1
- package/src/ardy/playback.js +1 -5
- package/src/ardy/range-pin.js +2 -2
- package/src/ardy/root-drop.js +0 -1
- package/src/ardy/timeline.css +184 -6
- package/src/ardy/timeline.jsx +121 -36
- package/src/ardy/to-cskel27.js +0 -2
- package/src/ardy/waypoints.js +0 -3
- package/src/ardy-bridge-contract.js +281 -0
- package/src/asset-pane.jsx +19 -9
- package/src/auto-color.js +1 -1
- package/src/burn-in.js +1 -1
- package/src/camera-block.js +1 -1
- package/src/camera-follow.js +75 -32
- package/src/camera-move.js +0 -6
- package/src/camera-tutorial.jsx +6 -7
- package/src/character-kind.js +79 -0
- package/src/command-bus.js +76 -17
- package/src/commands/cast.js +31 -7
- package/src/commands/elements/object.js +1 -1
- package/src/commands/elements/shot.js +1 -1
- package/src/commands/elements.js +1 -1
- package/src/commands/export.js +3 -3
- package/src/commands/motion.js +2 -2
- package/src/commands/objects.js +105 -10
- package/src/commands/project.js +15 -2
- package/src/commands/sculpt-images.js +53 -0
- package/src/commands/shot.js +88 -10
- package/src/commands/view.js +10 -4
- package/src/coplanar-depth.js +216 -0
- package/src/cuts.js +7 -6
- package/src/domains/cast.js +60 -43
- package/src/domains/motion/motion-intent-codec.js +50 -0
- package/src/domains/motion/motion-range-pin-rules.js +23 -0
- package/src/domains/motion/motion-video-capture.js +664 -0
- package/src/domains/motion.js +253 -742
- package/src/domains/objects.js +106 -5
- package/src/domains/scenes.js +79 -28
- package/src/domains/shots.js +33 -7
- package/src/error-boundary.jsx +1 -1
- package/src/execution-telemetry.js +5 -5
- package/src/facing-marks.js +14 -1
- package/src/first-success-guide.jsx +35 -4
- package/src/gizmo-claim.js +3 -1
- package/src/grid-floor.jsx +1 -4
- package/src/hierarchy-model.js +48 -35
- package/src/hierarchy-panel.css +1 -0
- package/src/hierarchy-panel.jsx +54 -102
- package/src/history.js +0 -2
- package/src/i2v-motion-client.js +1 -4
- package/src/ime.js +6 -0
- package/src/keyframe-pack.js +15 -6
- package/src/line-edit.js +4 -4
- package/src/live-control.js +6 -6
- package/src/matte-editor.js +2 -8
- package/src/matte.js +0 -19
- package/src/mesh-graph-clone.js +16 -2
- package/src/mesh-sniff.js +0 -1
- package/src/monotone-cubic.js +40 -0
- package/src/motion-store.js +4 -4
- package/src/motion-trail.js +5 -11
- package/src/multimodel-ingest.js +1 -3
- package/src/object-catalog.jsx +7 -5
- package/src/object-gizmo.jsx +45 -31
- package/src/object-path.js +310 -67
- package/src/object-travel.js +224 -0
- package/src/otio.js +8 -3
- package/src/panels/CharacterTransformPanel.jsx +24 -6
- package/src/panels/ObjectTransformPanel.jsx +4 -3
- package/src/panels/PoseImagePanel.jsx +205 -0
- package/src/panels/PromptBlocksPanel.jsx +3 -1
- package/src/panels/ProxyFigurePanel.jsx +56 -0
- package/src/panels/ReferenceImageField.jsx +1 -1
- package/src/panels/RigControlPanel.jsx +24 -3
- package/src/panels/RigPanel.jsx +2 -1
- package/src/panels/SculptPanel.jsx +405 -0
- package/src/panels/SubjectBox.jsx +1 -1
- package/src/panels/SubjectsPanel.jsx +0 -2
- package/src/panels/VideoCapturePanel.jsx +2 -1
- package/src/panels/pose-image.css +42 -0
- package/src/panels/pose.css +3 -0
- package/src/panels/sculpt.css +162 -0
- package/src/plan-diagram.js +184 -0
- package/src/planview.jsx +127 -80
- package/src/playground.js +3 -5
- package/src/pose-fade.js +77 -0
- package/src/pose-gizmo.css +23 -0
- package/src/pose-gizmo.js +96 -0
- package/src/posestudio.jsx +134 -83
- package/src/project-browser.css +53 -0
- package/src/project-browser.jsx +49 -4
- package/src/project.js +55 -24
- package/src/props.jsx +177 -36
- package/src/proxy-figure.jsx +69 -0
- package/src/range-pin-panel.jsx +0 -1
- package/src/receipt-findings.js +20 -0
- package/src/render-passes.js +0 -1
- package/src/resource-status.jsx +1 -1
- package/src/root-path.js +48 -0
- package/src/route-owner.js +52 -0
- package/src/scene-asset-cache.js +1 -2
- package/src/scene-assets.js +12 -4
- package/src/scene-fbx.js +1 -1
- package/src/scene-mesh-cache.js +0 -1
- package/src/scene-objects.js +319 -19
- package/src/scenes.js +58 -9
- package/src/sculpt-blueprint.js +65 -0
- package/src/sculpt-compare.js +384 -0
- package/src/sculpt-geometry.js +141 -0
- package/src/sculpt-mode.js +168 -0
- package/src/sculpt-recipe.js +385 -0
- package/src/sculpt-run.js +351 -0
- package/src/sculpt-stage.jsx +221 -0
- package/src/settings-menu.jsx +1 -1
- package/src/shell/BoardSlot.jsx +341 -0
- package/src/shell/BottomDock.jsx +60 -16
- package/src/shell/CompositionGuideButton.jsx +20 -0
- package/src/shell/DetailsSlot.jsx +26 -6
- package/src/shell/MenuBar.jsx +78 -6
- package/src/shell/OutlinerSlot.jsx +3 -1
- package/src/shell/ShotPreviewHeader.jsx +17 -0
- package/src/shell/TopBar.jsx +18 -3
- package/src/shell/ViewportToolbar.jsx +36 -31
- package/src/shell/dock.css +239 -0
- package/src/shell/glass-regions.css +3 -2
- package/src/shell/log-store.js +4 -4
- package/src/shell/mode.css +20 -0
- package/src/shell/studio-shell-context.js +4 -1
- package/src/shell/viewport.css +12 -0
- package/src/shot-authoring.js +69 -6
- package/src/stable-items.js +1 -1
- package/src/stage-attach-frames.js +169 -0
- package/src/stage-canvas-rigs.jsx +402 -0
- package/src/stage-capture.jsx +216 -0
- package/src/stage-characters.jsx +302 -0
- package/src/stage-editor-gizmos.jsx +1229 -0
- package/src/stage-labels-ko.js +204 -0
- package/src/stage-startup-defaults.js +182 -0
- package/src/starter-scenes.js +1 -1
- package/src/storyboard.js +24 -5
- package/src/studio-actions.js +42 -9
- package/src/studio-agent-commands.js +213 -23
- package/src/studio-agent-context.js +11 -9
- package/src/studio-agent-motion.js +2 -1
- package/src/studio-agent-protocol.js +60 -24
- package/src/studio-app-binding.js +56 -23
- package/src/studio-contact-sheet.js +3 -3
- package/src/studio-elements.js +11 -2
- package/src/studio-geometry-facts.js +114 -0
- package/src/styles.css +92 -9
- package/src/ui-scrub.js +4 -4
- package/src/ui.jsx +6 -4
- package/src/use-asset-shelf.js +60 -0
- package/src/use-camera-tutorial.js +50 -0
- package/src/use-embed-bridge.js +12 -0
- package/src/use-first-success-prompt.js +7 -0
- package/src/use-i2v-capture-modal.js +15 -0
- package/src/use-sculpt-mode.js +95 -0
- package/src/workflow/AgentPanel.jsx +106 -18
- package/src/workflow/WorkflowBuilder.jsx +1 -1
- package/src/workflow/agent-client.js +113 -45
- package/src/workflow/agent-panel.css +35 -0
- package/src/workflow/attachment-image.js +1 -1
- package/src/workflow/canvas-commands.js +3 -3
- package/src/workflow/cozy-scene-node.js +3 -3
- package/src/workflow/local-workflow.js +2 -2
- package/src/workflow/motion-input.js +3 -3
- package/src/workflow/node-schema.js +1 -1
- package/src/workflow/scene-asset-sync.js +2 -9
- package/src/workflow/shot-prompt-node.js +1 -1
- package/src/workflow/video-contract.js +1 -1
- package/src/workflow/workflow-resources.js +0 -8
- package/src/workflow-send.js +87 -0
- package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
- package/tools/check-eslint.mjs +57 -0
- package/tools/check-knip.mjs +60 -0
- package/tools/check-loc.mjs +54 -0
- package/tools/demo-worker/api-client.mjs +7 -13
- package/tools/demo-worker/generate.mjs +2 -4
- package/tools/eslint-baseline.json +23 -0
- package/tools/gt-render/browser.mjs +23 -0
- package/tools/headless-studio.mjs +85 -0
- package/tools/kimodo/effector-constraints.mjs +4 -4
- package/tools/kimodo/generate.mjs +1 -1
- package/tools/knip-baseline.json +331 -0
- package/tools/loc-baseline.json +126 -0
- package/tools/process-supervisor.mjs +1 -1
- package/tools/projflow/replay.mjs +4 -4
- package/tools/projflow/runner.mjs +3 -3
- package/tools/qa/studio-control-count.mjs +2 -0
- package/tools/run-tests.mjs +95 -7
- package/dist/assets/analytics-MVZOEW8O.js +0 -1
- package/dist/assets/app-BxBLC6GT.js +0 -4876
- package/dist/assets/app-CJdxvnrl.css +0 -1
- package/dist/assets/shot-prompt-hN0j_O-p.js +0 -17
- package/dist/assets/workflow-B5XZwVfe.js +0 -179
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { useStudioShell } from "./studio-shell-context.js";
|
|
3
|
+
import { ko } from "../locale.js";
|
|
4
|
+
import { assetRecord, rememberAsset } from "../scene-asset-cache.js";
|
|
5
|
+
import { ASSET_MAX_SOURCE_BYTES, importImageFile } from "../scene-assets.js";
|
|
6
|
+
import { STILL_HOLD_MAX } from "../shot-authoring.js";
|
|
7
|
+
import { startExportAttempt, track } from "../analytics.js";
|
|
8
|
+
import { bytesToDataUrl, panelVideoPrompt, sendPanelToWorkflow } from "../workflow-send.js";
|
|
9
|
+
import { panelStylizeRequest, requestAgentImage, stylizedPngBytes, stylizeErrorMessage } from "../agent-image-client.js";
|
|
10
|
+
|
|
11
|
+
// A card's greybox is re-captured this long after the last document edit, so
|
|
12
|
+
// a drag or a typing burst costs one capture pass, not one per change.
|
|
13
|
+
const THUMB_DEBOUNCE_MS = 500;
|
|
14
|
+
const PANEL_DRAG_TYPE = "application/x-cozyclay-panel";
|
|
15
|
+
|
|
16
|
+
/** The stylized picture behind a panel, as an object URL for its lifetime. */
|
|
17
|
+
function useStylizedUrl(assetId) {
|
|
18
|
+
const [url, setUrl] = useState(null);
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
setUrl(null);
|
|
21
|
+
if (!assetId) return undefined;
|
|
22
|
+
let live = true;
|
|
23
|
+
let objectUrl = null;
|
|
24
|
+
assetRecord(assetId).then((record) => {
|
|
25
|
+
if (!live || !record) return;
|
|
26
|
+
objectUrl = URL.createObjectURL(new Blob([record.bytes], { type: record.type }));
|
|
27
|
+
setUrl(objectUrl);
|
|
28
|
+
}, (error) => console.warn(`[cozyclay] stylized panel ${assetId} could not be read`, error));
|
|
29
|
+
return () => {
|
|
30
|
+
live = false;
|
|
31
|
+
if (objectUrl) URL.revokeObjectURL(objectUrl);
|
|
32
|
+
};
|
|
33
|
+
}, [assetId]);
|
|
34
|
+
return url;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Board: the storyboard's stills as a strip of panel cards, in still order.
|
|
38
|
+
* Every edit goes through the shot commands, so the Sequencer, undo and the
|
|
39
|
+
* Agent see the same document. */
|
|
40
|
+
export default function BoardSlot({ active = true }) {
|
|
41
|
+
const { shots, activeShot, tlFps, runStudioAction, selectTimelineShot, capturePanelThumbnail, packMetaForShot, setToast, sceneRevision, exportPanelPacks, recState } = useStudioShell();
|
|
42
|
+
const [stylizing, setStylizing] = useState({});
|
|
43
|
+
const stills = shots.filter((entry) => entry.kind === "still").sort((a, b) => a.startFrame - b.startFrame);
|
|
44
|
+
const [thumbs, setThumbs] = useState({});
|
|
45
|
+
const [dragId, setDragId] = useState(null);
|
|
46
|
+
const [drop, setDrop] = useState(null);
|
|
47
|
+
const stillsRef = useRef(stills);
|
|
48
|
+
stillsRef.current = stills;
|
|
49
|
+
const captureRef = useRef(capturePanelThumbnail);
|
|
50
|
+
captureRef.current = capturePanelThumbnail;
|
|
51
|
+
const stripRef = useRef(null);
|
|
52
|
+
const countRef = useRef(stills.length);
|
|
53
|
+
|
|
54
|
+
const stillsKey = stills.map((entry) => `${entry.id}:${entry.startFrame}`).join(",");
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!active || !stillsKey) return undefined;
|
|
57
|
+
const timer = setTimeout(() => {
|
|
58
|
+
const next = {};
|
|
59
|
+
for (const entry of stillsRef.current) {
|
|
60
|
+
try {
|
|
61
|
+
next[entry.id] = captureRef.current(entry);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.warn(`[cozyclay] panel ${entry.id} thumbnail failed`, error);
|
|
64
|
+
next[entry.id] = null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
setThumbs(next);
|
|
68
|
+
}, THUMB_DEBOUNCE_MS);
|
|
69
|
+
return () => clearTimeout(timer);
|
|
70
|
+
}, [active, sceneRevision, stillsKey]);
|
|
71
|
+
|
|
72
|
+
// A new panel lands at the end of the strip: bring it into view.
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (stills.length > countRef.current) stripRef.current?.lastElementChild?.scrollIntoView?.({ block: "nearest", inline: "nearest" });
|
|
75
|
+
countRef.current = stills.length;
|
|
76
|
+
}, [stills.length]);
|
|
77
|
+
|
|
78
|
+
const fps = tlFps || 24;
|
|
79
|
+
const addPanel = () => runStudioAction("shot.createStill", {});
|
|
80
|
+
|
|
81
|
+
// Append the panel picture (greybox, or the stylized image) and a Video
|
|
82
|
+
// node wired to it to the Workflow draft, then open the canvas. Nothing runs.
|
|
83
|
+
async function sendToWorkflow(entry, source) {
|
|
84
|
+
const attempt = startExportAttempt({ export_kind: "workflow_send", format: "png", surface: "studio" });
|
|
85
|
+
try {
|
|
86
|
+
let dataUrl;
|
|
87
|
+
let mimeType = "image/png";
|
|
88
|
+
if (source === "stylized") {
|
|
89
|
+
const record = await assetRecord(entry.stylizedAssetId);
|
|
90
|
+
if (!record) throw new Error(ko("the stylized image is missing", "스타일화 이미지를 찾을 수 없어요"));
|
|
91
|
+
mimeType = record.type || "image/png";
|
|
92
|
+
dataUrl = bytesToDataUrl(record.bytes, mimeType);
|
|
93
|
+
} else {
|
|
94
|
+
dataUrl = capturePanelThumbnail(entry);
|
|
95
|
+
}
|
|
96
|
+
if (!dataUrl) throw new Error(ko("the shot renderer is not ready", "샷 렌더러가 아직 준비되지 않았어요"));
|
|
97
|
+
const index = shots.findIndex((candidate) => candidate.id === entry.id);
|
|
98
|
+
const prompt = panelVideoPrompt(packMetaForShot(entry, index), entry.caption);
|
|
99
|
+
sendPanelToWorkflow({ dataUrl, mimeType, fileName: `panel-${stills.findIndex((candidate) => candidate.id === entry.id) + 1}.${mimeType.split("/")[1] || "png"}`, prompt });
|
|
100
|
+
attempt.succeed();
|
|
101
|
+
setToast(ko("Panel sent to Workflow", "패널을 워크플로로 보냈어요"));
|
|
102
|
+
window.open("/workflow/", "_blank", "noopener,noreferrer");
|
|
103
|
+
} catch (error) {
|
|
104
|
+
attempt.fail(error);
|
|
105
|
+
console.warn(`[cozyclay] panel ${entry.id} could not be sent to Workflow`, error);
|
|
106
|
+
setToast(ko(`Could not send the panel to Workflow: ${error?.message || error}`, `패널을 워크플로로 보내지 못했어요: ${error?.message || error}`));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Stylize: the panel's full-size frame through /agent/image. The picture is
|
|
111
|
+
// stored as an image asset first, then one shot.setStylized names it, so a
|
|
112
|
+
// failure anywhere leaves the panel as it was. Never runs on its own.
|
|
113
|
+
async function stylize(entry) {
|
|
114
|
+
if (stylizing[entry.id]) return;
|
|
115
|
+
setStylizing((current) => ({ ...current, [entry.id]: true }));
|
|
116
|
+
try {
|
|
117
|
+
// The thumbnail capture is the full-size export frame at the shot camera.
|
|
118
|
+
const imageDataUrl = capturePanelThumbnail(entry);
|
|
119
|
+
if (!imageDataUrl) throw new Error(ko("the shot renderer is not ready yet", "샷 렌더러가 아직 준비되지 않았어요"));
|
|
120
|
+
const meta = packMetaForShot(entry, shots.findIndex((candidate) => candidate.id === entry.id));
|
|
121
|
+
const result = await requestAgentImage(panelStylizeRequest({ meta, caption: entry.caption, imageDataUrl }));
|
|
122
|
+
const bytes = stylizedPngBytes(result?.dataUrl, ASSET_MAX_SOURCE_BYTES);
|
|
123
|
+
const asset = await rememberAsset(await importImageFile(new File([bytes], `${entry.name || "panel"} stylized.png`, { type: "image/png" })));
|
|
124
|
+
if (!runStudioAction("shot.setStylized", { shotId: entry.id, assetId: asset.id })) throw new Error(ko("the panel is no longer on the board", "패널이 보드에 더 이상 없어요"));
|
|
125
|
+
track("storyboard:panel_stylized", { outcome: "ok" });
|
|
126
|
+
} catch (error) {
|
|
127
|
+
track("storyboard:panel_stylized", { outcome: "error" });
|
|
128
|
+
console.warn(`[cozyclay] panel ${entry.id} stylize failed`, error);
|
|
129
|
+
setToast(stylizeErrorMessage(error));
|
|
130
|
+
} finally {
|
|
131
|
+
setStylizing(({ [entry.id]: _done, ...rest }) => rest);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function dropTarget(event, entry) {
|
|
136
|
+
const box = event.currentTarget.getBoundingClientRect();
|
|
137
|
+
return { id: entry.id, after: event.clientX > box.left + box.width / 2 };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function reorderTo(target) {
|
|
141
|
+
const moving = dragId;
|
|
142
|
+
setDragId(null);
|
|
143
|
+
setDrop(null);
|
|
144
|
+
if (!moving || !target || moving === target.id) return;
|
|
145
|
+
const others = stills.filter((entry) => entry.id !== moving);
|
|
146
|
+
const index = others.findIndex((entry) => entry.id === target.id) + (target.after ? 1 : 0);
|
|
147
|
+
if (stills.findIndex((entry) => entry.id === moving) === index) return;
|
|
148
|
+
runStudioAction("shot.reorder", { shotId: moving, index });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<section className="dock-board" aria-label={ko("Board", "보드")} data-testid="dock-board" hidden={!active}>
|
|
153
|
+
<header className="dock-board-head">
|
|
154
|
+
<span className="dock-board-count">{ko(`${stills.length} panels`, `패널 ${stills.length}개`)}</span>
|
|
155
|
+
{/* An empty board offers + Panel once, in its empty state. */}
|
|
156
|
+
{stills.length > 0 && (
|
|
157
|
+
<button type="button" className="dock-board-add" data-testid="board-add-panel" title={ko("Add a panel after the last one, framed by the current camera", "현재 카메라 구도로 마지막 패널 뒤에 패널 추가")} onClick={addPanel}>
|
|
158
|
+
{ko("+ Panel", "+ 패널")}
|
|
159
|
+
</button>
|
|
160
|
+
)}
|
|
161
|
+
</header>
|
|
162
|
+
{stills.length === 0 ? (
|
|
163
|
+
<div className="dock-board-empty" data-testid="board-empty">
|
|
164
|
+
<p>{ko("No panels yet. Each panel is one held picture of the scene through the shot camera.", "아직 패널이 없어요. 패널 하나는 샷 카메라로 본 장면 한 장을 일정 시간 보여 줍니다.")}</p>
|
|
165
|
+
<button type="button" className="dock-board-add" data-testid="board-empty-add" title={ko("Add the first panel, framed by the current camera", "현재 카메라 구도로 첫 패널 추가")} onClick={addPanel}>{ko("+ Panel", "+ 패널")}</button>
|
|
166
|
+
</div>
|
|
167
|
+
) : (
|
|
168
|
+
<ol className="dock-board-strip" ref={stripRef} data-testid="board-strip">
|
|
169
|
+
{stills.map((entry, index) => (
|
|
170
|
+
<PanelCard
|
|
171
|
+
key={entry.id}
|
|
172
|
+
shot={entry}
|
|
173
|
+
index={index}
|
|
174
|
+
fps={fps}
|
|
175
|
+
thumb={thumbs[entry.id] ?? null}
|
|
176
|
+
stylizing={Boolean(stylizing[entry.id])}
|
|
177
|
+
onStylize={() => stylize(entry)}
|
|
178
|
+
onUnstylize={() => runStudioAction("shot.setStylized", { shotId: entry.id, assetId: null })}
|
|
179
|
+
selected={activeShot?.id === entry.id}
|
|
180
|
+
dragging={dragId === entry.id}
|
|
181
|
+
dropSide={drop?.id === entry.id ? (drop.after ? "after" : "before") : null}
|
|
182
|
+
onSelect={() => selectTimelineShot(entry.id)}
|
|
183
|
+
onRemove={() => runStudioAction("shot.remove", { shotId: entry.id })}
|
|
184
|
+
onDuplicate={() => runStudioAction("shot.duplicate", { shotId: entry.id })}
|
|
185
|
+
onCaption={(caption) => runStudioAction("shot.setCaption", { shotId: entry.id, caption })}
|
|
186
|
+
onHold={(hold) => runStudioAction("shot.setHold", { shotId: entry.id, hold })}
|
|
187
|
+
onSendToWorkflow={(source) => sendToWorkflow(entry, source)}
|
|
188
|
+
exporting={recState === "recording"}
|
|
189
|
+
onExport={() => exportPanelPacks(false, entry.id)}
|
|
190
|
+
onDragStart={(event) => {
|
|
191
|
+
event.dataTransfer.effectAllowed = "move";
|
|
192
|
+
event.dataTransfer.setData(PANEL_DRAG_TYPE, entry.id);
|
|
193
|
+
setDragId(entry.id);
|
|
194
|
+
}}
|
|
195
|
+
onDragOver={(event) => {
|
|
196
|
+
if (!dragId) return;
|
|
197
|
+
event.preventDefault();
|
|
198
|
+
event.dataTransfer.dropEffect = "move";
|
|
199
|
+
const next = dropTarget(event, entry);
|
|
200
|
+
if (drop?.id !== next.id || drop?.after !== next.after) setDrop(next);
|
|
201
|
+
}}
|
|
202
|
+
onDrop={(event) => {
|
|
203
|
+
event.preventDefault();
|
|
204
|
+
reorderTo(dropTarget(event, entry));
|
|
205
|
+
}}
|
|
206
|
+
onDragEnd={() => { setDragId(null); setDrop(null); }}
|
|
207
|
+
/>
|
|
208
|
+
))}
|
|
209
|
+
</ol>
|
|
210
|
+
)}
|
|
211
|
+
</section>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function PanelCard({ shot, index, fps, thumb, stylizing, onStylize, onUnstylize, selected, dragging, dropSide, onSelect, onRemove, onDuplicate, onCaption, onHold, onSendToWorkflow, exporting, onExport, onDragStart, onDragOver, onDrop, onDragEnd }) {
|
|
216
|
+
const stylized = useStylizedUrl(shot.stylizedAssetId);
|
|
217
|
+
const [sendMenu, setSendMenu] = useState(false);
|
|
218
|
+
const hold = shot.endFrame - shot.startFrame + 1;
|
|
219
|
+
const seconds = Number((hold / fps).toFixed(2));
|
|
220
|
+
const stop = (event) => event.stopPropagation();
|
|
221
|
+
|
|
222
|
+
function commitCaption(event) {
|
|
223
|
+
const next = event.currentTarget.value.trim();
|
|
224
|
+
if (next !== (shot.caption ?? "")) onCaption(next);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function commitHold(event) {
|
|
228
|
+
const value = Number(event.currentTarget.value);
|
|
229
|
+
const frames = Math.round(value * fps);
|
|
230
|
+
if (!Number.isFinite(value) || frames < 1 || frames > STILL_HOLD_MAX) {
|
|
231
|
+
event.currentTarget.value = String(seconds);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (frames !== hold) onHold(frames);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<li
|
|
239
|
+
className="dock-board-card"
|
|
240
|
+
data-testid="board-card"
|
|
241
|
+
data-shot-id={shot.id}
|
|
242
|
+
data-selected={selected || undefined}
|
|
243
|
+
data-dragging={dragging || undefined}
|
|
244
|
+
data-drop={dropSide ?? undefined}
|
|
245
|
+
data-stylizing={stylizing || undefined}
|
|
246
|
+
tabIndex={0}
|
|
247
|
+
draggable
|
|
248
|
+
aria-label={ko(`Panel ${index + 1}`, `패널 ${index + 1}`)}
|
|
249
|
+
aria-current={selected ? "true" : undefined}
|
|
250
|
+
onClick={onSelect}
|
|
251
|
+
onKeyDown={(event) => {
|
|
252
|
+
if (event.target !== event.currentTarget) return;
|
|
253
|
+
if (event.key === "Delete") {
|
|
254
|
+
event.preventDefault();
|
|
255
|
+
event.stopPropagation();
|
|
256
|
+
onRemove();
|
|
257
|
+
} else if (event.key === "Enter") {
|
|
258
|
+
onSelect();
|
|
259
|
+
}
|
|
260
|
+
}}
|
|
261
|
+
onDragStart={onDragStart}
|
|
262
|
+
onDragOver={onDragOver}
|
|
263
|
+
onDrop={onDrop}
|
|
264
|
+
onDragEnd={onDragEnd}
|
|
265
|
+
>
|
|
266
|
+
<div className="dock-board-actions">
|
|
267
|
+
<button type="button" data-action="duplicate" title={ko("Duplicate this panel after itself", "이 패널을 바로 뒤에 복제")} onClick={(event) => { stop(event); onDuplicate(); }}>{ko("Duplicate", "복제")}</button>
|
|
268
|
+
<button type="button" data-action="delete" className="danger" title={ko("Delete this panel (Delete)", "이 패널 삭제 (Delete)")} onClick={(event) => { stop(event); onRemove(); }}>{ko("Delete", "삭제")}</button>
|
|
269
|
+
<button type="button" data-action="stylize" disabled={stylizing}
|
|
270
|
+
title={shot.stylizedAssetId
|
|
271
|
+
? ko("Generate a new stylized picture from this panel's frame and caption (replaces the current one)", "이 패널의 프레임과 캡션으로 스타일화 그림을 새로 생성 (지금 그림을 바꿔요)")
|
|
272
|
+
: ko("Generate a stylized picture from this panel's frame and caption", "이 패널의 프레임과 캡션으로 스타일화 그림 생성")}
|
|
273
|
+
onClick={(event) => { stop(event); onStylize(); }}>
|
|
274
|
+
{stylizing ? ko("Stylizing...", "스타일화 중...") : shot.stylizedAssetId ? ko("Re-stylize", "다시 스타일화") : ko("Stylize", "스타일화")}
|
|
275
|
+
</button>
|
|
276
|
+
{shot.stylizedAssetId && !stylizing && (
|
|
277
|
+
<button type="button" data-action="unstylize" title={ko("Remove the stylized picture; the greybox stays", "스타일화 그림 제거 (그레이박스는 남아요)")} onClick={(event) => { stop(event); onUnstylize(); }}>{ko("Remove", "제거")}</button>
|
|
278
|
+
)}
|
|
279
|
+
<button
|
|
280
|
+
type="button"
|
|
281
|
+
data-action="workflow"
|
|
282
|
+
title={ko("Send this panel to a Workflow video node", "이 패널을 워크플로 비디오 노드로 보내기")}
|
|
283
|
+
aria-haspopup={shot.stylizedAssetId ? "menu" : undefined}
|
|
284
|
+
aria-expanded={shot.stylizedAssetId ? sendMenu : undefined}
|
|
285
|
+
onClick={(event) => {
|
|
286
|
+
stop(event);
|
|
287
|
+
// Only a stylized panel has a choice to make.
|
|
288
|
+
if (shot.stylizedAssetId) setSendMenu((open) => !open);
|
|
289
|
+
else onSendToWorkflow("greybox");
|
|
290
|
+
}}
|
|
291
|
+
>{ko("Send to Workflow", "워크플로로")}</button>
|
|
292
|
+
{sendMenu && shot.stylizedAssetId && (
|
|
293
|
+
<div className="dock-board-menu" role="menu" aria-label={ko("Send to Workflow", "워크플로로 보내기")} onKeyDown={(event) => { if (event.key === "Escape") { event.stopPropagation(); setSendMenu(false); } }}>
|
|
294
|
+
<button type="button" role="menuitem" data-send-source="stylized" autoFocus onClick={(event) => { stop(event); setSendMenu(false); onSendToWorkflow("stylized"); }}>{ko("Stylized image", "스타일화 이미지")}</button>
|
|
295
|
+
<button type="button" role="menuitem" data-send-source="greybox" onClick={(event) => { stop(event); setSendMenu(false); onSendToWorkflow("greybox"); }}>{ko("Greybox", "그레이박스")}</button>
|
|
296
|
+
</div>
|
|
297
|
+
)}
|
|
298
|
+
<button type="button" data-action="export" disabled={exporting} title={exporting ? ko("Export — another export is running", "내보내기 — 다른 내보내기가 진행 중입니다") : ko("Export this panel as a pack (zip)", "이 패널을 팩(zip)으로 내보내기")} data-disabled-reason={exporting ? "export-running" : undefined} onClick={(event) => { stop(event); onExport(); }}>{ko("Export", "내보내기")}</button>
|
|
299
|
+
</div>
|
|
300
|
+
<div className="dock-board-thumb" data-stylized={stylized ? "true" : undefined}>
|
|
301
|
+
{stylized ? <img className="dock-board-image" src={stylized} alt="" draggable={false} /> : thumb && <img className="dock-board-image" src={thumb} alt="" draggable={false} />}
|
|
302
|
+
{stylized && thumb && <img className="dock-board-inset" src={thumb} alt="" draggable={false} />}
|
|
303
|
+
<span className="dock-board-index" data-testid="board-card-index">{index + 1}</span>
|
|
304
|
+
{stylizing && (
|
|
305
|
+
<span data-testid="board-card-stylizing" role="status"
|
|
306
|
+
style={{ position: "absolute", inset: 0, display: "grid", placeItems: "center", background: "rgba(0, 0, 0, .45)", color: "#fff", font: "500 11px var(--sans)" }}>
|
|
307
|
+
{ko("Stylizing...", "스타일화 중...")}
|
|
308
|
+
</span>
|
|
309
|
+
)}
|
|
310
|
+
</div>
|
|
311
|
+
<textarea
|
|
312
|
+
key={`caption:${shot.caption ?? ""}`}
|
|
313
|
+
className="dock-board-caption"
|
|
314
|
+
data-testid="board-card-caption"
|
|
315
|
+
aria-label={ko(`Panel ${index + 1} caption`, `패널 ${index + 1} 캡션`)}
|
|
316
|
+
placeholder={ko("Action or dialogue", "동작 또는 대사")}
|
|
317
|
+
defaultValue={shot.caption ?? ""}
|
|
318
|
+
maxLength={500}
|
|
319
|
+
rows={2}
|
|
320
|
+
onClick={stop}
|
|
321
|
+
onBlur={commitCaption}
|
|
322
|
+
/>
|
|
323
|
+
<label className="dock-board-hold" onClick={stop}>
|
|
324
|
+
<span>{ko("Hold", "홀드")}</span>
|
|
325
|
+
<input
|
|
326
|
+
key={`hold:${hold}`}
|
|
327
|
+
type="number"
|
|
328
|
+
data-testid="board-card-hold"
|
|
329
|
+
min={1 / fps}
|
|
330
|
+
max={STILL_HOLD_MAX / fps}
|
|
331
|
+
step={0.5}
|
|
332
|
+
defaultValue={seconds}
|
|
333
|
+
aria-label={ko(`Panel ${index + 1} hold in seconds`, `패널 ${index + 1} 홀드 (초)`)}
|
|
334
|
+
onBlur={commitHold}
|
|
335
|
+
onKeyDown={(event) => { if (event.key === "Enter") event.currentTarget.blur(); }}
|
|
336
|
+
/>
|
|
337
|
+
<span>{ko("s", "초")}</span>
|
|
338
|
+
</label>
|
|
339
|
+
</li>
|
|
340
|
+
);
|
|
341
|
+
}
|
package/src/shell/BottomDock.jsx
CHANGED
|
@@ -3,19 +3,21 @@ import { useStudioShell } from "./studio-shell-context.js";
|
|
|
3
3
|
import { logStore } from "./log-store.js";
|
|
4
4
|
import "./dock.css";
|
|
5
5
|
import LibrarySlot from "./LibrarySlot.jsx";
|
|
6
|
+
import BoardSlot from "./BoardSlot.jsx";
|
|
6
7
|
import { ko } from "../locale.js";
|
|
7
8
|
import Timeline from "../ardy/timeline.jsx";
|
|
8
9
|
import { DEFAULT_PLAYBACK_SPEED, SHOT_ASPECT_PRESETS, sceneObjectNameDisplayKo } from "../app-stage.jsx";
|
|
9
10
|
import { motionEditLayout, createMotionEdit } from "../ardy/motion-edit.js";
|
|
10
11
|
import { trackFeature } from "../analytics.js";
|
|
11
12
|
import { defaultRailRange } from "../camera-rail-schedule.js";
|
|
13
|
+
import { insertPathMark, pathMarks } from "../object-path.js";
|
|
12
14
|
|
|
13
15
|
// G11: the dock resizes within [220, 480] px, and never so far that the 3D
|
|
14
16
|
// viewport drops under 480 px (top bar 44 + status bar 24 + three 1 px gaps).
|
|
15
17
|
// Never below the sequencer's own grid (shell.css --shell-dock-default, 276
|
|
16
18
|
// px): a shorter dock would have to scroll its lanes.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
const DOCK_MIN_HEIGHT = 276;
|
|
20
|
+
const DOCK_MAX_HEIGHT = 480;
|
|
19
21
|
const VIEWPORT_MIN_HEIGHT = 480;
|
|
20
22
|
const SHELL_FIXED_ROWS = 44 + 24 + 3;
|
|
21
23
|
// v2: v1 heights were clamped to a 324px floor that left an empty band under
|
|
@@ -49,13 +51,25 @@ function readDockHeight() {
|
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
const DOCK_TABS = [
|
|
54
|
+
{ key: "board", label: () => ko("Board", "보드") },
|
|
52
55
|
{ key: "animation", label: () => ko("Animation", "애니메이션") },
|
|
53
56
|
{ key: "assets", label: () => ko("Assets", "에셋") },
|
|
54
57
|
];
|
|
55
58
|
|
|
56
|
-
/**
|
|
57
|
-
*
|
|
58
|
-
|
|
59
|
+
/** The dock's tabs for a project mode: a storyboard works in panels
|
|
60
|
+
* (Board | Assets), an animation in the Sequencer (Animation | Assets). */
|
|
61
|
+
function dockTabsFor(previsMode) {
|
|
62
|
+
const keys = previsMode === "storyboard" ? ["board", "assets"] : ["animation", "assets"];
|
|
63
|
+
return keys.map((key) => DOCK_TABS.find((entry) => entry.key === key));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Animation | Assets (Board | Assets in a storyboard): all stay mounted, so
|
|
67
|
+
* switching keeps the Sequencer's scroll and zoom and the Library's folder
|
|
68
|
+
* and search. Any stored tab the mode does not offer opens its first tab. */
|
|
69
|
+
export default function BottomDock({ tab: requestedTab = "animation", onTabChange, embedded = false }) {
|
|
70
|
+
const { previsMode } = useStudioShell();
|
|
71
|
+
const tabs = embedded ? dockTabsFor(null) : dockTabsFor(previsMode);
|
|
72
|
+
const tab = tabs.some((entry) => entry.key === requestedTab) ? requestedTab : tabs[0].key;
|
|
59
73
|
const {
|
|
60
74
|
tlFrame, motion, waypointMode, craneSelectedIndex,
|
|
61
75
|
isCameraSelection, addActiveCranePoint, deleteSelectedCranePoint, setCraneSelectedIndex, tlFrameCount,
|
|
@@ -65,7 +79,7 @@ export default function BottomDock({ tab = "animation", onTabChange, embedded =
|
|
|
65
79
|
resetMotionTrim, cutMotionAtPlayhead, changeMotionSegmentSpeed, removeMotionSegmentById, ikFrames,
|
|
66
80
|
rangePins, rangePinSelection, ikEditTool, rangePinPreview, footSnap,
|
|
67
81
|
bodyContact, shots, shotAspectKey, activeShotIdx, railDraw,
|
|
68
|
-
pathDraw,
|
|
82
|
+
pathDraw, pathTarget, setPathDraw, setRailDraw, setWorkspaceLayout,
|
|
69
83
|
changeSceneObject, timingTokenRef, beginSceneTransaction, endSceneTransaction, railCurve,
|
|
70
84
|
posing, ikAddKeyframe, ikDeleteKeyframe, setRangePinSelection, setIkEditTool,
|
|
71
85
|
setBodyContact, setToast, setFootSnap, setTlFrame, advanceFrame,
|
|
@@ -77,7 +91,7 @@ export default function BottomDock({ tab = "animation", onTabChange, embedded =
|
|
|
77
91
|
activeCamera, activeShotDuration, changeActiveCamera, cameraRail, previewCameraShot,
|
|
78
92
|
toggleCameraRailDraw, deleteCameraRail, selectTimelineShot, shotsDomain, runStudioAction,
|
|
79
93
|
clearMotion, subscribeToasts, exportStatus, exportPhaseLabel, ardyRunning, ardyStatus,
|
|
80
|
-
ardyOutcome,
|
|
94
|
+
ardyOutcome, pathPointIndex, setPathPointIndex,
|
|
81
95
|
} = useStudioShell();
|
|
82
96
|
const dockRef = useRef(null);
|
|
83
97
|
const [dockHeight, setDockHeight] = useState(readDockHeight);
|
|
@@ -190,7 +204,7 @@ export default function BottomDock({ tab = "animation", onTabChange, embedded =
|
|
|
190
204
|
onKeyDown={onDockResizeKey}
|
|
191
205
|
/>
|
|
192
206
|
{!embedded && <div className="dock-tabs" role="tablist" aria-label={ko("Dock", "도크")}>
|
|
193
|
-
{
|
|
207
|
+
{tabs.map((entry) => (
|
|
194
208
|
<button
|
|
195
209
|
type="button"
|
|
196
210
|
role="tab"
|
|
@@ -204,6 +218,7 @@ export default function BottomDock({ tab = "animation", onTabChange, embedded =
|
|
|
204
218
|
))}
|
|
205
219
|
</div>}
|
|
206
220
|
{!embedded && <LibrarySlot active={tab === "assets"} />}
|
|
221
|
+
{!embedded && previsMode === "storyboard" && <BoardSlot active={tab === "board"} />}
|
|
207
222
|
<div className="bottom-timeline" hidden={tab !== "animation"}>
|
|
208
223
|
<Timeline
|
|
209
224
|
frame={tlFrame}
|
|
@@ -222,8 +237,8 @@ export default function BottomDock({ tab = "animation", onTabChange, embedded =
|
|
|
222
237
|
workflowMode={workflowMode === "pose" ? "motion" : workflowMode}
|
|
223
238
|
waypointMode={waypointMode}
|
|
224
239
|
waypoints={waypoints}
|
|
225
|
-
pathSpeed={pathSpeed}
|
|
226
240
|
pendingWaypointFrame={pendingWaypointFrame}
|
|
241
|
+
previsMode={previsMode}
|
|
227
242
|
promptClips={promptClips}
|
|
228
243
|
selectedPromptId={selectedPromptId}
|
|
229
244
|
badge={stateBadge}
|
|
@@ -250,19 +265,46 @@ export default function BottomDock({ tab = "animation", onTabChange, embedded =
|
|
|
250
265
|
activeShotIdx={activeShotIdx}
|
|
251
266
|
railDraw={railDraw}
|
|
252
267
|
pathDraw={pathDraw}
|
|
253
|
-
pathObject={
|
|
268
|
+
pathObject={pathTarget ? { id: pathTarget.id, name: sceneObjectNameDisplayKo(pathTarget.name), path: pathTarget.path } : null}
|
|
254
269
|
onObjectPathDrawToggle={() => {
|
|
255
270
|
setPathDraw((current) => !current);
|
|
256
271
|
if (!pathDraw) setRailDraw(false);
|
|
257
272
|
setWorkspaceLayout((current) => ({ ...current, insetCollapsed: false }));
|
|
258
273
|
}}
|
|
259
274
|
onObjectPathChange={(path) => {
|
|
260
|
-
if (
|
|
275
|
+
if (pathTarget) changeSceneObject(pathTarget.id, { path }, timingTokenRef.current ?? undefined);
|
|
261
276
|
}}
|
|
277
|
+
objectPathPointIndex={pathPointIndex}
|
|
278
|
+
onObjectMarkAdd={(t) => {
|
|
279
|
+
const path = pathTarget?.path;
|
|
280
|
+
const added = path ? insertPathMark(path, t) : null;
|
|
281
|
+
if (!added) return;
|
|
282
|
+
const token = beginSceneTransaction({ owner: "object-path", cancel: () => {} });
|
|
283
|
+
changeSceneObject(pathTarget.id, { path: { ...path, marks: added.marks } }, token);
|
|
284
|
+
endSceneTransaction(token, { commit: true });
|
|
285
|
+
setPathPointIndex(added.index);
|
|
286
|
+
}}
|
|
287
|
+
onObjectMarkLean={(key, value, token) => {
|
|
288
|
+
const path = pathTarget?.path;
|
|
289
|
+
if (!path || pathPointIndex == null || (key !== "bank" && key !== "pitch")) return;
|
|
290
|
+
const marks = pathMarks(path);
|
|
291
|
+
if (pathPointIndex < 1 || pathPointIndex > marks.length) return;
|
|
292
|
+
const next = { ...path, marks: marks.map((mark, i) => (i + 1 === pathPointIndex ? { ...mark, [key]: value } : mark)) };
|
|
293
|
+
// A scrub hands its own transaction; a typed value is one entry.
|
|
294
|
+
if (token != null) {
|
|
295
|
+
changeSceneObject(pathTarget.id, { path: next }, token);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const own = beginSceneTransaction({ owner: "object-path", cancel: () => {} });
|
|
299
|
+
changeSceneObject(pathTarget.id, { path: next }, own);
|
|
300
|
+
endSceneTransaction(own, { commit: true });
|
|
301
|
+
}}
|
|
302
|
+
onObjectMarkScrubStart={beginSceneTransaction}
|
|
303
|
+
onObjectMarkScrubEnd={endSceneTransaction}
|
|
262
304
|
onObjectPathClear={() => {
|
|
263
|
-
if (!
|
|
305
|
+
if (!pathTarget) return;
|
|
264
306
|
const token = beginSceneTransaction({ owner: "object-path", cancel: () => {} });
|
|
265
|
-
changeSceneObject(
|
|
307
|
+
changeSceneObject(pathTarget.id, { path: null }, token);
|
|
266
308
|
endSceneTransaction(token, { commit: true });
|
|
267
309
|
}}
|
|
268
310
|
onObjectTimingGestureStart={() => {
|
|
@@ -372,7 +414,9 @@ export default function BottomDock({ tab = "animation", onTabChange, embedded =
|
|
|
372
414
|
onClearMotion={motion ? clearMotion : null}
|
|
373
415
|
/>
|
|
374
416
|
</div>
|
|
375
|
-
{
|
|
417
|
+
{/* Embeds keep their own shot monitor in the viewport (.vp-shot-preview);
|
|
418
|
+
the card is styled for the full studio only. */}
|
|
419
|
+
{(tab === "animation" || tab === "board") && !embedded && <ShotCard />}
|
|
376
420
|
</div>
|
|
377
421
|
);
|
|
378
422
|
}
|
|
@@ -438,12 +482,12 @@ function ShotCard() {
|
|
|
438
482
|
{ko("Exit", "나가기")}
|
|
439
483
|
<kbd>Esc</kbd>
|
|
440
484
|
</button>
|
|
441
|
-
) : (
|
|
485
|
+
) : activeShot ? (
|
|
442
486
|
<button type="button" className="dock-shot-look" aria-pressed="false" onClick={enterShotLook} title={ko("Look through the shot camera (Esc returns)", "샷 카메라 시점으로 보기 (Esc로 복귀)")}>
|
|
443
487
|
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M1.75 8S4 3.75 8 3.75 14.25 8 14.25 8 12 12.25 8 12.25 1.75 8 1.75 8z M8 6.25a1.75 1.75 0 1 1 0 3.5 1.75 1.75 0 1 1 0-3.5z" /></svg>
|
|
444
488
|
{ko("Look through", "샷 시점")}
|
|
445
489
|
</button>
|
|
446
|
-
)}
|
|
490
|
+
) : null}
|
|
447
491
|
</footer>
|
|
448
492
|
</section>
|
|
449
493
|
);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { GUIDE_LABELS, nextGuideMode } from "../shot-guides.js";
|
|
2
|
+
import { ko } from "../locale.js";
|
|
3
|
+
|
|
4
|
+
export default function CompositionGuideButton({ guideMode, onCycle }) {
|
|
5
|
+
return (
|
|
6
|
+
<button
|
|
7
|
+
type="button"
|
|
8
|
+
className={"vp-guide-cycle vp-embed-guide" + (guideMode === "off" ? "" : " on")}
|
|
9
|
+
aria-label={ko("Cycle composition guides", "구도 가이드 전환")}
|
|
10
|
+
aria-pressed={guideMode !== "off"}
|
|
11
|
+
title={ko(GUIDE_LABELS[guideMode].en, GUIDE_LABELS[guideMode].ko) + ko(" · click to cycle", " · 클릭으로 전환")}
|
|
12
|
+
onClick={() => onCycle(nextGuideMode(guideMode))}
|
|
13
|
+
>
|
|
14
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
|
|
15
|
+
<path d="M3 3h18v18H3z" />
|
|
16
|
+
<path d="M9 3v18M15 3v18M3 9h18M3 15h18" />
|
|
17
|
+
</svg>
|
|
18
|
+
</button>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { useStudioShell } from "./studio-shell-context.js";
|
|
2
2
|
import { ko } from "../locale.js";
|
|
3
3
|
import { sceneObjectNameDisplayKo, HIERARCHY_INSPECTOR_TITLES, CHARACTER_MODEL_LABELS } from "../app-stage.jsx";
|
|
4
|
-
import { CUTOUT_KIND } from "../scene-objects.js";
|
|
4
|
+
import { CUTOUT_KIND, EMPTY_KIND, SCULPT_KIND } from "../scene-objects.js";
|
|
5
5
|
import LightPanel from "../panels/LightPanel.jsx";
|
|
6
6
|
import CameraPanel from "../panels/CameraPanel.jsx";
|
|
7
7
|
import SubjectsPanel from "../panels/SubjectsPanel.jsx";
|
|
8
8
|
import CharacterTransformPanel from "../panels/CharacterTransformPanel.jsx";
|
|
9
9
|
import RigPanel from "../panels/RigPanel.jsx";
|
|
10
|
+
import ProxyFigurePanel from "../panels/ProxyFigurePanel.jsx";
|
|
10
11
|
import PosePanel from "../panels/PosePanel.jsx";
|
|
11
12
|
import VideoCapturePanel from "../panels/VideoCapturePanel.jsx";
|
|
12
13
|
import PromptBlocksPanel from "../panels/PromptBlocksPanel.jsx";
|
|
13
14
|
import RigControlPanel from "../panels/RigControlPanel.jsx";
|
|
15
|
+
import PoseImagePanel from "../panels/PoseImagePanel.jsx";
|
|
14
16
|
import EnvironmentPanel from "../panels/EnvironmentPanel.jsx";
|
|
15
17
|
import PropsPanel from "../panels/PropsPanel.jsx";
|
|
16
18
|
import ObjectTransformPanel from "../panels/ObjectTransformPanel.jsx";
|
|
@@ -29,8 +31,8 @@ const characterTitle = (index) => ko(`Character ${index + 1}`, `인물 ${index +
|
|
|
29
31
|
function detailsSelection({ selectedHierarchyId: id, selectedSceneObject, rigSelection, characters, shot, inspectorHasContent }) {
|
|
30
32
|
if (!inspectorHasContent) return null;
|
|
31
33
|
if (selectedSceneObject) {
|
|
32
|
-
const kind = selectedSceneObject.renderer === CUTOUT_KIND ? ko("Cutout", "컷아웃") : ko("Mesh", "메시");
|
|
33
|
-
return { name: sceneObjectNameDisplayKo(selectedSceneObject.name), type: `${ko("Prop", "소품")} · ${kind}` };
|
|
34
|
+
const kind = selectedSceneObject.renderer === CUTOUT_KIND ? ko("Cutout", "컷아웃") : selectedSceneObject.renderer === EMPTY_KIND ? ko("Empty", "빈 오브젝트") : selectedSceneObject.renderer === SCULPT_KIND ? ko("Sculpt", "조형") : ko("Mesh", "메시");
|
|
35
|
+
return { name: sceneObjectNameDisplayKo(selectedSceneObject.name), type: `${selectedSceneObject.renderer === EMPTY_KIND ? ko("Node", "노드") : ko("Prop", "소품")} · ${kind}` };
|
|
34
36
|
}
|
|
35
37
|
if (rigSelection) {
|
|
36
38
|
const index = characterIndexForRow(rigSelection.rowId, characters);
|
|
@@ -55,7 +57,7 @@ function detailsSelection({ selectedHierarchyId: id, selectedSceneObject, rigSel
|
|
|
55
57
|
|
|
56
58
|
export default function DetailsSlot() {
|
|
57
59
|
const {
|
|
58
|
-
selectedHierarchyId, sceneSaveError, studioAgentError, embedMode, studioAgentMode,
|
|
60
|
+
sculptPanel, selectedHierarchyId, sceneSaveError, studioAgentError, embedMode, studioAgentMode,
|
|
59
61
|
setStudioAgentMode, AgentPanel, setAgentCollapsed, scenes, activeSceneId,
|
|
60
62
|
buildStudioAgentContext, highlightAgentTargets, generateI2vMotionFromUi, selectedSceneObject, rigSelection,
|
|
61
63
|
inspectorActionsOpen, setInspectorActionsOpen, runStudioAction, deleteSelectedSceneObject, inspectorHasContent,
|
|
@@ -80,12 +82,13 @@ export default function DetailsSlot() {
|
|
|
80
82
|
generationBusy, bridgeChecking, lineReadinessState, runLineEdit, openMotionSetup,
|
|
81
83
|
recheckMotionHealth, resetLineCurve, exitLineEditMode, readinessState, ardyRunning,
|
|
82
84
|
cancelArdy, ardyStatus, ardyOutcome, tlFrame,
|
|
83
|
-
isRigSelection, ikChains, ikFocus, footSnap, collisionCleanupSupported,
|
|
85
|
+
isRigSelection, ikChains, ikFocus, footSnap, poseObjectOpacity, setPoseObjectOpacity, capturePoseImage, collisionCleanupSupported,
|
|
84
86
|
autoPhysicsRunning, physicsProgress, physicsPreview,
|
|
85
87
|
physicsShow, physicsOptions, platformFitRunning, platformFitProgress, platformFitLast,
|
|
86
88
|
platformFitApplied, changePhysicsOptions, runAutoPhysics, showPhysicsPreview, applyPhysicsPreview,
|
|
87
89
|
cancelPhysicsPreview, ikEditTool, setIkEditTool, showTrails, setShowTrails,
|
|
88
|
-
trailFalloffS, setTrailFalloffS, trailEdit, runTrailRegeneration, trailReadinessState,
|
|
90
|
+
trailFalloffS, setTrailFalloffS, trailEdit, trailTrackFocus, selectTrailTrack, clearTrailTrackFocus, runTrailRegeneration, trailReadinessState,
|
|
91
|
+
pendingIkEdit, applyPendingIkEdit, cancelPendingIkEdit,
|
|
89
92
|
rangePins, rangePinResiduals, rangePinSelection, rangePinPartPick, rangePinPreview,
|
|
90
93
|
sceneObjects, setRangePinSelection, setRangePinPartPick, previewRangePinDraft, applyRangePinDraft,
|
|
91
94
|
deleteRangePin, hasEnvSheet, environment, style, environmentImage,
|
|
@@ -154,6 +157,8 @@ export default function DetailsSlot() {
|
|
|
154
157
|
)}
|
|
155
158
|
</div>
|
|
156
159
|
<div className="inspector-scroll">
|
|
160
|
+
{/* Sculpt mode owns the column: its panel replaces the selection's settings. */}
|
|
161
|
+
{sculptPanel ?? (<>
|
|
157
162
|
{/* Nothing is selected that owns settings — say so rather than
|
|
158
163
|
showing an empty column the user has to interpret. */}
|
|
159
164
|
{!inspectorHasContent && (
|
|
@@ -164,6 +169,7 @@ export default function DetailsSlot() {
|
|
|
164
169
|
)}
|
|
165
170
|
</p>
|
|
166
171
|
)}
|
|
172
|
+
<PoseImagePanel hidden={workflowMode !== "pose"} capturePoseImage={capturePoseImage} />
|
|
167
173
|
{/* Shot TYPE presets live in the viewport toolbar dropdown — not
|
|
168
174
|
duplicated here. */}
|
|
169
175
|
|
|
@@ -204,6 +210,11 @@ export default function DetailsSlot() {
|
|
|
204
210
|
activeChar={activeChar}
|
|
205
211
|
/>
|
|
206
212
|
|
|
213
|
+
<ProxyFigurePanel
|
|
214
|
+
isCharacterSelection={isCharacterSelection}
|
|
215
|
+
activeChar={activeChar}
|
|
216
|
+
/>
|
|
217
|
+
|
|
207
218
|
<PosePanel
|
|
208
219
|
isCharacterSelection={isCharacterSelection}
|
|
209
220
|
activeCharIndex={activeCharIndex}
|
|
@@ -306,6 +317,8 @@ export default function DetailsSlot() {
|
|
|
306
317
|
ikChains={ikChains}
|
|
307
318
|
ikFocus={ikFocus}
|
|
308
319
|
footSnap={footSnap}
|
|
320
|
+
poseObjectOpacity={poseObjectOpacity}
|
|
321
|
+
setPoseObjectOpacity={setPoseObjectOpacity}
|
|
309
322
|
ikMode={ikMode}
|
|
310
323
|
collisionCleanupSupported={collisionCleanupSupported}
|
|
311
324
|
motion={motion}
|
|
@@ -332,6 +345,12 @@ export default function DetailsSlot() {
|
|
|
332
345
|
trailFalloffS={trailFalloffS}
|
|
333
346
|
setTrailFalloffS={setTrailFalloffS}
|
|
334
347
|
trailEdit={trailEdit}
|
|
348
|
+
trailTrackFocus={trailTrackFocus}
|
|
349
|
+
selectTrailTrack={selectTrailTrack}
|
|
350
|
+
clearTrailTrackFocus={clearTrailTrackFocus}
|
|
351
|
+
pendingIkEdit={pendingIkEdit}
|
|
352
|
+
applyPendingIkEdit={applyPendingIkEdit}
|
|
353
|
+
cancelPendingIkEdit={cancelPendingIkEdit}
|
|
335
354
|
generationBusy={generationBusy}
|
|
336
355
|
bridgeChecking={bridgeChecking}
|
|
337
356
|
bridge={bridge}
|
|
@@ -400,6 +419,7 @@ export default function DetailsSlot() {
|
|
|
400
419
|
objectColorDraft={objectColorDraft}
|
|
401
420
|
setObjectColorDraft={setObjectColorDraft}
|
|
402
421
|
/>
|
|
422
|
+
</>)}
|
|
403
423
|
</div>
|
|
404
424
|
{selectedSceneObject && (
|
|
405
425
|
<div className="inspector-footer">
|