cozyclay 3.1.1 → 3.1.3
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 +64 -0
- package/README.md +1 -1
- package/bin/agent/agent-routes.mjs +4 -4
- package/bin/agent/agent-runner.mjs +160 -151
- package/bin/agent/agent-stream.mjs +86 -0
- package/bin/agent/anthropic-subscription-events.mjs +120 -0
- package/bin/agent/anthropic-subscription-prompt.mjs +116 -0
- package/bin/agent/anthropic-subscription-stream.mjs +251 -0
- package/bin/agent/anthropic-subscription.mjs +73 -0
- package/bin/agent/claude-code-executable.mjs +93 -0
- package/bin/agent/cliproxy-provider.mjs +25 -11
- package/bin/agent/main-agent.mjs +21 -7
- package/bin/agent/main-prompt.mjs +7 -6
- package/bin/agent/model-roles.mjs +112 -0
- package/bin/agent/openai-compatible-provider.mjs +95 -0
- package/bin/agent/pi-tools.mjs +19 -10
- package/bin/agent/provider-login.mjs +3 -1
- package/bin/agent/provider-routes.mjs +2 -0
- package/bin/agent/providers.mjs +35 -130
- package/bin/agent/script-tool.mjs +2 -1
- package/bin/agent/studio-prompt.mjs +1 -1
- package/bin/agent/studio-rebase.mjs +5 -4
- package/bin/agent/studio-task-routes.mjs +1 -1
- package/bin/agent/studio-tasks.mjs +116 -25
- package/bin/agent/studio-tools.mjs +3 -2
- package/bin/cozyclay.mjs +4 -2
- package/bin/live-endpoint.mjs +2 -0
- package/dist/app/index.html +6 -6
- package/dist/assets/TaskBoard-U1Yl-4F1.js +1 -0
- package/dist/assets/app-BdRlMf4J.css +1 -0
- package/dist/assets/app-DwETmoQZ.js +5144 -0
- package/dist/assets/compile-worker-oy_P9HHh.js +6 -0
- package/dist/assets/motion3-compile-DDG3wklD.js +1 -0
- package/dist/assets/scene-asset-sync-BEjjhNEo.css +1 -0
- package/dist/assets/scene-asset-sync-Bz59Ghdz.js +13 -0
- package/dist/assets/task-board-DFfNa0xg.js +15 -0
- package/dist/assets/workflow-BMJGd_gZ.js +179 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/index.html +1 -1
- package/dist/sitemap.xml +1 -1
- package/dist/workflow/index.html +4 -4
- package/mcp/README.md +1 -1
- package/mcp/live-hub.mjs +5 -3
- package/mcp/tool-handlers.mjs +5 -5
- package/package.json +6 -4
- package/src/App.jsx +65 -87
- package/src/agent-image-client.js +2 -2
- package/src/asset-pane.jsx +14 -14
- package/src/bus-task-scopes.js +59 -11
- package/src/bus-task-targets.js +35 -9
- package/src/bus-tasks.js +23 -10
- package/src/character-kind.js +13 -12
- package/src/command-bus.js +7 -4
- package/src/commands/ai.js +1 -1
- package/src/commands/export.js +1 -1
- package/src/commands/motion3.js +27 -7
- package/src/commands/project.js +2 -1
- package/src/commands/shot.js +1 -1
- package/src/commands/view.js +2 -2
- package/src/domains/cast.js +15 -15
- package/src/domains/motion/motion-range-pin-rules.js +1 -1
- package/src/domains/motion.js +11 -11
- package/src/domains/objects.js +20 -30
- package/src/domains/scenes.js +14 -21
- package/src/domains/shots.js +3 -3
- package/src/export-workspace.js +244 -0
- package/src/first-success-guide.jsx +19 -3
- package/src/hierarchy-panel.jsx +6 -7
- package/src/keyframe-pack.js +5 -1
- package/src/live-control.js +14 -7
- package/src/locale-zh/app.js +124 -0
- package/src/locale-zh/domains.js +36 -0
- package/src/locale-zh/labels.js +148 -0
- package/src/locale-zh/panels.js +225 -0
- package/src/locale-zh/shell.js +230 -0
- package/src/locale-zh/timeline.js +145 -0
- package/src/locale-zh/workflow.js +104 -0
- package/src/locale-zh/workspace.js +190 -0
- package/src/locale-zh.js +21 -0
- package/src/locale.js +16 -6
- package/src/main.jsx +1 -1
- package/src/motion/beat-board-model.js +4 -4
- package/src/motion/beat-board.jsx +1 -1
- package/src/motion/ik-pick.js +24 -0
- package/src/motion/take-segments.jsx +5 -5
- package/src/motion/timeline.jsx +30 -40
- package/src/motion/waypoints.js +6 -6
- package/src/motion3/compile-job.js +15 -3
- package/src/motion3/compiler/channels.js +74 -24
- package/src/motion3/compiler/compile.js +2 -1
- package/src/motion3/compiler/idle.js +2 -0
- package/src/motion3/compiler/overlap.js +180 -0
- package/src/motion3/compiler/pose.js +6 -4
- package/src/motion3/compiler/validate-common.js +1 -0
- package/src/motion3/compiler/validate-parts.js +5 -1
- package/src/motion3/compiler/validate.js +30 -10
- package/src/motion3/correct.js +236 -0
- package/src/motion3/docs.js +8 -6
- package/src/motion3/face-lean.js +31 -0
- package/src/motion3/fix/index.js +5 -1
- package/src/motion3/fix/reach-body.js +172 -0
- package/src/motion3/hint-check.js +44 -0
- package/src/motion3/json-patch.js +120 -12
- package/src/motion3/lenient.js +46 -4
- package/src/motion3/object-keys.js +17 -5
- package/src/motion3/pipeline.js +46 -9
- package/src/motion3/qa/arm-checks.js +96 -0
- package/src/motion3/qa/asked.js +113 -0
- package/src/motion3/qa/goals.js +37 -8
- package/src/motion3/qa/hints-goal.js +6 -5
- package/src/motion3/qa/index.js +5 -2
- package/src/motion3/qa/key-checks.js +4 -3
- package/src/motion3/qa/motion-life.js +243 -0
- package/src/motion3/qa/spec-checks.js +15 -9
- package/src/motion3/qa/verdict.js +50 -0
- package/src/motion3/refs.js +38 -14
- package/src/motion3/repair-watch.js +132 -0
- package/src/motion3/repair.js +52 -19
- package/src/motion3/report.js +50 -9
- package/src/motion3/schema/topics.js +6 -4
- package/src/motion3/schema/types.js +1 -1
- package/src/motion3/skills/basic.js +15 -8
- package/src/motion3/skills/index.js +52 -18
- package/src/motion3/skills/info.js +1 -1
- package/src/motion3/skills/stance.js +6 -0
- package/src/motion3/skills/walk-fit.js +170 -0
- package/src/object-catalog.jsx +4 -5
- package/src/panels/CameraPanel.jsx +3 -3
- package/src/panels/HdrEnvironmentControls.jsx +2 -1
- package/src/panels/ObjectTransformPanel.jsx +19 -29
- package/src/panels/PoseImagePanel.jsx +11 -10
- package/src/panels/PosePanel.jsx +2 -2
- package/src/panels/ReferenceImageField.jsx +2 -2
- package/src/panels/RigControlPanel.jsx +2 -2
- package/src/panels/SculptPanel.jsx +11 -11
- package/src/panels/SubjectBox.jsx +3 -3
- package/src/panels/SubjectsPanel.jsx +1 -1
- package/src/posestudio.jsx +10 -8
- package/src/project-browser.css +7 -6
- package/src/project-browser.jsx +7 -6
- package/src/project-open-file.js +19 -0
- package/src/qa-label.js +1 -1
- package/src/range-pin-panel.jsx +14 -14
- package/src/resource-status.jsx +3 -3
- package/src/result-modal.jsx +13 -13
- package/src/settings-menu.jsx +4 -1
- package/src/shell/BoardSlot.jsx +5 -5
- package/src/shell/CompositionGuideButton.jsx +1 -1
- package/src/shell/ExportFolderPreference.jsx +103 -0
- package/src/shell/GhostsMenuGroup.jsx +1 -1
- package/src/shell/PreferencesDialog.jsx +3 -166
- package/src/shell/StatusBar.jsx +4 -4
- package/src/shell/ViewportToolbar.jsx +3 -3
- package/src/shell/add-camera-shot.js +2 -2
- package/src/shell/details-selection.js +1 -1
- package/src/shell/preference-controls.jsx +115 -0
- package/src/shell/preference-pages.jsx +72 -0
- package/src/shell/preferences.css +46 -0
- package/src/shot-guides.js +5 -5
- package/src/shot-prompt.js +29 -9
- package/src/stage-hdr.js +3 -3
- package/src/stage-labels-ko.js +2 -2
- package/src/stage-startup-defaults.js +3 -3
- package/src/studio-actions.js +4 -4
- package/src/studio-agent-motion.js +19 -9
- package/src/studio-agent-protocol.js +1 -1
- package/src/studio-app-binding.js +3 -2
- package/src/styles/tokens.css +1 -1
- package/src/styles.css +1 -1
- package/src/use-case-question.jsx +18 -18
- package/src/workflow/AgentPanel.jsx +6 -6
- package/src/workflow/AgentPlanCard.jsx +1 -1
- package/src/workflow/AgentProgressCard.jsx +4 -4
- package/src/workflow/AgentTurnSteps.jsx +1 -1
- package/src/workflow/CozySceneNode.jsx +1 -1
- package/src/workflow/LoginPanel.jsx +1 -1
- package/src/workflow/TaskBoard.jsx +1 -1
- package/src/workflow/TaskInline.jsx +1 -1
- package/src/workflow/WorkflowBuilder.jsx +2 -2
- package/src/workflow/agent-client.js +1 -1
- package/src/workflow/agent-turn.js +5 -5
- package/src/workflow/keyframe-pack-request.js +3 -10
- package/src/workflow/task-board.css +1 -1
- package/src/workflow/task-board.js +11 -9
- package/tools/loc-baseline.json +2 -3
- package/tools/run-tests.mjs +9 -9
- package/dist/assets/TaskBoard-ngdqiu7u.js +0 -1
- package/dist/assets/app-BlwOpest.css +0 -1
- package/dist/assets/app-DmR7jBkz.js +0 -5144
- package/dist/assets/compile-worker-D0pKioa0.js +0 -5
- package/dist/assets/motion3-compile-BB3_1-EB.js +0 -1
- package/dist/assets/scene-asset-sync-CBx7XRti.css +0 -1
- package/dist/assets/scene-asset-sync-Cgdtr1Yw.js +0 -13
- package/dist/assets/task-board-ErQvfoaz.js +0 -14
- package/dist/assets/workflow-zQM-EKW1.js +0 -179
|
@@ -1,185 +1,22 @@
|
|
|
1
1
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { createPortal } from "react-dom";
|
|
3
|
-
import {
|
|
4
|
-
import { loadAutoColor,
|
|
5
|
-
import { ko, LOCALE, setLocale } from "../locale.js";
|
|
6
|
-
import { readStoredGridView, writeStoredGridView } from "../grid-view.js";
|
|
7
|
-
import { readTheme, saveTheme } from "../theme.js";
|
|
3
|
+
import { ko } from "../locale.js";
|
|
4
|
+
import { getAnalyticsOptOut, loadAutoColor, preferenceSection, readStoredGridView } from "./preference-pages.jsx";
|
|
8
5
|
import "./preferences.css";
|
|
9
6
|
|
|
10
|
-
const LANGUAGES = [
|
|
11
|
-
{ id: "en", label: "English", action: "Switch to English" },
|
|
12
|
-
{ id: "ko", label: "한국어", action: "한국어로 전환" },
|
|
13
|
-
];
|
|
14
|
-
|
|
15
|
-
// Only pages whose settings reach the editor are listed; a control that
|
|
16
|
-
// changes nothing is not offered.
|
|
17
7
|
const NAV_GROUPS = [
|
|
18
8
|
{
|
|
19
9
|
label: ko("General", "일반"),
|
|
20
10
|
items: [
|
|
21
11
|
{ id: "Appearance", label: ko("Appearance", "모양") },
|
|
22
12
|
{ id: "Viewport", label: ko("Viewport", "뷰포트") },
|
|
13
|
+
{ id: "Export", label: ko("Export", "내보내기") },
|
|
23
14
|
{ id: "Hotkeys", label: ko("Hotkeys", "단축키") },
|
|
24
15
|
],
|
|
25
16
|
},
|
|
26
17
|
];
|
|
27
18
|
const NAV_LABELS = Object.fromEntries(NAV_GROUPS.flatMap((group) => group.items.map((item) => [item.id, item.label])));
|
|
28
19
|
|
|
29
|
-
const MOD = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform ?? "") ? "⌘" : "Ctrl ";
|
|
30
|
-
|
|
31
|
-
const HOTKEYS = [
|
|
32
|
-
["Move", "W", "이동"],
|
|
33
|
-
["Rotate", "E", "회전"],
|
|
34
|
-
["Scale", "R", "크기 조절"],
|
|
35
|
-
["Frame selection", "F", "선택 프레임"],
|
|
36
|
-
["Duplicate", `${MOD}D`, "복제"],
|
|
37
|
-
["Drop to floor", "End", "바닥에 놓기"],
|
|
38
|
-
];
|
|
39
|
-
|
|
40
|
-
function PreferenceRow({ title, description, children, testid }) {
|
|
41
|
-
return (
|
|
42
|
-
<div className="v2-preferences__row" data-testid={testid}>
|
|
43
|
-
<div className="v2-preferences__row-copy">
|
|
44
|
-
<span className="v2-preferences__row-title">{title}</span>
|
|
45
|
-
{description && <span className="v2-preferences__row-description">{description}</span>}
|
|
46
|
-
</div>
|
|
47
|
-
<div className="v2-preferences__row-control">{children}</div>
|
|
48
|
-
</div>
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// A page with one section leaves out its title: the page header already names it.
|
|
53
|
-
function PreferenceSection({ title, children }) {
|
|
54
|
-
return (
|
|
55
|
-
<section className="v2-preferences__section">
|
|
56
|
-
{title && <h2 className="v2-preferences__section-title">{title}</h2>}
|
|
57
|
-
<div className="v2-preferences__section-rows">{children}</div>
|
|
58
|
-
</section>
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function ToggleControl({ pressed, onChange, label, testid }) {
|
|
63
|
-
return (
|
|
64
|
-
<button
|
|
65
|
-
type="button"
|
|
66
|
-
className="v2-preferences__toggle"
|
|
67
|
-
role="switch"
|
|
68
|
-
aria-checked={pressed}
|
|
69
|
-
aria-pressed={pressed}
|
|
70
|
-
aria-label={label}
|
|
71
|
-
data-testid={testid}
|
|
72
|
-
onClick={() => onChange(!pressed)}
|
|
73
|
-
>
|
|
74
|
-
<span className="v2-preferences__toggle-thumb" aria-hidden="true" />
|
|
75
|
-
</button>
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function ReadonlyHotkeys() {
|
|
80
|
-
return (
|
|
81
|
-
<div className="v2-preferences__hotkeys" data-testid="preferences-hotkeys">
|
|
82
|
-
{HOTKEYS.map(([english, key, korean]) => (
|
|
83
|
-
<div className="v2-preferences__hotkey" key={english}>
|
|
84
|
-
<span>{ko(english, korean)}</span>
|
|
85
|
-
<kbd>{key}</kbd>
|
|
86
|
-
</div>
|
|
87
|
-
))}
|
|
88
|
-
</div>
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function ThemeControls() {
|
|
93
|
-
const [theme, setTheme] = useState(readTheme);
|
|
94
|
-
const options = [
|
|
95
|
-
{ id: "dark", label: ko("Dark", "다크") },
|
|
96
|
-
{ id: "light", label: ko("Light", "라이트") },
|
|
97
|
-
];
|
|
98
|
-
return (
|
|
99
|
-
<div className="v2-preferences__language-controls" role="group" aria-label={ko("Theme", "테마")}>
|
|
100
|
-
{options.map((option) => (
|
|
101
|
-
<button
|
|
102
|
-
type="button"
|
|
103
|
-
key={option.id}
|
|
104
|
-
className={"v2-preferences__language" + (theme === option.id ? " is-selected" : "")}
|
|
105
|
-
data-testid={`settings-theme-${option.id}`}
|
|
106
|
-
aria-pressed={theme === option.id}
|
|
107
|
-
onClick={() => setTheme(saveTheme(option.id))}
|
|
108
|
-
>
|
|
109
|
-
{option.label}
|
|
110
|
-
</button>
|
|
111
|
-
))}
|
|
112
|
-
</div>
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function preferenceSection(section, props) {
|
|
117
|
-
const { autoColor, setAutoColor, analyticsOptOut, setAnalyticsOptOutState, gridView, setGridView } = props;
|
|
118
|
-
const languageControls = (
|
|
119
|
-
<div className="v2-preferences__language-controls">
|
|
120
|
-
{LANGUAGES.map((language) => (
|
|
121
|
-
<button
|
|
122
|
-
type="button"
|
|
123
|
-
key={language.id}
|
|
124
|
-
className={"v2-preferences__language" + (LOCALE === language.id ? " is-selected" : "")}
|
|
125
|
-
data-testid={`settings-locale-${language.id}`}
|
|
126
|
-
aria-pressed={LOCALE === language.id}
|
|
127
|
-
title={language.action}
|
|
128
|
-
onClick={() => setLocale(language.id)}
|
|
129
|
-
>
|
|
130
|
-
{language.label}
|
|
131
|
-
</button>
|
|
132
|
-
))}
|
|
133
|
-
</div>
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
switch (section) {
|
|
137
|
-
case "Appearance":
|
|
138
|
-
return (
|
|
139
|
-
<>
|
|
140
|
-
<PreferenceSection title={ko("Display", "표시")}>
|
|
141
|
-
<PreferenceRow title={ko("Language", "언어")} description={ko("Choose the labels used throughout the editor.", "에디터에서 사용할 언어를 선택하세요.")}>
|
|
142
|
-
{languageControls}
|
|
143
|
-
</PreferenceRow>
|
|
144
|
-
<PreferenceRow title={ko("Theme", "테마")} description={ko("Dark keeps the studio dim around the stage; Light uses bright glass panels.", "다크는 무대 주변을 어둡게, 라이트는 밝은 유리 패널을 써요.")}>
|
|
145
|
-
<ThemeControls />
|
|
146
|
-
</PreferenceRow>
|
|
147
|
-
</PreferenceSection>
|
|
148
|
-
<PreferenceSection title={ko("Editor", "에디터")}>
|
|
149
|
-
<PreferenceRow title={ko("Auto colour", "자동 색")} description={ko("Give scene objects stable display colours.", "씬 오브젝트에 고정된 표시 색을 사용해요.")}>
|
|
150
|
-
<ToggleControl pressed={autoColor} onChange={(value) => { setAutoColor(value); saveAutoColor(value); }} label={ko("Auto colour", "자동 색")} testid="preferences-auto-color" />
|
|
151
|
-
</PreferenceRow>
|
|
152
|
-
<PreferenceRow title={ko("Anonymous analytics", "익명 사용 통계")} description={ko("Help improve CozyClay with anonymous product events.", "익명 제품 이벤트로 CozyClay 개선을 도와요.")}>
|
|
153
|
-
<ToggleControl
|
|
154
|
-
pressed={!analyticsOptOut}
|
|
155
|
-
onChange={async (value) => setAnalyticsOptOutState(await setAnalyticsOptOut(!value))}
|
|
156
|
-
label={ko("Anonymous analytics", "익명 사용 통계")}
|
|
157
|
-
testid="settings-analytics"
|
|
158
|
-
/>
|
|
159
|
-
</PreferenceRow>
|
|
160
|
-
</PreferenceSection>
|
|
161
|
-
</>
|
|
162
|
-
);
|
|
163
|
-
case "Viewport":
|
|
164
|
-
return (
|
|
165
|
-
<PreferenceSection>
|
|
166
|
-
<PreferenceRow title={ko("Reference grid", "기준 그리드")} description={ko("Use a dark reference grid instead of the clay deck.", "클레이 데크 대신 어두운 기준 그리드를 사용해요.")}>
|
|
167
|
-
<ToggleControl pressed={gridView} onChange={(value) => { setGridView(value); writeStoredGridView(globalThis.localStorage, value); }} label={ko("Reference grid", "기준 그리드")} testid="preferences-grid" />
|
|
168
|
-
</PreferenceRow>
|
|
169
|
-
</PreferenceSection>
|
|
170
|
-
);
|
|
171
|
-
case "Hotkeys":
|
|
172
|
-
return (
|
|
173
|
-
<PreferenceSection>
|
|
174
|
-
<p className="v2-preferences__section-note">{ko("Shortcuts are read-only here. They stay active whenever focus is outside a text field.", "단축키는 여기서 읽기 전용이에요. 텍스트 입력란 밖에서 항상 사용할 수 있어요.")}</p>
|
|
175
|
-
<ReadonlyHotkeys />
|
|
176
|
-
</PreferenceSection>
|
|
177
|
-
);
|
|
178
|
-
default:
|
|
179
|
-
return null;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
20
|
export default function PreferencesDialog({ open, onClose, initialSection = "Appearance" }) {
|
|
184
21
|
const dialogRef = useRef(null);
|
|
185
22
|
const searchRef = useRef(null);
|
package/src/shell/StatusBar.jsx
CHANGED
|
@@ -47,7 +47,7 @@ function ExportStatus({ status, label, recording }) {
|
|
|
47
47
|
const detail = [
|
|
48
48
|
message,
|
|
49
49
|
retryable && ko("Retry keeps the original shot, camera, range and settings; it does not change your edits.", "다시 시도하면 원래 샷·카메라·범위·설정을 사용하며 편집 내용은 바꾸지 않아요."),
|
|
50
|
-
handedOff > 0 && ko(`${handedOff} file(s) already handed off. Retry skips those
|
|
50
|
+
handedOff > 0 && ko(`${handedOff} file(s) already handed off. Retry skips those files.`, `파일 ${handedOff}개는 이미 전달했어요. 다시 시도할 때 해당 파일은 건너뛰어요.`, `已交付 ${handedOff} 个文件。重试时会跳过这些文件。`),
|
|
51
51
|
busy && stage === "mux" && ko("Finalizing MP4; this stage cannot be interrupted.", "MP4 마무리 중이에요. 이 단계는 중단할 수 없어요."),
|
|
52
52
|
].filter(Boolean).join(" ");
|
|
53
53
|
return (
|
|
@@ -86,9 +86,9 @@ function ExportStatus({ status, label, recording }) {
|
|
|
86
86
|
function relativeTime(ms) {
|
|
87
87
|
const minutes = Math.floor(ms / 60000);
|
|
88
88
|
if (minutes < 1) return ko("just now", "방금");
|
|
89
|
-
if (minutes < 60) return ko(`${minutes} min ago`, `${minutes}분
|
|
89
|
+
if (minutes < 60) return ko(`${minutes} min ago`, `${minutes}분 전`, `${minutes} 分钟前`);
|
|
90
90
|
const hours = Math.floor(minutes / 60);
|
|
91
|
-
return ko(`${hours} h ago`, `${hours}시간
|
|
91
|
+
return ko(`${hours} h ago`, `${hours}시간 전`, `${hours} 小时前`);
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
// "Saved 2 min ago" once a save completes in this session; otherwise the
|
|
@@ -109,7 +109,7 @@ export function SaveState({ status }) {
|
|
|
109
109
|
return () => clearInterval(timer);
|
|
110
110
|
}, [savedAt]);
|
|
111
111
|
const text = savedAt !== null && status.state === "saved" && !status.dirty
|
|
112
|
-
? ko(`Saved ${relativeTime(now - savedAt)}`, `저장됨 · ${relativeTime(now - savedAt)}`)
|
|
112
|
+
? ko(`Saved ${relativeTime(now - savedAt)}`, `저장됨 · ${relativeTime(now - savedAt)}`, `已保存 · ${relativeTime(now - savedAt)}`)
|
|
113
113
|
: status.text;
|
|
114
114
|
return (
|
|
115
115
|
<span
|
|
@@ -177,7 +177,7 @@ export default function ViewportToolbar() {
|
|
|
177
177
|
aria-pressed={snapEnabled}
|
|
178
178
|
title={ko(
|
|
179
179
|
`Grid snapping ${snapEnabled ? "on" : "off"} (${SNAP_MOVE} · ${SNAP_TURN}) — click to toggle, hold Ctrl during a drag to invert`,
|
|
180
|
-
`그리드 스냅 ${snapEnabled ? "켜짐" : "꺼짐"} (${SNAP_MOVE} · ${SNAP_TURN}) — 클릭해 전환, 드래그 중 Ctrl을 누르면 반대로 작동`,
|
|
180
|
+
`그리드 스냅 ${snapEnabled ? "켜짐" : "꺼짐"} (${SNAP_MOVE} · ${SNAP_TURN}) — 클릭해 전환, 드래그 중 Ctrl을 누르면 반대로 작동`, `网格吸附 ${snapEnabled ? "开" : "关"}(${SNAP_MOVE} · ${SNAP_TURN})— 点击切换,拖动时按住 Ctrl 反向`,
|
|
181
181
|
)}
|
|
182
182
|
onClick={() => setSnapEnabled((value) => !value)}
|
|
183
183
|
>
|
|
@@ -524,12 +524,12 @@ export default function ViewportToolbar() {
|
|
|
524
524
|
className={"view-menu-item vp-guide-cycle" + (guideMode === "off" ? "" : " active")}
|
|
525
525
|
aria-checked={guideMode !== "off"}
|
|
526
526
|
aria-pressed={guideMode !== "off"}
|
|
527
|
-
title={ko(GUIDE_LABELS[guideMode].en, GUIDE_LABELS[guideMode].ko) + ko(" · click to cycle", " · 클릭으로 전환")}
|
|
527
|
+
title={ko(GUIDE_LABELS[guideMode].en, GUIDE_LABELS[guideMode].ko, GUIDE_LABELS[guideMode].zh) + ko(" · click to cycle", " · 클릭으로 전환")}
|
|
528
528
|
onClick={() => runStudioAction("view.setGuideMode", { mode: nextGuideMode(guideMode) })}
|
|
529
529
|
>
|
|
530
530
|
<span className="view-menu-mark" aria-hidden="true">{guideMode !== "off" ? "✓" : ""}</span>
|
|
531
531
|
{ko("Composition guides", "구도 가이드")}
|
|
532
|
-
<small>{ko(GUIDE_LABELS[guideMode].en, GUIDE_LABELS[guideMode].ko)}</small>
|
|
532
|
+
<small>{ko(GUIDE_LABELS[guideMode].en, GUIDE_LABELS[guideMode].ko, GUIDE_LABELS[guideMode].zh)}</small>
|
|
533
533
|
</button>
|
|
534
534
|
<GhostsMenuGroup />
|
|
535
535
|
{/* Part colours repaint a BODY, so the section only exists
|
|
@@ -6,6 +6,6 @@ export function addCameraShot({ appContext, runStudioAction, setToast }) {
|
|
|
6
6
|
const made = shots.read().find((row) => row.id === receipt?.affectedIds.find((id) => !before.some((old) => old.id === id)));
|
|
7
7
|
if (!made) return;
|
|
8
8
|
setToast(made.kind === "still"
|
|
9
|
-
? ko(`${made.name} added — a storyboard panel at frame ${made.startFrame}`, `${made.name} 추가됨 — ${made.startFrame}프레임의 스토리보드
|
|
10
|
-
: ko(`${made.name} added — a camera cut at frame ${made.startFrame}`, `${made.name} 추가됨 — ${made.startFrame}프레임에서 카메라
|
|
9
|
+
? ko(`${made.name} added — a storyboard panel at frame ${made.startFrame}`, `${made.name} 추가됨 — ${made.startFrame}프레임의 스토리보드 패널`, `${made.name} 已添加 — 第 ${made.startFrame} 帧的故事板面板`)
|
|
10
|
+
: ko(`${made.name} added — a camera cut at frame ${made.startFrame}`, `${made.name} 추가됨 — ${made.startFrame}프레임에서 카메라 컷`, `${made.name} 已添加 — 第 ${made.startFrame} 帧的摄影机剪辑`));
|
|
11
11
|
}
|
|
@@ -9,7 +9,7 @@ import { LIGHT_KIND } from "../object-look.js";
|
|
|
9
9
|
const characterIndexForRow = (rowId, characters) => rowId === "characterA" ? 0
|
|
10
10
|
: rowId === "characterB" ? 1
|
|
11
11
|
: characters.findIndex((entry) => `character:${entry.id}` === rowId);
|
|
12
|
-
const characterTitle = (index) => ko(`Character ${index + 1}`, `인물 ${index + 1}`);
|
|
12
|
+
const characterTitle = (index) => ko(`Character ${index + 1}`, `인물 ${index + 1}`, `角色 ${index + 1}`);
|
|
13
13
|
|
|
14
14
|
/** The 2a selection header: the selected item's name and a "Kind · detail"
|
|
15
15
|
* type line. Null when nothing that owns settings is selected. */
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { ko, LOCALE, setLocale } from "../locale.js";
|
|
3
|
+
import { readTheme, saveTheme } from "../theme.js";
|
|
4
|
+
|
|
5
|
+
const MOD = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform ?? "") ? "⌘" : "Ctrl ";
|
|
6
|
+
|
|
7
|
+
const HOTKEYS = [
|
|
8
|
+
["Move", "W", "이동"],
|
|
9
|
+
["Rotate", "E", "회전"],
|
|
10
|
+
["Scale", "R", "크기 조절"],
|
|
11
|
+
["Frame selection", "F", "선택 프레임"],
|
|
12
|
+
["Duplicate", `${MOD}D`, "복제"],
|
|
13
|
+
["Drop to floor", "End", "바닥에 놓기"],
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export function PreferenceRow({ title, description, children, testid, full = false }) {
|
|
17
|
+
return (
|
|
18
|
+
<div className={"v2-preferences__row" + (full ? " v2-preferences__row--full" : "")} data-testid={testid}>
|
|
19
|
+
<div className="v2-preferences__row-copy">
|
|
20
|
+
<span className="v2-preferences__row-title">{title}</span>
|
|
21
|
+
{description && <span className="v2-preferences__row-description">{description}</span>}
|
|
22
|
+
</div>
|
|
23
|
+
<div className="v2-preferences__row-control">{children}</div>
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// A page with one section leaves out its title: the page header already names it.
|
|
29
|
+
export function PreferenceSection({ title, children }) {
|
|
30
|
+
return (
|
|
31
|
+
<section className="v2-preferences__section">
|
|
32
|
+
{title && <h2 className="v2-preferences__section-title">{title}</h2>}
|
|
33
|
+
<div className="v2-preferences__section-rows">{children}</div>
|
|
34
|
+
</section>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function ToggleControl({ pressed, onChange, label, testid }) {
|
|
39
|
+
return (
|
|
40
|
+
<button
|
|
41
|
+
type="button"
|
|
42
|
+
className="v2-preferences__toggle"
|
|
43
|
+
role="switch"
|
|
44
|
+
aria-checked={pressed}
|
|
45
|
+
aria-pressed={pressed}
|
|
46
|
+
aria-label={label}
|
|
47
|
+
data-testid={testid}
|
|
48
|
+
onClick={() => onChange(!pressed)}
|
|
49
|
+
>
|
|
50
|
+
<span className="v2-preferences__toggle-thumb" aria-hidden="true" />
|
|
51
|
+
</button>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function ThemeControls() {
|
|
56
|
+
const [theme, setTheme] = useState(readTheme);
|
|
57
|
+
const options = [
|
|
58
|
+
{ id: "dark", label: ko("Dark", "다크") },
|
|
59
|
+
{ id: "light", label: ko("Light", "라이트", "浅色") },
|
|
60
|
+
];
|
|
61
|
+
return (
|
|
62
|
+
<div className="v2-preferences__language-controls" role="group" aria-label={ko("Theme", "테마")}>
|
|
63
|
+
{options.map((option) => (
|
|
64
|
+
<button
|
|
65
|
+
type="button"
|
|
66
|
+
key={option.id}
|
|
67
|
+
className={"v2-preferences__language" + (theme === option.id ? " is-selected" : "")}
|
|
68
|
+
data-testid={`settings-theme-${option.id}`}
|
|
69
|
+
aria-pressed={theme === option.id}
|
|
70
|
+
onClick={() => setTheme(saveTheme(option.id))}
|
|
71
|
+
>
|
|
72
|
+
{option.label}
|
|
73
|
+
</button>
|
|
74
|
+
))}
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function LanguageControls() {
|
|
80
|
+
const options = [
|
|
81
|
+
{ id: "en", label: "English", action: "Switch to English" },
|
|
82
|
+
{ id: "ko", label: "한국어", action: "한국어로 전환" },
|
|
83
|
+
{ id: "zh", label: "简体中文", action: "切换到中文" },
|
|
84
|
+
];
|
|
85
|
+
return (
|
|
86
|
+
<div className="v2-preferences__language-controls">
|
|
87
|
+
{options.map((language) => (
|
|
88
|
+
<button
|
|
89
|
+
type="button"
|
|
90
|
+
key={language.id}
|
|
91
|
+
className={"v2-preferences__language" + (LOCALE === language.id ? " is-selected" : "")}
|
|
92
|
+
data-testid={`settings-locale-${language.id}`}
|
|
93
|
+
aria-pressed={LOCALE === language.id}
|
|
94
|
+
title={language.action}
|
|
95
|
+
onClick={() => setLocale(language.id)}
|
|
96
|
+
>
|
|
97
|
+
{language.label}
|
|
98
|
+
</button>
|
|
99
|
+
))}
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function ReadonlyHotkeys() {
|
|
105
|
+
return (
|
|
106
|
+
<div className="v2-preferences__hotkeys" data-testid="preferences-hotkeys">
|
|
107
|
+
{HOTKEYS.map(([english, key, korean]) => (
|
|
108
|
+
<div className="v2-preferences__hotkey" key={english}>
|
|
109
|
+
<span>{ko(english, korean)}</span>
|
|
110
|
+
<kbd>{key}</kbd>
|
|
111
|
+
</div>
|
|
112
|
+
))}
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { getAnalyticsOptOut, setAnalyticsOptOut } from "../analytics.js";
|
|
2
|
+
import { loadAutoColor, saveAutoColor } from "../auto-color.js";
|
|
3
|
+
import { readStoredGridView, writeStoredGridView } from "../grid-view.js";
|
|
4
|
+
import { ko } from "../locale.js";
|
|
5
|
+
import ExportFolderPreference from "./ExportFolderPreference.jsx";
|
|
6
|
+
import {
|
|
7
|
+
LanguageControls,
|
|
8
|
+
PreferenceRow,
|
|
9
|
+
PreferenceSection,
|
|
10
|
+
ReadonlyHotkeys,
|
|
11
|
+
ThemeControls,
|
|
12
|
+
ToggleControl,
|
|
13
|
+
} from "./preference-controls.jsx";
|
|
14
|
+
|
|
15
|
+
export function preferenceSection(section, props) {
|
|
16
|
+
const { autoColor, setAutoColor, analyticsOptOut, setAnalyticsOptOutState, gridView, setGridView } = props;
|
|
17
|
+
|
|
18
|
+
switch (section) {
|
|
19
|
+
case "Appearance":
|
|
20
|
+
return (
|
|
21
|
+
<>
|
|
22
|
+
<PreferenceSection title={ko("Display", "표시")}>
|
|
23
|
+
<PreferenceRow title={ko("Language", "언어")} description={ko("Choose the labels used throughout the editor.", "에디터에서 사용할 언어를 선택하세요.")}>
|
|
24
|
+
<LanguageControls />
|
|
25
|
+
</PreferenceRow>
|
|
26
|
+
<PreferenceRow title={ko("Theme", "테마")} description={ko("Dark keeps the studio dim around the stage; Light uses bright glass panels.", "다크는 무대 주변을 어둡게, 라이트는 밝은 유리 패널을 써요.")}>
|
|
27
|
+
<ThemeControls />
|
|
28
|
+
</PreferenceRow>
|
|
29
|
+
</PreferenceSection>
|
|
30
|
+
<PreferenceSection title={ko("Editor", "에디터")}>
|
|
31
|
+
<PreferenceRow title={ko("Auto colour", "자동 색")} description={ko("Give scene objects stable display colours.", "씬 오브젝트에 고정된 표시 색을 사용해요.")}>
|
|
32
|
+
<ToggleControl pressed={autoColor} onChange={(value) => { setAutoColor(value); saveAutoColor(value); }} label={ko("Auto colour", "자동 색")} testid="preferences-auto-color" />
|
|
33
|
+
</PreferenceRow>
|
|
34
|
+
<PreferenceRow title={ko("Anonymous analytics", "익명 사용 통계")} description={ko("Help improve CozyClay with anonymous product events.", "익명 제품 이벤트로 CozyClay 개선을 도와요.")}>
|
|
35
|
+
<ToggleControl
|
|
36
|
+
pressed={!analyticsOptOut}
|
|
37
|
+
onChange={async (value) => setAnalyticsOptOutState(await setAnalyticsOptOut(!value))}
|
|
38
|
+
label={ko("Anonymous analytics", "익명 사용 통계")}
|
|
39
|
+
testid="settings-analytics"
|
|
40
|
+
/>
|
|
41
|
+
</PreferenceRow>
|
|
42
|
+
</PreferenceSection>
|
|
43
|
+
</>
|
|
44
|
+
);
|
|
45
|
+
case "Viewport":
|
|
46
|
+
return (
|
|
47
|
+
<PreferenceSection>
|
|
48
|
+
<PreferenceRow title={ko("Reference grid", "기준 그리드")} description={ko("Use a dark reference grid instead of the clay deck.", "클레이 데크 대신 어두운 기준 그리드를 사용해요.")}>
|
|
49
|
+
<ToggleControl pressed={gridView} onChange={(value) => { setGridView(value); writeStoredGridView(globalThis.localStorage, value); }} label={ko("Reference grid", "기준 그리드")} testid="preferences-grid" />
|
|
50
|
+
</PreferenceRow>
|
|
51
|
+
</PreferenceSection>
|
|
52
|
+
);
|
|
53
|
+
case "Export":
|
|
54
|
+
return (
|
|
55
|
+
<PreferenceSection>
|
|
56
|
+
<ExportFolderPreference />
|
|
57
|
+
</PreferenceSection>
|
|
58
|
+
);
|
|
59
|
+
case "Hotkeys":
|
|
60
|
+
return (
|
|
61
|
+
<PreferenceSection>
|
|
62
|
+
<p className="v2-preferences__section-note">{ko("Shortcuts are read-only here. They stay active whenever focus is outside a text field.", "단축키는 여기서 읽기 전용이에요. 텍스트 입력란 밖에서 항상 사용할 수 있어요.")}</p>
|
|
63
|
+
<ReadonlyHotkeys />
|
|
64
|
+
</PreferenceSection>
|
|
65
|
+
);
|
|
66
|
+
default:
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Kept here so the dialog can load a new setting's initial value lazily.
|
|
72
|
+
export { getAnalyticsOptOut, loadAutoColor, readStoredGridView };
|
|
@@ -623,3 +623,49 @@
|
|
|
623
623
|
grid-template-columns: 1fr;
|
|
624
624
|
}
|
|
625
625
|
}
|
|
626
|
+
|
|
627
|
+
/* Export folder: full-width row because folder names and three controls do not
|
|
628
|
+
fit the 240px control column used by toggles and language pairs. */
|
|
629
|
+
.v2-preferences .v2-preferences__row--full .v2-preferences__row-copy {
|
|
630
|
+
margin-bottom: var(--sp-2, 12px);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
.v2-preferences .v2-preferences__row--full .v2-preferences__row-control {
|
|
634
|
+
justify-content: flex-start;
|
|
635
|
+
flex-wrap: wrap;
|
|
636
|
+
width: 100%;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
.v2-preferences .v2-preferences__export-folder {
|
|
640
|
+
display: flex;
|
|
641
|
+
flex-direction: column;
|
|
642
|
+
align-items: flex-start;
|
|
643
|
+
gap: var(--sp-1, 8px);
|
|
644
|
+
min-width: 0;
|
|
645
|
+
width: 100%;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
.v2-preferences .v2-preferences__export-folder .v2-preferences__language-controls {
|
|
649
|
+
justify-content: flex-start;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
.v2-preferences .v2-preferences__export-folder-name {
|
|
653
|
+
color: var(--text-1, #e4e4e7);
|
|
654
|
+
font-size: var(--prefs-ui-size, 12px);
|
|
655
|
+
line-height: 1.35;
|
|
656
|
+
overflow-wrap: anywhere;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
.v2-preferences .v2-preferences__export-folder-status {
|
|
660
|
+
color: var(--select, #e8a33d);
|
|
661
|
+
font-size: var(--prefs-secondary-size, 11px);
|
|
662
|
+
line-height: 1.35;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
.v2-preferences .v2-preferences__export-folder-note {
|
|
666
|
+
display: block;
|
|
667
|
+
margin-top: var(--sp-1, 8px);
|
|
668
|
+
color: var(--text-3, #636369);
|
|
669
|
+
font-size: var(--prefs-secondary-size, 11px);
|
|
670
|
+
line-height: 1.35;
|
|
671
|
+
}
|
package/src/shot-guides.js
CHANGED
|
@@ -9,11 +9,11 @@ export const GUIDE_STORAGE_KEY = "cozyclay.shot-guides.v1";
|
|
|
9
9
|
export const GUIDE_MODES = Object.freeze(["off", "thirds", "golden", "center", "safe"]);
|
|
10
10
|
|
|
11
11
|
export const GUIDE_LABELS = Object.freeze({
|
|
12
|
-
off: Object.freeze({ en: "Guides off", ko: "가이드 끔" }),
|
|
13
|
-
thirds: Object.freeze({ en: "Thirds", ko: "삼분할" }),
|
|
14
|
-
golden: Object.freeze({ en: "Golden ratio", ko: "골든 레이쇼" }),
|
|
15
|
-
center: Object.freeze({ en: "Center + diagonals", ko: "센터·대각선" }),
|
|
16
|
-
safe: Object.freeze({ en: "Safe areas", ko: "세이프 에어리어" }),
|
|
12
|
+
off: Object.freeze({ en: "Guides off", ko: "가이드 끔", zh: "参考线关闭" }),
|
|
13
|
+
thirds: Object.freeze({ en: "Thirds", ko: "삼분할", zh: "三分法" }),
|
|
14
|
+
golden: Object.freeze({ en: "Golden ratio", ko: "골든 레이쇼", zh: "黄金比例" }),
|
|
15
|
+
center: Object.freeze({ en: "Center + diagonals", ko: "센터·대각선", zh: "中心 + 对角线" }),
|
|
16
|
+
safe: Object.freeze({ en: "Safe areas", ko: "세이프 에어리어", zh: "安全框" }),
|
|
17
17
|
});
|
|
18
18
|
|
|
19
19
|
export function nextGuideMode(mode) {
|
package/src/shot-prompt.js
CHANGED
|
@@ -1,20 +1,40 @@
|
|
|
1
1
|
// Builds the labelled text prompt sent alongside a reference clip when a shot
|
|
2
|
-
// goes to an image or video model. Pure string building: no DOM
|
|
2
|
+
// goes to an image or video model. Pure string building: no DOM; the only
|
|
3
|
+
// import is the editor's own lens math, so the prompt and the viewport share
|
|
4
|
+
// one focal <-> fov calibration.
|
|
5
|
+
import { fovToFocalMm, shotAspectRatio } from "./shot.js";
|
|
3
6
|
|
|
4
7
|
const CAMERA_FOLLOWS_REFERENCE = "follow the reference clip's framing and camera motion exactly";
|
|
5
8
|
const NEUTRAL_LOOK = "clay previs, neutral look";
|
|
6
9
|
const MOTION_LINE = "motion and blocking come from the reference; keep timing";
|
|
7
10
|
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
export function
|
|
11
|
-
if (!Number.isFinite(
|
|
12
|
-
|
|
13
|
-
if (
|
|
14
|
-
if (equivalentFocalMm <= 40) return "normal";
|
|
11
|
+
// Focal length (on the editor's filmback) -> lens word, in crew terms:
|
|
12
|
+
// under 35 mm reads wide, 35-50 mm normal, longer than 50 mm long.
|
|
13
|
+
export function lensWordFromFocalMm(focalMm) {
|
|
14
|
+
if (!Number.isFinite(focalMm) || focalMm <= 0) return "normal";
|
|
15
|
+
if (focalMm < 35) return "wide";
|
|
16
|
+
if (focalMm <= 50) return "normal";
|
|
15
17
|
return "long";
|
|
16
18
|
}
|
|
17
19
|
|
|
20
|
+
// FOV degrees -> focal length through the editor's own calibration
|
|
21
|
+
// (shot.js fovToFocalMm on the given filmback) -> lens word. No second,
|
|
22
|
+
// independently calibrated conversion: the word matches the lens the editor
|
|
23
|
+
// reports for that field of view.
|
|
24
|
+
export function shotSizeFromFov(fovDeg, filmback = {}) {
|
|
25
|
+
if (!Number.isFinite(fovDeg) || fovDeg <= 0) return "normal";
|
|
26
|
+
const focalMm = fovToFocalMm((fovDeg * Math.PI) / 180, filmback.sensorId, filmback.aspectRatio);
|
|
27
|
+
// Round to 0.1 mm so a lens that round-trips through fov keeps its word.
|
|
28
|
+
return lensWordFromFocalMm(Math.round(focalMm * 10) / 10);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// The editor already reports the focal length it framed with; only a meta
|
|
32
|
+
// without one falls back to converting its fov on the meta's filmback.
|
|
33
|
+
function lensWord(meta) {
|
|
34
|
+
if (Number.isFinite(meta.focalMm) && meta.focalMm > 0) return lensWordFromFocalMm(meta.focalMm);
|
|
35
|
+
return shotSizeFromFov(meta.fovDeg, { sensorId: meta.sensorId, aspectRatio: shotAspectRatio(meta.aspect) });
|
|
36
|
+
}
|
|
37
|
+
|
|
18
38
|
function secondsText(seconds) {
|
|
19
39
|
return `${Number(seconds.toFixed(2))}s`;
|
|
20
40
|
}
|
|
@@ -68,7 +88,7 @@ export function buildShotPrompt(meta, options = {}) {
|
|
|
68
88
|
|
|
69
89
|
const lines = [
|
|
70
90
|
`SHOT: ${meta.shotTitle || "untitled shot"}, ${secondsText(seconds)}, ${meta.aspect || ""}`.trimEnd(),
|
|
71
|
-
`LENS: ${meta.focalMm}mm, ${
|
|
91
|
+
`LENS: ${meta.focalMm}mm, ${lensWord(meta)} shot`,
|
|
72
92
|
referenceOwnsCamera ? `CAMERA: ${CAMERA_FOLLOWS_REFERENCE}` : `CAMERA: ${cameraWords(meta.cameraMove)}`,
|
|
73
93
|
`LIGHT: ${lightWords(meta.keyLight)}`,
|
|
74
94
|
`CAST: ${(meta.cast || []).map((member) => member.name).join(", ") || "none"}`,
|
package/src/stage-hdr.js
CHANGED
|
@@ -23,11 +23,11 @@ const HDR_TYPES = Object.freeze(["image/vnd.radiance", "image/x-exr", "image/png
|
|
|
23
23
|
* default camera); hdrRotation turns it about +y.
|
|
24
24
|
*/
|
|
25
25
|
export const HDR_BUILTINS = Object.freeze([
|
|
26
|
-
{ id: "builtin:night-street", name: "Night street", ko: "밤거리", brightest: [0.72, 0.2, 0.66],
|
|
26
|
+
{ id: "builtin:night-street", name: "Night street", ko: "밤거리", zh: "夜晚街道", brightest: [0.72, 0.2, 0.66],
|
|
27
27
|
note: "dark blue night sky, a building skyline with lit windows, two warm sodium street lamps (front right and back left) pooling light on wet asphalt, a dim moon" },
|
|
28
|
-
{ id: "builtin:overcast-field", name: "Overcast field", ko: "흐린 들판", brightest: [-0.35, 0.66, -0.67],
|
|
28
|
+
{ id: "builtin:overcast-field", name: "Overcast field", ko: "흐린 들판", zh: "阴天田野", brightest: [-0.35, 0.66, -0.67],
|
|
29
29
|
note: "bright soft grey overcast sky (brighter overhead), a faint sun glow behind the clouds back left, a distant tree line and a green field" },
|
|
30
|
-
{ id: "builtin:studio-softbox", name: "Studio softbox", ko: "스튜디오 소프트박스", brightest: [-0.6, 0.4, 0.7],
|
|
30
|
+
{ id: "builtin:studio-softbox", name: "Studio softbox", ko: "스튜디오 소프트박스", zh: "影棚柔光箱", brightest: [-0.6, 0.4, 0.7],
|
|
31
31
|
note: "dark grey photo studio: a large white softbox front left, a tall strip light right, a soft overhead panel and a rim panel behind" },
|
|
32
32
|
].map((entry) => Object.freeze({ ...entry, brightest: Object.freeze(entry.brightest) })));
|
|
33
33
|
const BUILTIN_IDS = new Set(HDR_BUILTINS.map((entry) => entry.id));
|
package/src/stage-labels-ko.js
CHANGED
|
@@ -76,7 +76,7 @@ export const CAMERA_MOVE_LABELS_KO = new Map([
|
|
|
76
76
|
["Dolly-zoom (vertigo)", ko("Dolly-zoom (vertigo)", "돌리 줌(버티고)")],
|
|
77
77
|
["Whip pan", ko("Whip pan", "휩 팬")],
|
|
78
78
|
["Aerial / drone", ko("Aerial / drone", "공중 / 드론")],
|
|
79
|
-
[CUSTOM_MOVE, ko(CUSTOM_MOVE, "직접 입력…")],
|
|
79
|
+
[CUSTOM_MOVE, ko(CUSTOM_MOVE, "직접 입력…", "自定义…")],
|
|
80
80
|
]);
|
|
81
81
|
|
|
82
82
|
export const POSE_LABELS_KO = new Map([
|
|
@@ -120,7 +120,7 @@ export const SCENE_RENDERER_LABELS_KO = new Map([
|
|
|
120
120
|
["cone", ko("cone", "원뿔")],
|
|
121
121
|
["plane", ko("plane", "평면")],
|
|
122
122
|
[CUTOUT_KIND, ko("cutout", "컷아웃")],
|
|
123
|
-
[MESH_KIND, ko("Mesh", "모델")],
|
|
123
|
+
[MESH_KIND, ko("Mesh", "모델", "模型")],
|
|
124
124
|
["sculpt", ko("Sculpt", "조형")],
|
|
125
125
|
]);
|
|
126
126
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as THREE from "three";
|
|
2
|
-
import { ko
|
|
2
|
+
import { ko } from "./locale.js";
|
|
3
3
|
import { SHOT_ASPECT_RATIOS } from "./shot.js";
|
|
4
4
|
import {
|
|
5
5
|
SCENES_QUARANTINE_KEY,
|
|
@@ -159,9 +159,9 @@ export function loadSceneStartup() {
|
|
|
159
159
|
// funnel's "scene created" step never fires for a brand-new room.
|
|
160
160
|
startupCreatedScene: result.status === "absent" || result.status === "corrupt",
|
|
161
161
|
toast: result.status === "corrupt"
|
|
162
|
-
? ko(`Saved scenes were unreadable — starting fresh; the old data is kept under ${SCENES_QUARANTINE_KEY}`, `저장된 장면을 읽을 수 없어 새로 시작합니다. 기존 데이터는 ${SCENES_QUARANTINE_KEY}에
|
|
162
|
+
? ko(`Saved scenes were unreadable — starting fresh; the old data is kept under ${SCENES_QUARANTINE_KEY}`, `저장된 장면을 읽을 수 없어 새로 시작합니다. 기존 데이터는 ${SCENES_QUARANTINE_KEY}에 보관했어요.`, `无法读取已保存的场景 — 将重新开始;旧数据保留在 ${SCENES_QUARANTINE_KEY} 下。`)
|
|
163
163
|
: result.dropped > 0
|
|
164
|
-
? (
|
|
164
|
+
? ko(`${result.dropped} saved scene(s) could not be restored`, `저장된 장면 ${result.dropped}개를 복원하지 못했어요`, `${result.dropped} 个已保存的场景无法恢复`)
|
|
165
165
|
: null,
|
|
166
166
|
};
|
|
167
167
|
} catch {
|