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.
Files changed (195) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +1 -1
  3. package/bin/agent/agent-routes.mjs +4 -4
  4. package/bin/agent/agent-runner.mjs +160 -151
  5. package/bin/agent/agent-stream.mjs +86 -0
  6. package/bin/agent/anthropic-subscription-events.mjs +120 -0
  7. package/bin/agent/anthropic-subscription-prompt.mjs +116 -0
  8. package/bin/agent/anthropic-subscription-stream.mjs +251 -0
  9. package/bin/agent/anthropic-subscription.mjs +73 -0
  10. package/bin/agent/claude-code-executable.mjs +93 -0
  11. package/bin/agent/cliproxy-provider.mjs +25 -11
  12. package/bin/agent/main-agent.mjs +21 -7
  13. package/bin/agent/main-prompt.mjs +7 -6
  14. package/bin/agent/model-roles.mjs +112 -0
  15. package/bin/agent/openai-compatible-provider.mjs +95 -0
  16. package/bin/agent/pi-tools.mjs +19 -10
  17. package/bin/agent/provider-login.mjs +3 -1
  18. package/bin/agent/provider-routes.mjs +2 -0
  19. package/bin/agent/providers.mjs +35 -130
  20. package/bin/agent/script-tool.mjs +2 -1
  21. package/bin/agent/studio-prompt.mjs +1 -1
  22. package/bin/agent/studio-rebase.mjs +5 -4
  23. package/bin/agent/studio-task-routes.mjs +1 -1
  24. package/bin/agent/studio-tasks.mjs +116 -25
  25. package/bin/agent/studio-tools.mjs +3 -2
  26. package/bin/cozyclay.mjs +4 -2
  27. package/bin/live-endpoint.mjs +2 -0
  28. package/dist/app/index.html +6 -6
  29. package/dist/assets/TaskBoard-U1Yl-4F1.js +1 -0
  30. package/dist/assets/app-BdRlMf4J.css +1 -0
  31. package/dist/assets/app-DwETmoQZ.js +5144 -0
  32. package/dist/assets/compile-worker-oy_P9HHh.js +6 -0
  33. package/dist/assets/motion3-compile-DDG3wklD.js +1 -0
  34. package/dist/assets/scene-asset-sync-BEjjhNEo.css +1 -0
  35. package/dist/assets/scene-asset-sync-Bz59Ghdz.js +13 -0
  36. package/dist/assets/task-board-DFfNa0xg.js +15 -0
  37. package/dist/assets/workflow-BMJGd_gZ.js +179 -0
  38. package/dist/cozyclay-package.json +1 -1
  39. package/dist/index.html +1 -1
  40. package/dist/sitemap.xml +1 -1
  41. package/dist/workflow/index.html +4 -4
  42. package/mcp/README.md +1 -1
  43. package/mcp/live-hub.mjs +5 -3
  44. package/mcp/tool-handlers.mjs +5 -5
  45. package/package.json +6 -4
  46. package/src/App.jsx +65 -87
  47. package/src/agent-image-client.js +2 -2
  48. package/src/asset-pane.jsx +14 -14
  49. package/src/bus-task-scopes.js +59 -11
  50. package/src/bus-task-targets.js +35 -9
  51. package/src/bus-tasks.js +23 -10
  52. package/src/character-kind.js +13 -12
  53. package/src/command-bus.js +7 -4
  54. package/src/commands/ai.js +1 -1
  55. package/src/commands/export.js +1 -1
  56. package/src/commands/motion3.js +27 -7
  57. package/src/commands/project.js +2 -1
  58. package/src/commands/shot.js +1 -1
  59. package/src/commands/view.js +2 -2
  60. package/src/domains/cast.js +15 -15
  61. package/src/domains/motion/motion-range-pin-rules.js +1 -1
  62. package/src/domains/motion.js +11 -11
  63. package/src/domains/objects.js +20 -30
  64. package/src/domains/scenes.js +14 -21
  65. package/src/domains/shots.js +3 -3
  66. package/src/export-workspace.js +244 -0
  67. package/src/first-success-guide.jsx +19 -3
  68. package/src/hierarchy-panel.jsx +6 -7
  69. package/src/keyframe-pack.js +5 -1
  70. package/src/live-control.js +14 -7
  71. package/src/locale-zh/app.js +124 -0
  72. package/src/locale-zh/domains.js +36 -0
  73. package/src/locale-zh/labels.js +148 -0
  74. package/src/locale-zh/panels.js +225 -0
  75. package/src/locale-zh/shell.js +230 -0
  76. package/src/locale-zh/timeline.js +145 -0
  77. package/src/locale-zh/workflow.js +104 -0
  78. package/src/locale-zh/workspace.js +190 -0
  79. package/src/locale-zh.js +21 -0
  80. package/src/locale.js +16 -6
  81. package/src/main.jsx +1 -1
  82. package/src/motion/beat-board-model.js +4 -4
  83. package/src/motion/beat-board.jsx +1 -1
  84. package/src/motion/ik-pick.js +24 -0
  85. package/src/motion/take-segments.jsx +5 -5
  86. package/src/motion/timeline.jsx +30 -40
  87. package/src/motion/waypoints.js +6 -6
  88. package/src/motion3/compile-job.js +15 -3
  89. package/src/motion3/compiler/channels.js +74 -24
  90. package/src/motion3/compiler/compile.js +2 -1
  91. package/src/motion3/compiler/idle.js +2 -0
  92. package/src/motion3/compiler/overlap.js +180 -0
  93. package/src/motion3/compiler/pose.js +6 -4
  94. package/src/motion3/compiler/validate-common.js +1 -0
  95. package/src/motion3/compiler/validate-parts.js +5 -1
  96. package/src/motion3/compiler/validate.js +30 -10
  97. package/src/motion3/correct.js +236 -0
  98. package/src/motion3/docs.js +8 -6
  99. package/src/motion3/face-lean.js +31 -0
  100. package/src/motion3/fix/index.js +5 -1
  101. package/src/motion3/fix/reach-body.js +172 -0
  102. package/src/motion3/hint-check.js +44 -0
  103. package/src/motion3/json-patch.js +120 -12
  104. package/src/motion3/lenient.js +46 -4
  105. package/src/motion3/object-keys.js +17 -5
  106. package/src/motion3/pipeline.js +46 -9
  107. package/src/motion3/qa/arm-checks.js +96 -0
  108. package/src/motion3/qa/asked.js +113 -0
  109. package/src/motion3/qa/goals.js +37 -8
  110. package/src/motion3/qa/hints-goal.js +6 -5
  111. package/src/motion3/qa/index.js +5 -2
  112. package/src/motion3/qa/key-checks.js +4 -3
  113. package/src/motion3/qa/motion-life.js +243 -0
  114. package/src/motion3/qa/spec-checks.js +15 -9
  115. package/src/motion3/qa/verdict.js +50 -0
  116. package/src/motion3/refs.js +38 -14
  117. package/src/motion3/repair-watch.js +132 -0
  118. package/src/motion3/repair.js +52 -19
  119. package/src/motion3/report.js +50 -9
  120. package/src/motion3/schema/topics.js +6 -4
  121. package/src/motion3/schema/types.js +1 -1
  122. package/src/motion3/skills/basic.js +15 -8
  123. package/src/motion3/skills/index.js +52 -18
  124. package/src/motion3/skills/info.js +1 -1
  125. package/src/motion3/skills/stance.js +6 -0
  126. package/src/motion3/skills/walk-fit.js +170 -0
  127. package/src/object-catalog.jsx +4 -5
  128. package/src/panels/CameraPanel.jsx +3 -3
  129. package/src/panels/HdrEnvironmentControls.jsx +2 -1
  130. package/src/panels/ObjectTransformPanel.jsx +19 -29
  131. package/src/panels/PoseImagePanel.jsx +11 -10
  132. package/src/panels/PosePanel.jsx +2 -2
  133. package/src/panels/ReferenceImageField.jsx +2 -2
  134. package/src/panels/RigControlPanel.jsx +2 -2
  135. package/src/panels/SculptPanel.jsx +11 -11
  136. package/src/panels/SubjectBox.jsx +3 -3
  137. package/src/panels/SubjectsPanel.jsx +1 -1
  138. package/src/posestudio.jsx +10 -8
  139. package/src/project-browser.css +7 -6
  140. package/src/project-browser.jsx +7 -6
  141. package/src/project-open-file.js +19 -0
  142. package/src/qa-label.js +1 -1
  143. package/src/range-pin-panel.jsx +14 -14
  144. package/src/resource-status.jsx +3 -3
  145. package/src/result-modal.jsx +13 -13
  146. package/src/settings-menu.jsx +4 -1
  147. package/src/shell/BoardSlot.jsx +5 -5
  148. package/src/shell/CompositionGuideButton.jsx +1 -1
  149. package/src/shell/ExportFolderPreference.jsx +103 -0
  150. package/src/shell/GhostsMenuGroup.jsx +1 -1
  151. package/src/shell/PreferencesDialog.jsx +3 -166
  152. package/src/shell/StatusBar.jsx +4 -4
  153. package/src/shell/ViewportToolbar.jsx +3 -3
  154. package/src/shell/add-camera-shot.js +2 -2
  155. package/src/shell/details-selection.js +1 -1
  156. package/src/shell/preference-controls.jsx +115 -0
  157. package/src/shell/preference-pages.jsx +72 -0
  158. package/src/shell/preferences.css +46 -0
  159. package/src/shot-guides.js +5 -5
  160. package/src/shot-prompt.js +29 -9
  161. package/src/stage-hdr.js +3 -3
  162. package/src/stage-labels-ko.js +2 -2
  163. package/src/stage-startup-defaults.js +3 -3
  164. package/src/studio-actions.js +4 -4
  165. package/src/studio-agent-motion.js +19 -9
  166. package/src/studio-agent-protocol.js +1 -1
  167. package/src/studio-app-binding.js +3 -2
  168. package/src/styles/tokens.css +1 -1
  169. package/src/styles.css +1 -1
  170. package/src/use-case-question.jsx +18 -18
  171. package/src/workflow/AgentPanel.jsx +6 -6
  172. package/src/workflow/AgentPlanCard.jsx +1 -1
  173. package/src/workflow/AgentProgressCard.jsx +4 -4
  174. package/src/workflow/AgentTurnSteps.jsx +1 -1
  175. package/src/workflow/CozySceneNode.jsx +1 -1
  176. package/src/workflow/LoginPanel.jsx +1 -1
  177. package/src/workflow/TaskBoard.jsx +1 -1
  178. package/src/workflow/TaskInline.jsx +1 -1
  179. package/src/workflow/WorkflowBuilder.jsx +2 -2
  180. package/src/workflow/agent-client.js +1 -1
  181. package/src/workflow/agent-turn.js +5 -5
  182. package/src/workflow/keyframe-pack-request.js +3 -10
  183. package/src/workflow/task-board.css +1 -1
  184. package/src/workflow/task-board.js +11 -9
  185. package/tools/loc-baseline.json +2 -3
  186. package/tools/run-tests.mjs +9 -9
  187. package/dist/assets/TaskBoard-ngdqiu7u.js +0 -1
  188. package/dist/assets/app-BlwOpest.css +0 -1
  189. package/dist/assets/app-DmR7jBkz.js +0 -5144
  190. package/dist/assets/compile-worker-D0pKioa0.js +0 -5
  191. package/dist/assets/motion3-compile-BB3_1-EB.js +0 -1
  192. package/dist/assets/scene-asset-sync-CBx7XRti.css +0 -1
  193. package/dist/assets/scene-asset-sync-Cgdtr1Yw.js +0 -13
  194. package/dist/assets/task-board-ErQvfoaz.js +0 -14
  195. package/dist/assets/workflow-zQM-EKW1.js +0 -179
