cozyclay 1.10.0 → 2.0.2
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 +193 -0
- package/bin/agent/agent-routes.mjs +82 -72
- package/bin/agent/agent-runner.mjs +10 -3
- package/bin/agent/motion-runtime.mjs +32 -260
- package/bin/agent/providers.mjs +46 -3
- package/bin/agent/session-store.mjs +41 -5
- package/bin/agent/studio-prompt.mjs +1 -1
- package/bin/agent/studio-tools.mjs +48 -11
- package/bin/codex-auth.mjs +2 -1
- package/bin/cozyclay.mjs +38 -11
- package/bin/live/cli.mjs +22 -3
- package/bin/mcp-runtime.mjs +45 -20
- package/bin/telemetry-state.mjs +5 -2
- package/bin/update-check.mjs +15 -0
- package/dist/app/index.html +6 -6
- package/dist/assets/analytics-MVZOEW8O.js +1 -0
- package/dist/assets/app-BxBLC6GT.js +4876 -0
- package/dist/assets/app-CJdxvnrl.css +1 -0
- package/dist/assets/{demo-B-eg1CT8.js → demo-ByeYp-dY.js} +1 -1
- package/dist/assets/{first-shot-handoff-CGgnvNu8.js → first-shot-handoff-DgT28ZiV.js} +1 -1
- package/dist/assets/{landing-IbRp3FnQ.js → landing-BaXkpFip.js} +1 -1
- package/dist/assets/shot-prompt-CbqWcWYi.css +1 -0
- package/dist/assets/shot-prompt-hN0j_O-p.js +17 -0
- package/dist/assets/{ticket-BgIzOBOf.js → ticket-BcmsnHn5.js} +1 -1
- package/dist/assets/workflow-B5XZwVfe.js +179 -0
- package/dist/assets/workflow-DRdwoDBT.css +1 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/fonts/IBM-Plex-Sans-OFL.txt +92 -0
- package/dist/fonts/JetBrains-Mono-OFL.txt +93 -0
- package/dist/fonts/README.md +13 -9
- package/dist/fonts/ibm-plex-sans-400-latin.woff2 +0 -0
- package/dist/fonts/ibm-plex-sans-500-latin.woff2 +0 -0
- package/dist/fonts/ibm-plex-sans-600-latin.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-400-latin.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-500-latin.woff2 +0 -0
- package/dist/index.html +7 -7
- package/dist/privacy/index.html +13 -8
- package/dist/sitemap.xml +2 -2
- package/dist/workflow/index.html +5 -5
- package/mcp/live-hub.mjs +35 -93
- package/mcp/mesh-file.mjs +48 -0
- package/mcp/server.mjs +6 -77
- package/mcp/tool-handlers.mjs +219 -376
- package/package.json +2 -1
- package/src/App.jsx +1275 -9086
- package/src/analytics.js +393 -9
- package/src/app-context.js +211 -0
- package/src/app-stage.jsx +25 -24
- package/src/ardy/auto-fix-panel.css +108 -0
- package/src/ardy/fix-collisions.js +10 -66
- package/src/ardy/ik-drag.js +101 -0
- package/src/ardy/ik-key-json.js +43 -0
- package/src/ardy/ik.js +97 -67
- package/src/ardy/physics-panel.css +2 -0
- package/src/ardy/physics-panel.jsx +17 -5
- package/src/ardy/platform-fit-panel.css +64 -0
- package/src/ardy/platform-fit-panel.jsx +40 -0
- package/src/ardy/platform-fit.js +326 -0
- package/src/ardy/playback.js +166 -20
- package/src/ardy/range-pin.js +299 -0
- package/src/ardy/timeline-coordinates.js +26 -0
- package/src/ardy/timeline.css +1214 -0
- package/src/ardy/timeline.jsx +152 -41
- package/src/ardy/waypoints.js +3 -3
- package/src/asset-pane.css +462 -0
- package/src/asset-pane.jsx +461 -232
- package/src/camera-tutorial.jsx +38 -18
- package/src/command-bus.js +349 -0
- package/src/commands/ai.js +31 -0
- package/src/commands/cast.js +140 -0
- package/src/commands/elements/character.js +13 -0
- package/src/commands/elements/motion.js +14 -0
- package/src/commands/elements/object.js +22 -0
- package/src/commands/elements/scene.js +16 -0
- package/src/commands/elements/shot.js +9 -0
- package/src/commands/elements/stage.js +11 -0
- package/src/commands/elements.js +155 -0
- package/src/commands/export.js +20 -0
- package/src/commands/index.js +40 -0
- package/src/commands/motion.js +231 -0
- package/src/commands/objects.js +174 -0
- package/src/commands/project.js +53 -0
- package/src/commands/scene.js +81 -0
- package/src/commands/shared.js +19 -0
- package/src/commands/shot.js +103 -0
- package/src/commands/stage.js +17 -0
- package/src/commands/view.js +42 -0
- package/src/document-store.js +144 -0
- package/src/domains/cast.js +1007 -0
- package/src/domains/motion.js +3219 -0
- package/src/domains/objects.js +932 -0
- package/src/domains/scenes.js +823 -0
- package/src/domains/shots.js +404 -0
- package/src/domains/stage.js +93 -0
- package/src/dualview.jsx +113 -57
- package/src/facing-marks.js +3 -0
- package/src/first-success-guide.jsx +14 -14
- package/src/grid-view.js +18 -5
- package/src/hierarchy-panel.css +386 -0
- package/src/hierarchy-panel.jsx +88 -19
- package/src/{fal-motion-client.js → i2v-motion-client.js} +13 -13
- package/src/i2v-motion-studio.jsx +180 -0
- package/src/ik-camera.js +3 -0
- package/src/main.jsx +6 -0
- package/src/motion/generation.js +105 -0
- package/src/motion-readiness-ui.jsx +4 -4
- package/src/motion-readiness.js +11 -0
- package/src/motion-trail.js +366 -9
- package/src/object-gizmo.jsx +7 -1
- package/src/panels/CameraPanel.jsx +60 -0
- package/src/panels/CharacterTransformPanel.jsx +48 -0
- package/src/panels/EnvironmentPanel.jsx +40 -0
- package/src/panels/Foldout.jsx +32 -0
- package/src/panels/LightPanel.jsx +24 -0
- package/src/panels/ObjectTransformPanel.jsx +554 -0
- package/src/panels/PosePanel.jsx +97 -0
- package/src/panels/ProjectPanel.jsx +33 -0
- package/src/panels/PromptBlocksPanel.jsx +442 -0
- package/src/panels/PropsPanel.jsx +70 -0
- package/src/panels/ReferenceImageField.jsx +91 -0
- package/src/panels/RigControlPanel.jsx +78 -0
- package/src/panels/RigPanel.jsx +37 -0
- package/src/panels/SubjectBox.jsx +47 -0
- package/src/panels/SubjectsPanel.jsx +39 -0
- package/src/panels/VideoCapturePanel.jsx +180 -0
- package/src/panels/details.css +1171 -0
- package/src/panels/motion.css +197 -0
- package/src/panels/pose.css +39 -0
- package/src/planview.jsx +60 -31
- package/src/posestudio.jsx +59 -4
- package/src/project-browser.css +1073 -0
- package/src/project-browser.jsx +288 -123
- package/src/range-pin-object-transform.js +19 -0
- package/src/range-pin-panel.css +518 -0
- package/src/range-pin-panel.jsx +344 -0
- package/src/result-modal.jsx +5 -5
- package/src/room.jsx +79 -51
- package/src/scenes.js +9 -8
- package/src/semantic-edit.js +2 -0
- package/src/settings-menu.jsx +39 -105
- package/src/shell/BottomDock.jsx +450 -0
- package/src/shell/DetailsSlot.jsx +513 -0
- package/src/shell/LibrarySlot.jsx +86 -0
- package/src/shell/MenuBar.jsx +601 -0
- package/src/shell/OutlinerSlot.jsx +50 -0
- package/src/shell/PreferencesDialog.jsx +538 -0
- package/src/shell/PreferencesSlot.jsx +40 -0
- package/src/shell/StatusBar.jsx +125 -0
- package/src/shell/StudioShell.jsx +92 -0
- package/src/shell/TopBar.jsx +129 -0
- package/src/shell/ViewportToolbar.jsx +656 -0
- package/src/shell/agent-glass.css +296 -0
- package/src/shell/dock.css +93 -0
- package/src/shell/glass-regions.css +985 -0
- package/src/shell/glass.css +687 -0
- package/src/shell/log-store.js +52 -0
- package/src/shell/mode.css +9 -0
- package/src/shell/preferences.css +651 -0
- package/src/shell/shell.css +282 -0
- package/src/shell/studio-shell-context.js +19 -0
- package/src/shell/topbar.css +435 -0
- package/src/shell/viewport.css +451 -0
- package/src/store/authored-intent.js +22 -0
- package/src/store/runtime-adapters.js +92 -0
- package/src/store/scene-stage.js +21 -0
- package/src/store/use-document-store.js +14 -0
- package/src/studio-actions.js +210 -0
- package/src/studio-agent-commands.js +38 -268
- package/src/studio-agent-context.js +38 -9
- package/src/studio-agent-motion.js +122 -378
- package/src/studio-agent-protocol.js +66 -30
- package/src/studio-app-binding.js +379 -0
- package/src/studio-contact-sheet.js +73 -0
- package/src/studio-elements.js +58 -51
- package/src/styles/themes.css +200 -0
- package/src/styles/tokens.css +69 -0
- package/src/styles.css +456 -1748
- package/src/theme.js +44 -0
- package/src/timeline-extent.js +16 -0
- package/src/timeline-fps.js +13 -0
- package/src/trail-key-conflicts.js +39 -0
- package/src/trail-pick.js +59 -0
- package/src/ui.jsx +8 -8
- package/src/use-case-question.jsx +66 -0
- package/src/workflow/AgentPanel.jsx +86 -71
- package/src/workflow/agent-client.js +12 -2
- package/src/workflow/agent-panel.css +34 -32
- package/src/workflow/cozy-scene-node.css +2 -0
- package/src/workflow/workflow.css +2 -0
- package/tools/ardy/bridge.mjs +164 -146
- package/tools/ardy/visual-qa.mjs +5 -5
- package/tools/bench/EXP3.md +61 -0
- package/tools/bench/cclay_bench_extract_incam.py +125 -0
- package/tools/bench/cclay_bench_extract_obs.py +204 -0
- package/tools/bench/cclay_bench_runner.py +42 -0
- package/tools/bench/cube-contact.mjs +162 -0
- package/tools/bench/exp3.mjs +152 -0
- package/tools/bench/extract-bench-lib.mjs +216 -0
- package/tools/bench/extract-bench.mjs +302 -0
- package/tools/bench/fal-generate.mjs +88 -0
- package/tools/bench/fit/README.md +189 -0
- package/tools/bench/fit/camera.mjs +59 -0
- package/tools/bench/fit/contact.mjs +419 -0
- package/tools/bench/fit/footlock.mjs +234 -0
- package/tools/bench/fit/motion.mjs +85 -0
- package/tools/bench/fit/pin.mjs +27 -0
- package/tools/bench/fit/remote.mjs +105 -0
- package/tools/bench/fit-bench.mjs +126 -0
- package/tools/bench/fit-sanity.mjs +45 -0
- package/tools/bench/metrics.mjs +313 -0
- package/tools/bench/obs/depth.mjs +258 -0
- package/tools/bench/obs/extrinsics.mjs +175 -0
- package/tools/bench/obs/fit_mannequin_betas.py +340 -0
- package/tools/bench/obs/ground.mjs +294 -0
- package/tools/bench/obs/heading.mjs +151 -0
- package/tools/bench/obs/ladder.mjs +555 -0
- package/tools/bench/obs/mannequin-betas.json +124 -0
- package/tools/bench/obs/remote.mjs +83 -0
- package/tools/bench/obs/rest_joints.py +52 -0
- package/tools/bench/obs/ybot-targets.mjs +49 -0
- package/tools/bench/obs-bench.mjs +520 -0
- package/tools/bench/score.mjs +343 -0
- package/tools/bench/summarize.mjs +86 -0
- package/tools/dev/pages/privacy.html +13 -8
- package/tools/gt-render/browser.mjs +159 -0
- package/tools/gt-render/camera-math.mjs +276 -0
- package/tools/gt-render/page.mjs +267 -0
- package/tools/gt-render/render.mjs +514 -0
- package/tools/gt-render/scene-box.mjs +31 -0
- package/tools/gt-render/take-transform.mjs +87 -0
- package/tools/qa/css-rule-usage.mjs +283 -0
- package/tools/qa/studio-control-count.mjs +70 -32
- package/tools/run-tests.mjs +60 -3
- package/tools/track/DESIGN.md +513 -0
- package/tools/track/backfill-provenance.mjs +116 -0
- package/tools/track/budget.mjs +8 -0
- package/tools/track/check-rig.mjs +209 -0
- package/tools/track/diagnostics.schema.json +60 -0
- package/tools/track/export-rig.mjs +256 -0
- package/tools/track/fallback.mjs +7 -0
- package/tools/track/fk-parity-fixture.mjs +161 -0
- package/tools/track/gate.mjs +349 -0
- package/tools/track/masks.mjs +318 -0
- package/tools/track/metrics.mjs +201 -0
- package/tools/track/publish-obs.mjs +108 -0
- package/tools/track/py/check_env.py +61 -0
- package/tools/track/py/eval_lr.py +229 -0
- package/tools/track/py/lr_viterbi.py +294 -0
- package/tools/track/py/masks.py +405 -0
- package/tools/track/py/objective.py +382 -0
- package/tools/track/py/rig.py +508 -0
- package/tools/track/py/scene.py +326 -0
- package/tools/track/py/test_joint_indices.py +123 -0
- package/tools/track/py/test_lr_viterbi.py +195 -0
- package/tools/track/py/test_masks.py +229 -0
- package/tools/track/py/test_rig.py +288 -0
- package/tools/track/py/test_scene.py +354 -0
- package/tools/track/py/test_track.py +328 -0
- package/tools/track/py/track.py +411 -0
- package/tools/track/remote.mjs +140 -0
- package/tools/track/rig-dump.mjs +227 -0
- package/tools/track/run-box-tests.mjs +14 -0
- package/tools/track/run-box.mjs +168 -0
- package/tools/track/setup-box.sh +59 -0
- package/tools/track/study-2d.mjs +728 -0
- package/dist/assets/analytics-B1hnH66c.js +0 -1
- package/dist/assets/app-Cu1cexZR.css +0 -1
- package/dist/assets/app-Dxdh8kds.js +0 -4861
- package/dist/assets/shot-prompt-C_g2BHVc.css +0 -1
- package/dist/assets/shot-prompt-HVJRP4Gr.js +0 -17
- package/dist/assets/workflow-CJd-PDtV.js +0 -179
- package/dist/assets/workflow-DsudxKHF.css +0 -1
- package/src/fal-motion-studio.jsx +0 -180
- package/src/scene-history.js +0 -129
- package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
import { useEffect, useLayoutEffect, useState } from "react";
|
|
2
|
+
import { createPortal } from "react-dom";
|
|
3
|
+
import { useStudioShell } from "./studio-shell-context.js";
|
|
4
|
+
import { ko } from "../locale.js";
|
|
5
|
+
import { PRESETS, SHOT_ASPECT_PRESETS } from "../app-stage.jsx";
|
|
6
|
+
import { CAMERA_PRESETS } from "../camera-move.js";
|
|
7
|
+
import { PRIME_SET } from "../shot.js";
|
|
8
|
+
import { CatalogueEntries } from "../object-catalog.jsx";
|
|
9
|
+
import { CHARACTER_MODEL_IDS } from "../scenes.js";
|
|
10
|
+
import { elementByPath } from "../studio-elements.js";
|
|
11
|
+
import { saveAutoColor } from "../auto-color.js";
|
|
12
|
+
import { trackFeature } from "../analytics.js";
|
|
13
|
+
import "./viewport.css";
|
|
14
|
+
|
|
15
|
+
// G1: the single mode+tool pill. Keys 1-4 and W/E/R are bound in App's
|
|
16
|
+
// keydown handler; these buttons are the same doors for the pointer.
|
|
17
|
+
const MODES = [
|
|
18
|
+
{ id: "scene", key: "1", label: ko("Stage", "배치") },
|
|
19
|
+
{ id: "pose", key: "2", label: ko("Pose", "포즈") },
|
|
20
|
+
{ id: "camera", key: "3", label: ko("Camera", "카메라") },
|
|
21
|
+
{ id: "motion", key: "4", label: ko("Motion", "모션") },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
// #570 line icons (16px, 1.3 stroke) for the floating rails.
|
|
25
|
+
const ICON = {
|
|
26
|
+
scene: "M8 2 13.25 5v6L8 14 2.75 11V5z M2.75 5 8 8l5.25-3 M8 8v6",
|
|
27
|
+
pose: "M8 2.25a1.5 1.5 0 1 1 0 3 1.5 1.5 0 1 1 0-3z M5 7.25h6 M8 7.25v3.5 M6.25 13.75 8 10.75l1.75 3",
|
|
28
|
+
camera: "M2 5.75A1.5 1.5 0 0 1 3.5 4.25h6A1.5 1.5 0 0 1 11 5.75v4.5a1.5 1.5 0 0 1-1.5 1.5h-6A1.5 1.5 0 0 1 2 10.25z M11 7l3-1.5v5L11 9",
|
|
29
|
+
motion: "M1.75 9.5c1.5 0 2-4 3.25-4s1.75 5 3 5 1.75-5 3-5 1.75 4 3.25 4",
|
|
30
|
+
move: "M8 2v12 M2 8h12 M6.25 3.75 8 2l1.75 1.75 M6.25 12.25 8 14l1.75-1.75 M3.75 6.25 2 8l1.75 1.75 M12.25 6.25 14 8l-1.75 1.75",
|
|
31
|
+
rotate: "M13.25 8A5.25 5.25 0 1 1 11.7 4.3 M13.25 2.5v2.75H10.5",
|
|
32
|
+
scale: "M2.5 8.5v4a1 1 0 0 0 1 1h4 M8.5 2.5h5v5 M13.5 2.5 7.5 8.5",
|
|
33
|
+
ik: "M2.5 13.5 6.5 9l3 2.2 4-7.7 M6.5 9h.01 M9.5 11.2h.01",
|
|
34
|
+
trail: "M2 12.5c3 0 3-9 6-9s3 9 6 9",
|
|
35
|
+
pin: "M8 14.5v-4 M5 2h6 M6.2 2v3.6L4.5 10.5h7L9.8 5.6V2",
|
|
36
|
+
blocks: "M2 3.5h7.5v3.5H2z M6.5 9h7.5v3.5H6.5z",
|
|
37
|
+
refine: "M10.5 2.5l3 3L6 13H3v-3z",
|
|
38
|
+
plus: "M8 3.25v9.5 M3.25 8h9.5",
|
|
39
|
+
grid: "M2.5 5.75h11 M2.5 10.25h11 M5.75 2.5v11 M10.25 2.5v11",
|
|
40
|
+
angle: "M2.5 13.5h11 M2.5 13.5 11 5 M7 13.5a4.5 4.5 0 0 0-1.3-3.2",
|
|
41
|
+
view: "M2.75 5 8 2.5 13.25 5v6L8 13.5 2.75 11z M8 8v5.5 M2.75 5 8 8l5.25-3",
|
|
42
|
+
shading: "M8 2.5a5.5 5.5 0 1 1 0 11 5.5 5.5 0 1 1 0-11z M4.5 5.5a4 4 0 0 1 3-1.5",
|
|
43
|
+
show: "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",
|
|
44
|
+
speed: "M2.5 11a5.5 5.5 0 1 1 11 0 M8 11l2.5-3",
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
function RailIcon({ name, size = 16 }) {
|
|
48
|
+
return (
|
|
49
|
+
<svg className="vp-icon" width={size} height={size} viewBox="0 0 16 16" aria-hidden="true">
|
|
50
|
+
<path d={ICON[name]} />
|
|
51
|
+
</svg>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// The gizmo's snap increments (TRANSLATE_SNAP / ROTATE_SNAP in
|
|
56
|
+
// src/scene-objects.js). They are fixed, so the value group names them.
|
|
57
|
+
const SNAP_MOVE = "5cm";
|
|
58
|
+
const SNAP_TURN = "5°";
|
|
59
|
+
|
|
60
|
+
// Editor lens range: shotsDomain.changeLens and the FOV slider take 14-90°.
|
|
61
|
+
const FOV_MIN = 14;
|
|
62
|
+
const FOV_MAX = 90;
|
|
63
|
+
|
|
64
|
+
// "+ Add › Character" stands the new body one metre along X per existing
|
|
65
|
+
// character, inside the same stage clamp the Content drag applies.
|
|
66
|
+
const CHARACTER_BOUNDS = elementByPath("character.position").gizmo;
|
|
67
|
+
const characterSpawnX = (count) => Math.min(CHARACTER_BOUNDS.max.x, Math.max(CHARACTER_BOUNDS.min.x, count));
|
|
68
|
+
|
|
69
|
+
const trimNumber = (value, digits = 2) => String(Number(value.toFixed(digits)));
|
|
70
|
+
|
|
71
|
+
/** One open overlay menu at a time; outside pointerdown or Escape closes it. */
|
|
72
|
+
function useOverlayMenu() {
|
|
73
|
+
const [open, setOpen] = useState(null);
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (!open) return undefined;
|
|
76
|
+
const onPointerDown = (event) => {
|
|
77
|
+
if (event.target instanceof Element && event.target.closest(`[data-vp-menu="${open}"]`)) return;
|
|
78
|
+
setOpen(null);
|
|
79
|
+
};
|
|
80
|
+
const onKeyDown = (event) => {
|
|
81
|
+
if (event.key !== "Escape") return;
|
|
82
|
+
document.querySelector(`[data-vp-menu="${open}"] > button`)?.focus();
|
|
83
|
+
setOpen(null);
|
|
84
|
+
};
|
|
85
|
+
document.addEventListener("pointerdown", onPointerDown);
|
|
86
|
+
window.addEventListener("keydown", onKeyDown);
|
|
87
|
+
return () => {
|
|
88
|
+
document.removeEventListener("pointerdown", onPointerDown);
|
|
89
|
+
window.removeEventListener("keydown", onKeyDown);
|
|
90
|
+
};
|
|
91
|
+
}, [open]);
|
|
92
|
+
return {
|
|
93
|
+
open,
|
|
94
|
+
close: () => setOpen(null),
|
|
95
|
+
toggle: (id) => setOpen((current) => (current === id ? null : id)),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function MenuItem({ checked, role = "menuitemradio", className = "", children, ...props }) {
|
|
100
|
+
return (
|
|
101
|
+
<button type="button" role={role} aria-checked={checked} className={"vp-menu-item " + className} {...props}>
|
|
102
|
+
<span className="vp-menu-mark" aria-hidden="true">{checked ? "✓" : ""}</span>
|
|
103
|
+
{children}
|
|
104
|
+
</button>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The inset card's header (violet dot, shot name, lens) lives in App's
|
|
109
|
+
* `.vp-shot-preview-tag`; this region only fills in the words it shows. */
|
|
110
|
+
function ShotPreviewHeader({ name, lens }) {
|
|
111
|
+
const [host, setHost] = useState(null);
|
|
112
|
+
useLayoutEffect(() => {
|
|
113
|
+
setHost(document.querySelector(".vp-shot-preview .vp-shot-preview-tag"));
|
|
114
|
+
}, []);
|
|
115
|
+
if (!host) return null;
|
|
116
|
+
return createPortal(
|
|
117
|
+
<span className="vp-shot-preview-meta" data-testid="shot-preview-header">
|
|
118
|
+
<span className="vp-shot-preview-name">{name}</span>
|
|
119
|
+
<span className="vp-shot-preview-lens">{lens}</span>
|
|
120
|
+
</span>,
|
|
121
|
+
host,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export default function ViewportToolbar() {
|
|
126
|
+
const {
|
|
127
|
+
workflowMode, selectWorkflowMode, poseRefusal, gizmoMode, setGizmoMode, snapEnabled,
|
|
128
|
+
setSnapEnabled, preset, applyPreset, cameraPresetId, i2vMotionCameraLocked,
|
|
129
|
+
runStudioAction, shotAspectKey, fovDeg, shotsDomain, shot, shots, activeShot,
|
|
130
|
+
setNonce, workspaceLayout, viewMenuTriggerRef, viewMenuOpen,
|
|
131
|
+
setViewMenuOpen, viewLooksActive, gridView, setGridView,
|
|
132
|
+
autoColor, setAutoColor, isCharacterSelection, partColoursChoice, embedMode,
|
|
133
|
+
agentCollapsed, addSceneObject, spawnCharacter, characters, flySpeed,
|
|
134
|
+
ikEditTool, setIkEditTool, showTrails, setShowTrails, motion, setRangePinPartPick,
|
|
135
|
+
trailFalloffS, setTrailFalloffS, lineEditMode, exitLineEditMode, enterRefineMode,
|
|
136
|
+
refineDisabledReason,
|
|
137
|
+
} = useStudioShell();
|
|
138
|
+
const menu = useOverlayMenu();
|
|
139
|
+
|
|
140
|
+
const setAutoColorOn = (next) => {
|
|
141
|
+
if (next === autoColor) return;
|
|
142
|
+
saveAutoColor(next);
|
|
143
|
+
trackFeature("auto_color");
|
|
144
|
+
setAutoColor(next);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const refineReason = lineEditMode ? null : refineDisabledReason();
|
|
148
|
+
const tools = {
|
|
149
|
+
scene: [
|
|
150
|
+
{ key: "W", id: "move", name: ko("Move", "이동"), active: gizmoMode === "move", pick: () => setGizmoMode("move") },
|
|
151
|
+
{ key: "E", id: "rotate", name: ko("Rotate", "회전"), active: gizmoMode === "rotate", pick: () => setGizmoMode("rotate") },
|
|
152
|
+
{ key: "R", id: "scale", name: ko("Scale", "크기"), active: gizmoMode === "scale", pick: () => setGizmoMode("scale") },
|
|
153
|
+
],
|
|
154
|
+
pose: [
|
|
155
|
+
{ key: "W", id: "ik", name: ko("Pose fix (IK parts)", "포즈 수정 (IK 파츠)"), active: ikEditTool === "ik", pick: () => setIkEditTool("ik") },
|
|
156
|
+
{
|
|
157
|
+
key: "E", id: "trail", name: ko("Path fix (motion trail)", "경로 수정 (궤적선)"), active: ikEditTool === "trail",
|
|
158
|
+
refused: showTrails ? null : ko("Turn Trails on in Show to edit the motion path", "경로를 수정하려면 표시에서 궤적선을 켜세요"),
|
|
159
|
+
pick: () => setIkEditTool("trail"),
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
key: "R", id: "pin", name: ko("Pin (range pin)", "고정 (범위 고정)"), active: ikEditTool === "pin",
|
|
163
|
+
refused: motion ? null : ko("Pinning needs a motion take", "고정하려면 모션 테이크가 필요해요"),
|
|
164
|
+
pick: () => { setIkEditTool("pin"); setRangePinPartPick(null); },
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
camera: [
|
|
168
|
+
{ key: "W", id: "move", name: ko("Move camera", "카메라 이동"), active: gizmoMode === "move", pick: () => setGizmoMode("move") },
|
|
169
|
+
{ key: "E", id: "rotate", name: ko("Rotate camera", "카메라 회전"), active: gizmoMode === "rotate", pick: () => setGizmoMode("rotate") },
|
|
170
|
+
],
|
|
171
|
+
motion: [
|
|
172
|
+
{
|
|
173
|
+
key: "W", id: "blocks", name: ko("Blocks (select and move prompt blocks)", "블록 (프롬프트 블록 선택·이동)"), active: !lineEditMode,
|
|
174
|
+
pick: () => { if (lineEditMode) exitLineEditMode(); },
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
key: "E", id: "refine", name: ko("Refine (edit the path line)", "다듬기 (경로선 편집)"), active: lineEditMode,
|
|
178
|
+
refused: refineReason, pick: () => { if (!lineEditMode) enterRefineMode(); },
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
}[workflowMode] ?? [];
|
|
182
|
+
|
|
183
|
+
// Lens choices are the real primes that fit the editor's FOV range, turned
|
|
184
|
+
// into a vertical FOV on the current filmback (the shot.js lens relation).
|
|
185
|
+
const lensChoices = PRIME_SET
|
|
186
|
+
.map((mm) => ({ mm, fov: (2 * Math.atan(shot.usedSensorHeightMm / (2 * mm)) * 180) / Math.PI }))
|
|
187
|
+
.filter(({ fov }) => fov >= FOV_MIN && fov <= FOV_MAX);
|
|
188
|
+
const aspectLabel = SHOT_ASPECT_PRESETS[shotAspectKey]?.label ?? shotAspectKey;
|
|
189
|
+
|
|
190
|
+
const valueGroup = workflowMode === "scene" ? (
|
|
191
|
+
<button
|
|
192
|
+
type="button"
|
|
193
|
+
className={"vp-value vp-snap-value" + (snapEnabled ? " on" : "")}
|
|
194
|
+
data-testid="snap-toggle"
|
|
195
|
+
aria-pressed={snapEnabled}
|
|
196
|
+
title={ko(
|
|
197
|
+
`Grid snapping ${snapEnabled ? "on" : "off"} (${SNAP_MOVE} · ${SNAP_TURN}) — click to toggle, hold Ctrl during a drag to invert`,
|
|
198
|
+
`그리드 스냅 ${snapEnabled ? "켜짐" : "꺼짐"} (${SNAP_MOVE} · ${SNAP_TURN}) — 클릭해 전환, 드래그 중 Ctrl을 누르면 반대로 작동`,
|
|
199
|
+
)}
|
|
200
|
+
onClick={() => setSnapEnabled((value) => !value)}
|
|
201
|
+
>
|
|
202
|
+
<span className="vp-value-part"><RailIcon name="grid" size={14} />{SNAP_MOVE}</span>
|
|
203
|
+
<span className="vp-value-part"><RailIcon name="angle" size={14} />{SNAP_TURN}</span>
|
|
204
|
+
</button>
|
|
205
|
+
) : workflowMode === "pose" ? (
|
|
206
|
+
<div className="vp-menu-wrap" data-vp-menu="influence">
|
|
207
|
+
<button
|
|
208
|
+
type="button"
|
|
209
|
+
className="vp-value"
|
|
210
|
+
aria-haspopup="dialog"
|
|
211
|
+
aria-expanded={menu.open === "influence"}
|
|
212
|
+
title={ko("Influence range — how far an edit blends into nearby frames", "영향 범위 — 수정이 주변 프레임에 섞이는 길이")}
|
|
213
|
+
onClick={() => menu.toggle("influence")}
|
|
214
|
+
>
|
|
215
|
+
{trimNumber(trailFalloffS)}s
|
|
216
|
+
</button>
|
|
217
|
+
{menu.open === "influence" && (
|
|
218
|
+
<div className="vp-menu vp-slider-popover" role="dialog" aria-label={ko("Influence range", "영향 범위")}>
|
|
219
|
+
<input
|
|
220
|
+
type="range"
|
|
221
|
+
min={0.1}
|
|
222
|
+
max={2}
|
|
223
|
+
step={0.1}
|
|
224
|
+
value={trailFalloffS}
|
|
225
|
+
aria-label={ko("Influence range", "영향 범위")}
|
|
226
|
+
onChange={(event) => setTrailFalloffS(Number(event.target.value))}
|
|
227
|
+
/>
|
|
228
|
+
<output>{trimNumber(trailFalloffS)}s</output>
|
|
229
|
+
</div>
|
|
230
|
+
)}
|
|
231
|
+
</div>
|
|
232
|
+
) : workflowMode === "camera" ? (
|
|
233
|
+
<>
|
|
234
|
+
<div className="vp-menu-wrap" data-vp-menu="lens">
|
|
235
|
+
<button
|
|
236
|
+
type="button"
|
|
237
|
+
className="vp-value"
|
|
238
|
+
data-testid="lens-value"
|
|
239
|
+
aria-haspopup="menu"
|
|
240
|
+
aria-expanded={menu.open === "lens"}
|
|
241
|
+
title={ko("Lens — pick a prime or set the field of view", "렌즈 — 단렌즈를 고르거나 화각을 조절")}
|
|
242
|
+
onClick={() => menu.toggle("lens")}
|
|
243
|
+
>
|
|
244
|
+
{shot.focalMm}mm
|
|
245
|
+
</button>
|
|
246
|
+
{menu.open === "lens" && (
|
|
247
|
+
<div className="vp-menu" role="menu" aria-label={ko("Lens", "렌즈")}>
|
|
248
|
+
{lensChoices.map(({ mm, fov }) => (
|
|
249
|
+
<MenuItem
|
|
250
|
+
key={mm}
|
|
251
|
+
data-lens={mm}
|
|
252
|
+
checked={shot.focalMm === mm}
|
|
253
|
+
disabled={i2vMotionCameraLocked}
|
|
254
|
+
onClick={() => { shotsDomain.changeLens(fov); menu.close(); }}
|
|
255
|
+
>
|
|
256
|
+
{mm}mm
|
|
257
|
+
</MenuItem>
|
|
258
|
+
))}
|
|
259
|
+
<label className="viewport-fov-control">
|
|
260
|
+
<span>FOV</span>
|
|
261
|
+
<input
|
|
262
|
+
type="range"
|
|
263
|
+
min={FOV_MIN}
|
|
264
|
+
max={FOV_MAX}
|
|
265
|
+
step="1"
|
|
266
|
+
value={fovDeg}
|
|
267
|
+
disabled={i2vMotionCameraLocked}
|
|
268
|
+
onChange={(event) => shotsDomain.changeLens(Number(event.target.value))}
|
|
269
|
+
/>
|
|
270
|
+
<output>{Math.round(fovDeg)}°</output>
|
|
271
|
+
</label>
|
|
272
|
+
</div>
|
|
273
|
+
)}
|
|
274
|
+
</div>
|
|
275
|
+
<div className="vp-menu-wrap" data-vp-menu="ratio">
|
|
276
|
+
<button
|
|
277
|
+
type="button"
|
|
278
|
+
className="vp-value"
|
|
279
|
+
data-testid="ratio-value"
|
|
280
|
+
aria-haspopup="menu"
|
|
281
|
+
aria-expanded={menu.open === "ratio"}
|
|
282
|
+
title={ko("Output aspect ratio", "출력 화면 비율")}
|
|
283
|
+
onClick={() => menu.toggle("ratio")}
|
|
284
|
+
>
|
|
285
|
+
{aspectLabel.replace(/:1$/, "")}
|
|
286
|
+
</button>
|
|
287
|
+
{menu.open === "ratio" && (
|
|
288
|
+
<div className="vp-menu" role="menu" aria-label={ko("Output aspect ratio", "출력 화면 비율")}>
|
|
289
|
+
{Object.entries(SHOT_ASPECT_PRESETS).map(([key, value]) => (
|
|
290
|
+
<MenuItem
|
|
291
|
+
key={key}
|
|
292
|
+
data-aspect={key}
|
|
293
|
+
checked={key === shotAspectKey}
|
|
294
|
+
title={value.title}
|
|
295
|
+
onClick={() => { runStudioAction("stage.setFilmback", { shotAspect: key }); menu.close(); }}
|
|
296
|
+
>
|
|
297
|
+
{value.label}
|
|
298
|
+
</MenuItem>
|
|
299
|
+
))}
|
|
300
|
+
</div>
|
|
301
|
+
)}
|
|
302
|
+
</div>
|
|
303
|
+
</>
|
|
304
|
+
) : null;
|
|
305
|
+
|
|
306
|
+
return (
|
|
307
|
+
<div className="viewport-titlebar" data-testid="viewport-overlays">
|
|
308
|
+
<div className="vp-overlay-left">
|
|
309
|
+
<div className="vp-menu-wrap" data-vp-menu="add">
|
|
310
|
+
<button
|
|
311
|
+
type="button"
|
|
312
|
+
className="vp-pill vp-add-trigger add-object-trigger"
|
|
313
|
+
data-testid="viewport-add"
|
|
314
|
+
aria-haspopup="menu"
|
|
315
|
+
aria-expanded={menu.open === "add"}
|
|
316
|
+
title={ko("Add an object, a character or a camera", "오브젝트, 캐릭터, 카메라 추가")}
|
|
317
|
+
onClick={() => menu.toggle("add")}
|
|
318
|
+
>
|
|
319
|
+
<span className="vp-add-plus" aria-hidden="true"><RailIcon name="plus" /></span>
|
|
320
|
+
<span className="vp-label">{ko("Add", "추가")}</span>
|
|
321
|
+
</button>
|
|
322
|
+
{menu.open === "add" && (
|
|
323
|
+
<div className="vp-menu add-object-menu" role="menu" aria-label={ko("Add", "추가")}>
|
|
324
|
+
<CatalogueEntries onPick={(kind) => { addSceneObject(kind); menu.close(); }} />
|
|
325
|
+
<div className="add-object-group">
|
|
326
|
+
<span className="add-object-heading">{ko("Scene", "장면")}</span>
|
|
327
|
+
<button
|
|
328
|
+
type="button"
|
|
329
|
+
role="menuitem"
|
|
330
|
+
className="add-object-item"
|
|
331
|
+
data-add="character"
|
|
332
|
+
onClick={() => { spawnCharacter(CHARACTER_MODEL_IDS[0], characterSpawnX(characters.length), 0); menu.close(); }}
|
|
333
|
+
>
|
|
334
|
+
<span className="add-object-swatch vp-add-swatch-cast" aria-hidden="true" />
|
|
335
|
+
<span>{ko("Character", "캐릭터")}</span>
|
|
336
|
+
</button>
|
|
337
|
+
<button
|
|
338
|
+
type="button"
|
|
339
|
+
role="menuitem"
|
|
340
|
+
className="add-object-item"
|
|
341
|
+
data-add="camera"
|
|
342
|
+
onClick={() => { runStudioAction("shot.create"); menu.close(); }}
|
|
343
|
+
>
|
|
344
|
+
<span className="add-object-swatch vp-add-swatch-camera" aria-hidden="true" />
|
|
345
|
+
<span>{ko("Camera", "카메라")}</span>
|
|
346
|
+
</button>
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
)}
|
|
350
|
+
</div>
|
|
351
|
+
|
|
352
|
+
{tools.length > 0 && <span className="vp-toolbar-divider" aria-hidden="true" />}
|
|
353
|
+
<div className="vp-tool-keys" role="group" aria-label={ko("Tools", "도구")} data-transform-controls>
|
|
354
|
+
{tools.map((tool) => (
|
|
355
|
+
<button
|
|
356
|
+
type="button"
|
|
357
|
+
key={tool.key}
|
|
358
|
+
data-tool-key={tool.key}
|
|
359
|
+
data-tool={tool.id}
|
|
360
|
+
className={"vp-tool-key" + (tool.active ? " active" : "")}
|
|
361
|
+
aria-pressed={tool.active}
|
|
362
|
+
aria-disabled={tool.refused ? true : undefined}
|
|
363
|
+
aria-label={tool.name}
|
|
364
|
+
title={tool.refused || `${tool.name} (${tool.key})`}
|
|
365
|
+
onClick={() => { if (!tool.refused) tool.pick(); }}
|
|
366
|
+
>
|
|
367
|
+
<RailIcon name={tool.id} />
|
|
368
|
+
<span className="vp-tool-letter">{tool.key}</span>
|
|
369
|
+
</button>
|
|
370
|
+
))}
|
|
371
|
+
</div>
|
|
372
|
+
{valueGroup && (
|
|
373
|
+
<>
|
|
374
|
+
<span className="vp-toolbar-divider" aria-hidden="true" />
|
|
375
|
+
<div className="vp-values">{valueGroup}</div>
|
|
376
|
+
</>
|
|
377
|
+
)}
|
|
378
|
+
</div>
|
|
379
|
+
|
|
380
|
+
<div className="vp-mode-toolbar" role="toolbar" data-testid="mode-toolbar" aria-label={ko("Mode and tools", "모드와 도구")}>
|
|
381
|
+
<div className="vp-mode-keys" role="tablist" aria-label={ko("Workflow", "작업 모드")}>
|
|
382
|
+
{MODES.map((mode) => {
|
|
383
|
+
const active = workflowMode === mode.id;
|
|
384
|
+
const refused = mode.id === "pose" ? poseRefusal : null;
|
|
385
|
+
return (
|
|
386
|
+
<button
|
|
387
|
+
type="button"
|
|
388
|
+
role="tab"
|
|
389
|
+
key={mode.id}
|
|
390
|
+
data-mode-key={mode.key}
|
|
391
|
+
data-mode={mode.id}
|
|
392
|
+
className={"vp-mode-key" + (active ? " active" : "")}
|
|
393
|
+
aria-selected={active}
|
|
394
|
+
aria-disabled={refused ? true : undefined}
|
|
395
|
+
aria-label={mode.label}
|
|
396
|
+
title={refused || `${mode.label} (${mode.key})`}
|
|
397
|
+
onClick={() => selectWorkflowMode(mode.id)}
|
|
398
|
+
>
|
|
399
|
+
<RailIcon name={mode.id} />
|
|
400
|
+
<span className="vp-key-digit">{mode.key}</span>
|
|
401
|
+
<span className="vp-mode-name">{mode.label}</span>
|
|
402
|
+
</button>
|
|
403
|
+
);
|
|
404
|
+
})}
|
|
405
|
+
</div>
|
|
406
|
+
</div>
|
|
407
|
+
|
|
408
|
+
<div className="vp-overlay-right">
|
|
409
|
+
<div className="vp-pill vp-view-pill">
|
|
410
|
+
<div className="vp-menu-wrap" data-vp-menu="camera-view">
|
|
411
|
+
<button
|
|
412
|
+
type="button"
|
|
413
|
+
className="vp-pill-segment"
|
|
414
|
+
data-testid="view-camera-trigger"
|
|
415
|
+
aria-haspopup="menu"
|
|
416
|
+
aria-expanded={menu.open === "camera-view"}
|
|
417
|
+
title={ko("Views and shot cameras", "보기와 샷 카메라")}
|
|
418
|
+
onClick={() => menu.toggle("camera-view")}
|
|
419
|
+
>
|
|
420
|
+
<RailIcon name="view" />
|
|
421
|
+
<span className="vp-label">{ko("Perspective", "원근")}</span>
|
|
422
|
+
</button>
|
|
423
|
+
{menu.open === "camera-view" && (
|
|
424
|
+
<div className="vp-menu" role="menu" aria-label={ko("Views and shot cameras", "보기와 샷 카메라")}>
|
|
425
|
+
<span className="vp-menu-label">{ko("View", "보기")}</span>
|
|
426
|
+
<MenuItem checked onClick={menu.close}>{ko("Perspective", "원근")}</MenuItem>
|
|
427
|
+
<MenuItem
|
|
428
|
+
role="menuitemcheckbox"
|
|
429
|
+
checked={!workspaceLayout.insetCollapsed}
|
|
430
|
+
title={ko("Show the Top-View inset", "탑뷰 인셋 표시")}
|
|
431
|
+
onClick={() => runStudioAction("view.setInset", { collapsed: !workspaceLayout.insetCollapsed })}
|
|
432
|
+
>
|
|
433
|
+
{ko("Top", "탑")}
|
|
434
|
+
</MenuItem>
|
|
435
|
+
{shots.length > 0 && <span className="vp-menu-label">{ko("Shot cameras", "샷 카메라")}</span>}
|
|
436
|
+
{shots.map((entry) => (
|
|
437
|
+
<MenuItem
|
|
438
|
+
key={entry.id}
|
|
439
|
+
checked={activeShot?.id === entry.id}
|
|
440
|
+
title={ko("Show this shot's camera in the preview", "이 샷의 카메라를 미리보기에 표시")}
|
|
441
|
+
onClick={() => { runStudioAction("view.select", { shotId: entry.id }); menu.close(); }}
|
|
442
|
+
>
|
|
443
|
+
{entry.name}
|
|
444
|
+
</MenuItem>
|
|
445
|
+
))}
|
|
446
|
+
<span className="vp-menu-label">{ko("Shot framing", "샷 구도")}</span>
|
|
447
|
+
<label className="viewport-toolbar-field shot-field">
|
|
448
|
+
<span>{ko("Shot", "샷")}</span>
|
|
449
|
+
<select
|
|
450
|
+
aria-label={ko("Shot preset", "샷 프리셋")}
|
|
451
|
+
value={preset}
|
|
452
|
+
onChange={(event) => applyPreset(event.target.value)}
|
|
453
|
+
>
|
|
454
|
+
{Object.entries(PRESETS).map(([key, value]) => (
|
|
455
|
+
<option key={key} value={key}>{value.label}</option>
|
|
456
|
+
))}
|
|
457
|
+
</select>
|
|
458
|
+
</label>
|
|
459
|
+
<label className="viewport-toolbar-field ratio-field">
|
|
460
|
+
<span>{ko("Cam", "카메라")}</span>
|
|
461
|
+
<select
|
|
462
|
+
aria-label={ko("Camera preset", "카메라 프리셋")}
|
|
463
|
+
value={cameraPresetId ?? ""}
|
|
464
|
+
disabled={i2vMotionCameraLocked}
|
|
465
|
+
onChange={(event) => {
|
|
466
|
+
const id = event.target.value;
|
|
467
|
+
if (!id) { runStudioAction("stage.setFilmback", { cameraPresetId: null }); return; }
|
|
468
|
+
runStudioAction("shot.frame", { preset: id });
|
|
469
|
+
}}
|
|
470
|
+
>
|
|
471
|
+
<option value="">{ko("Free", "자유")}</option>
|
|
472
|
+
{Object.values(CAMERA_PRESETS).map((value) => (
|
|
473
|
+
<option key={value.id} value={value.id}>{value.label}</option>
|
|
474
|
+
))}
|
|
475
|
+
</select>
|
|
476
|
+
</label>
|
|
477
|
+
<button
|
|
478
|
+
type="button"
|
|
479
|
+
role="menuitem"
|
|
480
|
+
className="vp-menu-item"
|
|
481
|
+
title={ko("Recenter on subject", "피사체 다시 맞추기")}
|
|
482
|
+
aria-label={ko("Recenter on subject", "피사체 다시 맞추기")}
|
|
483
|
+
onClick={() => { setNonce((n) => n + 1); menu.close(); }}
|
|
484
|
+
>
|
|
485
|
+
<span className="vp-menu-mark" aria-hidden="true">◎</span>
|
|
486
|
+
{ko("Recenter on subject", "피사체 다시 맞추기")}
|
|
487
|
+
</button>
|
|
488
|
+
</div>
|
|
489
|
+
)}
|
|
490
|
+
</div>
|
|
491
|
+
<span className="vp-pill-divider" aria-hidden="true" />
|
|
492
|
+
<div className="vp-menu-wrap" data-vp-menu="shading">
|
|
493
|
+
<button
|
|
494
|
+
type="button"
|
|
495
|
+
className="vp-pill-segment"
|
|
496
|
+
data-testid="shading-trigger"
|
|
497
|
+
aria-haspopup="menu"
|
|
498
|
+
aria-expanded={menu.open === "shading"}
|
|
499
|
+
title={ko("Shading", "셰이딩")}
|
|
500
|
+
onClick={() => menu.toggle("shading")}
|
|
501
|
+
>
|
|
502
|
+
<RailIcon name="shading" />
|
|
503
|
+
<span className="vp-label">{autoColor ? ko("Auto Color", "자동 색") : ko("Clay Lit", "클레이 조명")}</span>
|
|
504
|
+
</button>
|
|
505
|
+
{menu.open === "shading" && (
|
|
506
|
+
<div className="vp-menu" role="menu" aria-label={ko("Shading", "셰이딩")}>
|
|
507
|
+
<MenuItem checked={!autoColor} onClick={() => { setAutoColorOn(false); menu.close(); }}>
|
|
508
|
+
{ko("Clay Lit", "클레이 조명")}
|
|
509
|
+
</MenuItem>
|
|
510
|
+
<MenuItem
|
|
511
|
+
checked={autoColor}
|
|
512
|
+
title={ko("Flat, distinct colour per object — captures include them while on", "오브젝트별 평면 구분 색 — 켜둔 동안 캡처에도 포함됩니다")}
|
|
513
|
+
onClick={() => { setAutoColorOn(true); menu.close(); }}
|
|
514
|
+
>
|
|
515
|
+
{ko("Auto Color", "자동 색")}
|
|
516
|
+
</MenuItem>
|
|
517
|
+
</div>
|
|
518
|
+
)}
|
|
519
|
+
</div>
|
|
520
|
+
<span className="vp-pill-divider" aria-hidden="true" />
|
|
521
|
+
{/* G8 "Show" = the View menu: every what-is-on-screen toggle, in
|
|
522
|
+
every mode. Items keep the menu open: these are toggles you
|
|
523
|
+
compare, not commands you fire. */}
|
|
524
|
+
<div className="view-menu-wrap">
|
|
525
|
+
<button
|
|
526
|
+
type="button"
|
|
527
|
+
className="vp-pill-segment view-menu-trigger"
|
|
528
|
+
data-testid="view-menu-trigger"
|
|
529
|
+
ref={viewMenuTriggerRef}
|
|
530
|
+
aria-haspopup="menu"
|
|
531
|
+
aria-expanded={viewMenuOpen}
|
|
532
|
+
title={ko("Viewport display toggles", "뷰포트 표시 토글")}
|
|
533
|
+
onClick={() => setViewMenuOpen((open) => !open)}
|
|
534
|
+
>
|
|
535
|
+
<RailIcon name="show" />
|
|
536
|
+
<span className="vp-label">{ko("Show", "표시")}</span>
|
|
537
|
+
{viewLooksActive && <span className="view-menu-dot" data-testid="view-menu-dot" aria-hidden="true" />}
|
|
538
|
+
</button>
|
|
539
|
+
{viewMenuOpen && (
|
|
540
|
+
<div
|
|
541
|
+
className="vp-menu view-menu"
|
|
542
|
+
role="menu"
|
|
543
|
+
aria-label={ko("Viewport display", "뷰포트 표시")}
|
|
544
|
+
>
|
|
545
|
+
{/* aria-pressed rides along with aria-checked: the toggles
|
|
546
|
+
published that state contract in their old homes and QA
|
|
547
|
+
still reads it, so the move keeps the signpost (R9). */}
|
|
548
|
+
<button
|
|
549
|
+
type="button"
|
|
550
|
+
role="menuitemcheckbox"
|
|
551
|
+
className={"view-menu-item grid-view-switch" + (gridView ? " active" : "")}
|
|
552
|
+
aria-checked={gridView}
|
|
553
|
+
aria-pressed={gridView}
|
|
554
|
+
title={ko("Blender-style viewport — dark void with a reference grid instead of the deck", "Blender식 뷰포트 — 데크 대신 어두운 배경과 기준 그리드")}
|
|
555
|
+
onClick={() => setGridView((v) => !v)}
|
|
556
|
+
>
|
|
557
|
+
<span className="view-menu-mark" aria-hidden="true">{gridView ? "✓" : ""}</span>
|
|
558
|
+
{ko("Reference grid", "기준 그리드")}
|
|
559
|
+
</button>
|
|
560
|
+
<button
|
|
561
|
+
type="button"
|
|
562
|
+
role="menuitemcheckbox"
|
|
563
|
+
className={"view-menu-item auto-color-toggle" + (autoColor ? " active" : "")}
|
|
564
|
+
aria-checked={autoColor}
|
|
565
|
+
aria-pressed={autoColor}
|
|
566
|
+
title={ko(
|
|
567
|
+
"Distinct display colors per object — captures include them while on",
|
|
568
|
+
"오브젝트별 구분 색 — 켜둔 동안 캡처에도 포함됩니다",
|
|
569
|
+
)}
|
|
570
|
+
onClick={() => setAutoColorOn(!autoColor)}
|
|
571
|
+
>
|
|
572
|
+
<span className="view-menu-mark" aria-hidden="true">{autoColor ? "✓" : ""}</span>
|
|
573
|
+
{ko("Auto Color", "자동 색")}
|
|
574
|
+
</button>
|
|
575
|
+
<button
|
|
576
|
+
type="button"
|
|
577
|
+
role="menuitemcheckbox"
|
|
578
|
+
className={"view-menu-item trails-toggle" + (showTrails ? " active" : "")}
|
|
579
|
+
aria-checked={showTrails}
|
|
580
|
+
aria-pressed={showTrails}
|
|
581
|
+
title={ko("Motion trails while posing", "포즈 편집 중 궤적선")}
|
|
582
|
+
onClick={() => {
|
|
583
|
+
if (showTrails && ikEditTool === "trail") setIkEditTool("ik");
|
|
584
|
+
setShowTrails(!showTrails);
|
|
585
|
+
}}
|
|
586
|
+
>
|
|
587
|
+
<span className="view-menu-mark" aria-hidden="true">{showTrails ? "✓" : ""}</span>
|
|
588
|
+
{ko("Trails", "궤적선")}
|
|
589
|
+
</button>
|
|
590
|
+
{/* Part colours repaint a BODY, so the section only exists
|
|
591
|
+
while a character is selected (R2). */}
|
|
592
|
+
{isCharacterSelection && (
|
|
593
|
+
<div className="view-menu-group" role="group" aria-label={ko("Body part colours", "부위 색상")}>
|
|
594
|
+
<span className="view-menu-label" aria-hidden="true">{ko("Body part colours", "부위 색상")}</span>
|
|
595
|
+
{[
|
|
596
|
+
{ value: "off", label: ko("Off", "끕") },
|
|
597
|
+
{ value: "shaded", label: ko("Shaded", "음영") },
|
|
598
|
+
{ value: "flat", label: ko("Flat", "평면") },
|
|
599
|
+
].map((option) => {
|
|
600
|
+
const checked = option.value === partColoursChoice;
|
|
601
|
+
return (
|
|
602
|
+
<button
|
|
603
|
+
type="button"
|
|
604
|
+
key={option.value}
|
|
605
|
+
role="menuitemradio"
|
|
606
|
+
className={"view-menu-item part-colour-option" + (checked ? " active" : "")}
|
|
607
|
+
data-part-colours={option.value}
|
|
608
|
+
aria-checked={checked}
|
|
609
|
+
onClick={() => runStudioAction("view.setPartColours", { mode: option.value })}
|
|
610
|
+
>
|
|
611
|
+
<span className="view-menu-mark" aria-hidden="true">{checked ? "✓" : ""}</span>
|
|
612
|
+
{option.label}
|
|
613
|
+
</button>
|
|
614
|
+
);
|
|
615
|
+
})}
|
|
616
|
+
</div>
|
|
617
|
+
)}
|
|
618
|
+
{/* Panel visibility belongs to the same menu (R4): the
|
|
619
|
+
agent column is something you show, not a mode, so it
|
|
620
|
+
gets a checkmark here instead of a topbar button. */}
|
|
621
|
+
{!embedMode && (
|
|
622
|
+
<div className="view-menu-group" role="group" aria-label={ko("Panels", "패널")}>
|
|
623
|
+
<span className="view-menu-label" aria-hidden="true">{ko("Panels", "패널")}</span>
|
|
624
|
+
<button
|
|
625
|
+
type="button"
|
|
626
|
+
role="menuitemcheckbox"
|
|
627
|
+
className={"view-menu-item agent-panel-toggle" + (agentCollapsed ? "" : " active")}
|
|
628
|
+
aria-checked={!agentCollapsed}
|
|
629
|
+
aria-pressed={!agentCollapsed}
|
|
630
|
+
title={ko("Show the agent chat column (Cmd/Ctrl+B)", "에이전트 채팅 열 표시 (Cmd/Ctrl+B)")}
|
|
631
|
+
onClick={() => window.dispatchEvent(new CustomEvent("cozyclay:agent-panel-toggle"))}
|
|
632
|
+
>
|
|
633
|
+
<span className="view-menu-mark" aria-hidden="true">{agentCollapsed ? "" : "✓"}</span>
|
|
634
|
+
{ko("Agent panel", "에이전트 패널")}
|
|
635
|
+
</button>
|
|
636
|
+
</div>
|
|
637
|
+
)}
|
|
638
|
+
</div>
|
|
639
|
+
)}
|
|
640
|
+
</div>
|
|
641
|
+
</div>
|
|
642
|
+
<span className="vp-rail-divider" aria-hidden="true" />
|
|
643
|
+
<span
|
|
644
|
+
className="vp-pill vp-speed"
|
|
645
|
+
data-testid="fly-speed"
|
|
646
|
+
title={ko("Fly speed — scroll while right-dragging to change it", "비행 속도 — 오른쪽 드래그 중 스크롤로 조절")}
|
|
647
|
+
>
|
|
648
|
+
<RailIcon name="speed" size={14} />
|
|
649
|
+
<span className="vp-speed-value">{trimNumber(flySpeed, 1)}×</span>
|
|
650
|
+
</span>
|
|
651
|
+
</div>
|
|
652
|
+
|
|
653
|
+
<ShotPreviewHeader name={activeShot?.name ?? ko("Shot", "샷")} lens={`${shot.focalMm}mm`} />
|
|
654
|
+
</div>
|
|
655
|
+
);
|
|
656
|
+
}
|