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 +1 @@
|
|
|
1
|
-
{"payload":"{\"distribution\":\"npm\",\"package\":\"cozyclay\",\"version\":\"3.1.
|
|
1
|
+
{"payload":"{\"distribution\":\"npm\",\"package\":\"cozyclay\",\"version\":\"3.1.3\",\"repository\":\"NomaDamas/CozyClay\",\"content_sha256\":\"9c94e22b92abbaaee2aefc22fdf0e2a5fe5de745d82739751ae08884b1f655bd\"}","signature":"sY+Pss1UzGZHXmVNKQRKwpxGLw6xN1iMtej6DnUUrBusSndF/2SePRXbdL0PO1CiM6Y6nsECF9EeuIZo125ZDQ=="}
|
package/dist/index.html
CHANGED
|
@@ -394,7 +394,7 @@
|
|
|
394
394
|
<div class="wrap start">
|
|
395
395
|
<div class="start-head">
|
|
396
396
|
<h2>Quick start</h2>
|
|
397
|
-
<p>Node.js 22.
|
|
397
|
+
<p>Node.js 22.19+ and a Chromium browser.</p>
|
|
398
398
|
<a href="#controls" id="controls-link">Full control list</a>
|
|
399
399
|
</div>
|
|
400
400
|
<div class="start-body">
|
package/dist/sitemap.xml
CHANGED
package/dist/workflow/index.html
CHANGED
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
<meta name="theme-color" content="#101116" />
|
|
9
9
|
<meta name="description" content="CozyClay visual workflow builder" />
|
|
10
10
|
<title>CozyClay Workflow</title>
|
|
11
|
-
<script type="module" crossorigin src="/assets/workflow-
|
|
11
|
+
<script type="module" crossorigin src="/assets/workflow-BMJGd_gZ.js"></script>
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-hePW80VL.js">
|
|
13
13
|
<link rel="modulepreload" crossorigin href="/assets/modulepreload-polyfill-P2Xu9kJm.js">
|
|
14
|
-
<link rel="modulepreload" crossorigin href="/assets/task-board-
|
|
14
|
+
<link rel="modulepreload" crossorigin href="/assets/task-board-DFfNa0xg.js">
|
|
15
15
|
<link rel="modulepreload" crossorigin href="/assets/fflate.module-KbGj5-H6.js">
|
|
16
|
-
<link rel="modulepreload" crossorigin href="/assets/scene-asset-sync-
|
|
17
|
-
<link rel="stylesheet" crossorigin href="/assets/scene-asset-sync-
|
|
16
|
+
<link rel="modulepreload" crossorigin href="/assets/scene-asset-sync-Bz59Ghdz.js">
|
|
17
|
+
<link rel="stylesheet" crossorigin href="/assets/scene-asset-sync-BEjjhNEo.css">
|
|
18
18
|
<link rel="stylesheet" crossorigin href="/assets/workflow-DRdwoDBT.css">
|
|
19
19
|
</head>
|
|
20
20
|
<body>
|
package/mcp/README.md
CHANGED
|
@@ -35,7 +35,7 @@ Then point a client at it. For Claude Desktop, in `claude_desktop_config.json`:
|
|
|
35
35
|
}
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
Restart the client;
|
|
38
|
+
Restart the client; 27 tools appear.
|
|
39
39
|
|
|
40
40
|
Prefer a long-lived endpoint? `node server.mjs --http 5183` serves Streamable HTTP at
|
|
41
41
|
`http://127.0.0.1:5183/mcp` (one isolated session per client), with a plain status page at `/`.
|
package/mcp/live-hub.mjs
CHANGED
|
@@ -436,10 +436,12 @@ export class LiveHub {
|
|
|
436
436
|
void this.serveController(socket, frame);
|
|
437
437
|
return;
|
|
438
438
|
}
|
|
439
|
-
|
|
440
|
-
|
|
439
|
+
// Meta and event frames both act for this socket's workspace. A meta frame
|
|
440
|
+
// relabels the tab after it opens another project; the handle stays.
|
|
441
|
+
const workspaceHandle = [...this.editors.entries()].find(([, editor]) => editor === socket)?.[0];
|
|
442
|
+
if (frame?.type === "meta" && workspaceHandle && frame.meta && typeof frame.meta === "object") this.workspaceMeta.set(workspaceHandle, frame.meta);
|
|
443
|
+
else if (frame?.type === "event" && typeof frame.name === "string" && frame.payload && typeof frame.payload === "object") {
|
|
441
444
|
if (workspaceHandle) this.onEvent?.({ workspaceHandle, workspaceId: this.workspaceId(workspaceHandle), name: frame.name, payload: frame.payload });
|
|
442
|
-
return;
|
|
443
445
|
}
|
|
444
446
|
if (frame?.type !== "result" || typeof frame.id !== "string") return;
|
|
445
447
|
const pending = this.pending.get(frame.id);
|
package/mcp/tool-handlers.mjs
CHANGED
|
@@ -71,6 +71,7 @@ import {
|
|
|
71
71
|
import { classifyMove, captureFraming, moveSlate } from "../src/camera-move.js";
|
|
72
72
|
import { DEFAULT_PREVIS_MODE, PREVIS_MODES, createProjectDocument, readProjectDocument } from "../src/project.js";
|
|
73
73
|
import { kindRefusal } from "../src/character-kind.js";
|
|
74
|
+
import { documentWithCamera, splitSavedCamera } from "./project-camera.mjs";
|
|
74
75
|
|
|
75
76
|
/** The authoring state. One scene document, one camera, one project name. */
|
|
76
77
|
export const state = {
|
|
@@ -1697,7 +1698,7 @@ export const createToolHandlers = ({ projectRootPromise } = {}) => {
|
|
|
1697
1698
|
// the current stage shape rather than trusted as-is.
|
|
1698
1699
|
const scenes = readSceneDocument(serializeSceneDocument(result.project.scenesDocument));
|
|
1699
1700
|
if (!scenes.document) return text(`That project was written by a newer CozyClay: ${full}`);
|
|
1700
|
-
const nextDocument = scenes.document;
|
|
1701
|
+
const { document: nextDocument, camera: savedCamera } = splitSavedCamera(scenes.document);
|
|
1701
1702
|
if (liveHub?.connected) {
|
|
1702
1703
|
try {
|
|
1703
1704
|
const loaded = await executeStudioCommand({ ...admission, action: "load_scenes", args: { document: nextDocument } });
|
|
@@ -1713,11 +1714,10 @@ export const createToolHandlers = ({ projectRootPromise } = {}) => {
|
|
|
1713
1714
|
}
|
|
1714
1715
|
}
|
|
1715
1716
|
state.doc = nextDocument;
|
|
1717
|
+
Object.assign(state.camera, savedCamera);
|
|
1716
1718
|
state.previsMode = result.project.previsMode ?? DEFAULT_PREVIS_MODE;
|
|
1717
1719
|
state.name = result.project.name;
|
|
1718
|
-
state
|
|
1719
|
-
state.focusLocked = false;
|
|
1720
|
-
state.markedFraming = null;
|
|
1720
|
+
Object.assign(state, { focus: null, focusLocked: false, markedFraming: null });
|
|
1721
1721
|
return text(`Opened ${full}.\n\n${sceneReport()}`);
|
|
1722
1722
|
},
|
|
1723
1723
|
),
|
|
@@ -1753,7 +1753,7 @@ export const createToolHandlers = ({ projectRootPromise } = {}) => {
|
|
|
1753
1753
|
return text(`Could not write project: ${error.message}`);
|
|
1754
1754
|
}
|
|
1755
1755
|
const project = createProjectDocument({
|
|
1756
|
-
scenesDocument: state.doc,
|
|
1756
|
+
scenesDocument: documentWithCamera(state.doc, state.camera),
|
|
1757
1757
|
workspaceLayout: null,
|
|
1758
1758
|
customPoses: [],
|
|
1759
1759
|
name: state.name,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cozyclay",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.3",
|
|
4
4
|
"description": "Open source previs software in the browser: block a scene, pose characters, author camera moves and cuts, then take the same shots to an AI video model.",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "node tools/dev-full.mjs --host 127.0.0.1 --port 5180",
|
|
@@ -61,9 +61,11 @@
|
|
|
61
61
|
"license": "AGPL-3.0-or-later",
|
|
62
62
|
"type": "module",
|
|
63
63
|
"dependencies": {
|
|
64
|
-
"@
|
|
65
|
-
"@earendil-works/pi-
|
|
66
|
-
"@earendil-works/pi-
|
|
64
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.306",
|
|
65
|
+
"@earendil-works/pi-agent-core": "1.4.0",
|
|
66
|
+
"@earendil-works/pi-ai": "1.4.0",
|
|
67
|
+
"@earendil-works/pi-codemode": "1.4.0",
|
|
68
|
+
"zod": "^4.6.7"
|
|
67
69
|
},
|
|
68
70
|
"devDependencies": {
|
|
69
71
|
"@eslint/js": "^10.0.1",
|
package/src/App.jsx
CHANGED
|
@@ -151,7 +151,8 @@ import {
|
|
|
151
151
|
trackActivation,
|
|
152
152
|
trackFeature,
|
|
153
153
|
} from "./analytics.js";
|
|
154
|
-
import { ko, isKo } from "./locale.js";
|
|
154
|
+
import { ko, isKo, isZh } from "./locale.js";
|
|
155
|
+
import { saveExport } from "./export-workspace.js";
|
|
155
156
|
import { characterCapabilities, kindRefusal, proxyFigureBounds } from "./character-kind.js";
|
|
156
157
|
import { STARTER_SCENES, resolveSceneParam } from "./starter-scenes.js";
|
|
157
158
|
import { PART_COLOURS } from "./part-colours.js";
|
|
@@ -1146,7 +1147,7 @@ export default function App() {
|
|
|
1146
1147
|
// A capsule figure has no rig at all, so it gets its own reason rather than
|
|
1147
1148
|
// the "load a character" one, which would read as a loading problem.
|
|
1148
1149
|
const activeCapabilities = characterCapabilities(activeChar);
|
|
1149
|
-
const poseRefusal = !activeCapabilities.pose ? kindRefusal("pose", isKo) : ikChains ? null : ko("Load a character to pose it — Pose mode edits its rig with IK", "캐릭터를 불러와야 포즈를 잡을 수 있어요 — 포즈 모드는 IK로 리그를 편집합니다");
|
|
1150
|
+
const poseRefusal = !activeCapabilities.pose ? kindRefusal("pose", isKo, isZh) : ikChains ? null : ko("Load a character to pose it — Pose mode edits its rig with IK", "캐릭터를 불러와야 포즈를 잡을 수 있어요 — 포즈 모드는 IK로 리그를 편집합니다", "加载角色后才能摆姿势 — 姿势模式用 IK 编辑其骨骼");
|
|
1150
1151
|
function selectWorkflowMode(next) {
|
|
1151
1152
|
if (next === "pose" && poseRefusal) {
|
|
1152
1153
|
setToast(poseRefusal);
|
|
@@ -1154,7 +1155,7 @@ export default function App() {
|
|
|
1154
1155
|
}
|
|
1155
1156
|
// A storyboard is stills: there is no Motion department to open.
|
|
1156
1157
|
if (next === "motion" && storyboardProject) {
|
|
1157
|
-
setToast(ko(STORYBOARD_MOTION_REFUSAL.en, STORYBOARD_MOTION_REFUSAL.ko));
|
|
1158
|
+
setToast(ko(STORYBOARD_MOTION_REFUSAL.en, STORYBOARD_MOTION_REFUSAL.ko, STORYBOARD_MOTION_REFUSAL.zh));
|
|
1158
1159
|
return;
|
|
1159
1160
|
}
|
|
1160
1161
|
setWorkflowMode(next);
|
|
@@ -1257,7 +1258,7 @@ export default function App() {
|
|
|
1257
1258
|
setSculptBlueprint((current) => ({ ...current, state: "idle", sheet: null, error: null }));
|
|
1258
1259
|
setSculptReference(await objectsDomain.storeSculptReference(file));
|
|
1259
1260
|
} catch (error) {
|
|
1260
|
-
setToast(ko(`Could not read that image: ${error.message}`, `이미지를 읽지 못했어요: ${error.message}`));
|
|
1261
|
+
setToast(ko(`Could not read that image: ${error.message}`, `이미지를 읽지 못했어요: ${error.message}`, `无法读取该图片:${error.message}`));
|
|
1261
1262
|
}
|
|
1262
1263
|
}
|
|
1263
1264
|
function startSculptRun() {
|
|
@@ -1477,7 +1478,7 @@ export default function App() {
|
|
|
1477
1478
|
|
|
1478
1479
|
const rejectUnsupportedDrop = (count) => setToast(ko(
|
|
1479
1480
|
`${count} file${count > 1 ? "s" : ""} not supported — use PNG, JPG, WebP, GIF or a .glb / .obj / .fbx (iPhone HEIC photos need converting first)`,
|
|
1480
|
-
`지원하지 않는 파일 ${count}개 — PNG, JPG, WebP, GIF 또는 .glb / .obj / .fbx만 가능해요 (아이폰 HEIC 사진은 먼저 변환해 주세요)`,
|
|
1481
|
+
`지원하지 않는 파일 ${count}개 — PNG, JPG, WebP, GIF 또는 .glb / .obj / .fbx만 가능해요 (아이폰 HEIC 사진은 먼저 변환해 주세요)`, `不支持 ${count} 个文件 — 请使用 PNG、JPG、WebP、GIF 或 .glb / .obj / .fbx(iPhone HEIC 照片需先转换)`,
|
|
1481
1482
|
));
|
|
1482
1483
|
const stageDrop = {
|
|
1483
1484
|
onImages: (files) => importCutouts(files),
|
|
@@ -1785,7 +1786,7 @@ export default function App() {
|
|
|
1785
1786
|
// so run_action can give the agent the reason the user was shown.
|
|
1786
1787
|
const toastSinkRef = useRef(new Set());
|
|
1787
1788
|
const setToast = useCallback((value, english) => {
|
|
1788
|
-
const toast = resolveStudioToast(value, isKo, ko);
|
|
1789
|
+
const toast = resolveStudioToast(value, isKo, ko, isZh);
|
|
1789
1790
|
if (english !== undefined) toast.message = english;
|
|
1790
1791
|
if (typeof toast.uiMessage === "string" && toast.uiMessage) for (const sink of toastSinkRef.current) sink(toast);
|
|
1791
1792
|
showToast(toast.uiMessage);
|
|
@@ -1947,7 +1948,7 @@ export default function App() {
|
|
|
1947
1948
|
deleted = true;
|
|
1948
1949
|
return true;
|
|
1949
1950
|
} catch (error) {
|
|
1950
|
-
setToast(
|
|
1951
|
+
setToast(ko(`Could not delete that image — ${error.message}`, `이미지를 삭제하지 못했어요 — ${error.message}`, `无法删除该图片 — ${error.message}`));
|
|
1951
1952
|
return false;
|
|
1952
1953
|
} finally {
|
|
1953
1954
|
db?.close?.();
|
|
@@ -1970,9 +1971,9 @@ export default function App() {
|
|
|
1970
1971
|
setAssetTrash((current) => current.filter((asset) => asset.id !== record.id));
|
|
1971
1972
|
setAssetUndoOffered(false);
|
|
1972
1973
|
restored = true;
|
|
1973
|
-
setToast(
|
|
1974
|
+
setToast(ko(`${record.name || "Image"} restored`, `${record.name || "이미지"} 복원됨`, `${record.name || "图片"}已恢复`));
|
|
1974
1975
|
} catch (error) {
|
|
1975
|
-
setToast(
|
|
1976
|
+
setToast(ko(`Could not restore that image — ${error.message}`, `이미지를 복원하지 못했어요 — ${error.message}`, `无法恢复该图片 — ${error.message}`));
|
|
1976
1977
|
} finally {
|
|
1977
1978
|
if (restored) await refreshAssetShelf();
|
|
1978
1979
|
setDeletingAssetId(null);
|
|
@@ -2463,6 +2464,17 @@ export default function App() {
|
|
|
2463
2464
|
capture_plan_png: (args = {}) => appContext.live.state.capturePlanPng(requestedCaptureOutput(args) ?? undefined),
|
|
2464
2465
|
};
|
|
2465
2466
|
|
|
2467
|
+
const liveSceneName = scenes.find((entry) => entry.id === activeSceneId)?.name ?? "";
|
|
2468
|
+
const liveMeta = () => ({
|
|
2469
|
+
project: projectName ?? "Untitled", scene: liveSceneName, cast: appContext.live.characters.length,
|
|
2470
|
+
// The Workflow page's embedded preview is a live editor too; mark it apart from the authoring tab.
|
|
2471
|
+
// Commands: what this editor answers; a stale tab from an older build answers a different set.
|
|
2472
|
+
...(embedMode ? { embed: true } : {}), commands: [...Object.keys(liveQueries), ...Object.keys(studioBindingRef.current.handlers)],
|
|
2473
|
+
});
|
|
2474
|
+
// The client is created once; opening a project must still relabel the tab
|
|
2475
|
+
// in live status and the --workspace <project> selector (#864).
|
|
2476
|
+
useEffect(() => { liveControlRef.current?.setMeta(liveMeta()); }, [projectName, liveSceneName, appContext.live.characters.length]);
|
|
2477
|
+
|
|
2466
2478
|
useEffect(() => {
|
|
2467
2479
|
const enabled = import.meta.env.DEV || window.__COZYCLAY_LIVE__ === true;
|
|
2468
2480
|
if (!enabled) return undefined;
|
|
@@ -2474,19 +2486,7 @@ export default function App() {
|
|
|
2474
2486
|
// Tapped so a sculpt run hears the agent's object.sculpt / sculpt.compare.
|
|
2475
2487
|
handlers: sculptTap.wrap({ ...liveQueries, ...studioBindingRef.current.handlers }),
|
|
2476
2488
|
workspaceId: liveWorkspaceIdRef.current,
|
|
2477
|
-
meta:
|
|
2478
|
-
project: projectName ?? "Untitled",
|
|
2479
|
-
scene: scenes.find((entry) => entry.id === activeSceneId)?.name ?? "",
|
|
2480
|
-
cast: appContext.live.characters.length,
|
|
2481
|
-
// The Workflow page embeds this same Studio as a preview. It is a
|
|
2482
|
-
// live editor too, so an agent choosing a workspace must be able to
|
|
2483
|
-
// tell the preview apart from the tab the user is authoring in.
|
|
2484
|
-
...(embedMode ? { embed: true } : {}),
|
|
2485
|
-
// Which live commands this editor answers. A stale tab from an
|
|
2486
|
-
// older build (or a different app on the same port) answers a
|
|
2487
|
-
// different set; the agent picks a workspace that has what it needs.
|
|
2488
|
-
commands: [...Object.keys(liveQueries), ...Object.keys(studioBindingRef.current.handlers)],
|
|
2489
|
-
},
|
|
2489
|
+
meta: liveMeta(),
|
|
2490
2490
|
// The shared client intentionally has no disconnect UI callback.
|
|
2491
2491
|
// Observe only this owned socket; a stale handle must never be sent.
|
|
2492
2492
|
WebSocketImpl: class extends WebSocket {
|
|
@@ -2776,7 +2776,7 @@ export default function App() {
|
|
|
2776
2776
|
case "unsupported_codec": return ko("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 등에서 하드웨어 가속을 켠 뒤 다시 시도하세요.");
|
|
2777
2777
|
case "encode_failed": return ko("Video encoding failed. Retry the same request. If resources are tight, shorten the shot range or lower output resolution before starting a new export.", "영상 인코딩에 실패했어요. 같은 요청을 다시 시도하세요. 리소스가 부족하면 샷 범위를 줄이거나 출력 해상도를 낮춘 뒤 새로 내보내세요.");
|
|
2778
2778
|
case "render_failed": return ko("Frame rendering failed. Let the scene finish loading, then retry. If it repeats, shorten the range or reduce output resolution for a new export.", "프레임 렌더링에 실패했어요. 장면 로딩이 끝난 뒤 다시 시도하세요. 반복되면 범위나 출력 해상도를 줄여 새로 내보내세요.");
|
|
2779
|
-
case "aborted": return ko("Export cancelled. No further
|
|
2779
|
+
case "aborted": return ko("Export cancelled. No further files will be handed off.", "내보내기를 취소했어요. 추가 파일은 전달하지 않아요.");
|
|
2780
2780
|
default: return ko("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.", "내보내기를 완료하지 못했어요. 같은 요청을 다시 시도하세요. 메모리·리소스 문제라면 무거운 탭을 닫거나 범위·출력 해상도를 줄여 새로 내보내세요.");
|
|
2781
2781
|
}
|
|
2782
2782
|
}
|
|
@@ -2821,7 +2821,7 @@ export default function App() {
|
|
|
2821
2821
|
job.controller.signal.throwIfAborted();
|
|
2822
2822
|
attempt?.succeed();
|
|
2823
2823
|
updateExportStatus(job, "completed", { message: request.download
|
|
2824
|
-
? ko("Export completed.
|
|
2824
|
+
? ko("Export completed. Check your export folder or browser downloads; the OS save is not confirmed.", "내보내기를 완료했어요. 내보내기 폴더나 브라우저 다운로드를 확인하세요. OS 저장 완료는 확인할 수 없어요.")
|
|
2825
2825
|
: ko("Export pipeline completed. The requesting tool handles the file handoff.", "내보내기 처리를 완료했어요. 요청한 도구에서 파일 전달을 처리합니다.") });
|
|
2826
2826
|
if (!request.external) {
|
|
2827
2827
|
if (request.kind === "video") { track("export:video_succeeded", { format: "mp4" }); trackFeature("export_video"); }
|
|
@@ -3010,11 +3010,9 @@ export default function App() {
|
|
|
3010
3010
|
const slate = (moveSequence?.slate ?? "shot").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "shot";
|
|
3011
3011
|
// Default plate filename: const name = `cozyclay-${slate}.mp4`
|
|
3012
3012
|
const name = fileName ?? `cozyclay-${slate}.mp4`;
|
|
3013
|
-
|
|
3014
|
-
try { saveDownload(url, name); }
|
|
3015
|
-
finally { setTimeout(() => URL.revokeObjectURL(url), 10_000); }
|
|
3013
|
+
await saveExport(result.blob, name);
|
|
3016
3014
|
setRecordedVideoName(name);
|
|
3017
|
-
setToast((isKo, ko) =>
|
|
3015
|
+
setToast((isKo, ko) => ko(`${name} exported · ${result.frameCount} frames`, `${name} 내보내기 완료 · ${result.frameCount}프레임`, `${name} 已导出 · ${result.frameCount} 帧`));
|
|
3018
3016
|
return { ...result, fileName: name };
|
|
3019
3017
|
}
|
|
3020
3018
|
return result;
|
|
@@ -3079,13 +3077,13 @@ export default function App() {
|
|
|
3079
3077
|
{busy && !message && <p>{stage === "mux"
|
|
3080
3078
|
? ko("Finalizing MP4. Progress is indeterminate; this stage cannot be interrupted.", "MP4 마무리 중이에요. 진행률은 알 수 없으며 이 단계는 중단할 수 없어요.")
|
|
3081
3079
|
: stage === "flush" ? ko("Finishing encoder output. Progress is indeterminate.", "인코더 출력을 마무리하고 있어요. 진행률은 알 수 없어요.")
|
|
3082
|
-
: kind === "frame" ? ko("Handing off existing PNGs.
|
|
3083
|
-
: stage === "archive" ? ko("Building the ZIP. Cancel takes effect after the current work unit, before
|
|
3084
|
-
: stage === "depth" || stage === "frames" ? ko("Cancel stops at the next frame boundary, before
|
|
3080
|
+
: kind === "frame" ? ko("Handing off existing PNGs. The file handoff cannot be cancelled.", "기존 PNG를 전달하고 있어요. 파일 전달은 취소할 수 없어요.")
|
|
3081
|
+
: stage === "archive" ? ko("Building the ZIP. Cancel takes effect after the current work unit, before the file handoff.", "ZIP을 만드는 중이에요. 취소는 현재 작업이 끝난 뒤 파일 전달 전에 적용돼요.")
|
|
3082
|
+
: stage === "depth" || stage === "frames" ? ko("Cancel stops at the next frame boundary, before the file handoff.", "취소하면 파일 전달 전 다음 프레임 경계에서 멈춰요.")
|
|
3085
3083
|
: phase === "preparing" ? ko("Preparing the renderer and checking MP4 support…", "렌더러를 준비하고 MP4 지원을 확인하고 있어요…")
|
|
3086
3084
|
: ko("Encoding the requested frames, not yet a completed file.", "요청한 프레임을 인코딩 중이에요. 아직 파일이 완성되지 않았어요.")}</p>}
|
|
3087
3085
|
{retryable && <p>{ko("Retry keeps the original shot, camera, range and settings; it does not change your edits.", "다시 시도하면 원래 샷·카메라·범위·설정을 사용하며 편집 내용은 바꾸지 않아요.")}</p>}
|
|
3088
|
-
{handedOff > 0 && <p>{ko(`${handedOff} file(s) already handed off. Retry skips those
|
|
3086
|
+
{handedOff > 0 && <p>{ko(`${handedOff} file(s) already handed off. Retry skips those files; check your export folder or browser downloads because OS saves cannot be confirmed.`, `파일 ${handedOff}개는 이미 전달했어요. 다시 시도할 때 해당 파일은 건너뛰어요. OS 저장은 확인할 수 없으니 내보내기 폴더나 브라우저 다운로드를 확인하세요.`, `已交付 ${handedOff} 个文件。重试时会跳过这些文件;由于无法确认系统保存,请检查导出文件夹或浏览器下载。`)}</p>}
|
|
3089
3087
|
<div className="export-status-actions">
|
|
3090
3088
|
{busy && cancellable && <button type="button" data-testid="export-cancel" onClick={stopShotRecording}>{ko("Cancel export", "내보내기 취소")}</button>}
|
|
3091
3089
|
{retryable && <button type="button" data-testid="export-retry" disabled={recState === "recording"} onClick={() => void retryExport()}>{ko("Retry same export", "같은 내보내기 재시도")}</button>}
|
|
@@ -3094,7 +3092,7 @@ export default function App() {
|
|
|
3094
3092
|
);
|
|
3095
3093
|
}
|
|
3096
3094
|
|
|
3097
|
-
function downloadOtioCutList() {
|
|
3095
|
+
async function downloadOtioCutList() {
|
|
3098
3096
|
if (!shots.length) {
|
|
3099
3097
|
setToast(ko("Add at least one Shot before exporting OTIO", "OTIO를 내보내려면 샷을 하나 이상 추가하세요"));
|
|
3100
3098
|
return;
|
|
@@ -3113,20 +3111,15 @@ export default function App() {
|
|
|
3113
3111
|
};
|
|
3114
3112
|
const serialized = serializeOtio(exportScene, shots);
|
|
3115
3113
|
const blob = new Blob([serialized], { type: "application/json" });
|
|
3116
|
-
const url = URL.createObjectURL(blob);
|
|
3117
|
-
const anchor = document.createElement("a");
|
|
3118
3114
|
const slug = (activeScene?.name ?? "cut-list")
|
|
3119
3115
|
.toLowerCase()
|
|
3120
3116
|
.replace(/[^a-z0-9]+/g, "-")
|
|
3121
3117
|
.replace(/^-+|-+$/g, "") || "cut-list";
|
|
3122
|
-
|
|
3123
|
-
anchor.download = `cozyclay-${slug}.otio`;
|
|
3124
|
-
anchor.click();
|
|
3125
|
-
setTimeout(() => URL.revokeObjectURL(url), 10_000);
|
|
3118
|
+
await saveExport(blob, `cozyclay-${slug}.otio`);
|
|
3126
3119
|
const frameCount = shots.reduce((total, shot) => total + shot.endFrame - shot.startFrame + 1, 0);
|
|
3127
|
-
setToast(
|
|
3128
|
-
|
|
3129
|
-
|
|
3120
|
+
setToast(ko(`OTIO saved · ${shots.length} shots · ${frameCount} frames`,
|
|
3121
|
+
`OTIO 저장됨 · ${shots.length}샷 · ${frameCount}프레임`,
|
|
3122
|
+
`OTIO 已保存 · ${shots.length} 个镜头 · ${frameCount} 帧`));
|
|
3130
3123
|
} catch (error) {
|
|
3131
3124
|
setToast(error?.message || String(error));
|
|
3132
3125
|
}
|
|
@@ -3141,15 +3134,6 @@ export default function App() {
|
|
|
3141
3134
|
* - a storyboard contact sheet of the whole cut.
|
|
3142
3135
|
*/
|
|
3143
3136
|
|
|
3144
|
-
function saveDownload(href, name) {
|
|
3145
|
-
const anchor = document.createElement("a");
|
|
3146
|
-
anchor.href = href;
|
|
3147
|
-
anchor.download = name;
|
|
3148
|
-
document.body.appendChild(anchor);
|
|
3149
|
-
anchor.click();
|
|
3150
|
-
anchor.remove();
|
|
3151
|
-
}
|
|
3152
|
-
|
|
3153
3137
|
function loadImage(dataUrl) {
|
|
3154
3138
|
return new Promise((resolve, reject) => {
|
|
3155
3139
|
const image = new Image();
|
|
@@ -3217,13 +3201,13 @@ export default function App() {
|
|
|
3217
3201
|
updateExportStatus(job, "preparing", { stage: "frames", cancellable: true });
|
|
3218
3202
|
await exportBoundary(job);
|
|
3219
3203
|
const packShot = { title: entry.name, index: index + 1, startFrame: entry.startFrame, endFrame: entry.endFrame };
|
|
3220
|
-
onProgress?.(ko(`Rendering frames for "${entry.name}"`, `"${entry.name}" 프레임 렌더링
|
|
3204
|
+
onProgress?.(ko(`Rendering frames for "${entry.name}"`, `"${entry.name}" 프레임 렌더링 중`, `正在渲染“${entry.name}”的帧`));
|
|
3221
3205
|
const firstUrl = captureShotFramePng(entry.startFrame);
|
|
3222
3206
|
if (!firstUrl) throw Object.assign(new Error(ko("The shot renderer is not ready", "샷 렌더러가 아직 준비되지 않았어요")), { exportFailureCode: "render_failed" });
|
|
3223
3207
|
await exportBoundary(job);
|
|
3224
3208
|
const lastUrl = entry.endFrame > entry.startFrame ? captureShotFramePng(entry.endFrame) : null;
|
|
3225
3209
|
await exportBoundary(job);
|
|
3226
|
-
onProgress?.(ko(`Recording the clip for "${entry.name}"`, `"${entry.name}" 클립 녹화
|
|
3210
|
+
onProgress?.(ko(`Recording the clip for "${entry.name}"`, `"${entry.name}" 클립 녹화 중`, `正在录制“${entry.name}”的片段`));
|
|
3227
3211
|
const recorded = await runShotExport({ startFrame: entry.startFrame, endFrame: entry.endFrame, download: false }, job);
|
|
3228
3212
|
updateExportStatus(job, "finalizing", { stage: "archive", cancellable: true });
|
|
3229
3213
|
await exportBoundary(job);
|
|
@@ -3276,24 +3260,23 @@ export default function App() {
|
|
|
3276
3260
|
if (!job.request.context.shots.length) throw new Error(ko("Add at least one Shot before exporting a keyframe pack", "키프레임 팩을 내보내려면 샷을 하나 이상 추가하세요"));
|
|
3277
3261
|
for (const [order, index] of targets.entries()) {
|
|
3278
3262
|
if (job.request.handedOff.has(index)) continue;
|
|
3279
|
-
job.label = ko(`Shot ${order + 1} of ${targets.length}`, `샷 ${order + 1} / ${targets.length}`);
|
|
3263
|
+
job.label = ko(`Shot ${order + 1} of ${targets.length}`, `샷 ${order + 1} / ${targets.length}`, `镜头 ${order + 1} / ${targets.length}`);
|
|
3280
3264
|
const pack = await buildShotKeyframePack(job.request.context.shots[index], index, null, job);
|
|
3281
3265
|
job.controller.signal.throwIfAborted();
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
setToast(isKo ? `${pack.name} 다운로드 요청 · 파일 ${pack.entries.length}개` : `Download requested: ${pack.name} · ${pack.entries.length} files`);
|
|
3266
|
+
await saveExport(new Blob([pack.bytes], { type: "application/zip" }), pack.name);
|
|
3267
|
+
job.request.handedOff.add(index);
|
|
3268
|
+
setToast(ko(`${pack.name} exported · ${pack.entries.length} files`, `${pack.name} 내보내기 완료 · 파일 ${pack.entries.length}개`, `${pack.name} 已导出 · ${pack.entries.length} 个文件`));
|
|
3286
3269
|
await exportBoundary(job);
|
|
3287
3270
|
}
|
|
3288
3271
|
}));
|
|
3289
3272
|
}
|
|
3290
3273
|
|
|
3291
3274
|
/** Depth and normal conditioning passes of the framing on screen now. */
|
|
3292
|
-
function exportRenderPasses() {
|
|
3275
|
+
async function exportRenderPasses() {
|
|
3293
3276
|
try {
|
|
3294
3277
|
const dataUrls = renderPassDataUrls();
|
|
3295
|
-
for (const kind of ["depth", "normal"])
|
|
3296
|
-
setToast(ko("Depth and normal passes
|
|
3278
|
+
for (const kind of ["depth", "normal"]) await saveExport(dataUrls[kind], passFileName(kind));
|
|
3279
|
+
setToast(ko("Depth and normal passes exported", "뎁스·노멀 패스를 내보냈어요"));
|
|
3297
3280
|
trackFeature("export_render_pass");
|
|
3298
3281
|
} catch (error) {
|
|
3299
3282
|
setToast(error?.message || String(error));
|
|
@@ -3352,8 +3335,8 @@ export default function App() {
|
|
|
3352
3335
|
cell: { width: 480, height: 300 },
|
|
3353
3336
|
createCanvas: storyboardCanvas,
|
|
3354
3337
|
});
|
|
3355
|
-
|
|
3356
|
-
setToast(
|
|
3338
|
+
await saveExport(canvas.toDataURL("image/png"), "cozyclay-storyboard.png");
|
|
3339
|
+
setToast(ko(`Storyboard saved · ${cells.length} shots`, `스토리보드 저장됨 · ${cells.length}샷`, `故事板已保存 · ${cells.length} 个镜头`));
|
|
3357
3340
|
trackFeature("export_storyboard");
|
|
3358
3341
|
} catch (error) {
|
|
3359
3342
|
setToast(error?.message || String(error));
|
|
@@ -3443,8 +3426,8 @@ export default function App() {
|
|
|
3443
3426
|
}
|
|
3444
3427
|
const canvas = composeStoryboard({ shots: cells, columns: Math.min(3, cells.length), cell: { width: 480, height: 300 }, createCanvas: storyboardCanvas });
|
|
3445
3428
|
job.controller.signal.throwIfAborted();
|
|
3446
|
-
|
|
3447
|
-
setToast(
|
|
3429
|
+
await saveExport(canvas.toDataURL("image/png"), "cozyclay-storyboard.png");
|
|
3430
|
+
setToast(ko(`Contact sheet exported · ${cells.length} panels`, `콘택트 시트 내보내기 완료 · 패널 ${cells.length}개`, `缩略图总览已导出 · ${cells.length} 个面板`));
|
|
3448
3431
|
return { fileName: "cozyclay-storyboard.png", panelCount: cells.length };
|
|
3449
3432
|
}));
|
|
3450
3433
|
}
|
|
@@ -3487,7 +3470,7 @@ export default function App() {
|
|
|
3487
3470
|
updateExportStatus(job, "preparing", { stage: "frames", cancellable: true });
|
|
3488
3471
|
const packs = [];
|
|
3489
3472
|
for (const [step, entry] of targets.entries()) {
|
|
3490
|
-
job.label = ko(`Panel ${step + 1} of ${targets.length}`, `패널 ${step + 1} / ${targets.length}`);
|
|
3473
|
+
job.label = ko(`Panel ${step + 1} of ${targets.length}`, `패널 ${step + 1} / ${targets.length}`, `面板 ${step + 1} / ${targets.length}`);
|
|
3491
3474
|
packs.push(await panelPackArgs(entry, contextPanels.indexOf(entry), all));
|
|
3492
3475
|
await exportBoundary(job);
|
|
3493
3476
|
}
|
|
@@ -3497,10 +3480,8 @@ export default function App() {
|
|
|
3497
3480
|
const bytes = every ? buildZip(entries) : buildKeyframePack(packs[0]);
|
|
3498
3481
|
const name = every ? "cozyclay-panels.zip" : `cozyclay-${packs[0].folder}-${slugifyTitle(target.name)}.zip`;
|
|
3499
3482
|
await exportBoundary(job);
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
finally { setTimeout(() => URL.revokeObjectURL(url), 10_000); }
|
|
3503
|
-
setToast(isKo ? `${name} 다운로드 요청 · 파일 ${entries.length}개` : `Download requested: ${name} · ${entries.length} files`);
|
|
3483
|
+
await saveExport(new Blob([bytes], { type: "application/zip" }), name);
|
|
3484
|
+
setToast(ko(`${name} exported · ${entries.length} files`, `${name} 내보내기 완료 · 파일 ${entries.length}개`, `${name} 已导出 · ${entries.length} 个文件`));
|
|
3504
3485
|
return { fileName: name, entries: entries.map((entry) => entry.name) };
|
|
3505
3486
|
}));
|
|
3506
3487
|
}
|
|
@@ -3819,7 +3800,7 @@ export default function App() {
|
|
|
3819
3800
|
if (!result.ok) return result;
|
|
3820
3801
|
await rehydrateProjectAssets(result.project, result.warnings);
|
|
3821
3802
|
applyProject(result.project);
|
|
3822
|
-
setToast(`${
|
|
3803
|
+
setToast(`${ko(`Project opened: ${result.project.name}`, `프로젝트 열림: ${result.project.name}`, `项目已打开:${result.project.name}`)}${projectProblemsNotice(result.problems)}`);
|
|
3823
3804
|
return result;
|
|
3824
3805
|
},
|
|
3825
3806
|
manifest: () => projectManifest,
|
|
@@ -4186,27 +4167,24 @@ export default function App() {
|
|
|
4186
4167
|
}
|
|
4187
4168
|
const snapshot = structuredClone(result);
|
|
4188
4169
|
const frameJob = { source: result, done: false, request: null };
|
|
4189
|
-
frameJob.request = exportRequest("frame", (job) => {
|
|
4170
|
+
frameJob.request = exportRequest("frame", async (job) => {
|
|
4190
4171
|
if (!snapshot?.frame) throw Object.assign(new Error("The captured frame is not ready"), { exportFailureCode: "render_failed" });
|
|
4191
4172
|
updateExportStatus(job, "finalizing", { stage: "download", cancellable: false });
|
|
4192
|
-
const save = (
|
|
4173
|
+
const save = async (source, name) => {
|
|
4193
4174
|
if (job.request.handedOff.has(name)) return;
|
|
4194
|
-
|
|
4175
|
+
await saveExport(source, name);
|
|
4195
4176
|
job.request.handedOff.add(name);
|
|
4196
4177
|
};
|
|
4197
4178
|
if (snapshot.partColours && !job.request.handedOff.has("blocking-frame-palette.json")) {
|
|
4198
|
-
|
|
4199
|
-
const url = URL.createObjectURL(blob);
|
|
4200
|
-
try { save(url, "blocking-frame-palette.json"); }
|
|
4201
|
-
finally { setTimeout(() => URL.revokeObjectURL(url), 1000); }
|
|
4179
|
+
await save(new Blob([JSON.stringify({ partColours: snapshot.partColours }, null, 2)], { type: "application/json" }), "blocking-frame-palette.json");
|
|
4202
4180
|
}
|
|
4203
4181
|
if (snapshot.frameB) {
|
|
4204
|
-
save(snapshot.frame, "blocking-frame-A-start.png");
|
|
4205
|
-
save(snapshot.frameB, "blocking-frame-B-end.png");
|
|
4206
|
-
} else save(snapshot.frame, "blocking-frame.png");
|
|
4182
|
+
await save(snapshot.frame, "blocking-frame-A-start.png");
|
|
4183
|
+
await save(snapshot.frameB, "blocking-frame-B-end.png");
|
|
4184
|
+
} else await save(snapshot.frame, "blocking-frame.png");
|
|
4207
4185
|
frameJob.done = true;
|
|
4208
4186
|
setResult((current) => current === frameJob.source ? { ...current, downloaded: true } : current);
|
|
4209
|
-
setToast(ko("Frame
|
|
4187
|
+
setToast(ko("Frame export finished. Check your export folder or browser downloads.", "프레임 내보내기가 끝났어요. 내보내기 폴더나 브라우저 다운로드를 확인하세요."));
|
|
4210
4188
|
});
|
|
4211
4189
|
frameExportRef.current = frameJob;
|
|
4212
4190
|
return executeExportRequest(frameJob.request);
|
|
@@ -5006,9 +4984,9 @@ export default function App() {
|
|
|
5006
4984
|
setPathPointIndex(null);
|
|
5007
4985
|
setPathDraw(false);
|
|
5008
4986
|
const metrics = pathMetrics(createObjectPath({ points }));
|
|
5009
|
-
setToast(
|
|
5010
|
-
|
|
5011
|
-
|
|
4987
|
+
setToast(ko(`Travel path drawn — ${metrics.length.toFixed(1)} m, ${points.length} points`,
|
|
4988
|
+
`이동 경로 완성 — ${metrics.length.toFixed(1)} m, 점 ${points.length}개`,
|
|
4989
|
+
`移动路径已完成 — ${metrics.length.toFixed(1)} m,${points.length} 个点`));
|
|
5012
4990
|
}}
|
|
5013
4991
|
onCameraChange={commitManualCameraFraming}
|
|
5014
4992
|
/>
|
|
@@ -5328,7 +5306,7 @@ export default function App() {
|
|
|
5328
5306
|
<FirstSuccessGuide open={firstSuccessGuideOpen} previsMode={scenesDomain.previsMode} onDismiss={() => setFirstSuccessGuideOpen(false)} />
|
|
5329
5307
|
<UseCaseQuestion
|
|
5330
5308
|
open={useCaseAskOpen}
|
|
5331
|
-
isKo={isKo}
|
|
5309
|
+
isKo={isKo} isZh={isZh}
|
|
5332
5310
|
onAnswer={(useCase, team) => {
|
|
5333
5311
|
recordUseCase(useCase, team);
|
|
5334
5312
|
setUseCaseAskOpen(false);
|
|
@@ -5365,7 +5343,7 @@ export default function App() {
|
|
|
5365
5343
|
)}
|
|
5366
5344
|
{restoreOffer && (
|
|
5367
5345
|
<div className="asset-delete-toast" role="status">
|
|
5368
|
-
<span>{
|
|
5346
|
+
<span>{ko(`Restore last project${restoreOffer.name ? `: ${restoreOffer.name}` : ""}`, `마지막 프로젝트 복원${restoreOffer.name ? `: ${restoreOffer.name}` : ""}`, `恢复上次项目${restoreOffer.name ? `:${restoreOffer.name}` : ""}`)}</span>
|
|
5369
5347
|
<button
|
|
5370
5348
|
type="button"
|
|
5371
5349
|
onClick={async () => {
|
|
@@ -76,7 +76,7 @@ export function stylizeErrorMessage(error) {
|
|
|
76
76
|
if (error?.code === "entitlement") return ko("This account cannot generate images.", "이 계정은 이미지를 생성할 수 없어요.");
|
|
77
77
|
if (error?.status === 401 || error?.code === "auth") return ko("Sign in with ChatGPT in the Agent panel first.", "먼저 Agent 패널에서 ChatGPT로 로그인하세요.");
|
|
78
78
|
const reason = error?.message || ko("unknown error", "알 수 없는 오류");
|
|
79
|
-
return ko(`Stylize failed — ${reason}`, `스타일화 실패 — ${reason}`);
|
|
79
|
+
return ko(`Stylize failed — ${reason}`, `스타일화 실패 — ${reason}`, `风格化失败 — ${reason}`);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
/** The generated PNG's bytes, refused past `maxBytes` with a reason. */
|
|
@@ -86,7 +86,7 @@ export function stylizedPngBytes(dataUrl, maxBytes) {
|
|
|
86
86
|
const binary = atob(match[1]);
|
|
87
87
|
if (binary.length > maxBytes) {
|
|
88
88
|
const mb = Math.round(maxBytes / (1024 * 1024));
|
|
89
|
-
throw new Error(ko(`The stylized image is larger than ${mb} MB, so it was not added.`, `스타일화된 이미지가 ${mb} MB보다 커서 추가하지
|
|
89
|
+
throw new Error(ko(`The stylized image is larger than ${mb} MB, so it was not added.`, `스타일화된 이미지가 ${mb} MB보다 커서 추가하지 않았어요.`, `风格化图像超过 ${mb} MB,因此未添加。`));
|
|
90
90
|
}
|
|
91
91
|
const bytes = new Uint8Array(binary.length);
|
|
92
92
|
for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
|