@@ -0,0 +1,244 @@
1
+ // Optional export folder. When the browser grants a directory, download-type
2
+ // exports are written there; every failure path falls back to the browser's
3
+ // normal download so an export is never lost.
4
+
5
+ const DB_NAME = "cozyclay-export-workspace";
6
+ const DB_VERSION = 1;
7
+ const STORE = "handles";
8
+ const FOLDER_KEY = "folder";
9
+ const MAX_NAME = 180;
10
+ let writeQueue = Promise.resolve();
11
+
12
+ function splitExtension(name) {
13
+ const dot = name.lastIndexOf(".");
14
+ return dot > 0 ? [name.slice(0, dot), name.slice(dot)] : [name, ""];
15
+ }
16
+
17
+ /** A file name that cannot leave the chosen directory or break the file system. */
18
+ function safeNameCharacter(character) {
19
+ const code = character.codePointAt(0);
20
+ if (code < 0x20 || code === 0x7f || '<>:"/\\|?*'.includes(character)) return "-";
21
+ return character;
22
+ }
23
+
24
+ export function sanitizeExportName(name) {
25
+ const leaf = String(name ?? "").split(/[\\/]/).pop() ?? "";
26
+ const cleaned = [...leaf]
27
+ .map(safeNameCharacter)
28
+ .join("")
29
+ .replace(/^[.\s]+/, "")
30
+ .replace(/[.\s]+$/, "")
31
+ .trim();
32
+ if (!cleaned) return "cozyclay-export";
33
+ if (cleaned.length <= MAX_NAME) return cleaned;
34
+ const [base, extension] = splitExtension(cleaned);
35
+ return `${base.slice(0, Math.max(1, MAX_NAME - extension.length))}${extension}`;
36
+ }
37
+
38
+ /** Browser download naming: shot.zip, shot (1).zip, shot (2).zip, ... */
39
+ export function collisionName(name, index) {
40
+ const [base, extension] = splitExtension(String(name));
41
+ return `${base} (${index})${extension}`;
42
+ }
43
+
44
+ async function fileHandleForWrite(directory, name) {
45
+ const safe = sanitizeExportName(name);
46
+ for (let index = 0; index < 10_000; index += 1) {
47
+ const candidate = index === 0 ? safe : collisionName(safe, index);
48
+ try {
49
+ await directory.getFileHandle(candidate);
50
+ } catch (error) {
51
+ if (error?.name === "NotFoundError") {
52
+ return { handle: await directory.getFileHandle(candidate, { create: true }), name: candidate };
53
+ }
54
+ // A folder with the export's name is a collision, not a reason to fail.
55
+ if (error?.name === "TypeMismatchError") continue;
56
+ throw error;
57
+ }
58
+ }
59
+ throw new Error("No free export file name.");
60
+ }
61
+
62
+ async function writeBlobNow(directory, blob, name) {
63
+ const target = await fileHandleForWrite(directory, name);
64
+ let writable = null;
65
+ try {
66
+ writable = await target.handle.createWritable();
67
+ await writable.write(blob);
68
+ await writable.close();
69
+ return { name: target.name, bytes: blob.size };
70
+ } catch (error) {
71
+ try { await writable?.abort(); } catch { /* the stream may already be closed */ }
72
+ try { await directory.removeEntry(target.name); } catch { /* best-effort cleanup */ }
73
+ throw error;
74
+ }
75
+ }
76
+
77
+ /** Write one export without overwriting an existing file. Writes are serialized
78
+ * so two overlapping exports cannot both claim the same free name. */
79
+ export function writeBlobToDirectory(directory, blob, name) {
80
+ const run = () => writeBlobNow(directory, blob, name);
81
+ const result = writeQueue.then(run, run);
82
+ writeQueue = result.then(() => undefined, () => undefined);
83
+ return result;
84
+ }
85
+
86
+ function clickDownload(source, name) {
87
+ if (typeof document === "undefined") throw new Error("A browser download is unavailable here.");
88
+ if (typeof source === "string" && !/^(?:blob:|data:|https?:\/\/|\/)/i.test(source)) {
89
+ throw new Error("Unsupported export source.");
90
+ }
91
+ const anchor = document.createElement("a");
92
+ let url = null;
93
+ if (typeof source === "string") {
94
+ anchor.href = source;
95
+ } else {
96
+ url = URL.createObjectURL(source);
97
+ anchor.href = url;
98
+ }
99
+ anchor.download = sanitizeExportName(name);
100
+ document.body.appendChild(anchor);
101
+ anchor.click();
102
+ anchor.remove();
103
+ if (url) setTimeout(() => URL.revokeObjectURL(url), 10_000);
104
+ }
105
+
106
+ async function toBlob(source) {
107
+ if (source instanceof Blob) return source;
108
+ if (typeof source === "string") {
109
+ const response = await fetch(source);
110
+ if (!response.ok) throw new Error(`Export source could not be read: ${response.status}`);
111
+ return response.blob();
112
+ }
113
+ throw new TypeError("An export must be a Blob or a URL.");
114
+ }
115
+
116
+ function openDb() {
117
+ return new Promise((resolve, reject) => {
118
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
119
+ request.onupgradeneeded = () => request.result.createObjectStore(STORE);
120
+ request.onsuccess = () => resolve(request.result);
121
+ request.onerror = () => reject(request.error);
122
+ request.onblocked = () => reject(new Error("Export folder storage is blocked."));
123
+ });
124
+ }
125
+
126
+ async function storedHandle() {
127
+ try {
128
+ const db = await openDb();
129
+ try {
130
+ return await new Promise((resolve, reject) => {
131
+ const request = db.transaction(STORE, "readonly").objectStore(STORE).get(FOLDER_KEY);
132
+ request.onsuccess = () => resolve(request.result ?? null);
133
+ request.onerror = () => reject(request.error);
134
+ });
135
+ } finally {
136
+ db.close();
137
+ }
138
+ } catch {
139
+ return null;
140
+ }
141
+ }
142
+
143
+ async function storeHandle(handle) {
144
+ const db = await openDb();
145
+ try {
146
+ await new Promise((resolve, reject) => {
147
+ const transaction = db.transaction(STORE, "readwrite");
148
+ transaction.objectStore(STORE).put(handle, FOLDER_KEY);
149
+ transaction.oncomplete = () => resolve();
150
+ transaction.onerror = () => reject(transaction.error);
151
+ });
152
+ } finally {
153
+ db.close();
154
+ }
155
+ }
156
+
157
+ async function permissionOf(handle) {
158
+ if (!handle) return "denied";
159
+ if (typeof handle.queryPermission !== "function") return "granted";
160
+ try {
161
+ return await handle.queryPermission({ mode: "readwrite" });
162
+ } catch {
163
+ return "denied";
164
+ }
165
+ }
166
+
167
+ export function canChooseExportWorkspace() {
168
+ return typeof window !== "undefined" && typeof window.showDirectoryPicker === "function";
169
+ }
170
+
171
+ /** Ask for the folder; the picker grants read/write for this session. */
172
+ export async function chooseExportWorkspace() {
173
+ if (!canChooseExportWorkspace()) throw new Error("This browser cannot choose an export folder.");
174
+ const handle = await window.showDirectoryPicker({ id: "cozyclay-export-folder", mode: "readwrite" });
175
+ await storeHandle(handle);
176
+ return handle;
177
+ }
178
+
179
+ /** The remembered folder and its current permission, if any. */
180
+ export async function loadExportWorkspace() {
181
+ const handle = await storedHandle();
182
+ if (!handle) return null;
183
+ return { handle, permission: await permissionOf(handle) };
184
+ }
185
+
186
+ export async function requestExportWorkspacePermission(handle) {
187
+ if (await permissionOf(handle) === "granted") return "granted";
188
+ if (typeof handle?.requestPermission !== "function") return "granted";
189
+ try {
190
+ return await handle.requestPermission({ mode: "readwrite" });
191
+ } catch {
192
+ return "denied";
193
+ }
194
+ }
195
+
196
+ export async function clearExportWorkspace() {
197
+ let db = null;
198
+ try {
199
+ db = await openDb();
200
+ await new Promise((resolve, reject) => {
201
+ const transaction = db.transaction(STORE, "readwrite");
202
+ transaction.objectStore(STORE).delete(FOLDER_KEY);
203
+ transaction.oncomplete = () => resolve();
204
+ transaction.onerror = () => reject(transaction.error);
205
+ });
206
+ return true;
207
+ } catch {
208
+ return false;
209
+ } finally {
210
+ db?.close();
211
+ }
212
+ }
213
+
214
+ /** Save an export to the chosen folder, or hand it to the browser on any miss. */
215
+ export async function saveExport(source, name, options = {}) {
216
+ const injectedDirectory = Object.prototype.hasOwnProperty.call(options, "directory");
217
+ const download = options.download ?? clickDownload;
218
+ if (!injectedDirectory && !canChooseExportWorkspace()) {
219
+ download(source, name);
220
+ return { method: "download", name: sanitizeExportName(name), reason: "unsupported" };
221
+ }
222
+ let directory = options.directory ?? null;
223
+ let permission = injectedDirectory ? (options.permission ?? "granted") : "denied";
224
+ if (!injectedDirectory) {
225
+ const workspace = await loadExportWorkspace();
226
+ directory = workspace?.handle ?? null;
227
+ permission = workspace?.permission ?? "denied";
228
+ }
229
+ if (!directory) {
230
+ download(source, name);
231
+ return { method: "download", name: sanitizeExportName(name), reason: "no-workspace" };
232
+ }
233
+ if (permission !== "granted") {
234
+ download(source, name);
235
+ return { method: "download", name: sanitizeExportName(name), reason: "workspace-permission" };
236
+ }
237
+ try {
238
+ const written = await writeBlobToDirectory(directory, await toBlob(source), name);
239
+ return { method: "workspace", ...written, folderName: directory.name ?? "Export folder" };
240
+ } catch (error) {
241
+ download(source, name);
242
+ return { method: "download", name: sanitizeExportName(name), reason: "workspace-write-failed", error };
243
+ }
244
+ }
@@ -5,26 +5,34 @@ const ANIMATION_STEPS = [
5
5
  {
6
6
  en: "Select a character in the Outliner.",
7
7
  ko: "아웃라이너에서 캐릭터를 선택하세요.",
8
+ zh: "在大纲视图中选择一个角色。",
8
9
  hintEn: "Click Character 1 (or its row) to make it active.",
9
10
  hintKo: "캐릭터 1(또는 캐릭터 행)을 클릭하면 활성화됩니다.",
11
+ hintZh: "点击角色 1(或其所在行)使其成为当前角色。",
10
12
  },
11
13
  {
12
14
  en: "Move it in the Scene view.",
13
15
  ko: "장면 보기에서 캐릭터를 움직이세요.",
16
+ zh: "在场景视图中移动它。",
14
17
  hintEn: "Drag the character marker on the floor to place it.",
15
18
  hintKo: "바닥의 캐릭터 마커를 드래그해 배치하세요.",
19
+ hintZh: "拖动地面上的角色标记来摆放位置。",
16
20
  },
17
21
  {
18
22
  en: "Ask the Agent for a motion.",
19
23
  ko: "에이전트에게 동작을 요청하세요.",
24
+ zh: "让智能体生成一个动作。",
20
25
  hintEn: "Open it with Cmd/Ctrl+B and say what the character does; the take lands on the Body lane.",
21
26
  hintKo: "Cmd/Ctrl+B로 열고 캐릭터가 할 동작을 말하면 테이크가 바디 레인에 올라갑니다.",
27
+ hintZh: "按 Cmd/Ctrl+B 打开,说出角色要做的动作;动作片段会落在身体轨道上。",
22
28
  },
23
29
  {
24
30
  en: "Scrub the timeline or press Space to play.",
25
31
  ko: "타임라인을 문지르거나 Space를 눌러 재생하세요.",
32
+ zh: "拖动时间线或按空格播放。",
26
33
  hintEn: "Seeing the playhead move is your first visible result.",
27
34
  hintKo: "재생 헤드가 움직이면 첫 결과를 확인한 것입니다.",
35
+ hintZh: "看到播放头移动,就是你的第一个可见结果。",
28
36
  },
29
37
  ];
