cozyclay 3.1.2 → 3.1.4
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 +37 -0
- package/README.md +1 -1
- 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/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/cozyclay.mjs +2 -48
- package/bin/star-prompt.mjs +123 -0
- package/dist/app/index.html +5 -5
- package/dist/assets/TaskBoard-U1Yl-4F1.js +1 -0
- package/dist/assets/app-BdRlMf4J.css +1 -0
- package/dist/assets/app-KXdgBC-S.js +5144 -0
- package/dist/assets/{motion3-compile-dgGhrvPK.js → motion3-compile-DDG3wklD.js} +1 -1
- package/dist/assets/scene-asset-sync-Bz59Ghdz.js +13 -0
- package/dist/assets/task-board-DFfNa0xg.js +15 -0
- package/dist/assets/{workflow-C87p6E1E.js → workflow-BMJGd_gZ.js} +1 -1
- package/dist/cozyclay-package.json +1 -1
- package/dist/workflow/index.html +3 -3
- package/package.json +6 -4
- package/src/App.jsx +28 -28
- package/src/agent-image-client.js +2 -2
- package/src/asset-pane.jsx +14 -14
- package/src/character-kind.js +13 -12
- 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/first-success-guide.jsx +19 -3
- package/src/hierarchy-panel.jsx +6 -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/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 +8 -8
- 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 +9 -9
- 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 +10 -10
- package/src/settings-menu.jsx +4 -1
- package/src/shell/BoardSlot.jsx +5 -5
- package/src/shell/CompositionGuideButton.jsx +1 -1
- package/src/shell/GhostsMenuGroup.jsx +1 -1
- 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 +2 -1
- package/src/shot-guides.js +5 -5
- 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 +2 -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 +4 -4
- package/src/workflow/AgentPlanCard.jsx +1 -1
- package/src/workflow/AgentProgressCard.jsx +4 -4
- package/src/workflow/AgentTurnSteps.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/agent-client.js +1 -1
- package/src/workflow/agent-turn.js +5 -5
- package/src/workflow/task-board.js +4 -4
- package/tools/loc-baseline.json +2 -2
- package/tools/run-tests.mjs +5 -5
- package/dist/assets/TaskBoard-GR8xZm5c.js +0 -1
- package/dist/assets/app-Bhe0TpGz.css +0 -1
- package/dist/assets/app-C4QTcDcb.js +0 -5144
- package/dist/assets/scene-asset-sync-qoxcdrhj.js +0 -13
- package/dist/assets/task-board-BqReHzOi.js +0 -15
|
@@ -1 +1 @@
|
|
|
1
|
-
{"payload":"{\"distribution\":\"npm\",\"package\":\"cozyclay\",\"version\":\"3.1.
|
|
1
|
+
{"payload":"{\"distribution\":\"npm\",\"package\":\"cozyclay\",\"version\":\"3.1.4\",\"repository\":\"NomaDamas/CozyClay\",\"content_sha256\":\"446eb39e6cfa4c6433606a13d003df6415fc2a3e46b141a0927fbd46d56fb528\"}","signature":"N1D4JWm0u9nVkXEHFe9jlavu/aG1/3wqaEwUFMYa4un3l63+tsV3RqN1ixPu0KNErwQLT0tz8YPSci1qD1OlDA=="}
|
package/dist/workflow/index.html
CHANGED
|
@@ -8,12 +8,12 @@
|
|
|
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-
|
|
16
|
+
<link rel="modulepreload" crossorigin href="/assets/scene-asset-sync-Bz59Ghdz.js">
|
|
17
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>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cozyclay",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.4",
|
|
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,7 @@ 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
155
|
import { saveExport } from "./export-workspace.js";
|
|
156
156
|
import { characterCapabilities, kindRefusal, proxyFigureBounds } from "./character-kind.js";
|
|
157
157
|
import { STARTER_SCENES, resolveSceneParam } from "./starter-scenes.js";
|
|
@@ -1147,7 +1147,7 @@ export default function App() {
|
|
|
1147
1147
|
// A capsule figure has no rig at all, so it gets its own reason rather than
|
|
1148
1148
|
// the "load a character" one, which would read as a loading problem.
|
|
1149
1149
|
const activeCapabilities = characterCapabilities(activeChar);
|
|
1150
|
-
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 编辑其骨骼");
|
|
1151
1151
|
function selectWorkflowMode(next) {
|
|
1152
1152
|
if (next === "pose" && poseRefusal) {
|
|
1153
1153
|
setToast(poseRefusal);
|
|
@@ -1155,7 +1155,7 @@ export default function App() {
|
|
|
1155
1155
|
}
|
|
1156
1156
|
// A storyboard is stills: there is no Motion department to open.
|
|
1157
1157
|
if (next === "motion" && storyboardProject) {
|
|
1158
|
-
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));
|
|
1159
1159
|
return;
|
|
1160
1160
|
}
|
|
1161
1161
|
setWorkflowMode(next);
|
|
@@ -1258,7 +1258,7 @@ export default function App() {
|
|
|
1258
1258
|
setSculptBlueprint((current) => ({ ...current, state: "idle", sheet: null, error: null }));
|
|
1259
1259
|
setSculptReference(await objectsDomain.storeSculptReference(file));
|
|
1260
1260
|
} catch (error) {
|
|
1261
|
-
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}`));
|
|
1262
1262
|
}
|
|
1263
1263
|
}
|
|
1264
1264
|
function startSculptRun() {
|
|
@@ -1478,7 +1478,7 @@ export default function App() {
|
|
|
1478
1478
|
|
|
1479
1479
|
const rejectUnsupportedDrop = (count) => setToast(ko(
|
|
1480
1480
|
`${count} file${count > 1 ? "s" : ""} not supported — use PNG, JPG, WebP, GIF or a .glb / .obj / .fbx (iPhone HEIC photos need converting first)`,
|
|
1481
|
-
`지원하지 않는 파일 ${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 照片需先转换)`,
|
|
1482
1482
|
));
|
|
1483
1483
|
const stageDrop = {
|
|
1484
1484
|
onImages: (files) => importCutouts(files),
|
|
@@ -1786,7 +1786,7 @@ export default function App() {
|
|
|
1786
1786
|
// so run_action can give the agent the reason the user was shown.
|
|
1787
1787
|
const toastSinkRef = useRef(new Set());
|
|
1788
1788
|
const setToast = useCallback((value, english) => {
|
|
1789
|
-
const toast = resolveStudioToast(value, isKo, ko);
|
|
1789
|
+
const toast = resolveStudioToast(value, isKo, ko, isZh);
|
|
1790
1790
|
if (english !== undefined) toast.message = english;
|
|
1791
1791
|
if (typeof toast.uiMessage === "string" && toast.uiMessage) for (const sink of toastSinkRef.current) sink(toast);
|
|
1792
1792
|
showToast(toast.uiMessage);
|
|
@@ -1948,7 +1948,7 @@ export default function App() {
|
|
|
1948
1948
|
deleted = true;
|
|
1949
1949
|
return true;
|
|
1950
1950
|
} catch (error) {
|
|
1951
|
-
setToast(
|
|
1951
|
+
setToast(ko(`Could not delete that image — ${error.message}`, `이미지를 삭제하지 못했어요 — ${error.message}`, `无法删除该图片 — ${error.message}`));
|
|
1952
1952
|
return false;
|
|
1953
1953
|
} finally {
|
|
1954
1954
|
db?.close?.();
|
|
@@ -1971,9 +1971,9 @@ export default function App() {
|
|
|
1971
1971
|
setAssetTrash((current) => current.filter((asset) => asset.id !== record.id));
|
|
1972
1972
|
setAssetUndoOffered(false);
|
|
1973
1973
|
restored = true;
|
|
1974
|
-
setToast(
|
|
1974
|
+
setToast(ko(`${record.name || "Image"} restored`, `${record.name || "이미지"} 복원됨`, `${record.name || "图片"}已恢复`));
|
|
1975
1975
|
} catch (error) {
|
|
1976
|
-
setToast(
|
|
1976
|
+
setToast(ko(`Could not restore that image — ${error.message}`, `이미지를 복원하지 못했어요 — ${error.message}`, `无法恢复该图片 — ${error.message}`));
|
|
1977
1977
|
} finally {
|
|
1978
1978
|
if (restored) await refreshAssetShelf();
|
|
1979
1979
|
setDeletingAssetId(null);
|
|
@@ -3012,7 +3012,7 @@ export default function App() {
|
|
|
3012
3012
|
const name = fileName ?? `cozyclay-${slate}.mp4`;
|
|
3013
3013
|
await saveExport(result.blob, name);
|
|
3014
3014
|
setRecordedVideoName(name);
|
|
3015
|
-
setToast((isKo, ko) =>
|
|
3015
|
+
setToast((isKo, ko) => ko(`${name} exported · ${result.frameCount} frames`, `${name} 내보내기 완료 · ${result.frameCount}프레임`, `${name} 已导出 · ${result.frameCount} 帧`));
|
|
3016
3016
|
return { ...result, fileName: name };
|
|
3017
3017
|
}
|
|
3018
3018
|
return result;
|
|
@@ -3083,7 +3083,7 @@ export default function App() {
|
|
|
3083
3083
|
: phase === "preparing" ? ko("Preparing the renderer and checking MP4 support…", "렌더러를 준비하고 MP4 지원을 확인하고 있어요…")
|
|
3084
3084
|
: ko("Encoding the requested frames, not yet a completed file.", "요청한 프레임을 인코딩 중이에요. 아직 파일이 완성되지 않았어요.")}</p>}
|
|
3085
3085
|
{retryable && <p>{ko("Retry keeps the original shot, camera, range and settings; it does not change your edits.", "다시 시도하면 원래 샷·카메라·범위·설정을 사용하며 편집 내용은 바꾸지 않아요.")}</p>}
|
|
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 저장은 확인할 수 없으니 내보내기 폴더나 브라우저 다운로드를
|
|
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>}
|
|
3087
3087
|
<div className="export-status-actions">
|
|
3088
3088
|
{busy && cancellable && <button type="button" data-testid="export-cancel" onClick={stopShotRecording}>{ko("Cancel export", "내보내기 취소")}</button>}
|
|
3089
3089
|
{retryable && <button type="button" data-testid="export-retry" disabled={recState === "recording"} onClick={() => void retryExport()}>{ko("Retry same export", "같은 내보내기 재시도")}</button>}
|
|
@@ -3117,9 +3117,9 @@ export default function App() {
|
|
|
3117
3117
|
.replace(/^-+|-+$/g, "") || "cut-list";
|
|
3118
3118
|
await saveExport(blob, `cozyclay-${slug}.otio`);
|
|
3119
3119
|
const frameCount = shots.reduce((total, shot) => total + shot.endFrame - shot.startFrame + 1, 0);
|
|
3120
|
-
setToast(
|
|
3121
|
-
|
|
3122
|
-
|
|
3120
|
+
setToast(ko(`OTIO saved · ${shots.length} shots · ${frameCount} frames`,
|
|
3121
|
+
`OTIO 저장됨 · ${shots.length}샷 · ${frameCount}프레임`,
|
|
3122
|
+
`OTIO 已保存 · ${shots.length} 个镜头 · ${frameCount} 帧`));
|
|
3123
3123
|
} catch (error) {
|
|
3124
3124
|
setToast(error?.message || String(error));
|
|
3125
3125
|
}
|
|
@@ -3201,13 +3201,13 @@ export default function App() {
|
|
|
3201
3201
|
updateExportStatus(job, "preparing", { stage: "frames", cancellable: true });
|
|
3202
3202
|
await exportBoundary(job);
|
|
3203
3203
|
const packShot = { title: entry.name, index: index + 1, startFrame: entry.startFrame, endFrame: entry.endFrame };
|
|
3204
|
-
onProgress?.(ko(`Rendering frames for "${entry.name}"`, `"${entry.name}" 프레임 렌더링
|
|
3204
|
+
onProgress?.(ko(`Rendering frames for "${entry.name}"`, `"${entry.name}" 프레임 렌더링 중`, `正在渲染“${entry.name}”的帧`));
|
|
3205
3205
|
const firstUrl = captureShotFramePng(entry.startFrame);
|
|
3206
3206
|
if (!firstUrl) throw Object.assign(new Error(ko("The shot renderer is not ready", "샷 렌더러가 아직 준비되지 않았어요")), { exportFailureCode: "render_failed" });
|
|
3207
3207
|
await exportBoundary(job);
|
|
3208
3208
|
const lastUrl = entry.endFrame > entry.startFrame ? captureShotFramePng(entry.endFrame) : null;
|
|
3209
3209
|
await exportBoundary(job);
|
|
3210
|
-
onProgress?.(ko(`Recording the clip for "${entry.name}"`, `"${entry.name}" 클립 녹화
|
|
3210
|
+
onProgress?.(ko(`Recording the clip for "${entry.name}"`, `"${entry.name}" 클립 녹화 중`, `正在录制“${entry.name}”的片段`));
|
|
3211
3211
|
const recorded = await runShotExport({ startFrame: entry.startFrame, endFrame: entry.endFrame, download: false }, job);
|
|
3212
3212
|
updateExportStatus(job, "finalizing", { stage: "archive", cancellable: true });
|
|
3213
3213
|
await exportBoundary(job);
|
|
@@ -3260,12 +3260,12 @@ export default function App() {
|
|
|
3260
3260
|
if (!job.request.context.shots.length) throw new Error(ko("Add at least one Shot before exporting a keyframe pack", "키프레임 팩을 내보내려면 샷을 하나 이상 추가하세요"));
|
|
3261
3261
|
for (const [order, index] of targets.entries()) {
|
|
3262
3262
|
if (job.request.handedOff.has(index)) continue;
|
|
3263
|
-
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}`);
|
|
3264
3264
|
const pack = await buildShotKeyframePack(job.request.context.shots[index], index, null, job);
|
|
3265
3265
|
job.controller.signal.throwIfAborted();
|
|
3266
3266
|
await saveExport(new Blob([pack.bytes], { type: "application/zip" }), pack.name);
|
|
3267
3267
|
job.request.handedOff.add(index);
|
|
3268
|
-
setToast(
|
|
3268
|
+
setToast(ko(`${pack.name} exported · ${pack.entries.length} files`, `${pack.name} 내보내기 완료 · 파일 ${pack.entries.length}개`, `${pack.name} 已导出 · ${pack.entries.length} 个文件`));
|
|
3269
3269
|
await exportBoundary(job);
|
|
3270
3270
|
}
|
|
3271
3271
|
}));
|
|
@@ -3336,7 +3336,7 @@ export default function App() {
|
|
|
3336
3336
|
createCanvas: storyboardCanvas,
|
|
3337
3337
|
});
|
|
3338
3338
|
await saveExport(canvas.toDataURL("image/png"), "cozyclay-storyboard.png");
|
|
3339
|
-
setToast(
|
|
3339
|
+
setToast(ko(`Storyboard saved · ${cells.length} shots`, `스토리보드 저장됨 · ${cells.length}샷`, `故事板已保存 · ${cells.length} 个镜头`));
|
|
3340
3340
|
trackFeature("export_storyboard");
|
|
3341
3341
|
} catch (error) {
|
|
3342
3342
|
setToast(error?.message || String(error));
|
|
@@ -3427,7 +3427,7 @@ export default function App() {
|
|
|
3427
3427
|
const canvas = composeStoryboard({ shots: cells, columns: Math.min(3, cells.length), cell: { width: 480, height: 300 }, createCanvas: storyboardCanvas });
|
|
3428
3428
|
job.controller.signal.throwIfAborted();
|
|
3429
3429
|
await saveExport(canvas.toDataURL("image/png"), "cozyclay-storyboard.png");
|
|
3430
|
-
setToast(
|
|
3430
|
+
setToast(ko(`Contact sheet exported · ${cells.length} panels`, `콘택트 시트 내보내기 완료 · 패널 ${cells.length}개`, `缩略图总览已导出 · ${cells.length} 个面板`));
|
|
3431
3431
|
return { fileName: "cozyclay-storyboard.png", panelCount: cells.length };
|
|
3432
3432
|
}));
|
|
3433
3433
|
}
|
|
@@ -3470,7 +3470,7 @@ export default function App() {
|
|
|
3470
3470
|
updateExportStatus(job, "preparing", { stage: "frames", cancellable: true });
|
|
3471
3471
|
const packs = [];
|
|
3472
3472
|
for (const [step, entry] of targets.entries()) {
|
|
3473
|
-
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}`);
|
|
3474
3474
|
packs.push(await panelPackArgs(entry, contextPanels.indexOf(entry), all));
|
|
3475
3475
|
await exportBoundary(job);
|
|
3476
3476
|
}
|
|
@@ -3481,7 +3481,7 @@ export default function App() {
|
|
|
3481
3481
|
const name = every ? "cozyclay-panels.zip" : `cozyclay-${packs[0].folder}-${slugifyTitle(target.name)}.zip`;
|
|
3482
3482
|
await exportBoundary(job);
|
|
3483
3483
|
await saveExport(new Blob([bytes], { type: "application/zip" }), name);
|
|
3484
|
-
setToast(
|
|
3484
|
+
setToast(ko(`${name} exported · ${entries.length} files`, `${name} 내보내기 완료 · 파일 ${entries.length}개`, `${name} 已导出 · ${entries.length} 个文件`));
|
|
3485
3485
|
return { fileName: name, entries: entries.map((entry) => entry.name) };
|
|
3486
3486
|
}));
|
|
3487
3487
|
}
|
|
@@ -3800,7 +3800,7 @@ export default function App() {
|
|
|
3800
3800
|
if (!result.ok) return result;
|
|
3801
3801
|
await rehydrateProjectAssets(result.project, result.warnings);
|
|
3802
3802
|
applyProject(result.project);
|
|
3803
|
-
setToast(`${
|
|
3803
|
+
setToast(`${ko(`Project opened: ${result.project.name}`, `프로젝트 열림: ${result.project.name}`, `项目已打开:${result.project.name}`)}${projectProblemsNotice(result.problems)}`);
|
|
3804
3804
|
return result;
|
|
3805
3805
|
},
|
|
3806
3806
|
manifest: () => projectManifest,
|
|
@@ -4984,9 +4984,9 @@ export default function App() {
|
|
|
4984
4984
|
setPathPointIndex(null);
|
|
4985
4985
|
setPathDraw(false);
|
|
4986
4986
|
const metrics = pathMetrics(createObjectPath({ points }));
|
|
4987
|
-
setToast(
|
|
4988
|
-
|
|
4989
|
-
|
|
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} 个点`));
|
|
4990
4990
|
}}
|
|
4991
4991
|
onCameraChange={commitManualCameraFraming}
|
|
4992
4992
|
/>
|
|
@@ -5306,7 +5306,7 @@ export default function App() {
|
|
|
5306
5306
|
<FirstSuccessGuide open={firstSuccessGuideOpen} previsMode={scenesDomain.previsMode} onDismiss={() => setFirstSuccessGuideOpen(false)} />
|
|
5307
5307
|
<UseCaseQuestion
|
|
5308
5308
|
open={useCaseAskOpen}
|
|
5309
|
-
isKo={isKo}
|
|
5309
|
+
isKo={isKo} isZh={isZh}
|
|
5310
5310
|
onAnswer={(useCase, team) => {
|
|
5311
5311
|
recordUseCase(useCase, team);
|
|
5312
5312
|
setUseCaseAskOpen(false);
|
|
@@ -5343,7 +5343,7 @@ export default function App() {
|
|
|
5343
5343
|
)}
|
|
5344
5344
|
{restoreOffer && (
|
|
5345
5345
|
<div className="asset-delete-toast" role="status">
|
|
5346
|
-
<span>{
|
|
5346
|
+
<span>{ko(`Restore last project${restoreOffer.name ? `: ${restoreOffer.name}` : ""}`, `마지막 프로젝트 복원${restoreOffer.name ? `: ${restoreOffer.name}` : ""}`, `恢复上次项目${restoreOffer.name ? `:${restoreOffer.name}` : ""}`)}</span>
|
|
5347
5347
|
<button
|
|
5348
5348
|
type="button"
|
|
5349
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);
|
package/src/asset-pane.jsx
CHANGED
|
@@ -162,8 +162,8 @@ function StorageAssetRow({ id, onDelete, deleting, usageCount = 0, graphSignatur
|
|
|
162
162
|
const failed = thumb === null;
|
|
163
163
|
const mesh = Boolean(thumb?.mesh);
|
|
164
164
|
const name = thumb?.name || (failed ? ko("(unreadable image)", "(읽을 수 없는 이미지)") : mesh ? ko("Untitled model", "이름 없는 모델") : ko("Untitled image", "이름 없는 이미지"));
|
|
165
|
-
const usageLabel = ko(`Used by ${usageCount} scene object${usageCount === 1 ? "" : "s"}`, `${usageCount}개 씬 오브젝트에서 사용
|
|
166
|
-
const deleteLabel = ko(`Delete ${name} from storage`, `${name}을(를) 저장소에서
|
|
165
|
+
const usageLabel = ko(`Used by ${usageCount} scene object${usageCount === 1 ? "" : "s"}`, `${usageCount}개 씬 오브젝트에서 사용 중`, `被 ${usageCount} 个场景物体使用`);
|
|
166
|
+
const deleteLabel = ko(`Delete ${name} from storage`, `${name}을(를) 저장소에서 삭제`, `从存储中删除 ${name}`);
|
|
167
167
|
const confirmationId = `asset-storage-warning-${id}`;
|
|
168
168
|
const kindLabel = thumb?.kind === "mesh" ? ko("Model", "모델") : thumb?.kind === "environment" ? ko("HDR environment", "HDR 환경") : thumb?.kind === "matte" ? ko("Matte", "매트") : ko("Image", "이미지");
|
|
169
169
|
return (
|
|
@@ -183,7 +183,7 @@ function StorageAssetRow({ id, onDelete, deleting, usageCount = 0, graphSignatur
|
|
|
183
183
|
{confirmation ? (
|
|
184
184
|
<div className="asset-storage-confirm">
|
|
185
185
|
<span id={confirmationId} role="alert">{inUse
|
|
186
|
-
? ko(`Permanently delete this image from storage? It is used by ${usageCount} scene object${usageCount === 1 ? "" : "s"} and those objects will lose it.`, `저장소에서 이 이미지를 영구 삭제할까요? ${usageCount}개 씬 오브젝트가 사용 중이며 해당 오브젝트에서
|
|
186
|
+
? ko(`Permanently delete this image from storage? It is used by ${usageCount} scene object${usageCount === 1 ? "" : "s"} and those objects will lose it.`, `저장소에서 이 이미지를 영구 삭제할까요? ${usageCount}개 씬 오브젝트가 사용 중이며 해당 오브젝트에서 사라집니다.`, `要从存储中永久删除此图片吗?它正被 ${usageCount} 个场景物体使用,那些物体会失去它。`)
|
|
187
187
|
: ko("Unused by every scene. Delete it?", "모든 씬에서 사용되지 않아요. 삭제할까요?")}</span>
|
|
188
188
|
<button type="button" className="asset-storage-delete" aria-label={deleteLabel} aria-describedby={confirmationId} disabled={deleting} onClick={async () => {
|
|
189
189
|
if (await onDelete(id, inUse ? usageCount : undefined, confirmation.graphSignature)) setConfirmation(null);
|
|
@@ -209,7 +209,7 @@ function StorageManager({ unusedAssetIds, usedAssetIds, usageCounts, graphSignat
|
|
|
209
209
|
<h3>{ko("Manage storage", "저장 공간 관리")}</h3>
|
|
210
210
|
<p>{ko("Review unused and in-use stored images. Deleted images can be restored until this page is reloaded.", "사용하지 않는 이미지와 사용 중인 저장 이미지를 확인하세요. 삭제한 이미지는 이 페이지를 새로 고치기 전까지 복원할 수 있어요.")}</p>
|
|
211
211
|
</div>
|
|
212
|
-
{trashCount > 0 && <button type="button" className="asset-storage-undo" onClick={onUndo}>{ko(`Undo last delete (${trashCount})`, `마지막 삭제 실행 취소 (${trashCount})
|
|
212
|
+
{trashCount > 0 && <button type="button" className="asset-storage-undo" onClick={onUndo}>{ko(`Undo last delete (${trashCount})`, `마지막 삭제 실행 취소 (${trashCount})`, `撤销上次删除(${trashCount})`)}</button>}
|
|
213
213
|
</div>
|
|
214
214
|
{loading ? (
|
|
215
215
|
<div className="asset-storage-list" aria-busy="true">
|
|
@@ -301,8 +301,8 @@ function ImageAssetTile({ id, onAssetGrab, onAssetPlace, selectedKey, onSelect,
|
|
|
301
301
|
type={failed ? ko("Unreadable", "읽을 수 없음") : ko("Image", "이미지")}
|
|
302
302
|
failed={failed}
|
|
303
303
|
title={failed
|
|
304
|
-
? ko(`${label} — could not decode; delete it from Manage storage`, `${label} — 불러오지 못했어요. 저장소 관리에서 삭제할 수
|
|
305
|
-
: ko(`Drag ${label} into the scene, or double-click to place it at the origin`, `${label}을(를) 씬에 드래그하거나 더블클릭해 원점에
|
|
304
|
+
? ko(`${label} — could not decode; delete it from Manage storage`, `${label} — 불러오지 못했어요. 저장소 관리에서 삭제할 수 있어요`, `${label} — 无法解码;请在存储管理中删除`)
|
|
305
|
+
: ko(`Drag ${label} into the scene, or double-click to place it at the origin`, `${label}을(를) 씬에 드래그하거나 더블클릭해 원점에 놓으세요`, `将 ${label} 拖入场景,或双击将其放在原点`)}
|
|
306
306
|
preview={thumb
|
|
307
307
|
? <img className="content-tile-img" src={thumb.url} alt="" draggable={false} />
|
|
308
308
|
: <span className="content-tile-skeleton" aria-hidden="true" />}
|
|
@@ -329,8 +329,8 @@ function MeshAssetTile({ id, onAssetGrab, onAssetPlace, selectedKey, onSelect, q
|
|
|
329
329
|
type={failed ? ko("Unreadable", "읽을 수 없음") : ko("Model", "모델")}
|
|
330
330
|
failed={failed}
|
|
331
331
|
title={failed
|
|
332
|
-
? ko(`${label} — could not read; delete it from Manage storage`, `${label} — 불러오지 못했어요. 저장소 관리에서 삭제할 수
|
|
333
|
-
: ko(`Drag ${label} into the scene, or double-click to place it at the origin`, `${label}을(를) 씬에 드래그하거나 더블클릭해 원점에
|
|
332
|
+
? ko(`${label} — could not read; delete it from Manage storage`, `${label} — 불러오지 못했어요. 저장소 관리에서 삭제할 수 있어요`, `${label} — 无法读取;请在存储管理中删除`)
|
|
333
|
+
: ko(`Drag ${label} into the scene, or double-click to place it at the origin`, `${label}을(를) 씬에 드래그하거나 더블클릭해 원점에 놓으세요`, `将 ${label} 拖入场景,或双击将其放在原点`)}
|
|
334
334
|
preview={failed ? <span className="content-tile-skeleton" aria-hidden="true" /> : <MeshPreview />}
|
|
335
335
|
selected={selectedKey === key}
|
|
336
336
|
onSelect={onSelect}
|
|
@@ -386,7 +386,7 @@ function catalogueTile(entry, props) {
|
|
|
386
386
|
assetKey={key}
|
|
387
387
|
label={label}
|
|
388
388
|
type={ko("Shape", "도형")}
|
|
389
|
-
title={ko(`Drag ${entry.label} into the scene, or double-click to place it at the origin`, `${label}을(를) 씬에 드래그하거나 더블클릭해 원점에
|
|
389
|
+
title={ko(`Drag ${entry.label} into the scene, or double-click to place it at the origin`, `${label}을(를) 씬에 드래그하거나 더블클릭해 원점에 놓으세요`, `将 ${entry.label} 拖入场景,或双击将其放在原点`)}
|
|
390
390
|
preview={<ObjectPreview kind={entry.kind} color={entry.color} />}
|
|
391
391
|
selected={props.selectedKey === key}
|
|
392
392
|
onSelect={props.onSelect}
|
|
@@ -418,7 +418,7 @@ export function FolderGrid({ folder, query, ...props }) {
|
|
|
418
418
|
assetKey={key}
|
|
419
419
|
label={asset.label}
|
|
420
420
|
type={ko("Character", "인물")}
|
|
421
|
-
title={ko(`Drag ${asset.label} into the scene, or double-click to place it at the origin`, `${asset.label}을(를) 씬에 드래그하거나 더블클릭해 원점에
|
|
421
|
+
title={ko(`Drag ${asset.label} into the scene, or double-click to place it at the origin`, `${asset.label}을(를) 씬에 드래그하거나 더블클릭해 원점에 놓으세요`, `将 ${asset.label} 拖入场景,或双击将其放在原点`)}
|
|
422
422
|
preview={<CharacterPreview model={asset.id} />}
|
|
423
423
|
selected={selectedKey === key}
|
|
424
424
|
onSelect={onSelect}
|
|
@@ -454,7 +454,7 @@ export function FolderGrid({ folder, query, ...props }) {
|
|
|
454
454
|
assetKey={key}
|
|
455
455
|
label={label}
|
|
456
456
|
type={ko("Camera", "카메라")}
|
|
457
|
-
title={ko(`${label} — double-click to open it in Camera mode`, `${label} — 더블클릭하면 카메라 모드에서
|
|
457
|
+
title={ko(`${label} — double-click to open it in Camera mode`, `${label} — 더블클릭하면 카메라 모드에서 엽니다`, `${label} — 双击可在摄影机模式中打开`)}
|
|
458
458
|
preview={<CameraPreview />}
|
|
459
459
|
selected={selectedKey === key}
|
|
460
460
|
onSelect={onSelect}
|
|
@@ -474,7 +474,7 @@ export function FolderGrid({ folder, query, ...props }) {
|
|
|
474
474
|
assetKey={key}
|
|
475
475
|
label={label}
|
|
476
476
|
type={ko("Motion", "모션")}
|
|
477
|
-
title={ko(`${label} — double-click to load this take`, `${label} — 더블클릭하면 이 테이크를
|
|
477
|
+
title={ko(`${label} — double-click to load this take`, `${label} — 더블클릭하면 이 테이크를 불러옵니다`, `${label} — 双击加载此动作片段`)}
|
|
478
478
|
preview={<MotionPreview />}
|
|
479
479
|
selected={selectedKey === key}
|
|
480
480
|
onSelect={onSelect}
|
|
@@ -484,7 +484,7 @@ export function FolderGrid({ folder, query, ...props }) {
|
|
|
484
484
|
});
|
|
485
485
|
if (!takeVersions.length) empty = ko("No takes for this character yet. Ask the agent to compile one.", "이 인물의 테이크가 아직 없어요. 에이전트에게 모션 컴파일을 요청하세요.");
|
|
486
486
|
} else if (folder === "poses") {
|
|
487
|
-
tiles = poses.map((pose, index) => ({ pose, index, label: pose?.name || pose?.label || ko(`Pose ${index + 1}`, `포즈 ${index + 1}`) }))
|
|
487
|
+
tiles = poses.map((pose, index) => ({ pose, index, label: pose?.name || pose?.label || ko(`Pose ${index + 1}`, `포즈 ${index + 1}`, `姿势 ${index + 1}`) }))
|
|
488
488
|
.filter(({ label }) => matchesQuery(label, query))
|
|
489
489
|
.map(({ pose, index, label }) => {
|
|
490
490
|
const key = `pose:${pose?.id ?? index}`;
|
|
@@ -634,7 +634,7 @@ export default function AssetPane({
|
|
|
634
634
|
data-testid="content-search"
|
|
635
635
|
value={search}
|
|
636
636
|
placeholder={tab === "content" ? ko("Search objects", "오브젝트 검색") : ko("Search", "검색")}
|
|
637
|
-
aria-label={tab === "content" ? ko(`Search ${folderLabel}`, `${folderLabel}
|
|
637
|
+
aria-label={tab === "content" ? ko(`Search ${folderLabel}`, `${folderLabel} 검색`, `搜索 ${folderLabel}`) : ko("Search the log", "로그 검색")}
|
|
638
638
|
onChange={(event) => setSearch(event.target.value)}
|
|
639
639
|
/>
|
|
640
640
|
</header>
|
package/src/character-kind.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// (PROXY_FIGURE_MODEL) is a placement-only stand-in with no bones at all, so
|
|
4
4
|
// those tools refuse it by name instead of failing on a missing rig.
|
|
5
5
|
import { isProxyFigure } from "./scenes.js";
|
|
6
|
-
import { isKo } from "./locale.js";
|
|
6
|
+
import { isKo, isZh } from "./locale.js";
|
|
7
7
|
import { StudioProtocolError } from "./studio-agent-protocol.js";
|
|
8
8
|
|
|
9
9
|
export const CHARACTER_CAPABILITIES = Object.freeze(["rig", "ik", "pose", "motion"]);
|
|
@@ -45,29 +45,30 @@ export function characterCapabilities(character) {
|
|
|
45
45
|
|
|
46
46
|
// The noun each refusal names. A feature id not listed here is used verbatim.
|
|
47
47
|
const FEATURE_LABELS = {
|
|
48
|
-
rig: ["the rig", "리그"],
|
|
49
|
-
ik: ["IK", "IK"],
|
|
50
|
-
pose: ["Pose mode", "포즈 모드"],
|
|
51
|
-
motion: ["Compiled motion", "모션 컴파일"],
|
|
52
|
-
take: ["Loading a take", "테이크 불러오기"],
|
|
48
|
+
rig: ["the rig", "리그", "骨骼"],
|
|
49
|
+
ik: ["IK", "IK", "IK"],
|
|
50
|
+
pose: ["Pose mode", "포즈 모드", "姿势模式"],
|
|
51
|
+
motion: ["Compiled motion", "모션 컴파일", "编译动作"],
|
|
52
|
+
take: ["Loading a take", "테이크 불러오기", "加载动作片段"],
|
|
53
53
|
};
|
|
54
54
|
|
|
55
55
|
// A character's kind (rig or capsule figure) is fixed at creation; character.update refuses a kind change.
|
|
56
56
|
export const KIND_FIXED_REFUSAL = Object.freeze({
|
|
57
57
|
en: "A character's kind is chosen when it is created; add a new character instead.",
|
|
58
58
|
ko: "캐릭터 종류는 만들 때 정합니다. 새 캐릭터를 추가하세요.",
|
|
59
|
+
zh: "角色的类型在创建时确定;请改为添加新角色。",
|
|
59
60
|
});
|
|
60
61
|
|
|
61
|
-
export function kindRefusal(feature, ko = false) {
|
|
62
|
-
const label = FEATURE_LABELS[feature]?.[ko ? 1 : 0] ?? feature;
|
|
63
|
-
return
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
export function kindRefusal(feature, ko = false, zh = false) {
|
|
63
|
+
const label = FEATURE_LABELS[feature]?.[ko ? 1 : zh ? 2 : 0] ?? feature;
|
|
64
|
+
if (ko) return `캡슐 인물은 리그가 없어요 - ${label}는 리그 캐릭터에서만 됩니다.`;
|
|
65
|
+
if (zh) return `胶囊人物没有骨骼 — ${label}仅适用于带骨骼的角色。`;
|
|
66
|
+
return `Capsule figures have no rig - ${label} works on rigged characters only.`;
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
/** Throw TARGET_NOT_READY when `character` is a capsule figure. The protocol
|
|
69
70
|
* message is English (the agent reads it); `uiMessage` is the locale's. */
|
|
70
71
|
export function refuseRigOnly(character, feature) {
|
|
71
72
|
if (!isProxyFigure(character)) return;
|
|
72
|
-
throw Object.assign(new StudioProtocolError("TARGET_NOT_READY", kindRefusal(feature, false)), { uiMessage: kindRefusal(feature, isKo) });
|
|
73
|
+
throw Object.assign(new StudioProtocolError("TARGET_NOT_READY", kindRefusal(feature, false)), { uiMessage: kindRefusal(feature, isKo, isZh) });
|
|
73
74
|
}
|
package/src/commands/project.js
CHANGED
|
@@ -10,7 +10,8 @@ const rename = { id: "project.rename", label: "Rename project", description: "Re
|
|
|
10
10
|
const save = { ...studioActionDeclaration("project.save"), input: input({ name }) };
|
|
11
11
|
const saveAs = { ...save, id: "project.saveAs", label: "Save project as" };
|
|
12
12
|
const exportAsAnimation = studioActionDeclaration("project.exportAsAnimation");
|
|
13
|
-
|
|
13
|
+
// The deadline covers the user in the file picker, so it is the hub's ceiling, not the 30 s default.
|
|
14
|
+
const opening = { ...document("project.open", "Open a project file", { serialized: { type: "string", maxLength: 400_000_000 }, handleToken: token, projectToken: token }), timeoutMs: 300_000 };
|
|
14
15
|
const fresh = document("project.new", "Start a new project", { name, previsMode: { type: "string", enum: [...PREVIS_MODES] } });
|
|
15
16
|
const starter = document("project.openStarter", "Open a starter project", { id: token, source: { type: "string", enum: ["starter", "launch"] }, name });
|
|
16
17
|
const restore = { ...document("project.restore", "Restore the remembered project", { handleToken: token }), exposure: "ui-only" };
|
package/src/commands/shot.js
CHANGED
|
@@ -68,7 +68,7 @@ export function register(registry, ports) {
|
|
|
68
68
|
const next = reflowStillShots(shots), frameCount = stillFrameCount(next);
|
|
69
69
|
if (frameCount > STILL_FRAME_COUNT_MAX) {
|
|
70
70
|
const en = `Storyboard is full (${STILL_FRAME_COUNT_MAX} frames); shorten a hold or remove a panel.`;
|
|
71
|
-
throw studioActionRefusal('INVALID_ARGUMENT', en, ko(en, `스토리보드가 가득 찼습니다 (${STILL_FRAME_COUNT_MAX}프레임). 홀드를 줄이거나 패널을
|
|
71
|
+
throw studioActionRefusal('INVALID_ARGUMENT', en, ko(en, `스토리보드가 가득 찼습니다 (${STILL_FRAME_COUNT_MAX}프레임). 홀드를 줄이거나 패널을 지우세요.`, `故事板已满(${STILL_FRAME_COUNT_MAX} 帧);请缩短停留时长或删除一个面板。`));
|
|
72
72
|
}
|
|
73
73
|
const clip = next.find(shot => shot.kind !== 'still' && next.some(still => still.kind === 'still' && still.startFrame <= shot.endFrame && still.endFrame >= shot.startFrame));
|
|
74
74
|
if (clip) fail('TARGET_NOT_READY', `The stills would overlap the clip shot ${clip.name} (frames ${clip.startFrame}-${clip.endFrame}); move or remove it first.`);
|
package/src/commands/view.js
CHANGED
|
@@ -14,7 +14,7 @@ const operations = Object.entries({ ...groups, 'view.update': Object.keys(fields
|
|
|
14
14
|
}));
|
|
15
15
|
export const declarations = Object.freeze([...operations, ...["view.setPartColours", "view.setGuideMode", "view.setInset", "view.setGhosts"].map(studioActionDeclaration)]);
|
|
16
16
|
// A storyboard project has no Motion department; every door says so the same way.
|
|
17
|
-
export const STORYBOARD_MOTION_REFUSAL = Object.freeze({ en: 'Motion tools are not part of a Storyboard project.', ko: '모션 도구는 스토리보드 프로젝트에 없어요.' });
|
|
17
|
+
export const STORYBOARD_MOTION_REFUSAL = Object.freeze({ en: 'Motion tools are not part of a Storyboard project.', ko: '모션 도구는 스토리보드 프로젝트에 없어요.', zh: '故事板项目不包含动作工具。' });
|
|
18
18
|
export const viewCommand = args => Object.entries(groups).find(([, keys]) => Object.keys(args).every(key => keys.includes(key)))?.[0] ?? 'view.update';
|
|
19
19
|
|
|
20
20
|
export function register(registry, ports) {
|
|
@@ -24,7 +24,7 @@ export function register(registry, ports) {
|
|
|
24
24
|
const state = ports.readView();
|
|
25
25
|
// Pose mode is IK editing: without a rig to solve there is nothing to pose.
|
|
26
26
|
if (args.mode === 'pose' && args.mode !== state.view.mode && !ports.canPose()) fail('TARGET_NOT_READY', 'Pose mode needs a loaded character rig to edit with IK.');
|
|
27
|
-
if (args.mode === 'motion' && state.previsMode === 'storyboard') throw studioActionRefusal('TARGET_NOT_READY', STORYBOARD_MOTION_REFUSAL.en, ko(STORYBOARD_MOTION_REFUSAL.en, STORYBOARD_MOTION_REFUSAL.ko));
|
|
27
|
+
if (args.mode === 'motion' && state.previsMode === 'storyboard') throw studioActionRefusal('TARGET_NOT_READY', STORYBOARD_MOTION_REFUSAL.en, ko(STORYBOARD_MOTION_REFUSAL.en, STORYBOARD_MOTION_REFUSAL.ko, STORYBOARD_MOTION_REFUSAL.zh));
|
|
28
28
|
const selection = args.selection === undefined ? state.selection : args.selection;
|
|
29
29
|
if (selection) {
|
|
30
30
|
const found = selection.kind === 'scene' ? selection.id === state.host.sceneId : selection.kind === 'camera' ? selection.id === 'camera'
|
package/src/domains/cast.js
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
nextCharacterId,
|
|
20
20
|
MAX_WAYPOINTS,
|
|
21
21
|
} from "../app-stage.jsx";
|
|
22
|
-
import { ko
|
|
22
|
+
import { ko } from "../locale.js";
|
|
23
23
|
import { createIkState } from "../motion/ik.js";
|
|
24
24
|
import { judgeNextWaypoint } from "../motion/waypoints.js";
|
|
25
25
|
import { StudioProtocolError } from "../studio-agent-protocol.js";
|
|
@@ -435,7 +435,7 @@ export function useCast(appContext) {
|
|
|
435
435
|
appContext.shared.setTlFrame(target);
|
|
436
436
|
setWaypointMode(true);
|
|
437
437
|
selectActiveCharacterInHierarchy();
|
|
438
|
-
appContext.notify(
|
|
438
|
+
appContext.notify(ko(`Root waypoint at frame ${target} selected — drag the pin in the Top-View to reposition.`, `프레임 ${target}의 루트 웨이포인트를 선택했어요. 탑뷰에서 점을 드래그해 위치를 조정하세요.`, `已选中第 ${target} 帧的根路径点 — 在顶视图中拖动该点调整位置。`));
|
|
439
439
|
return;
|
|
440
440
|
}
|
|
441
441
|
setPendingWaypointFrame(target);
|
|
@@ -443,7 +443,7 @@ export function useCast(appContext) {
|
|
|
443
443
|
appContext.shared.setTlFrame(target);
|
|
444
444
|
setWaypointMode(true);
|
|
445
445
|
selectActiveCharacterInHierarchy();
|
|
446
|
-
appContext.notify(
|
|
446
|
+
appContext.notify(ko(`Frame ${target} is reserved — click the Shot-view floor to drop the root waypoint there.`, `프레임 ${target}이 예약됐어요. 샷 뷰 바닥을 클릭하면 그 위치에 루트 웨이포인트가 생성됩니다.`, `第 ${target} 帧已预留 — 点击镜头视图地面即可在该位置放置根路径点。`));
|
|
447
447
|
}
|
|
448
448
|
|
|
449
449
|
/* One root-path core for every cast member, shared by the Shot-view floor
|
|
@@ -483,7 +483,7 @@ export function useCast(appContext) {
|
|
|
483
483
|
const ordered = [...readCharacterWaypoints(characterId)].sort((a, b) => a.frame - b.frame);
|
|
484
484
|
if (ordered.length + 1 > MAX_WAYPOINTS) {
|
|
485
485
|
throw studioActionRefusal("TARGET_NOT_READY", `The root path is capped at ${MAX_WAYPOINTS} waypoints; remove one first.`,
|
|
486
|
-
|
|
486
|
+
ko(`The root path is capped at ${MAX_WAYPOINTS} waypoints`, `루트 경로는 웨이포인트 ${MAX_WAYPOINTS}개까지 사용할 수 있어요`, `根路径最多 ${MAX_WAYPOINTS} 个路径点`));
|
|
487
487
|
}
|
|
488
488
|
const x = clampRootPosition(point.x);
|
|
489
489
|
const z = clampRootPosition(point.z);
|
|
@@ -498,7 +498,7 @@ export function useCast(appContext) {
|
|
|
498
498
|
}
|
|
499
499
|
if (ordered.some((waypoint) => waypoint.frame === at)) {
|
|
500
500
|
throw studioActionRefusal("INVALID_ARGUMENT", `Frame ${at} already has a root waypoint — pick an empty frame or move that one.`,
|
|
501
|
-
|
|
501
|
+
ko(`Frame ${at} already has a root waypoint — pick an empty frame on the timeline.`, `프레임 ${at}에는 이미 루트 웨이포인트가 있어요. 타임라인에서 빈 프레임을 선택하세요.`, `第 ${at} 帧已有根路径点 — 请在时间线上选择空白帧。`));
|
|
502
502
|
}
|
|
503
503
|
// The generator cannot refuse an impossible pin, so placement is the
|
|
504
504
|
// last moment to: block out-of-band legs with the fix named.
|
|
@@ -507,7 +507,7 @@ export function useCast(appContext) {
|
|
|
507
507
|
const waypoint = { id: createStableItemId("waypoint"), frame: at, x, z, heading: null };
|
|
508
508
|
const next = [...ordered.slice(0, index), waypoint, ...ordered.slice(index)];
|
|
509
509
|
const verdict = validateWaypointAt(next, index, waypoint, start);
|
|
510
|
-
if (!verdict.ok) throw studioActionRefusal("INVALID_ARGUMENT", `Not placed — ${verdict.error}`,
|
|
510
|
+
if (!verdict.ok) throw studioActionRefusal("INVALID_ARGUMENT", `Not placed — ${verdict.error}`, ko(`Not placed — ${verdict.error}`, `배치하지 못했어요 — ${verdict.error}`, `未能放置 — ${verdict.error}`));
|
|
511
511
|
writeCharacterWaypoints(characterId, next);
|
|
512
512
|
return { waypoint, index, warnings: verdict.warnings };
|
|
513
513
|
}
|
|
@@ -523,7 +523,7 @@ export function useCast(appContext) {
|
|
|
523
523
|
const verdict = validateWaypointAt(next, index, moved, { frame: 0, x: character.x, z: character.z });
|
|
524
524
|
if (!verdict.ok) {
|
|
525
525
|
throw studioActionRefusal("INVALID_ARGUMENT", `This position doesn't fit the root path: ${verdict.error}`,
|
|
526
|
-
|
|
526
|
+
ko(`This position doesn't fit the root path: ${verdict.error}`, `이 위치는 루트 경로에 맞지 않아요: ${verdict.error}`, `此位置不符合根路径:${verdict.error}`));
|
|
527
527
|
}
|
|
528
528
|
writeCharacterWaypoints(characterId, next);
|
|
529
529
|
return { waypoint: moved, index, warnings: verdict.warnings };
|
|
@@ -572,9 +572,9 @@ export function useCast(appContext) {
|
|
|
572
572
|
setActiveWaypointId(waypoint.id);
|
|
573
573
|
setPendingWaypointFrame(null);
|
|
574
574
|
const { warnings } = validateWaypointAt(path, index, waypoint);
|
|
575
|
-
const placed =
|
|
576
|
-
|
|
577
|
-
|
|
575
|
+
const placed = ko(`Waypoint ${index + 1} — frame ${waypoint.frame} ${pendingFrame != null ? "(at the reserved frame)" : pinned ? "(at the playhead)" : `(~${(waypoint.frame / appContext.shared.tlFps).toFixed(1)}s at a walk)`}`,
|
|
576
|
+
`루트 웨이포인트 ${index + 1} 추가: 프레임 ${waypoint.frame}${pendingFrame != null ? " (타임라인 예약 프레임)" : pinned ? " (재생 헤드 위치)" : ` (~${(waypoint.frame / appContext.shared.tlFps).toFixed(1)}초 걷기 기준)`}`,
|
|
577
|
+
`已添加第 ${index + 1} 个路径点 — 第 ${waypoint.frame} 帧${pendingFrame != null ? "(预留帧)" : pinned ? "(播放头位置)" : `(按步行约 ${(waypoint.frame / appContext.shared.tlFps).toFixed(1)} 秒)`}`);
|
|
578
578
|
appContext.notify(warnings.length ? `${placed} · ⚠ ${warnings[0]}` : placed);
|
|
579
579
|
}
|
|
580
580
|
|
|
@@ -587,7 +587,7 @@ export function useCast(appContext) {
|
|
|
587
587
|
const path = readCharacterWaypoints(activeChar.id);
|
|
588
588
|
const index = path.findIndex((entry) => entry.id === id);
|
|
589
589
|
const { warnings } = index === -1 ? { warnings: [] } : validateWaypointAt(path, index, path[index]);
|
|
590
|
-
if (warnings.length) appContext.notify(
|
|
590
|
+
if (warnings.length) appContext.notify(ko(`Root waypoint moved: ${warnings[0]}`, `루트 웨이포인트 이동됨: ${warnings[0]}`, `根路径点已移动:${warnings[0]}`));
|
|
591
591
|
}
|
|
592
592
|
|
|
593
593
|
function removeWaypoint(id) {
|
|
@@ -639,7 +639,7 @@ export function useCast(appContext) {
|
|
|
639
639
|
trackFeature("pose_save");
|
|
640
640
|
const pose = {
|
|
641
641
|
id: `custom_${Date.now()}`,
|
|
642
|
-
label:
|
|
642
|
+
label: ko(`My Pose ${customPoses.length + 1}`, `내 포즈 ${customPoses.length + 1}`, `我的姿势 ${customPoses.length + 1}`),
|
|
643
643
|
prompt: "in the exact body pose shown in the blocking frame",
|
|
644
644
|
bones: capturePose(activeRig),
|
|
645
645
|
// A take frame carries its measured hips height; bottling the frame
|
|
@@ -650,8 +650,8 @@ export function useCast(appContext) {
|
|
|
650
650
|
domain.run('cast.savePose', { pose });
|
|
651
651
|
setStudioPick(pose.id);
|
|
652
652
|
appContext.notify(appContext.shared.motion
|
|
653
|
-
? ko(`Saved this frame's pose to the library as “${pose.label}”`, `지금 프레임의 자세를 “${pose.label}”로 라이브러리에
|
|
654
|
-
: ko(`Saved the current pose to the library as “${pose.label}”`, `지금 자세를 “${pose.label}”로 라이브러리에
|
|
653
|
+
? ko(`Saved this frame's pose to the library as “${pose.label}”`, `지금 프레임의 자세를 “${pose.label}”로 라이브러리에 저장했어요`, `已将当前帧的姿势以“${pose.label}”保存到姿势库`)
|
|
654
|
+
: ko(`Saved the current pose to the library as “${pose.label}”`, `지금 자세를 “${pose.label}”로 라이브러리에 저장했어요`, `已将当前姿势以“${pose.label}”保存到姿势库`));
|
|
655
655
|
}
|
|
656
656
|
|
|
657
657
|
/** A handle drag is an authored pose. When it ends, the rig as it now
|
|
@@ -677,7 +677,7 @@ export function useCast(appContext) {
|
|
|
677
677
|
trackFeature("pose_save");
|
|
678
678
|
const pose = {
|
|
679
679
|
id: `custom_${Date.now()}`,
|
|
680
|
-
label:
|
|
680
|
+
label: ko(`My Pose ${customPoses.length + 1}`, `내 포즈 ${customPoses.length + 1}`, `我的姿势 ${customPoses.length + 1}`),
|
|
681
681
|
prompt: "in the exact body pose shown in the blocking frame",
|
|
682
682
|
bones: capturePose(rig),
|
|
683
683
|
custom: true,
|
|
@@ -19,5 +19,5 @@ export function overlappingRangePin(draft, state) {
|
|
|
19
19
|
return [...ensureRangePinState(state).pins.values()].find((pin) => pin.id !== draft.id && rangePinTracks(pin).some((track) => rangePinTracks(draft).includes(track)) && Math.max(pin.startFrame, draft.startFrame) <= Math.min(pin.endFrame, draft.endFrame));
|
|
20
20
|
}
|
|
21
21
|
export function rangePinOverlapMessage(pin) {
|
|
22
|
-
return pin ? ko(`A required body or limb track already has an overlapping pin (${pin.startFrame}–${pin.endFrame}).`, `필요한 몸통 또는 팔다리 파츠에 겹치는 고정이 있어요 (${pin.startFrame}–${pin.endFrame})
|
|
22
|
+
return pin ? ko(`A required body or limb track already has an overlapping pin (${pin.startFrame}–${pin.endFrame}).`, `필요한 몸통 또는 팔다리 파츠에 겹치는 고정이 있어요 (${pin.startFrame}–${pin.endFrame}).`, `所需的身体或肢体轨道上已有重叠的固定(${pin.startFrame}–${pin.endFrame})。`) : "";
|
|
23
23
|
}
|