cozyclay 1.9.0 → 2.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 +171 -0
- package/bin/agent/agent-routes.mjs +74 -70
- 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/cozyclay.mjs +26 -8
- package/bin/live/cli.mjs +22 -3
- package/bin/mcp-runtime.mjs +35 -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-C1sdVbg4.css +1 -0
- package/dist/assets/app-CA55tcI3.js +4876 -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-Yrt99wZx.js +17 -0
- package/dist/assets/{ticket-BgIzOBOf.js → ticket-BcmsnHn5.js} +1 -1
- package/dist/assets/workflow-DRdwoDBT.css +1 -0
- package/dist/assets/workflow-dNPGY9xz.js +179 -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 +6 -6
- package/dist/workflow/index.html +5 -5
- package/mcp/LIVE-PROTOCOL.md +1 -1
- 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 +224 -377
- package/package.json +2 -1
- package/src/App.jsx +1273 -9075
- 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/collision-blockers.js +9 -3
- package/src/ardy/fix-collisions.js +10 -66
- package/src/ardy/ground.js +4 -4
- package/src/ardy/ik-drag.js +101 -0
- package/src/ardy/ik-key-json.js +43 -0
- package/src/ardy/ik.js +122 -76
- 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 +1199 -0
- package/src/ardy/timeline.jsx +149 -41
- package/src/ardy/waypoints.js +2 -2
- package/src/asset-pane.css +462 -0
- package/src/asset-pane.jsx +461 -232
- 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 +817 -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-model.js +24 -16
- package/src/hierarchy-panel.css +386 -0
- package/src/hierarchy-panel.jsx +150 -21
- 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/otio.js +2 -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 +284 -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/scene-objects.js +37 -1
- package/src/scenes.js +5 -0
- 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 +41 -271
- package/src/studio-agent-context.js +39 -10
- package/src/studio-agent-motion.js +122 -373
- package/src/studio-agent-protocol.js +67 -31
- 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 +460 -1742
- package/src/theme.js +44 -0
- package/src/timeline-extent.js +16 -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 +82 -69
- 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/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -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/morphgs/assets/gen_truth.py +39 -0
- package/tools/morphgs/assets/mesh_ori_rig.txt +27 -0
- package/tools/morphgs/assets/playback-check.mjs +30 -0
- package/tools/morphgs/demo-gate.sh +26 -0
- package/tools/morphgs/fbx2morphgs.mjs +68 -0
- package/tools/morphgs/morphgs-to-cskel27.mjs +105 -0
- package/tools/morphgs/patches/preprocess_src-none-mode.patch +76 -0
- package/tools/morphgs/setup-on-cluster.sh +126 -0
- package/tools/qa/css-rule-usage.mjs +283 -0
- package/tools/qa/studio-control-count.mjs +70 -32
- package/tools/run-tests.mjs +134 -12
- 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-BWusbqgO.js +0 -4861
- package/dist/assets/app-qKDo4PBX.css +0 -1
- package/dist/assets/shot-prompt-CHUtw6af.js +0 -17
- package/dist/assets/shot-prompt-C_g2BHVc.css +0 -1
- package/dist/assets/workflow-CVwzMhz2.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
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { createPortal } from "react-dom";
|
|
3
|
+
import { getAnalyticsOptOut, setAnalyticsOptOut } from "../analytics.js";
|
|
4
|
+
import { loadAutoColor, saveAutoColor } from "../auto-color.js";
|
|
5
|
+
import { motionReadinessMessage } from "../motion-readiness-ui.jsx";
|
|
6
|
+
import { ko, LOCALE, setLocale } from "../locale.js";
|
|
7
|
+
import { readStoredGridView, writeStoredGridView } from "../grid-view.js";
|
|
8
|
+
import { readTheme, saveTheme } from "../theme.js";
|
|
9
|
+
import "./preferences.css";
|
|
10
|
+
|
|
11
|
+
const LANGUAGES = [
|
|
12
|
+
{ id: "en", label: "English", action: "Switch to English" },
|
|
13
|
+
{ id: "ko", label: "한국어", action: "한국어로 전환" },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const NAV_GROUPS = [
|
|
17
|
+
{
|
|
18
|
+
label: ko("General", "일반"),
|
|
19
|
+
items: [
|
|
20
|
+
{ id: "Appearance", label: ko("Appearance", "모양") },
|
|
21
|
+
{ id: "Viewport", label: ko("Viewport", "뷰포트") },
|
|
22
|
+
{ id: "Input & Camera", label: ko("Input & Camera", "입력 및 카메라") },
|
|
23
|
+
{ id: "Hotkeys", label: ko("Hotkeys", "단축키") },
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
label: ko("Motion", "모션"),
|
|
28
|
+
items: [
|
|
29
|
+
{ id: "ARDY Connection", label: ko("ARDY Connection", "ARDY 연결") },
|
|
30
|
+
{ id: "IK Correction", label: ko("IK Correction", "IK 보정") },
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
label: ko("Integrations", "통합"),
|
|
35
|
+
items: [{ id: "MCP Server", label: ko("MCP Server", "MCP 서버") }],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
label: ko("Project", "프로젝트"),
|
|
39
|
+
items: [
|
|
40
|
+
{ id: "Units & Frame rate", label: ko("Units & Frame rate", "단위 및 프레임 레이트") },
|
|
41
|
+
{ id: "Autosave", label: ko("Autosave", "자동 저장") },
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const HOTKEYS = [
|
|
47
|
+
["Move", "W", "이동"],
|
|
48
|
+
["Rotate", "E", "회전"],
|
|
49
|
+
["Scale", "R", "크기 조절"],
|
|
50
|
+
["Frame selection", "F", "선택 프레임"],
|
|
51
|
+
["Duplicate", "Ctrl D", "복제"],
|
|
52
|
+
["Drop to floor", "End", "바닥에 놓기"],
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
const PREFERENCE_KEYS = Object.freeze({
|
|
56
|
+
grid: "cozyclay.grid-view.v1",
|
|
57
|
+
invertY: "cozyclay.preferences.invert-y.v1",
|
|
58
|
+
orbit: "cozyclay.preferences.orbit-selection.v1",
|
|
59
|
+
snap: "cozyclay.preferences.snapping.v1",
|
|
60
|
+
ctrlSnap: "cozyclay.preferences.ctrl-snapping.v1",
|
|
61
|
+
footLock: "cozyclay.preferences.foot-lock.v1",
|
|
62
|
+
bodyContact: "cozyclay.preferences.body-contact.v1",
|
|
63
|
+
autosave: "cozyclay.preferences.autosave.v1",
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
function readFlag(key, fallback = false) {
|
|
67
|
+
try {
|
|
68
|
+
return localStorage.getItem(key) === "1";
|
|
69
|
+
} catch {
|
|
70
|
+
return fallback;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function writeFlag(key, value) {
|
|
75
|
+
try {
|
|
76
|
+
localStorage.setItem(key, value ? "1" : "0");
|
|
77
|
+
} catch {
|
|
78
|
+
// Viewer preferences are best effort when storage is unavailable.
|
|
79
|
+
}
|
|
80
|
+
if (typeof window !== "undefined") {
|
|
81
|
+
window.dispatchEvent(new CustomEvent("cozyclay:preferences-changed", { detail: { key, value } }));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function PreferenceRow({ title, description, children, testid }) {
|
|
86
|
+
return (
|
|
87
|
+
<div className="v2-preferences__row" data-testid={testid}>
|
|
88
|
+
<div className="v2-preferences__row-copy">
|
|
89
|
+
<span className="v2-preferences__row-title">{title}</span>
|
|
90
|
+
{description && <span className="v2-preferences__row-description">{description}</span>}
|
|
91
|
+
</div>
|
|
92
|
+
<div className="v2-preferences__row-control">{children}</div>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function PreferenceSection({ title, children }) {
|
|
98
|
+
return (
|
|
99
|
+
<section className="v2-preferences__section">
|
|
100
|
+
<h2 className="v2-preferences__section-title">{title}</h2>
|
|
101
|
+
<div className="v2-preferences__section-rows">{children}</div>
|
|
102
|
+
</section>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function ToggleControl({ pressed, onChange, label, testid }) {
|
|
107
|
+
return (
|
|
108
|
+
<button
|
|
109
|
+
type="button"
|
|
110
|
+
className="v2-preferences__toggle"
|
|
111
|
+
role="switch"
|
|
112
|
+
aria-checked={pressed}
|
|
113
|
+
aria-pressed={pressed}
|
|
114
|
+
aria-label={label}
|
|
115
|
+
data-testid={testid}
|
|
116
|
+
onClick={() => onChange(!pressed)}
|
|
117
|
+
>
|
|
118
|
+
<span className="v2-preferences__toggle-thumb" aria-hidden="true" />
|
|
119
|
+
</button>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function SelectControl({ value, options, onChange, label, testid }) {
|
|
124
|
+
const current = options.indexOf(value);
|
|
125
|
+
return (
|
|
126
|
+
<button
|
|
127
|
+
type="button"
|
|
128
|
+
className="v2-preferences__select"
|
|
129
|
+
aria-label={label}
|
|
130
|
+
data-testid={testid}
|
|
131
|
+
onClick={() => onChange(options[(current + 1) % options.length])}
|
|
132
|
+
>
|
|
133
|
+
<span>{value}</span>
|
|
134
|
+
<span className="v2-preferences__select-caret" aria-hidden="true">▾</span>
|
|
135
|
+
</button>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function RangeControl({ value, min, max, step, label, unit = "", onChange, testid }) {
|
|
140
|
+
const percentage = ((value - min) / (max - min)) * 100;
|
|
141
|
+
return (
|
|
142
|
+
<div className="v2-preferences__range-wrap">
|
|
143
|
+
<input
|
|
144
|
+
type="range"
|
|
145
|
+
className="v2-preferences__range"
|
|
146
|
+
min={min}
|
|
147
|
+
max={max}
|
|
148
|
+
step={step}
|
|
149
|
+
value={value}
|
|
150
|
+
aria-label={label}
|
|
151
|
+
data-testid={testid}
|
|
152
|
+
style={{ "--prefs-range-progress": `${percentage}%` }}
|
|
153
|
+
onChange={(event) => onChange(Number(event.target.value))}
|
|
154
|
+
/>
|
|
155
|
+
<span className="v2-preferences__range-value">{value}{unit}</span>
|
|
156
|
+
</div>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function ReadonlyHotkeys() {
|
|
161
|
+
return (
|
|
162
|
+
<div className="v2-preferences__hotkeys" data-testid="preferences-hotkeys">
|
|
163
|
+
{HOTKEYS.map(([english, key, korean]) => (
|
|
164
|
+
<div className="v2-preferences__hotkey" key={english}>
|
|
165
|
+
<span>{ko(english, korean)}</span>
|
|
166
|
+
<kbd>{key}</kbd>
|
|
167
|
+
</div>
|
|
168
|
+
))}
|
|
169
|
+
</div>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function MotionConnection({ state, checking, onRetry }) {
|
|
174
|
+
const unavailable = state === "unavailable";
|
|
175
|
+
const retryLabel = checking
|
|
176
|
+
? ko("Checking…", "확인 중…")
|
|
177
|
+
: unavailable
|
|
178
|
+
? ko("Retry connection", "연결 다시 확인")
|
|
179
|
+
: ko("Check connection", "연결 확인");
|
|
180
|
+
return (
|
|
181
|
+
<div className="v2-preferences__motion-setup" data-testid="motion-setup" data-state={state}>
|
|
182
|
+
<div className="v2-preferences__motion-status" data-state={state} role="status" aria-busy={checking}>
|
|
183
|
+
<span className="v2-preferences__status-dot" aria-hidden="true" />
|
|
184
|
+
<span>{motionReadinessMessage(state)}</span>
|
|
185
|
+
</div>
|
|
186
|
+
<p>{ko(
|
|
187
|
+
"Motion generation is optional. Sample motion, camera editing and exports work without a connection.",
|
|
188
|
+
"모션 생성은 선택 사항이에요. 연결 없이도 샘플 모션, 카메라 편집, 내보내기를 쓸 수 있어요.",
|
|
189
|
+
)}</p>
|
|
190
|
+
<p>{ko(
|
|
191
|
+
"Retry checks the local motion bridge without changing the current scene.",
|
|
192
|
+
"다시 확인해도 현재 씬은 바뀌지 않고 로컬 모션 브리지 상태만 확인해요.",
|
|
193
|
+
)}</p>
|
|
194
|
+
<button
|
|
195
|
+
type="button"
|
|
196
|
+
className="v2-preferences__primary-button"
|
|
197
|
+
data-testid="motion-health-retry"
|
|
198
|
+
disabled={checking}
|
|
199
|
+
onClick={() => onRetry?.()}
|
|
200
|
+
>
|
|
201
|
+
{retryLabel}
|
|
202
|
+
</button>
|
|
203
|
+
</div>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function ThemeControls() {
|
|
208
|
+
const [theme, setTheme] = useState(readTheme);
|
|
209
|
+
const options = [
|
|
210
|
+
{ id: "dark", label: ko("Dark", "다크") },
|
|
211
|
+
{ id: "light", label: ko("Light", "라이트") },
|
|
212
|
+
];
|
|
213
|
+
return (
|
|
214
|
+
<div className="v2-preferences__language-controls" role="group" aria-label={ko("Theme", "테마")}>
|
|
215
|
+
{options.map((option) => (
|
|
216
|
+
<button
|
|
217
|
+
type="button"
|
|
218
|
+
key={option.id}
|
|
219
|
+
className={"v2-preferences__language" + (theme === option.id ? " is-selected" : "")}
|
|
220
|
+
data-testid={`settings-theme-${option.id}`}
|
|
221
|
+
aria-pressed={theme === option.id}
|
|
222
|
+
onClick={() => setTheme(saveTheme(option.id))}
|
|
223
|
+
>
|
|
224
|
+
{option.label}
|
|
225
|
+
</button>
|
|
226
|
+
))}
|
|
227
|
+
</div>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function preferenceSection(section, props) {
|
|
232
|
+
const {
|
|
233
|
+
autoColor, setAutoColor, analyticsOptOut, setAnalyticsOptOutState,
|
|
234
|
+
gridView, setGridView, flySpeed, setFlySpeed, sensitivity, setSensitivity,
|
|
235
|
+
orbitSelection, setOrbitSelection, invertY, setInvertY, snapping, setSnapping,
|
|
236
|
+
ctrlSnap, setCtrlSnap, footLock, setFootLock, bodyContact, setBodyContact,
|
|
237
|
+
shading, setShading, gridSize, setGridSize, rotationStep, setRotationStep,
|
|
238
|
+
units, setUnits, frameRate, setFrameRate, autosave, setAutosave,
|
|
239
|
+
motionState, motionChecking, onRetry, cameraTutorial,
|
|
240
|
+
} = props;
|
|
241
|
+
const languageControls = (
|
|
242
|
+
<div className="v2-preferences__language-controls">
|
|
243
|
+
{LANGUAGES.map((language) => (
|
|
244
|
+
<button
|
|
245
|
+
type="button"
|
|
246
|
+
key={language.id}
|
|
247
|
+
className={"v2-preferences__language" + (LOCALE === language.id ? " is-selected" : "")}
|
|
248
|
+
data-testid={`settings-locale-${language.id}`}
|
|
249
|
+
aria-pressed={LOCALE === language.id}
|
|
250
|
+
title={language.action}
|
|
251
|
+
onClick={() => setLocale(language.id)}
|
|
252
|
+
>
|
|
253
|
+
{language.label}
|
|
254
|
+
</button>
|
|
255
|
+
))}
|
|
256
|
+
</div>
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
switch (section) {
|
|
260
|
+
case "Appearance":
|
|
261
|
+
return (
|
|
262
|
+
<>
|
|
263
|
+
<PreferenceSection title={ko("Language", "언어")}>
|
|
264
|
+
<PreferenceRow title={ko("Language", "언어")} description={ko("Choose the labels used throughout the editor.", "에디터에서 사용할 언어를 선택하세요.")}>
|
|
265
|
+
{languageControls}
|
|
266
|
+
</PreferenceRow>
|
|
267
|
+
<PreferenceRow title={ko("Theme", "테마")} description={ko("Dark keeps the studio dim around the stage; Light uses bright glass panels.", "다크는 무대 주변을 어둡게, 라이트는 밝은 유리 패널을 써요.")}>
|
|
268
|
+
<ThemeControls />
|
|
269
|
+
</PreferenceRow>
|
|
270
|
+
</PreferenceSection>
|
|
271
|
+
<PreferenceSection title={ko("Editor", "에디터")}>
|
|
272
|
+
<PreferenceRow title={ko("Auto colour", "자동 색")} description={ko("Give scene objects stable display colours.", "씬 오브젝트에 고정된 표시 색을 사용해요.")}>
|
|
273
|
+
<ToggleControl pressed={autoColor} onChange={(value) => { setAutoColor(value); saveAutoColor(value); }} label={ko("Auto colour", "자동 색")} testid="preferences-auto-color" />
|
|
274
|
+
</PreferenceRow>
|
|
275
|
+
<PreferenceRow title={ko("Anonymous analytics", "익명 사용 통계")} description={ko("Help improve CozyClay with anonymous product events.", "익명 제품 이벤트로 CozyClay 개선을 도와요.")}>
|
|
276
|
+
<ToggleControl
|
|
277
|
+
pressed={!analyticsOptOut}
|
|
278
|
+
onChange={async (value) => setAnalyticsOptOutState(await setAnalyticsOptOut(!value))}
|
|
279
|
+
label={ko("Anonymous analytics", "익명 사용 통계")}
|
|
280
|
+
testid="settings-analytics"
|
|
281
|
+
/>
|
|
282
|
+
</PreferenceRow>
|
|
283
|
+
</PreferenceSection>
|
|
284
|
+
{cameraTutorial && <div className="v2-preferences__help">{cameraTutorial}</div>}
|
|
285
|
+
</>
|
|
286
|
+
);
|
|
287
|
+
case "Viewport":
|
|
288
|
+
return (
|
|
289
|
+
<PreferenceSection title={ko("Viewport", "뷰포트")}>
|
|
290
|
+
<PreferenceRow title={ko("Reference grid", "기준 그리드")} description={ko("Use a dark reference grid instead of the clay deck.", "클레이 데크 대신 어두운 기준 그리드를 사용해요.")}>
|
|
291
|
+
<ToggleControl pressed={gridView} onChange={(value) => { setGridView(value); writeStoredGridView(globalThis.localStorage, value); }} label={ko("Reference grid", "기준 그리드")} testid="preferences-grid" />
|
|
292
|
+
</PreferenceRow>
|
|
293
|
+
<PreferenceRow title={ko("Shading", "셰이딩")} description={ko("Choose the viewport lighting preview.", "뷰포트 조명 미리보기를 선택하세요.")}>
|
|
294
|
+
<SelectControl value={shading} options={[ko("Clay Lit", "클레이 조명"), ko("Solid", "솔리드")]} onChange={setShading} label={ko("Shading mode", "셰이딩 모드")} testid="preferences-shading" />
|
|
295
|
+
</PreferenceRow>
|
|
296
|
+
</PreferenceSection>
|
|
297
|
+
);
|
|
298
|
+
case "Input & Camera":
|
|
299
|
+
return (
|
|
300
|
+
<>
|
|
301
|
+
<PreferenceSection title={ko("Camera", "카메라")}>
|
|
302
|
+
<PreferenceRow title={ko("Fly speed", "비행 속도")} description={ko("Right mouse button + WASD", "마우스 오른쪽 버튼 + WASD")}>
|
|
303
|
+
<RangeControl value={flySpeed} min={1} max={6} step={1} label={ko("Fly speed", "비행 속도")} onChange={setFlySpeed} testid="preferences-fly-speed" />
|
|
304
|
+
</PreferenceRow>
|
|
305
|
+
<PreferenceRow title={ko("Mouse sensitivity", "마우스 감도")} description={ko("Pointer movement while looking around.", "둘러볼 때 포인터 이동 감도예요.")}>
|
|
306
|
+
<RangeControl value={sensitivity} min={0.1} max={1} step={0.05} label={ko("Mouse sensitivity", "마우스 감도")} onChange={setSensitivity} testid="preferences-sensitivity" />
|
|
307
|
+
</PreferenceRow>
|
|
308
|
+
<PreferenceRow title={ko("Orbit around selection", "선택 항목 중심으로 회전")} description={ko("Alt-drag keeps the selected item at the pivot.", "Alt를 누르고 드래그하면 선택 항목을 중심으로 돌아요.")}>
|
|
309
|
+
<ToggleControl pressed={orbitSelection} onChange={(value) => { setOrbitSelection(value); writeFlag(PREFERENCE_KEYS.orbit, value); }} label={ko("Orbit around selection", "선택 항목 중심으로 회전")} testid="preferences-orbit" />
|
|
310
|
+
</PreferenceRow>
|
|
311
|
+
<PreferenceRow title={ko("Invert Y", "Y축 반전")} description={ko("Reverse vertical camera movement.", "카메라의 수직 움직임을 반대로 해요.")}>
|
|
312
|
+
<ToggleControl pressed={invertY} onChange={(value) => { setInvertY(value); writeFlag(PREFERENCE_KEYS.invertY, value); }} label={ko("Invert Y", "Y축 반전")} testid="preferences-invert-y" />
|
|
313
|
+
</PreferenceRow>
|
|
314
|
+
</PreferenceSection>
|
|
315
|
+
<PreferenceSection title={ko("Snapping", "스냅")}>
|
|
316
|
+
<PreferenceRow title={ko("Snapping", "스냅")} description={ko("Keep transforms on the active grid.", "변환을 활성 그리드에 맞춰요.")}>
|
|
317
|
+
<ToggleControl pressed={snapping} onChange={(value) => { setSnapping(value); writeFlag(PREFERENCE_KEYS.snap, value); }} label={ko("Snapping", "스냅")} testid="preferences-snapping" />
|
|
318
|
+
</PreferenceRow>
|
|
319
|
+
<PreferenceRow title={ko("Grid size", "그리드 크기")} description={ko("Stage movement increment.", "스테이지 이동 단위예요.")}>
|
|
320
|
+
<SelectControl value={gridSize} options={["10 cm", "25 cm", "50 cm"]} onChange={setGridSize} label={ko("Grid size", "그리드 크기")} testid="preferences-grid-size" />
|
|
321
|
+
</PreferenceRow>
|
|
322
|
+
<PreferenceRow title={ko("Rotation step", "회전 단위")} description={ko("Stage rotation increment.", "스테이지 회전 단위예요.")}>
|
|
323
|
+
<SelectControl value={rotationStep} options={["15°", "30°", "45°"]} onChange={setRotationStep} label={ko("Rotation step", "회전 단위")} testid="preferences-rotation-step" />
|
|
324
|
+
</PreferenceRow>
|
|
325
|
+
<PreferenceRow title={ko("Ctrl inverts snapping", "Ctrl로 스냅 반전")} description={ko("Hold Ctrl to temporarily reverse snapping.", "Ctrl을 누르면 스냅을 잠시 반대로 적용해요.")}>
|
|
326
|
+
<ToggleControl pressed={ctrlSnap} onChange={(value) => { setCtrlSnap(value); writeFlag(PREFERENCE_KEYS.ctrlSnap, value); }} label={ko("Ctrl inverts snapping", "Ctrl로 스냅 반전")} testid="preferences-ctrl-snapping" />
|
|
327
|
+
</PreferenceRow>
|
|
328
|
+
</PreferenceSection>
|
|
329
|
+
<PreferenceSection title={ko("Hotkeys", "단축키")}>
|
|
330
|
+
<ReadonlyHotkeys />
|
|
331
|
+
</PreferenceSection>
|
|
332
|
+
</>
|
|
333
|
+
);
|
|
334
|
+
case "Hotkeys":
|
|
335
|
+
return (
|
|
336
|
+
<PreferenceSection title={ko("Hotkeys", "단축키")}>
|
|
337
|
+
<p className="v2-preferences__section-note">{ko("Shortcuts are read-only here. They stay active whenever focus is outside a text field.", "단축키는 여기서 읽기 전용이에요. 텍스트 입력란 밖에서 항상 사용할 수 있어요.")}</p>
|
|
338
|
+
<ReadonlyHotkeys />
|
|
339
|
+
</PreferenceSection>
|
|
340
|
+
);
|
|
341
|
+
case "ARDY Connection":
|
|
342
|
+
return (
|
|
343
|
+
<PreferenceSection title={ko("ARDY Connection", "ARDY 연결")}>
|
|
344
|
+
<PreferenceRow title={ko("Motion setup", "모션 설정")} description={ko("Check today's motion generation route.", "현재 모션 생성 경로를 확인해요.")}>
|
|
345
|
+
<span className="v2-preferences__value">{motionReadinessMessage(motionState)}</span>
|
|
346
|
+
</PreferenceRow>
|
|
347
|
+
<div className="v2-preferences__row v2-preferences__row--full">
|
|
348
|
+
<MotionConnection state={motionState} checking={motionChecking} onRetry={onRetry} />
|
|
349
|
+
</div>
|
|
350
|
+
</PreferenceSection>
|
|
351
|
+
);
|
|
352
|
+
case "IK Correction":
|
|
353
|
+
return (
|
|
354
|
+
<PreferenceSection title={ko("IK Correction", "IK 보정")}>
|
|
355
|
+
<PreferenceRow title={ko("Foot lock", "발 고정")} description={ko("Keep planted feet stable through the take.", "테이크 전체에서 발을 안정적으로 고정해요.")}>
|
|
356
|
+
<ToggleControl pressed={footLock} onChange={(value) => { setFootLock(value); writeFlag(PREFERENCE_KEYS.footLock, value); }} label={ko("Foot lock", "발 고정")} testid="preferences-foot-lock" />
|
|
357
|
+
</PreferenceRow>
|
|
358
|
+
<PreferenceRow title={ko("Body contact", "신체 접촉")} description={ko("Keep contact points when applying corrections.", "보정을 적용할 때 접촉 지점을 유지해요.")}>
|
|
359
|
+
<ToggleControl pressed={bodyContact} onChange={(value) => { setBodyContact(value); writeFlag(PREFERENCE_KEYS.bodyContact, value); }} label={ko("Body contact", "신체 접촉")} testid="preferences-body-contact" />
|
|
360
|
+
</PreferenceRow>
|
|
361
|
+
</PreferenceSection>
|
|
362
|
+
);
|
|
363
|
+
case "MCP Server":
|
|
364
|
+
return (
|
|
365
|
+
<PreferenceSection title={ko("MCP Server", "MCP 서버")}>
|
|
366
|
+
<PreferenceRow title={ko("Status", "상태")} description={ko("Agent connections are managed by the running studio.", "에이전트 연결은 실행 중인 스튜디오가 관리해요.")}>
|
|
367
|
+
<span className="v2-preferences__status-value"><span className="v2-preferences__status-dot is-muted" aria-hidden="true" />{ko("Not connected", "연결되지 않음")}</span>
|
|
368
|
+
</PreferenceRow>
|
|
369
|
+
<PreferenceRow title={ko("Port", "포트")} description={ko("The local command endpoint.", "로컬 명령 엔드포인트예요.")}>
|
|
370
|
+
<span className="v2-preferences__value v2-preferences__value--mono">8765</span>
|
|
371
|
+
</PreferenceRow>
|
|
372
|
+
</PreferenceSection>
|
|
373
|
+
);
|
|
374
|
+
case "Units & Frame rate":
|
|
375
|
+
return (
|
|
376
|
+
<PreferenceSection title={ko("Units & Frame rate", "단위 및 프레임 레이트")}>
|
|
377
|
+
<PreferenceRow title={ko("Units", "단위")} description={ko("Display distances in the inspector.", "인스펙터에서 거리를 표시하는 단위예요.")}>
|
|
378
|
+
<SelectControl value={units} options={[ko("Centimeters", "센티미터"), ko("Meters", "미터")]} onChange={setUnits} label={ko("Units", "단위")} testid="preferences-units" />
|
|
379
|
+
</PreferenceRow>
|
|
380
|
+
<PreferenceRow title={ko("Frame rate", "프레임 레이트")} description={ko("Playback and timeline frame rate.", "재생 및 타임라인 프레임 레이트예요.")}>
|
|
381
|
+
<SelectControl value={frameRate} options={["24 fps", "30 fps", "60 fps"]} onChange={setFrameRate} label={ko("Frame rate", "프레임 레이트")} testid="preferences-frame-rate" />
|
|
382
|
+
</PreferenceRow>
|
|
383
|
+
</PreferenceSection>
|
|
384
|
+
);
|
|
385
|
+
case "Autosave":
|
|
386
|
+
return (
|
|
387
|
+
<PreferenceSection title={ko("Autosave", "자동 저장")}>
|
|
388
|
+
<PreferenceRow title={ko("Autosave", "자동 저장")} description={ko("Save a project snapshot after an edit settles.", "편집이 끝나면 프로젝트 스냅샷을 저장해요.")}>
|
|
389
|
+
<ToggleControl pressed={autosave} onChange={(value) => { setAutosave(value); writeFlag(PREFERENCE_KEYS.autosave, value); }} label={ko("Autosave", "자동 저장")} testid="preferences-autosave" />
|
|
390
|
+
</PreferenceRow>
|
|
391
|
+
<PreferenceRow title={ko("Interval", "간격")} description={ko("The current editor default.", "현재 에디터 기본값이에요.")}>
|
|
392
|
+
<span className="v2-preferences__value">2 min</span>
|
|
393
|
+
</PreferenceRow>
|
|
394
|
+
</PreferenceSection>
|
|
395
|
+
);
|
|
396
|
+
default:
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export default function PreferencesDialog({ open, onClose, motionSetup, initialSection = "Input & Camera", cameraTutorial }) {
|
|
402
|
+
const dialogRef = useRef(null);
|
|
403
|
+
const searchRef = useRef(null);
|
|
404
|
+
const closeRef = useRef(onClose);
|
|
405
|
+
closeRef.current = onClose;
|
|
406
|
+
const [activeSection, setActiveSection] = useState(initialSection);
|
|
407
|
+
const [query, setQuery] = useState("");
|
|
408
|
+
const [analyticsOptOut, setAnalyticsOptOutState] = useState(getAnalyticsOptOut);
|
|
409
|
+
const [autoColor, setAutoColor] = useState(() => loadAutoColor());
|
|
410
|
+
const [gridView, setGridView] = useState(() => readStoredGridView(globalThis.localStorage));
|
|
411
|
+
const [flySpeed, setFlySpeed] = useState(3);
|
|
412
|
+
const [sensitivity, setSensitivity] = useState(0.55);
|
|
413
|
+
const [orbitSelection, setOrbitSelection] = useState(() => readFlag(PREFERENCE_KEYS.orbit, true));
|
|
414
|
+
const [invertY, setInvertY] = useState(() => readFlag(PREFERENCE_KEYS.invertY));
|
|
415
|
+
const [snapping, setSnapping] = useState(() => readFlag(PREFERENCE_KEYS.snap, true));
|
|
416
|
+
const [ctrlSnap, setCtrlSnap] = useState(() => readFlag(PREFERENCE_KEYS.ctrlSnap, true));
|
|
417
|
+
const [footLock, setFootLock] = useState(() => readFlag(PREFERENCE_KEYS.footLock, true));
|
|
418
|
+
const [bodyContact, setBodyContact] = useState(() => readFlag(PREFERENCE_KEYS.bodyContact, true));
|
|
419
|
+
const [shading, setShading] = useState(() => ko("Clay Lit", "클레이 조명"));
|
|
420
|
+
const [gridSize, setGridSize] = useState("10 cm");
|
|
421
|
+
const [rotationStep, setRotationStep] = useState("15°");
|
|
422
|
+
const [units, setUnits] = useState(() => ko("Centimeters", "센티미터"));
|
|
423
|
+
const [frameRate, setFrameRate] = useState("24 fps");
|
|
424
|
+
const [autosave, setAutosave] = useState(() => readFlag(PREFERENCE_KEYS.autosave, true));
|
|
425
|
+
const setupProps = motionSetup?.props ?? {};
|
|
426
|
+
const motionState = setupProps.state ?? "unavailable";
|
|
427
|
+
const motionChecking = setupProps.checking === true;
|
|
428
|
+
const onRetry = setupProps.onRetry;
|
|
429
|
+
|
|
430
|
+
const filteredGroups = useMemo(() => {
|
|
431
|
+
const term = query.trim().toLocaleLowerCase();
|
|
432
|
+
if (!term) return NAV_GROUPS;
|
|
433
|
+
return NAV_GROUPS.map((group) => ({
|
|
434
|
+
...group,
|
|
435
|
+
items: group.items.filter((item) => item.label.toLocaleLowerCase().includes(term)),
|
|
436
|
+
})).filter((group) => group.items.length > 0);
|
|
437
|
+
}, [query]);
|
|
438
|
+
|
|
439
|
+
useEffect(() => {
|
|
440
|
+
if (!open) return undefined;
|
|
441
|
+
setActiveSection(initialSection);
|
|
442
|
+
setQuery("");
|
|
443
|
+
const frame = requestAnimationFrame(() => searchRef.current?.focus());
|
|
444
|
+
const onKeyDown = (event) => {
|
|
445
|
+
if (event.key === "Escape") {
|
|
446
|
+
event.preventDefault();
|
|
447
|
+
closeRef.current?.();
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
if (event.key !== "Tab") return;
|
|
451
|
+
const focusable = [...dialogRef.current?.querySelectorAll("button:not([disabled]), input:not([disabled]), [tabindex=\"0\"]") ?? []];
|
|
452
|
+
if (focusable.length === 0) return;
|
|
453
|
+
const first = focusable[0];
|
|
454
|
+
const last = focusable.at(-1);
|
|
455
|
+
if (event.shiftKey && document.activeElement === first) {
|
|
456
|
+
event.preventDefault();
|
|
457
|
+
last.focus();
|
|
458
|
+
} else if (!event.shiftKey && document.activeElement === last) {
|
|
459
|
+
event.preventDefault();
|
|
460
|
+
first.focus();
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
document.addEventListener("keydown", onKeyDown);
|
|
464
|
+
return () => {
|
|
465
|
+
cancelAnimationFrame(frame);
|
|
466
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
467
|
+
};
|
|
468
|
+
}, [open, initialSection]);
|
|
469
|
+
|
|
470
|
+
if (!open || typeof document === "undefined") return null;
|
|
471
|
+
const currentProps = {
|
|
472
|
+
autoColor, setAutoColor, analyticsOptOut,
|
|
473
|
+
setAnalyticsOptOutState,
|
|
474
|
+
gridView, setGridView, flySpeed, setFlySpeed, sensitivity, setSensitivity,
|
|
475
|
+
orbitSelection, setOrbitSelection, invertY, setInvertY, snapping, setSnapping,
|
|
476
|
+
ctrlSnap, setCtrlSnap, footLock, setFootLock, bodyContact, setBodyContact,
|
|
477
|
+
shading, setShading, gridSize, setGridSize, rotationStep, setRotationStep,
|
|
478
|
+
units, setUnits, frameRate, setFrameRate, autosave, setAutosave,
|
|
479
|
+
motionState, motionChecking, onRetry, cameraTutorial,
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
return createPortal(
|
|
483
|
+
<div className="v2-preferences" data-testid="preferences-dialog-backdrop" onPointerDown={(event) => { if (event.target === event.currentTarget) closeRef.current?.(); }}>
|
|
484
|
+
<div
|
|
485
|
+
className="v2-preferences__dialog settings-menu"
|
|
486
|
+
role="dialog"
|
|
487
|
+
aria-modal="true"
|
|
488
|
+
aria-labelledby="v2-preferences-title"
|
|
489
|
+
ref={dialogRef}
|
|
490
|
+
tabIndex={-1}
|
|
491
|
+
>
|
|
492
|
+
<aside className="v2-preferences__nav" aria-label={ko("Preferences navigation", "환경설정 탐색")}>
|
|
493
|
+
<div className="v2-preferences__nav-title-row">
|
|
494
|
+
<strong className="v2-preferences__nav-title">{ko("Preferences", "환경설정")}</strong>
|
|
495
|
+
<button type="button" className="v2-preferences__close" data-testid="preferences-close" onClick={() => closeRef.current?.()} aria-label={ko("Close preferences", "환경설정 닫기")}>×</button>
|
|
496
|
+
</div>
|
|
497
|
+
<label className="v2-preferences__search">
|
|
498
|
+
<span className="v2-preferences__search-icon" aria-hidden="true">⌕</span>
|
|
499
|
+
<input ref={searchRef} data-testid="preferences-search" type="search" value={query} onChange={(event) => setQuery(event.target.value)} placeholder={ko("Search", "검색")} aria-label={ko("Search preferences", "환경설정 검색")} />
|
|
500
|
+
</label>
|
|
501
|
+
<nav className="v2-preferences__nav-list" aria-label={ko("Preference sections", "환경설정 섹션")}>
|
|
502
|
+
{filteredGroups.map((group) => (
|
|
503
|
+
<div className="v2-preferences__nav-group" key={group.label}>
|
|
504
|
+
<h2 className="v2-preferences__nav-group-title" data-testid="preferences-nav-group">{group.label}</h2>
|
|
505
|
+
{group.items.map((item) => (
|
|
506
|
+
<button
|
|
507
|
+
type="button"
|
|
508
|
+
key={item.id}
|
|
509
|
+
className={"v2-preferences__nav-item" + (activeSection === item.id ? " is-active" : "")}
|
|
510
|
+
data-testid="preferences-nav-item"
|
|
511
|
+
aria-current={activeSection === item.id ? "page" : undefined}
|
|
512
|
+
onClick={() => setActiveSection(item.id)}
|
|
513
|
+
>
|
|
514
|
+
{item.label}
|
|
515
|
+
</button>
|
|
516
|
+
))}
|
|
517
|
+
</div>
|
|
518
|
+
))}
|
|
519
|
+
</nav>
|
|
520
|
+
</aside>
|
|
521
|
+
<main className="v2-preferences__content">
|
|
522
|
+
<header className="v2-preferences__content-header">
|
|
523
|
+
<h1 id="v2-preferences-title">{ko(activeSection, activeSection)}</h1>
|
|
524
|
+
<button type="button" className="v2-preferences__reset" data-testid="preferences-reset" onClick={() => {
|
|
525
|
+
setFlySpeed(3); setSensitivity(0.55); setOrbitSelection(true); setInvertY(false); setSnapping(true); setCtrlSnap(true);
|
|
526
|
+
setShading(ko("Clay Lit", "클레이 조명")); setGridSize("10 cm"); setRotationStep("15°"); setUnits(ko("Centimeters", "센티미터")); setFrameRate("24 fps"); setAutosave(true);
|
|
527
|
+
writeFlag(PREFERENCE_KEYS.orbit, true); writeFlag(PREFERENCE_KEYS.invertY, false); writeFlag(PREFERENCE_KEYS.snap, true); writeFlag(PREFERENCE_KEYS.ctrlSnap, true); writeFlag(PREFERENCE_KEYS.autosave, true);
|
|
528
|
+
}}>{ko("Reset to defaults", "기본값으로 재설정")}</button>
|
|
529
|
+
</header>
|
|
530
|
+
<div className="v2-preferences__content-scroll">
|
|
531
|
+
{preferenceSection(activeSection, currentProps)}
|
|
532
|
+
</div>
|
|
533
|
+
</main>
|
|
534
|
+
</div>
|
|
535
|
+
</div>,
|
|
536
|
+
document.body,
|
|
537
|
+
);
|
|
538
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import { flushSync } from "react-dom";
|
|
3
|
+
import SettingsMenu from "../settings-menu.jsx";
|
|
4
|
+
import { MotionSetup } from "../motion-readiness-ui.jsx";
|
|
5
|
+
import { useStudioShell } from "./studio-shell-context.js";
|
|
6
|
+
|
|
7
|
+
// Temporary adapter until the Preferences region replaces SettingsMenu.
|
|
8
|
+
// SettingsMenu retains its trigger, focus, dismissal and motion-setup behavior.
|
|
9
|
+
// Mirror its published expanded state so future menus can also open it through
|
|
10
|
+
// the shell context, without mounting a second copy or changing SettingsMenu.
|
|
11
|
+
export default function PreferencesSlot() {
|
|
12
|
+
const {
|
|
13
|
+
preferencesOpen, setPreferencesOpen, motionSetupReveal, motionSetupKind,
|
|
14
|
+
trailReadinessState, lineReadinessState, readinessState, bridgeChecking,
|
|
15
|
+
recheckMotionHealth,
|
|
16
|
+
} = useStudioShell();
|
|
17
|
+
const hostRef = useRef(null);
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const trigger = hostRef.current.querySelector('[data-testid="settings-menu-trigger"]');
|
|
20
|
+
const observer = new MutationObserver(() => {
|
|
21
|
+
// Publish dismissal before another menu can request an open. A queued
|
|
22
|
+
// mirror could otherwise overwrite that request (or make it a no-op).
|
|
23
|
+
flushSync(() => setPreferencesOpen(trigger.getAttribute("aria-expanded") === "true"));
|
|
24
|
+
});
|
|
25
|
+
observer.observe(trigger, { attributes: true, attributeFilter: ["aria-expanded"] });
|
|
26
|
+
return () => observer.disconnect();
|
|
27
|
+
}, [setPreferencesOpen]);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const trigger = hostRef.current.querySelector('[data-testid="settings-menu-trigger"]');
|
|
30
|
+
if ((trigger.getAttribute("aria-expanded") === "true") !== preferencesOpen) trigger.click();
|
|
31
|
+
}, [preferencesOpen]);
|
|
32
|
+
return (
|
|
33
|
+
<div className="preferences-slot" ref={hostRef}>
|
|
34
|
+
<SettingsMenu
|
|
35
|
+
motionSetupReveal={motionSetupReveal}
|
|
36
|
+
motionSetup={<MotionSetup state={motionSetupKind === "trail" ? trailReadinessState : motionSetupKind === "line" ? lineReadinessState : readinessState} checking={bridgeChecking} onRetry={recheckMotionHealth} />}
|
|
37
|
+
/>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|