30
38
 
@@ -32,26 +40,34 @@ const STORYBOARD_STEPS = [
32
40
  {
33
41
  en: "Open the Agent panel (Cmd/Ctrl+B).",
34
42
  ko: "에이전트 패널을 여세요 (Cmd/Ctrl+B).",
43
+ zh: "打开智能体面板(Cmd/Ctrl+B)。",
35
44
  hintEn: "The Agent panel is where you describe each panel.",
36
45
  hintKo: "에이전트 패널에서 각 패널을 설명합니다.",
46
+ hintZh: "在智能体面板中逐个描述每个面板。",
37
47
  },
38
48
  {
39
49
  en: "Describe one scene in a sentence.",
40
50
  ko: "장면 하나를 한 문장으로 설명하세요.",
51
+ zh: "用一句话描述一个场景。",
41
52
  hintEn: "One sentence per turn is enough for one panel.",
42
53
  hintKo: "패널 하나에는 한 문장이면 충분합니다.",
54
+ hintZh: "每个面板一轮只说一句话就够了。",
43
55
  },
44
56
  {
45
57
  en: "Your panel appears on the Board.",
46
58
  ko: "패널이 보드에 나타납니다.",
59
+ zh: "你的面板会出现在看板上。",
47
60
  hintEn: "Open the Board tab to see the new panel card.",
48
61
  hintKo: "보드 탭에서 새 패널 카드를 확인하세요.",
62
+ hintZh: "打开看板标签页查看新的面板卡片。",
49
63
  },
50
64
  {
51
65
  en: "Stylize it, or export the panel pack.",
52
66
  ko: "스타일을 입히거나 패널 팩으로 내보내세요.",
67
+ zh: "对它进行风格化,或导出面板包。",
53
68
  hintEn: "Use Stylize on the card, or export the panel pack.",
54
69
  hintKo: "카드에서 스타일을 입히거나 패널 팩으로 내보내세요.",
70
+ hintZh: "在卡片上使用风格化,或导出面板包。",
55
71
  },
56
72
  ];
57
73
 
@@ -73,7 +89,7 @@ export default function FirstSuccessGuide({ open, onDismiss, previsMode = "anima
73
89
  <div className="v2-first-success-guide-head first-success-guide-head">
74
90
  <div>
75
91
  <strong id="first-success-guide-title">{ko("Your first 60 seconds", "첫 60초")}</strong>
76
- <span>{complete ? ko("Core loop complete", "핵심 흐름 완료") : ko(`Step ${step + 1} of ${STEPS.length}`, `${step + 1} / ${STEPS.length} 단계`)}</span>
92
+ <span>{complete ? ko("Core loop complete", "핵심 흐름 완료", "核心流程完成") : ko(`Step ${step + 1} of ${STEPS.length}`, `${step + 1} / ${STEPS.length} 단계`, `第 ${step + 1} / ${STEPS.length} 步`)}</span>
77
93
  </div>
78
94
  <button type="button" className="v2-first-success-guide-close first-success-guide-close" onClick={onDismiss} aria-label={ko("Dismiss guide", "가이드 닫기")}>×</button>
79
95
  </div>
@@ -91,12 +107,12 @@ export default function FirstSuccessGuide({ open, onDismiss, previsMode = "anima
91
107
  {STEPS.map((item, index) => (
92
108
  <li key={item.en} className={index < step ? "done" : index === step ? "current" : ""}>
93
109
  <span className="v2-first-success-guide-step-mark first-success-guide-step-mark">{index < step ? "✓" : index + 1}</span>
94
- <div><strong>{ko(item.en, item.ko)}</strong>{index === step && <span>{ko(item.hintEn, item.hintKo)}</span>}</div>
110
+ <div><strong>{ko(item.en, item.ko, item.zh)}</strong>{index === step && <span>{ko(item.hintEn, item.hintKo, item.hintZh)}</span>}</div>
95
111
  </li>
96
112
  ))}
97
113
  </ol>
98
114
  <button type="button" className="v2-first-success-guide-next first-success-guide-next btn primary" onClick={() => setStep((value) => value + 1)}>
99
- {ko(step === STEPS.length - 1 ? "Mark complete" : "I did this", step === STEPS.length - 1 ? "완료로 표시" : "했어요")}
115
+ {ko(step === STEPS.length - 1 ? "Mark complete" : "I did this", step === STEPS.length - 1 ? "완료로 표시" : "했어요", step === STEPS.length - 1 ? "标记完成" : "我做了这一步")}
100
116
  </button>
101
117
  </>
102
118
  )}
@@ -1,6 +1,6 @@
1
1
  import { useEffect, useMemo, useRef, useState } from "react";
2
2
  import { createPortal } from "react-dom";
3
- import { ko, isKo } from "./locale.js";
3
+ import { ko } from "./locale.js";
4
4
  import { buildHierarchyNodes } from "./hierarchy-model.js";
5
5
  import { sceneObjectIdFromHierarchy } from "./scene-objects.js";
6
6
  import { CatalogueEntries, displayObjectLabel } from "./object-catalog.jsx";
@@ -62,9 +62,8 @@ export const HIERARCHY_DRAG_MIME = "application/x-cclay-hierarchy";
62
62
  const carriesHierarchyRow = (event) => !!event.dataTransfer?.types?.includes?.(HIERARCHY_DRAG_MIME);
63
63
 
64
64
  function displaySceneName(name) {
65
- if (!isKo) return name;
66
65
  const generatedName = /^SCENE\s+(\d+)$/i.exec(name);
67
- return generatedName ? ko(`Scene ${generatedName[1]}`, `장면 ${generatedName[1]}`) : name;
66
+ return generatedName ? ko(name, `장면 ${generatedName[1]}`, `场景 ${generatedName[1]}`) : name;
68
67
  }
69
68
 
70
69
  /**
@@ -112,7 +111,7 @@ function ScenePill({ scenes, activeSceneId, onSceneSelect, onSceneCreate, onRena
112
111
  function displayHierarchyLabel(node) {
113
112
  if (node.kind === "object") return displayObjectLabel(node.label);
114
113
  if (node.kind === "scene") return displaySceneName(node.label);
115
- return isKo ? (HIERARCHY_LABELS_KO[node.label] ?? node.label) : node.label;
114
+ return HIERARCHY_LABELS_KO[node.label] ? ko(node.label, HIERARCHY_LABELS_KO[node.label]) : node.label;
116
115
  }
117
116
 
118
117
  const OUTLINER_TYPES = {
@@ -466,7 +465,7 @@ function TreeRow({
466
465
  <button
467
466
  type="button"
468
467
  className="hierarchy-toggle"
469
- aria-label={isKo ? `${label} ${expanded ? "접기" : "펼치기"}` : `${expanded ? "Collapse" : "Expand"} ${label}`}
468
+ aria-label={ko(`${expanded ? "Collapse" : "Expand"} ${label}`, `${label} ${expanded ? "접기" : "펼치기"}`, `${expanded ? "折叠" : "展开"} ${label}`)}
470
469
  onClick={() => onToggle(node.id)}
471
470
  >
472
471
  {expanded ? "▾" : "▸"}
@@ -486,7 +485,7 @@ function TreeRow({
486
485
  ref={inputRef}
487
486
  className="hierarchy-rename-input"
488
487
  defaultValue={node.label}
489
- aria-label={isKo ? `${label} 이름 바꾸기` : `Rename ${label}`}
488
+ aria-label={ko(`Rename ${label}`, `${label} 이름 바꾸기`, `重命名 ${label}`)}
490
489
  autoFocus
491
490
  onFocus={(event) => event.currentTarget.select()}
492
491
  onKeyDown={(event) => {
@@ -523,7 +522,7 @@ function TreeRow({
523
522
  className="hierarchy-eye"
524
523
  data-hidden={node.hidden ? "true" : undefined}
525
524
  aria-pressed={node.hidden === true}
526
- aria-label={node.hidden ? (isKo ? `${label} 표시` : `Show ${label}`) : (isKo ? `${label} 숨기기` : `Hide ${label}`)}
525
+ aria-label={node.hidden ? ko(`Show ${label}`, `${label} 표시`, `显示 ${label}`) : ko(`Hide ${label}`, `${label} 숨기기`, `隐藏 ${label}`)}
527
526
  onClick={(event) => {
528
527
  event.preventDefault();
529
528
  event.stopPropagation();
@@ -55,10 +55,14 @@ function readmeText({ shot, fps, hasLast, hasClip, ext, hasStylized }) {
55
55
  lines.push(
56
56
  "- camera.json: camera reference. The recorded camera move, fps, and",
57
57
  " frame range to reproduce the framing over time.",
58
- "- blocking-depth.mp4: shot-wide normalised depth video; lighter pixels are closer.",
59
58
  "- prompt.txt: the text prompt describing the intended action.",
60
59
  );
61
60
  if (hasStylized) lines.push("- stylized.png: the stylized picture of this panel; first.png is its greybox.");
61
+ lines.push(
62
+ "",
63
+ "Not in this pack: the shot-wide depth video (blocking-depth.mp4) is a",
64
+ "separate export - use Depth (mp4) in the export menu.",
65
+ );
62
66
  return lines.join("\n") + "\n";
63
67
  }
64
68
 
@@ -11,8 +11,11 @@ const MOTION_TELEMETRY_EVENTS = new Set([
11
11
  const LIVE_TELEMETRY_EVENTS = new Set([...MOTION_TELEMETRY_EVENTS, ...EXECUTION_TELEMETRY_EVENTS.filter((event) => event.startsWith("mcp:"))]);
12
12
 
13
13
  const LIVE_CONTROL_PORT = import.meta.env?.VITE_COZYCLAY_LIVE_PORT ?? "5184";
14
- export const liveControlUrl = (port = LIVE_CONTROL_PORT) => `ws://127.0.0.1:${port}/live`;
15
- const LIVE_CONTROL_URL = liveControlUrl();
14
+ // The npm launcher serves a prebuilt bundle, so the build-time port above is
15
+ // fixed; it injects the hub port it actually started as __COZYCLAY_LIVE_PORT__
16
+ // (#862). A valid runtime value wins over the build-time default.
17
+ export const resolveLiveControlPort = (runtime = globalThis.__COZYCLAY_LIVE_PORT__) => (/^[1-9]\d{0,4}$/.test(String(runtime)) && Number(runtime) < 65536 ? String(runtime) : LIVE_CONTROL_PORT);
18
+ export const liveControlUrl = (port = resolveLiveControlPort()) => `ws://127.0.0.1:${port}/live`;
16
19
  // Reconnect schedule: 1 s doubling to a 15 s cap with +-25 % jitter, so a hub
17
20
  // that comes back does not take every editor's retry in the same millisecond.
18
21
  const LIVE_RECONNECT_BASE_MS = 1_000;
@@ -147,10 +150,8 @@ export async function dispatchLiveFrame(data, handlers = {}) {
147
150
  }
148
151
  }
149
152
 
150
- /**
151
- * Open the editor's one-way client connection. Failures are intentionally
152
- * silent: a studio remains a fully local editor when the MCP server is absent.
153
- */
153
+ // Open the editor's one-way client connection. Failures are intentionally
154
+ // silent: a studio remains a fully local editor when the MCP server is absent.
154
155
  export function createLiveControl({
155
156
  handlers = {},
156
157
  onWorkspace = () => {},
@@ -164,7 +165,7 @@ export function createLiveControl({
164
165
  // Optional identity for the hub's live_status listing (scene/project names).
165
166
  meta = null,
166
167
  WebSocketImpl = globalThis.WebSocket,
167
- url = LIVE_CONTROL_URL,
168
+ url = liveControlUrl(),
168
169
  timers = defaultTimers,
169
170
  random = Math.random,
170
171
  // Aliased on purpose: an unqualified call to a binding named `listen` reads
@@ -377,6 +378,12 @@ export function createLiveControl({
377
378
  setHandlers(nextHandlers) {
378
379
  currentHandlers = nextHandlers && typeof nextHandlers === "object" ? nextHandlers : {};
379
380
  },
381
+ // Opening another project renames the tab; the next hello carries the new
382
+ // label and an open socket tells the hub now, so status stays truthful.
383
+ setMeta(nextMeta) {
384
+ meta = nextMeta && typeof nextMeta === "object" ? nextMeta : null;
385
+ if (meta) send({ type: "meta", meta });
386
+ },
380
387
  close() {
381
388
  stopped = true;
382
389
  clearRetry();
@@ -0,0 +1,124 @@
1
+ // Chinese UI copy for App-level toasts, export status, error boundary and first-run guide.
2
+ export const ZH_APP = {
3
+ "Load a character to pose it — Pose mode edits its rig with IK": "加载角色后才能摆姿势 — 姿势模式用 IK 编辑其骨骼",
4
+ "That picture format is not supported — use PNG, JPG, WebP or GIF": "不支持该图片格式 — 请使用 PNG、JPG、WebP 或 GIF",
5
+ "A newer edit comes after this deletion. Use Undo history instead.": "此删除之后还有更新的编辑。请改用撤销历史。",
6
+ "Double-click the line to add a dot": "双击线条添加一个点",
7
+ "That image is no longer in storage": "该图片已不在存储中",
8
+ "This image's usage changed, so it was not deleted. Please review it again.": "该图片的使用情况已变化,因此未删除。请重新检查。",
9
+ "This image's scene references changed, so it was not deleted. Please review it again.": "该图片的场景引用已变化,因此未删除。请重新检查。",
10
+ "That image is used by a scene and was not deleted": "该图片正被场景使用,未删除",
11
+ "That image is now used by a scene and was not deleted": "该图片现在正被场景使用,未删除",
12
+ "The scene changed while deleting, so the image was kept. Please review storage again.": "删除期间场景发生变化,图片已保留。请重新检查存储。",
13
+ "The live editor is disconnected. Reconnect before sending.": "实时编辑器已断开。请重新连接后再发送。",
14
+ "MP4 encoding is unavailable. Use a current browser with H.264 WebCodecs support (such as Chrome or Edge), enable hardware acceleration, then retry.": "MP4 编码不可用。请使用支持 H.264 WebCodecs 的最新浏览器(如 Chrome 或 Edge),开启硬件加速后重试。",
15
+ "Video encoding failed. Retry the same request. If resources are tight, shorten the shot range or lower output resolution before starting a new export.": "视频编码失败。请重试相同请求。若资源紧张,请缩短镜头范围或降低输出分辨率后重新导出。",
16
+ "Frame rendering failed. Let the scene finish loading, then retry. If it repeats, shorten the range or reduce output resolution for a new export.": "帧渲染失败。请等场景加载完成后再试。若反复出现,请缩短范围或降低输出分辨率后重新导出。",
17
+ "Export cancelled. No further files will be handed off.": "导出已取消。不会再交付其他文件。",
18
+ "Export could not finish. Retry the same request. For memory or resource problems, close other heavy tabs or use a shorter range / lower output resolution in a new export.": "导出未能完成。请重试相同请求。若为内存或资源问题,请关闭其他占用较大的标签页,或在新的导出中使用更短的范围/更低的分辨率。",
19
+ "An export is already running": "已有导出正在进行",
20
+ "Export completed. Check your export folder or browser downloads; the OS save is not confirmed.": "导出已完成。请查看导出文件夹或浏览器下载;系统保存未确认。",
21
+ "Export pipeline completed. The requesting tool handles the file handoff.": "导出流程已完成。文件交付由发起请求的工具处理。",
22
+ "Cancelling at the current work boundary…": "正在当前工作边界处取消…",
23
+ "Preparing": "准备中",
24
+ "Encoding": "编码中",
25
+ "Finalizing": "收尾中",
26
+ "Completed": "已完成",
27
+ "Failed": "失败",
28
+ "Cancelled": "已取消",
29
+ "Depth range sampled": "已采样深度范围",
30
+ "Frames submitted to encoder": "已提交给编码器的帧",
31
+ "Finalizing MP4. Progress is indeterminate; this stage cannot be interrupted.": "正在完成 MP4。进度不确定;此阶段无法中断。",
32
+ "Finishing encoder output. Progress is indeterminate.": "正在完成编码器输出。进度不确定。",
33
+ "Handing off existing PNGs. The file handoff cannot be cancelled.": "正在交付现有 PNG。文件交付无法取消。",
34
+ "Building the ZIP. Cancel takes effect after the current work unit, before the file handoff.": "正在构建 ZIP。取消将在当前工作单元结束后、文件交付前生效。",
35
+ "Cancel stops at the next frame boundary, before the file handoff.": "取消将在下一个帧边界停止,届时尚未交付文件。",
36
+ "Preparing the renderer and checking MP4 support…": "正在准备渲染器并检查 MP4 支持…",
37
+ "Encoding the requested frames, not yet a completed file.": "正在编码所请求的帧,尚未生成完整文件。",
38
+ "Retry keeps the original shot, camera, range and settings; it does not change your edits.": "重试会沿用原镜头、摄影机、范围与设置,不会改变你的编辑。",
39
+ "Cancel export": "取消导出",
40
+ "Retry same export": "重试相同导出",
41
+ "Add at least one Shot before exporting OTIO": "导出 OTIO 前请至少添加一个镜头",
42
+ "The shot renderer is not ready": "镜头渲染器尚未就绪",
43
+ "Add at least one Shot before exporting a keyframe pack": "导出关键帧包前请至少添加一个镜头",
44
+ "Depth and normal passes exported": "深度和法线通道已导出",
45
+ "Add at least one Shot before exporting a storyboard": "导出故事板前请至少添加一个镜头",
46
+ "Composing the storyboard…": "正在编排故事板…",
47
+ "Add a panel first — storyboard exports describe panels": "请先添加面板 — 故事板导出需要面板",
48
+ "ROOT PATH": "根路径",
49
+ "Prompt copied to clipboard": "提示词已复制到剪贴板",
50
+ "Frame export finished. Check your export folder or browser downloads.": "帧导出完成。请查看导出文件夹或浏览器下载。",
51
+ "Redone": "已重做",
52
+ "Undone": "已撤销",
53
+ "Saving…": "保存中…",
54
+ "Save failed": "保存失败",
55
+ "Not saved": "未保存",
56
+ "Unsaved changes": "未保存的更改",
57
+ "Saved": "已保存",
58
+ "Sign in with ChatGPT in the Agent panel to generate a blueprint.": "请在智能体面板中登录 ChatGPT 以生成蓝图。",
59
+ "Sign in to the Agent (⌘B) to sculpt from an image.": "请登录智能体(⌘B)以从图像雕塑。",
60
+ "The live editor is disconnected — reconnect to sculpt.": "实时编辑器已断开 — 请重新连接后再雕塑。",
61
+ "No model is available — add a provider key in the Agent panel.": "没有可用模型 — 请在智能体面板中添加服务商密钥。",
62
+ "New sculpt": "新建雕塑",
63
+ "Add a reference image in the Sculpt panel and press Start — or select a sculpt and press 5.": "在雕塑面板中添加参考图并按开始 — 或选中雕塑并按 5。",
64
+ "The graphics context was lost — restoring the stage. If it stays black, reload the page; your work is autosaved.": "图形上下文已丢失 — 正在恢复舞台。若持续黑屏,请刷新页面;你的工作已自动保存。",
65
+ "Graphics restored.": "图形已恢复。",
66
+ "Dot added — drag it to bend the route": "已添加点 — 拖动它使路径弯曲",
67
+ "3D graphics (WebGL) is turned off in this browser": "此浏览器的 3D 图形(WebGL)已关闭",
68
+ "Quit the browser completely (Cmd+Q) and reopen it, or turn on \"Use graphics acceleration when available\" in its settings. chrome://gpu shows the WebGL status.": "请完全退出浏览器(Cmd+Q)后重新打开,或在设置中开启“尽可能使用图形加速”。chrome://gpu 可查看 WebGL 状态。",
69
+ "The 3D view lost its graphics context": "3D 视图丢失了图形上下文",
70
+ "Waiting for the browser to restore it. If this stays, reload the studio — scenes autosave.": "正在等待浏览器恢复。若持续如此,请刷新工作室 — 场景会自动保存。",
71
+ "Reload": "刷新",
72
+ "Click or ▸ to expand · drag to move": "点击或 ▸ 展开 · 拖动移动",
73
+ "Click or ▾ to fold · drag to move": "点击或 ▾ 折叠 · 拖动移动",
74
+ "Expand inset view": "展开小窗视图",
75
+ "Collapse inset view": "折叠小窗视图",
76
+ "Shot view": "镜头视图",
77
+ "Top-View": "顶视图",
78
+ "Resize inset view": "调整小窗大小",
79
+ "Shot": "镜头",
80
+ "Look through the shot camera": "通过镜头摄影机观察",
81
+ "Look through the shot camera — right-drag, WASD and orbit set the recording lens (Esc returns)": "通过镜头摄影机观察 — 右键拖动、WASD 和环绕设置录制镜头(Esc 返回)",
82
+ "Look through": "镜头视角",
83
+ "Return to the editor view (Esc)": "返回编辑器视图 (Esc)",
84
+ "Shot camera": "镜头摄影机",
85
+ "Esc · exit": "Esc · 退出",
86
+ "SUBJECT OUT OF FRAME": "角色不在画面内",
87
+ "A new version of CozyClay is ready.": "CozyClay 新版本已就绪。",
88
+ "Reload to update": "刷新以更新",
89
+ "Later": "稍后",
90
+ "Object deleted.": "物体已删除。",
91
+ "Undo object deletion": "撤销物体删除",
92
+ "Undo": "撤销",
93
+ "Project access was not granted.": "未获得项目访问权限。",
94
+ "Restore": "恢复",
95
+ "Dismiss": "关闭",
96
+ "Image deleted. This session can undo it.": "图片已删除。本次会话内可撤销。",
97
+ "This account cannot generate images.": "此账号无法生成图像。",
98
+ "Sign in with ChatGPT in the Agent panel first.": "请先在智能体面板中登录 ChatGPT。",
99
+ "unknown error": "未知错误",
100
+ "The image route did not return a PNG.": "图像接口未返回 PNG。",
101
+ "Draft": "草稿",
102
+ "The studio hit a render error": "工作室遇到渲染错误",
103
+ "Scenes autosave as you work, so reloading resumes from the last saved state.": "场景会随工作自动保存,刷新后会从上次保存的状态继续。",
104
+ "Reload the studio": "刷新工作室",
105
+ "Your first 60 seconds": "你的最初 60 秒",
106
+ "Core loop complete": "核心流程完成",
107
+ "Dismiss guide": "关闭指南",
108
+ "You made your first panel.": "你做出了第一个面板。",
109
+ "You made your first shot.": "你做出了第一个镜头。",
110
+ "Describe, review on the Board, then stylize or export. You can close this guide and keep adding panels.": "描述、在看板上检查,然后风格化或导出。你可以关闭本指南并继续添加面板。",
111
+ "Select, move, ask, and play are the core CozyClay loop. You can close this guide and keep blocking.": "选择、移动、提问和播放是 CozyClay 的核心流程。你可以关闭本指南并继续摆位。",
112
+ "Continue editing": "继续编辑",
113
+ "Follow these four actions to see a result quickly.": "跟随这四个步骤,快速看到结果。",
114
+ "Skip guide": "跳过指南",
115
+ "QA clean": "QA 通过",
116
+ "Settings": "设置",
117
+ "Language and analytics": "语言与统计",
118
+ "Medium": "中景",
119
+ "Wide": "广角",
120
+ "Close-Up": "特写",
121
+ "Low Angle": "低角度",
122
+ "High Angle": "高角度",
123
+ "Saved scenes were written by a newer CozyClay — they have been left untouched and this session will not save": "已保存的场景由更新版本的 CozyClay 创建 — 已保持原样,本次会话也不会保存",
124
+ };
@@ -0,0 +1,36 @@
1
+ // Chinese UI copy for domain refusals, notifications and project actions.
2
+ // Keyed by the English default in ko(en, ko) so those call sites stay untouched.
3
+ export const ZH_DOMAINS = {
4
+ "Character added to the scene": "角色已添加到场景",
5
+ "The active character's rig is not loaded": "当前角色的骨骼尚未加载",
6
+ "The path already fills the clip — extend the duration or clear a waypoint": "路径已填满整个片段 — 请延长时长或清除一个路径点",
7
+ "Root path constraints off": "根路径约束已关闭",
8
+ "Root path on — click the set floor in the Shot view to drop waypoints; Subject 1 is the frame 0 start": "根路径已开启 — 在镜头视图中点击布景地面放置路径点;角色 1 是第 0 帧的起点",
9
+ "Edited pose": "已编辑的姿势",
10
+ "Pose saved": "姿势已保存",
11
+ "Nothing to undo": "没有可撤销的操作",
12
+ "Nothing to redo": "没有可重做的操作",
13
+ "Confirm Replace existing keys before applying this pin.": "应用此固定前请确认替换现有键。",
14
+ "Pose mode — preview the correction, then apply it to the selected motion block": "姿势模式 — 先预览修正,再应用到所选动作片段",
15
+ "Pose mode — drag handles in the main view; the shot camera stays frozen in the inset": "姿势模式 — 在主视图中拖动手柄;镜头摄影机在小窗中保持冻结",
16
+ "Pose mode off — keyed poses keep playing": "姿势模式已关闭 — 已打关键帧的姿势会继续播放",
17
+ "Nothing to drop": "没有可放置的对象",
18
+ "Cutout": "抠图",
19
+ "its picture is missing from the store": "其图片不在存储中",
20
+ "Grouped under a new Empty": "已归入新的空物体",
21
+ "Could not group this object": "无法编组此物体",
22
+ "Missing character": "角色不存在",
23
+ "Character 1": "角色 1",
24
+ "Character 2": "角色 2",
25
+ "Root": "根",
26
+ "Could not save the project": "无法保存项目",
27
+ "Could not export the animation project": "无法导出动画项目",
28
+ "Discard unsaved changes and start a new project?": "放弃未保存的更改并新建项目?",
29
+ "That starter scene is not in this build": "此版本中没有该起始场景",
30
+ "Could not open the project": "无法打开项目",
31
+ "This browser would not read that file. Open it once more.": "此浏览器未能读取该文件,请再打开一次。",
32
+ "Project access was not granted — allow access and try again.": "未获得项目访问权限 — 请允许访问后重试。",
33
+ "SCENE 01": "场景 01",
34
+ "Draw the selected Shot's rail in the Top-View": "在顶视图中绘制所选镜头的轨道",
35
+ "Camera rail deleted — Follow keeps the current distance": "摄影机轨道已删除 — 跟随模式保持当前距离",
36
+ };