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
package/src/motion/timeline.jsx
CHANGED
|
@@ -2,7 +2,7 @@ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
|
2
2
|
import { frameFromClientX, groupKeyRuns, KEY_RUN_MIN, rangePinBandGeometry, shotBlockGeometry } from "./timeline-coordinates.js";
|
|
3
3
|
import { createPlaybackClock } from "./playback-clock.js";
|
|
4
4
|
|
|
5
|
-
import { ko
|
|
5
|
+
import { ko } from "../locale.js";
|
|
6
6
|
import { isImeComposing } from "../ime.js";
|
|
7
7
|
import { buildRail, craneHeightAt } from "../camera-follow.js";
|
|
8
8
|
import { insertPathMark, pathMarks, pathMetrics, pathProgressAt, MAX_PATH_LEAN } from "../object-path.js";
|
|
@@ -276,7 +276,7 @@ function CraneHeightEditor({ crane, railRange, durationFrames, selectedIndex, on
|
|
|
276
276
|
// an end point sits ON the window edge; clamp it inside so the knob stays whole. The clamp is the
|
|
277
277
|
// knob's own radius: a wider one pinned every knob of a 20 px Shot strip to one row, off its curve.
|
|
278
278
|
style={{ left: `clamp(5px, ${xFor(point.t) * 100}%, calc(100% - 5px))`, top: `clamp(5px, ${yFor(point.height) * 100}%, calc(100% - 5px))` }}
|
|
279
|
-
aria-label={ko(`Crane point ${index + 1}, ${point.height.toFixed(1)} metres`, `크레인 점 ${index + 1}, ${point.height.toFixed(1)}
|
|
279
|
+
aria-label={ko(`Crane point ${index + 1}, ${point.height.toFixed(1)} metres`, `크레인 점 ${index + 1}, ${point.height.toFixed(1)}미터`, `摇臂点 ${index + 1},${point.height.toFixed(1)} 米`)}
|
|
280
280
|
onPointerDown={(event) => {
|
|
281
281
|
event.preventDefault();
|
|
282
282
|
event.stopPropagation();
|
|
@@ -780,7 +780,7 @@ function ObjectTravelTrack({ object, frame, frameCount, fps, pathDraw, onPathDra
|
|
|
780
780
|
<span className="tl-track-label">{ko("Speed", "속도 곡선")}</span>
|
|
781
781
|
<div className="tl-lane sg-lane">
|
|
782
782
|
<SpeedGraph
|
|
783
|
-
facts={metrics && span ? `${metrics.length.toFixed(1)} m · ${seconds.toFixed(1)}${
|
|
783
|
+
facts={metrics && span ? `${metrics.length.toFixed(1)} m · ${seconds.toFixed(1)}${ko("s", "초", "秒")}` : null}
|
|
784
784
|
timing={path.timing ?? null}
|
|
785
785
|
windowFrac={span.fills ? 1 : span.end / Math.max(1, frameCount - 1)}
|
|
786
786
|
frame={frame}
|
|
@@ -828,7 +828,7 @@ function CameraBlockEditor({
|
|
|
828
828
|
const numberValue = (event) => Number(event.currentTarget.value);
|
|
829
829
|
const metric = (value, places = 1) => Number(value).toFixed(places);
|
|
830
830
|
return (
|
|
831
|
-
<section className="tl-camera-editor" aria-label={ko(`Camera controls for ${shot.name}`, `${shot.name} 카메라
|
|
831
|
+
<section className="tl-camera-editor" aria-label={ko(`Camera controls for ${shot.name}`, `${shot.name} 카메라 컨트롤`, `${shot.name} 的摄影机控制`)}>
|
|
832
832
|
<strong className="tl-camera-editor-title">
|
|
833
833
|
<span className="tl-subject-kind">{ko("CAMERA", "카메라")}</span>
|
|
834
834
|
{shot.name}
|
|
@@ -1666,7 +1666,7 @@ export default function Timeline({
|
|
|
1666
1666
|
title={ko("Enable or disable root path constraints (P)", "루트 경로 제약 켜기/끄기 (P)")}
|
|
1667
1667
|
onClick={() => handlers.current.onWaypointToggle?.()}
|
|
1668
1668
|
>
|
|
1669
|
-
{
|
|
1669
|
+
{ko(`Waypoint ${waypointMode ? "on" : "off"}`, `웨이포인트 ${waypointMode ? "켜짐" : "꺼짐"}`, `路径点${waypointMode ? "开" : "关"}`)}
|
|
1670
1670
|
</button>
|
|
1671
1671
|
</div>
|
|
1672
1672
|
<div className="tl-head-group tl-pose-tools" role="group" aria-label={ko("Pose correction tools", "포즈 보정 도구")}>
|
|
@@ -1687,7 +1687,7 @@ export default function Timeline({
|
|
|
1687
1687
|
title={ko("Foot snap — keep the feet planted while you move the body (hips); the knees bend instead of the feet sinking through the floor", "발 스냅 — 몸(엉덩이)을 움직여도 발을 바닥에 고정합니다. 발이 바닥으로 가라앉는 대신 무릎이 구부러집니다")}
|
|
1688
1688
|
onClick={() => handlers.current.onFootSnapToggle?.()}
|
|
1689
1689
|
>
|
|
1690
|
-
{
|
|
1690
|
+
{ko(`Snap ${footSnap ? "on" : "off"}`, `스냅 ${footSnap ? "켜짐" : "꺼짐"}`, `吸附${footSnap ? "开" : "关"}`)}
|
|
1691
1691
|
</button>
|
|
1692
1692
|
<button
|
|
1693
1693
|
type="button"
|
|
@@ -1697,7 +1697,7 @@ export default function Timeline({
|
|
|
1697
1697
|
title={ko("Body contact — keep hands, knees, feet, head, and hips above the floor", "바닥 접촉 — 손, 무릎, 발, 머리, 엉덩이가 바닥 아래로 내려가지 않게 합니다")}
|
|
1698
1698
|
onClick={() => handlers.current.onBodyContactToggle?.()}
|
|
1699
1699
|
>
|
|
1700
|
-
{
|
|
1700
|
+
{ko(`Body contact ${bodyContact ? "on" : "off"}`, `바닥 접촉 ${bodyContact ? "켜짐" : "꺼짐"}`, `身体接触${bodyContact ? "开" : "关"}`)}
|
|
1701
1701
|
</button>
|
|
1702
1702
|
</>)}
|
|
1703
1703
|
</>)}
|
|
@@ -1719,18 +1719,10 @@ export default function Timeline({
|
|
|
1719
1719
|
{waypointMode && (
|
|
1720
1720
|
<span className={"tl-wp-hint" + (waypointFrames.length < 2 || pathSpeed?.warn ? " warn" : "")}>
|
|
1721
1721
|
{waypointFrames.length < 2
|
|
1722
|
-
? ko("Click the set floor in the Shot view to drop waypoints", "샷 뷰의 세트 바닥을 클릭해 웨이포인트를 놓으세요")
|
|
1723
|
-
:
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
? ` · ${pathSpeed.min.toFixed(1)}–${pathSpeed.max.toFixed(1)} m/s${pathSpeed.warn ? " — 자연스러운 이동 속도 0.5–3 m/s 범위를 벗어남" : ""}`
|
|
1727
|
-
: "") +
|
|
1728
|
-
" · 세트 바닥을 클릭해 더 추가"
|
|
1729
|
-
: `${waypointFrames.length} root waypoints` +
|
|
1730
|
-
(pathSpeed
|
|
1731
|
-
? ` · ${pathSpeed.min.toFixed(1)}–${pathSpeed.max.toFixed(1)} m/s${pathSpeed.warn ? " — outside the natural 0.5–3 m/s locomotion band" : ""}`
|
|
1732
|
-
: "") +
|
|
1733
|
-
" · click the set floor to add more"}
|
|
1722
|
+
? ko("Click the set floor in the Shot view to drop waypoints", "샷 뷰의 세트 바닥을 클릭해 웨이포인트를 놓으세요", "在镜头视图中点击布景地面放置路径点")
|
|
1723
|
+
: ko(`${waypointFrames.length} root waypoints` + (pathSpeed ? ` · ${pathSpeed.min.toFixed(1)}–${pathSpeed.max.toFixed(1)} m/s${pathSpeed.warn ? " — outside the natural 0.5–3 m/s locomotion band" : ""}` : "") + " · click the set floor to add more",
|
|
1724
|
+
`루트 웨이포인트 ${waypointFrames.length}개` + (pathSpeed ? ` · ${pathSpeed.min.toFixed(1)}–${pathSpeed.max.toFixed(1)} m/s${pathSpeed.warn ? " — 자연스러운 이동 속도 0.5–3 m/s 범위를 벗어남" : ""}` : "") + " · 세트 바닥을 클릭해 더 추가",
|
|
1725
|
+
`${waypointFrames.length} 个根路径点` + (pathSpeed ? ` · ${pathSpeed.min.toFixed(1)}–${pathSpeed.max.toFixed(1)} m/s${pathSpeed.warn ? " — 超出自然行走 0.5–3 m/s 区间" : ""}` : "") + " · 点击布景地面继续添加")}
|
|
1734
1726
|
</span>
|
|
1735
1727
|
)}
|
|
1736
1728
|
{badge && <span className={"tl-badge " + badge.kind}>{badge.label}</span>}
|
|
@@ -1766,7 +1758,7 @@ export default function Timeline({
|
|
|
1766
1758
|
<div className="tl-body" ref={bodyRef}>
|
|
1767
1759
|
<div className={"tl-surface" + (!shots.length ? " empty-shots" : "") + (pathObject ? " prop-subject" : "")} style={{ "--tl-zoom": surfaceZoom }}>
|
|
1768
1760
|
<div className="tl-ruler">
|
|
1769
|
-
<span className="tl-ruler-label">{ko("Frame", "프레임")}</span>
|
|
1761
|
+
<span className="tl-ruler-label">{ko("Frame", "프레임", "帧")}</span>
|
|
1770
1762
|
<div
|
|
1771
1763
|
className="tl-ruler-lane"
|
|
1772
1764
|
ref={rulerRef}
|
|
@@ -1846,7 +1838,7 @@ export default function Timeline({
|
|
|
1846
1838
|
{name === "2D Root" && pathSpeed && (
|
|
1847
1839
|
<em
|
|
1848
1840
|
className={"tl-path-speed" + (pathSpeed.warn ? " warn" : "")}
|
|
1849
|
-
title={
|
|
1841
|
+
title={ko(`Leg speeds ${pathSpeed.min.toFixed(1)}–${pathSpeed.max.toFixed(1)} m/s — natural gait is 0.8–1.2 m/s`, `핀 구간 속도 ${pathSpeed.min.toFixed(1)}~${pathSpeed.max.toFixed(1)} m/s — 자연 보행은 0.8~1.2 m/s`, `腿速 ${pathSpeed.min.toFixed(1)}–${pathSpeed.max.toFixed(1)} m/s — 自然步态为 0.8–1.2 m/s`)}
|
|
1850
1842
|
>
|
|
1851
1843
|
{pathSpeed.min === pathSpeed.max
|
|
1852
1844
|
? `${pathSpeed.min.toFixed(1)} m/s`
|
|
@@ -1869,7 +1861,7 @@ export default function Timeline({
|
|
|
1869
1861
|
<button
|
|
1870
1862
|
className="tl-track-add ik"
|
|
1871
1863
|
type="button"
|
|
1872
|
-
title={
|
|
1864
|
+
title={ko(`Key the current pose at frame ${frame}`, `현재 포즈를 ${frame}프레임에 키로 저장`, `在当前帧 ${frame} 记录姿势关键帧`)}
|
|
1873
1865
|
onClick={() => handlers.current.onIkKeyframeAdd?.()}
|
|
1874
1866
|
>
|
|
1875
1867
|
+
|
|
@@ -1895,7 +1887,7 @@ export default function Timeline({
|
|
|
1895
1887
|
const endFrame = Number.isFinite(Number(range?.endFrame)) ? Number(range.endFrame) : Number(range?.end ?? startFrame);
|
|
1896
1888
|
const start = Math.max(0, Math.min(displayFrameCount - 1, Math.round(startFrame)));
|
|
1897
1889
|
const end = Math.max(start, Math.min(displayFrameCount - 1, Math.round(endFrame)));
|
|
1898
|
-
return <span key={`preview:${index}`} className="tl-preview-range" style={{ "--tl-f-start": framePct(start, displayFrameCount), "--tl-f-end": framePct(end, displayFrameCount) }} aria-label={ko(`Preview range ${start}–${end}`, `미리보기 범위 ${start}–${end}`)} />;
|
|
1890
|
+
return <span key={`preview:${index}`} className="tl-preview-range" style={{ "--tl-f-start": framePct(start, displayFrameCount), "--tl-f-end": framePct(end, displayFrameCount) }} aria-label={ko(`Preview range ${start}–${end}`, `미리보기 범위 ${start}–${end}`, `预览范围 ${start}–${end}`)} />;
|
|
1899
1891
|
})}
|
|
1900
1892
|
{name === SHOTS_LANE && shots.length === 0 && (
|
|
1901
1893
|
<div className="tl-shot-empty">
|
|
@@ -1940,7 +1932,7 @@ export default function Timeline({
|
|
|
1940
1932
|
className={"tl-shot-block" + (index === cameraBlockIdx ? " selected" : "") + (index === activeShotIdx ? " active" : "") + (movingShotId === shot.id ? " moving" : "") + (!railOff && railRange && railLengthByShot.has(shot.id) ? " has-dolly" : "")}
|
|
1941
1933
|
data-shot-kind={shot.kind === "still" ? "still" : undefined}
|
|
1942
1934
|
style={{ "--tl-f-start": geometry.startPct, "--tl-f-end": geometry.endPct }}
|
|
1943
|
-
title={
|
|
1935
|
+
title={ko(`${shot.name} · frames ${shot.startFrame}–${lastFrame} · drag to reorder, trim cuts at either edge, click the empty lower strip to add a camera key`, `${shot.name} · ${shot.startFrame}–${lastFrame}프레임 · 드래그해 순서 이동, 양끝으로 컷 조절, 아래 빈 줄을 클릭해 카메라 키 추가`, `${shot.name} · 第 ${shot.startFrame}–${lastFrame} 帧 · 拖动调整顺序,拖动两端修剪剪辑,点击下方空条添加摄影机关键帧`)}
|
|
1944
1936
|
onPointerDown={(e) => beginShotMove(e, shot, index)}
|
|
1945
1937
|
onPointerMove={moveShot}
|
|
1946
1938
|
onPointerUp={endShotMove}
|
|
@@ -1951,7 +1943,7 @@ export default function Timeline({
|
|
|
1951
1943
|
<button
|
|
1952
1944
|
type="button"
|
|
1953
1945
|
className="tl-shot-edge start"
|
|
1954
|
-
aria-label={ko(`Resize start of ${shot.name}`, `${shot.name} 시작점
|
|
1946
|
+
aria-label={ko(`Resize start of ${shot.name}`, `${shot.name} 시작점 조절`, `调整 ${shot.name} 的起点`)}
|
|
1955
1947
|
onPointerDown={(e) => beginShotBoundaryDrag(e, index, "start")}
|
|
1956
1948
|
onPointerMove={moveShotBoundary}
|
|
1957
1949
|
onPointerUp={endShotBoundaryDrag}
|
|
@@ -1962,7 +1954,7 @@ export default function Timeline({
|
|
|
1962
1954
|
<button
|
|
1963
1955
|
type="button"
|
|
1964
1956
|
className="tl-shot-edge end"
|
|
1965
|
-
aria-label={ko(`Resize end of ${shot.name}`, `${shot.name} 끝 길이
|
|
1957
|
+
aria-label={ko(`Resize end of ${shot.name}`, `${shot.name} 끝 길이 조절`, `调整 ${shot.name} 的终点`)}
|
|
1966
1958
|
onPointerDown={(e) => beginShotBoundaryDrag(e, index, "end")}
|
|
1967
1959
|
onPointerMove={moveShotBoundary}
|
|
1968
1960
|
onPointerUp={endShotBoundaryDrag}
|
|
@@ -2065,8 +2057,8 @@ export default function Timeline({
|
|
|
2065
2057
|
type="button"
|
|
2066
2058
|
className="tl-shot-key-surface"
|
|
2067
2059
|
aria-label={shot.camera?.mode === "rail" && shot.camera?.craneHeight
|
|
2068
|
-
? ko(`Add crane point in ${shot.name}`, `${shot.name}에 크레인 점
|
|
2069
|
-
: ko(`Add camera key in ${shot.name}`, `${shot.name}에 카메라 키
|
|
2060
|
+
? ko(`Add crane point in ${shot.name}`, `${shot.name}에 크레인 점 추가`, `在 ${shot.name} 中添加摇臂点`)
|
|
2061
|
+
: ko(`Add camera key in ${shot.name}`, `${shot.name}에 카메라 키 추가`, `在 ${shot.name} 中添加摄影机关键帧`)}
|
|
2070
2062
|
title={shot.camera?.mode === "rail" && shot.camera?.craneHeight
|
|
2071
2063
|
? ko("Click the crane graph to add a point; click a point and drag it to change height", "크레인 그래프를 클릭해 점 추가 · 점을 눌러 끌어 높이 조절")
|
|
2072
2064
|
: ko("Click at a frame to store the current camera framing", "프레임 위치를 클릭해 현재 카메라 프레이밍을 저장합니다")}
|
|
@@ -2089,7 +2081,7 @@ export default function Timeline({
|
|
|
2089
2081
|
"--tl-pin-ramp-end": geometry.rampEndPct,
|
|
2090
2082
|
"--tl-pin-color": "var(--select, #e8a33d)",
|
|
2091
2083
|
}}
|
|
2092
|
-
title={ko(`${pin.track} pin · frames ${pin.startFrame}–${pin.endFrame}`, `${pin.track} 고정 · ${pin.startFrame}–${pin.endFrame}
|
|
2084
|
+
title={ko(`${pin.track} pin · frames ${pin.startFrame}–${pin.endFrame}`, `${pin.track} 고정 · ${pin.startFrame}–${pin.endFrame}프레임`, `${pin.track} 固定 · 第 ${pin.startFrame}–${pin.endFrame} 帧`)}
|
|
2093
2085
|
onPointerDown={(event) => {
|
|
2094
2086
|
if (event.button !== 0) return;
|
|
2095
2087
|
event.stopPropagation();
|
|
@@ -2108,7 +2100,7 @@ export default function Timeline({
|
|
|
2108
2100
|
return <span
|
|
2109
2101
|
className="tl-pin-pending"
|
|
2110
2102
|
style={{ "--tl-f-start": geometry.startPct, "--tl-f-end": geometry.endPct }}
|
|
2111
|
-
aria-label={ko(`Pending pin range ${geometry.startFrame}–${geometry.endFrame}`, `대기 중인 고정 범위 ${geometry.startFrame}–${geometry.endFrame}`)}
|
|
2103
|
+
aria-label={ko(`Pending pin range ${geometry.startFrame}–${geometry.endFrame}`, `대기 중인 고정 범위 ${geometry.startFrame}–${geometry.endFrame}`, `待定固定范围 ${geometry.startFrame}–${geometry.endFrame}`)}
|
|
2112
2104
|
/>;
|
|
2113
2105
|
})()}
|
|
2114
2106
|
{name === IK_LANE && motion?.segments && <TakeSegmentClips motion={motion} frame={frame} frameCount={frameCount} displayFrameCount={displayFrameCount} clipPct={clipPct} tools={motionTools} />}{name === IK_LANE && <BeatBoard motion={motion} frame={frame} displayFrameCount={displayFrameCount} clipPct={clipPct} onJump={(f) => handlers.current.onScrub?.(f)} />}
|
|
@@ -2121,7 +2113,7 @@ export default function Timeline({
|
|
|
2121
2113
|
key={f}
|
|
2122
2114
|
className={"tl-marker ik" + (f === frame ? " current" : "")}
|
|
2123
2115
|
style={{ "--tl-f": framePct(f, displayFrameCount) }}
|
|
2124
|
-
title={
|
|
2116
|
+
title={ko(`Full-body IK key at frame ${f} — click to jump, right-click to remove`, `${f}프레임의 전신 IK 키 — 클릭해 이동, 오른쪽 클릭으로 삭제`, `第 ${f} 帧的全身 IK 键 — 点击跳转,右键删除`)}
|
|
2125
2117
|
onPointerDown={(e) => {
|
|
2126
2118
|
if (e.button !== 0) return;
|
|
2127
2119
|
e.stopPropagation();
|
|
@@ -2142,9 +2134,9 @@ export default function Timeline({
|
|
|
2142
2134
|
key={`run:${run.start}`}
|
|
2143
2135
|
className={"tl-ik-run" + (frame >= run.start && frame <= run.end ? " current" : "")}
|
|
2144
2136
|
style={{ "--tl-f": framePct(run.start, displayFrameCount), "--tl-f2": framePct(run.end, displayFrameCount) }}
|
|
2145
|
-
title={
|
|
2146
|
-
|
|
2147
|
-
|
|
2137
|
+
title={ko(`IK keys ${run.start}–${run.end} (${run.length}) — click to jump to that frame, right-click to remove that frame's key`,
|
|
2138
|
+
`${run.start}–${run.end} 전신 IK 키 (${run.length}개) — 클릭해 그 프레임으로 이동, 오른쪽 클릭으로 그 프레임 키 삭제`,
|
|
2139
|
+
`IK 键 ${run.start}–${run.end}(${run.length} 个)— 点击跳转到该帧,右键删除该帧的键`)}
|
|
2148
2140
|
onPointerDown={(e) => {
|
|
2149
2141
|
if (e.button !== 0) return;
|
|
2150
2142
|
e.stopPropagation();
|
|
@@ -2181,7 +2173,7 @@ export default function Timeline({
|
|
|
2181
2173
|
key={f}
|
|
2182
2174
|
className={"tl-marker wp" + (f === pendingWaypointFrame ? " pending" : "")}
|
|
2183
2175
|
style={{ "--tl-f": framePct(f, displayFrameCount) }}
|
|
2184
|
-
title={
|
|
2176
|
+
title={ko(`Root waypoint at frame ${f} — click to select, right-click to remove; drag marker ${waypointFrames.indexOf(f) + 1} in Top-View to move`, `${f}프레임의 루트 웨이포인트 — 클릭해 선택, 오른쪽 클릭으로 삭제. 탑뷰에서 ${waypointFrames.indexOf(f) + 1}번 마커를 드래그해 이동`, `第 ${f} 帧的根路径点 — 点击选中,右键删除;在顶视图中拖动第 ${waypointFrames.indexOf(f) + 1} 个标记移动`)}
|
|
2185
2177
|
onPointerDown={(e) => {
|
|
2186
2178
|
// Select only on the primary button — a right click
|
|
2187
2179
|
// must reach contextmenu so it removes without scrubbing.
|
|
@@ -2202,7 +2194,7 @@ export default function Timeline({
|
|
|
2202
2194
|
key={`${shot.id}:${key.frame}`}
|
|
2203
2195
|
className="tl-marker cam" data-edge={key.frame <= shot.startFrame ? "start" : key.frame >= shot.endFrame ? "end" : undefined}
|
|
2204
2196
|
style={{ "--tl-f": framePct(key.frame, displayFrameCount) }}
|
|
2205
|
-
title={
|
|
2197
|
+
title={ko(`Camera key at frame ${key.frame} — click to jump, drag to re-time, right-click to remove`, `${key.frame}프레임의 카메라 키 — 클릭해 이동, 드래그로 시간 변경, 오른쪽 클릭으로 삭제`, `第 ${key.frame} 帧的摄影机关键帧 — 点击跳转,拖动改变时间,右键删除`)}
|
|
2206
2198
|
onPointerDown={(e) => beginCameraKeyDrag(e, key, index)}
|
|
2207
2199
|
onPointerMove={moveCameraKeyDrag}
|
|
2208
2200
|
onPointerUp={endCameraKeyDrag}
|
|
@@ -2229,10 +2221,8 @@ export default function Timeline({
|
|
|
2229
2221
|
{waypointMode && (
|
|
2230
2222
|
<span className={"tl-wp-hint" + (waypointFrames.length < 2 ? " warn" : "")}>
|
|
2231
2223
|
{waypointFrames.length < 2
|
|
2232
|
-
? ko("Click the set floor in the Shot view to add waypoints", "샷 뷰의 세트 바닥을 클릭해 웨이포인트를 추가하세요")
|
|
2233
|
-
:
|
|
2234
|
-
? `루트 웨이포인트 ${waypointFrames.length}개 · 세트 바닥을 클릭해 더 추가`
|
|
2235
|
-
: `${waypointFrames.length} root waypoints · click the set floor to add more`}
|
|
2224
|
+
? ko("Click the set floor in the Shot view to add waypoints", "샷 뷰의 세트 바닥을 클릭해 웨이포인트를 추가하세요", "在镜头视图中点击布景地面添加路径点")
|
|
2225
|
+
: ko(`${waypointFrames.length} root waypoints · click the set floor to add more`, `루트 웨이포인트 ${waypointFrames.length}개 · 세트 바닥을 클릭해 더 추가`, `${waypointFrames.length} 个根路径点 · 点击布景地面继续添加`)}
|
|
2236
2226
|
</span>
|
|
2237
2227
|
)}
|
|
2238
2228
|
<button
|
package/src/motion/waypoints.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ko } from "../locale.js";
|
|
2
2
|
|
|
3
3
|
// Root path pins: the limits keep an authored path a person could walk, and a
|
|
4
4
|
// capsule figure that rides the path moves at a believable pace.
|
|
@@ -28,7 +28,7 @@ export function judgeNextWaypoint(last, candidate, fps, prev = null) {
|
|
|
28
28
|
if (gap < PATH_LIMITS.minPinGapFrames) {
|
|
29
29
|
return {
|
|
30
30
|
ok: false,
|
|
31
|
-
error:
|
|
31
|
+
error: ko(`pins need at least ${PATH_LIMITS.minPinGapFrames} frames between them (${(PATH_LIMITS.minPinGapFrames / fps).toFixed(1)} s) — scrub past frame ${last.frame + PATH_LIMITS.minPinGapFrames}`, `핀 사이에는 최소 ${PATH_LIMITS.minPinGapFrames}프레임(${(PATH_LIMITS.minPinGapFrames / fps).toFixed(1)}초)이 필요해요 — ${last.frame + PATH_LIMITS.minPinGapFrames}프레임 이후로 이동해 주세요`, `路径点之间至少需要 ${PATH_LIMITS.minPinGapFrames} 帧(${(PATH_LIMITS.minPinGapFrames / fps).toFixed(1)} 秒)— 请移动到第 ${last.frame + PATH_LIMITS.minPinGapFrames} 帧之后`),
|
|
32
32
|
warnings: [],
|
|
33
33
|
};
|
|
34
34
|
}
|
|
@@ -40,7 +40,7 @@ export function judgeNextWaypoint(last, candidate, fps, prev = null) {
|
|
|
40
40
|
const neededFrames = Math.ceil((dist / PATH_LIMITS.speedMaxMps) * fps);
|
|
41
41
|
return {
|
|
42
42
|
ok: false,
|
|
43
|
-
error:
|
|
43
|
+
error: ko(`${speed.toFixed(1)} m/s is faster than anyone moves (max ${PATH_LIMITS.speedMaxMps}) — pin frame ${last.frame + neededFrames} or later, or click closer`, `${speed.toFixed(1)} m/s는 사람이 움직이기엔 너무 빨라요(최대 ${PATH_LIMITS.speedMaxMps}) — ${last.frame + neededFrames}프레임 이후에 핀을 찍거나 더 가까운 곳을 눌러 주세요`, `${speed.toFixed(1)} m/s 比人体移动速度快(上限 ${PATH_LIMITS.speedMaxMps})— 请在第 ${last.frame + neededFrames} 帧或之后打点,或点击更近的位置`),
|
|
44
44
|
warnings: [],
|
|
45
45
|
};
|
|
46
46
|
}
|
|
@@ -49,7 +49,7 @@ export function judgeNextWaypoint(last, candidate, fps, prev = null) {
|
|
|
49
49
|
const neededDist = (PATH_LIMITS.speedMinMps * dt).toFixed(1);
|
|
50
50
|
return {
|
|
51
51
|
ok: false,
|
|
52
|
-
error:
|
|
52
|
+
error: ko(`${speed.toFixed(2)} m/s is below a sustainable walk (min ${PATH_LIMITS.speedMinMps}) — pin frame ~${walkFrame} instead, or click at least ${neededDist} m away`, `${speed.toFixed(2)} m/s는 자연스럽게 걷기엔 너무 느려요(최소 ${PATH_LIMITS.speedMinMps}) — 대신 약 ${walkFrame}프레임에 핀을 찍거나 최소 ${neededDist} m 이상 떨어진 곳을 눌러 주세요`, `${speed.toFixed(2)} m/s 低于可持续步行速度(下限 ${PATH_LIMITS.speedMinMps})— 请改为在第 ${walkFrame} 帧附近打点,或点击至少 ${neededDist} m 外的位置`),
|
|
53
53
|
warnings: [],
|
|
54
54
|
};
|
|
55
55
|
}
|
|
@@ -60,7 +60,7 @@ export function judgeNextWaypoint(last, candidate, fps, prev = null) {
|
|
|
60
60
|
const prevSpeed = prevDist / ((last.frame - prev.frame) / fps);
|
|
61
61
|
const ratio = Math.max(speed, prevSpeed) / Math.max(Math.min(speed, prevSpeed), 1e-6);
|
|
62
62
|
if (ratio > PATH_LIMITS.speedRatioWarn) {
|
|
63
|
-
warnings.push(
|
|
63
|
+
warnings.push(ko(`${ratio.toFixed(1)}x speed change from the previous leg — expect a visible gait shift`, `이전 구간보다 속도가 ${ratio.toFixed(1)}배 달라요 — 걸음 변화가 눈에 보일 수 있어요`, `与前一段相比速度变化 ${ratio.toFixed(1)} 倍 — 步态变化可能很明显`));
|
|
64
64
|
}
|
|
65
65
|
const before = Math.atan2(last.x - prev.x, last.z - prev.z);
|
|
66
66
|
const after = Math.atan2(candidate.x - last.x, candidate.z - last.z);
|
|
@@ -68,7 +68,7 @@ export function judgeNextWaypoint(last, candidate, fps, prev = null) {
|
|
|
68
68
|
if (turn > Math.PI) turn = 2 * Math.PI - turn;
|
|
69
69
|
const turnRate = (turn * 180) / Math.PI / dt;
|
|
70
70
|
if (turnRate > PATH_LIMITS.turnRateWarnDegPerS) {
|
|
71
|
-
warnings.push(
|
|
71
|
+
warnings.push(ko(`${Math.round((turn * 180) / Math.PI)}° turn in ${dt.toFixed(1)} s is sharper than a walking arc — give it more frames or distance`, `${dt.toFixed(1)}초 동안 ${Math.round((turn * 180) / Math.PI)}° 회전은 걷는 동선치고 너무 급해요 — 프레임이나 거리를 더 주세요`, `${dt.toFixed(1)} 秒内转向 ${Math.round((turn * 180) / Math.PI)}° 比步行弧线更急 — 请增加帧数或距离`));
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
}
|
package/src/object-catalog.jsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from "react";
|
|
2
|
-
import { ko
|
|
2
|
+
import { ko } from "./locale.js";
|
|
3
3
|
import { EMPTY_KIND, OBJECT_MENU_ENTRIES } from "./scene-objects.js";
|
|
4
4
|
|
|
5
5
|
const GROUP_LABELS_KO = {
|
|
@@ -25,14 +25,13 @@ const OBJECT_LABELS_KO = {
|
|
|
25
25
|
};
|
|
26
26
|
|
|
27
27
|
function displayObjectGroupName(name) {
|
|
28
|
-
return
|
|
28
|
+
return GROUP_LABELS_KO[name] ? ko(name, GROUP_LABELS_KO[name]) : name;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export function displayObjectLabel(label) {
|
|
32
|
-
if (!isKo) return label;
|
|
33
32
|
const numbered = label.match(/^(.+?) (\d+)$/);
|
|
34
|
-
if (numbered && OBJECT_LABELS_KO[numbered[1]]) return `${OBJECT_LABELS_KO[numbered[1]]} ${numbered[2]}`;
|
|
35
|
-
return OBJECT_LABELS_KO[label]
|
|
33
|
+
if (numbered && OBJECT_LABELS_KO[numbered[1]]) return `${ko(numbered[1], OBJECT_LABELS_KO[numbered[1]])} ${numbered[2]}`;
|
|
34
|
+
return OBJECT_LABELS_KO[label] ? ko(label, OBJECT_LABELS_KO[label]) : label;
|
|
36
35
|
}
|
|
37
36
|
|
|
38
37
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Foldout from "./Foldout.jsx";
|
|
2
|
-
import { ko
|
|
2
|
+
import { ko } from "../locale.js";
|
|
3
3
|
import { Field } from "../ui.jsx";
|
|
4
4
|
import { VIDEO_MODEL_PRESETS } from "../model-presets.js";
|
|
5
5
|
import { useBus } from "../app-context.js";
|
|
@@ -21,7 +21,7 @@ export default function CameraPanel({ isCameraSelection, shot, moveSequence, cam
|
|
|
21
21
|
) : (
|
|
22
22
|
<p className="inspector-hint v2-details-hint">
|
|
23
23
|
{cameraKeys.length === 1
|
|
24
|
-
? (
|
|
24
|
+
? ko(`Locked-off hold from frame ${cameraKeys[0].frame}. Click the empty lower strip of a Shot block to add a move.`, `프레임 ${cameraKeys[0].frame}부터 고정 샷이에요. 샷 블록 아래 빈 줄을 클릭해 움직임을 추가하세요.`, `从第 ${cameraKeys[0].frame} 帧起为固定机位。点击镜头块下方的空条添加运动。`)
|
|
25
25
|
: ko("Click a Shot block's lower strip to key the current framing at that frame.", "샷 블록 아래 빈 줄을 클릭하면 해당 프레임에 현재 프레이밍을 저장합니다.")}
|
|
26
26
|
</p>
|
|
27
27
|
)}
|
|
@@ -29,7 +29,7 @@ export default function CameraPanel({ isCameraSelection, shot, moveSequence, cam
|
|
|
29
29
|
<h3 className="move-head v2-details-subsection-title">{ko("Follow cam", "팔로우 카메라")}</h3>
|
|
30
30
|
<p className="camera-editor-pointer">
|
|
31
31
|
{activeShot
|
|
32
|
-
? ko(`Editing ${activeShot.name} in the timeline camera bar below.`, `아래 타임라인 카메라 바에서 ${activeShot.name}을
|
|
32
|
+
? ko(`Editing ${activeShot.name} in the timeline camera bar below.`, `아래 타임라인 카메라 바에서 ${activeShot.name}을 편집합니다.`, `在下方时间线摄影机栏中编辑 ${activeShot.name}。`)
|
|
33
33
|
: ko("Select a Shot block below to edit its camera.", "아래에서 샷 블록을 선택하면 카메라를 편집할 수 있습니다.")}
|
|
34
34
|
</p>
|
|
35
35
|
|
|
@@ -5,6 +5,7 @@ import { HDR_BUILTINS } from "../stage-hdr.js";
|
|
|
5
5
|
import { hdrEnvironmentCatalogue, importHdrFile, refreshHdrLibrary, subscribeHdrLibrary } from "../stage-hdr-library.js";
|
|
6
6
|
|
|
7
7
|
const KO_NAMES = Object.fromEntries(HDR_BUILTINS.map(({ id, ko: name }) => [id, name]));
|
|
8
|
+
const ZH_NAMES = Object.fromEntries(HDR_BUILTINS.map(({ id, zh: name }) => [id, name]));
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* The Light panel's HDR environment: load a .hdr / .exr / equirectangular
|
|
@@ -34,7 +35,7 @@ export default function HdrEnvironmentControls({ lighting, run, setLighting }) {
|
|
|
34
35
|
}
|
|
35
36
|
};
|
|
36
37
|
const options = [{ value: "", label: ko("None (the look's sky)", "없음 (룩의 하늘)") },
|
|
37
|
-
...environments.map(({ id, name, source }) => ({ value: id, label: source === "builtin" ? ko(name, KO_NAMES[id] ?? name) : name }))];
|
|
38
|
+
...environments.map(({ id, name, source }) => ({ value: id, label: source === "builtin" ? ko(name, KO_NAMES[id] ?? name, ZH_NAMES[id] ?? name) : name }))];
|
|
38
39
|
return (
|
|
39
40
|
<div data-testid="hdr-environment">
|
|
40
41
|
<Field label={ko("HDR environment", "HDR 환경")}>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useContext, useEffect, useRef } from "react";
|
|
2
2
|
import { AppContext } from "../app-context.js";
|
|
3
3
|
import Foldout from "./Foldout.jsx";
|
|
4
|
-
import { ko
|
|
4
|
+
import { ko } from "../locale.js";
|
|
5
5
|
import { Field, Vector3Row } from "../ui.jsx";
|
|
6
6
|
import { sceneObjectNameDisplayKo } from "../app-stage.jsx";
|
|
7
7
|
import { MESH_KIND, CUTOUT_KIND, EMPTY_KIND, SCULPT_KIND, CUTOUT_DEFAULT_HEIGHT, OBJECT_COLORS, normalizeObjectColor } from "../scene-objects.js";
|
|
@@ -54,15 +54,7 @@ export default function ObjectTransformPanel({
|
|
|
54
54
|
</p>
|
|
55
55
|
<label className="check snap-toggle details-toggle-row">
|
|
56
56
|
<span>
|
|
57
|
-
{
|
|
58
|
-
<>
|
|
59
|
-
그리드 스냅 — <kbd>Ctrl</kbd>을 누르면 반대로 작동
|
|
60
|
-
</>
|
|
61
|
-
) : (
|
|
62
|
-
<>
|
|
63
|
-
Snap to grid — hold <kbd>Ctrl</kbd> to invert
|
|
64
|
-
</>
|
|
65
|
-
)}
|
|
57
|
+
{ko(<>Snap to grid — hold <kbd>Ctrl</kbd> to invert</>, <>그리드 스냅 — <kbd>Ctrl</kbd>을 누르면 반대로 작동</>, <>网格吸附 — 按住 <kbd>Ctrl</kbd> 反向</>)}
|
|
66
58
|
</span>
|
|
67
59
|
<input type="checkbox" checked={snapEnabled} onChange={(event) => setSnapEnabled(event.target.checked)} />
|
|
68
60
|
</label>
|
|
@@ -184,9 +176,9 @@ export default function ObjectTransformPanel({
|
|
|
184
176
|
/>
|
|
185
177
|
</Field>
|
|
186
178
|
<p className="inspector-hint">
|
|
187
|
-
{
|
|
188
|
-
|
|
189
|
-
|
|
179
|
+
{ko(`Width follows the picture's aspect (${(selectedSceneObject.aspect ?? 1).toFixed(2)}) as you change the height. Set it on its own — or drag the gizmo's X axis — to stretch the picture. Measure against something you know: a door is 2 m, a person 1.8 m.`,
|
|
180
|
+
`높이를 바꾸면 너비는 사진 비율(${(selectedSceneObject.aspect ?? 1).toFixed(2)})을 따라갑니다. 너비만 따로 정하거나 기즈모의 가로축을 끌면 사진이 늘어납니다. 사진 속에서 크기를 알 수 있는 것(문 2 m, 사람 1.8 m)에 맞추세요.`,
|
|
181
|
+
`改变高度时,宽度会跟随图片比例(${(selectedSceneObject.aspect ?? 1).toFixed(2)})。单独设置宽度 — 或拖动变换控件的 X 轴 — 会拉伸图片。请参照已知尺寸:门约 2 m,人约 1.8 m。`)}
|
|
190
182
|
</p>
|
|
191
183
|
{Math.abs((selectedSceneObject.stretch ?? 1) - 1) > 0.005 && (
|
|
192
184
|
<p className="inspector-hint">
|
|
@@ -196,9 +188,9 @@ export default function ObjectTransformPanel({
|
|
|
196
188
|
data-field="cutout-unstretch"
|
|
197
189
|
onClick={() => run(...updateArgs({ stretch: 1 }))}
|
|
198
190
|
>
|
|
199
|
-
{
|
|
200
|
-
|
|
201
|
-
|
|
191
|
+
{ko(`Back to the picture's proportions (now ${((selectedSceneObject.stretch ?? 1) * 100).toFixed(0)}%)`,
|
|
192
|
+
`사진 비율로 되돌리기 (지금 ${((selectedSceneObject.stretch ?? 1) * 100).toFixed(0)}%)`,
|
|
193
|
+
`恢复图片原始比例(当前 ${((selectedSceneObject.stretch ?? 1) * 100).toFixed(0)}%)`)}
|
|
202
194
|
</button>
|
|
203
195
|
</p>
|
|
204
196
|
)}
|
|
@@ -223,9 +215,9 @@ export default function ObjectTransformPanel({
|
|
|
223
215
|
/>
|
|
224
216
|
<p className="inspector-hint">
|
|
225
217
|
{matteStats.painted
|
|
226
|
-
?
|
|
227
|
-
|
|
228
|
-
|
|
218
|
+
? ko(`${Math.round(matteStats.coverage * 100)}% of the picture marked — purple is what goes.`,
|
|
219
|
+
`사진의 ${Math.round(matteStats.coverage * 100)}%가 선택됨 — 보라색이 지워집니다.`,
|
|
220
|
+
`已标记图片的 ${Math.round(matteStats.coverage * 100)}% — 紫色部分将被移除。`)
|
|
229
221
|
: ko(
|
|
230
222
|
"Drag over the background — the cut grows out from wherever the brush touches.",
|
|
231
223
|
"배경 위를 드래그하세요 — 브러시가 닿은 곳에서 같은 배경으로 번져 나가며 잘립니다.",
|
|
@@ -421,11 +413,9 @@ export default function ObjectTransformPanel({
|
|
|
421
413
|
onClick={() => {
|
|
422
414
|
const added = matteEditorRef.current?.autoDetect(matteTolerance) ?? 0;
|
|
423
415
|
if (!added) {
|
|
424
|
-
app.notify(
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
: "Auto-detect found nothing new to paint — raise the tolerance, or paint it by hand",
|
|
428
|
-
);
|
|
416
|
+
app.notify(ko("Auto-detect found nothing new to paint — raise the tolerance, or paint it by hand",
|
|
417
|
+
"자동 인식이 더 칠할 곳을 찾지 못했어요 — 허용치를 높이거나 직접 칠하세요",
|
|
418
|
+
"自动识别没有发现可继续涂抹的区域 — 请提高容差或手动涂抹"));
|
|
429
419
|
}
|
|
430
420
|
}}
|
|
431
421
|
>
|
|
@@ -440,9 +430,9 @@ export default function ObjectTransformPanel({
|
|
|
440
430
|
{matteBusy
|
|
441
431
|
? ko("Removing…", "지우는 중…")
|
|
442
432
|
: matteStats.painted
|
|
443
|
-
?
|
|
444
|
-
|
|
445
|
-
|
|
433
|
+
? ko(`Remove what is purple — ${Math.round(matteStats.coverage * 100)}% of the picture`,
|
|
434
|
+
`보라색 부분 지우기 — 사진의 ${Math.round(matteStats.coverage * 100)}%`,
|
|
435
|
+
`移除紫色部分 — 占图片 ${Math.round(matteStats.coverage * 100)}%`)
|
|
446
436
|
: ko("Nothing is marked yet", "아직 선택된 부분이 없습니다")}
|
|
447
437
|
</button>
|
|
448
438
|
<p className="inspector-hint">
|
|
@@ -476,7 +466,7 @@ export default function ObjectTransformPanel({
|
|
|
476
466
|
key={color}
|
|
477
467
|
className={"object-color" + (selectedSceneObject.color === color ? " active" : "")}
|
|
478
468
|
style={{ background: color }}
|
|
479
|
-
aria-label={
|
|
469
|
+
aria-label={ko(`Colour ${color}`, `색상 ${color}`, `颜色 ${color}`)}
|
|
480
470
|
aria-pressed={selectedSceneObject.color === color}
|
|
481
471
|
onClick={(event) => {
|
|
482
472
|
run(...updateArgs({ color }));
|
|
@@ -495,7 +485,7 @@ export default function ObjectTransformPanel({
|
|
|
495
485
|
key={color}
|
|
496
486
|
className={"object-color" + (selectedSceneObject.color === color ? " active" : "")}
|
|
497
487
|
style={{ background: color }}
|
|
498
|
-
aria-label={
|
|
488
|
+
aria-label={ko(`Recent colour ${color}`, `최근 색상 ${color}`, `最近颜色 ${color}`)}
|
|
499
489
|
aria-pressed={selectedSceneObject.color === color}
|
|
500
490
|
onClick={(event) => {
|
|
501
491
|
run(...updateArgs({ color }));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useEffect, useReducer, useRef, useState } from "react";
|
|
2
2
|
import { createPortal } from "react-dom";
|
|
3
3
|
import Foldout from "./Foldout.jsx";
|
|
4
|
-
import { ko
|
|
4
|
+
import { ko } from "../locale.js";
|
|
5
5
|
import { readReferenceImage } from "./ReferenceImageField.jsx";
|
|
6
6
|
import { createHttpTransport } from "../workflow/agent-client.js";
|
|
7
7
|
import { saveExport } from "../export-workspace.js";
|
|
@@ -24,7 +24,7 @@ function setPoseImageState(patch) {
|
|
|
24
24
|
|
|
25
25
|
function poseImageErrorMessage(error) {
|
|
26
26
|
if (error?.status === 401 || error?.code === "auth") return ko("Sign in with ChatGPT in the Agent panel first.", "먼저 Agent 패널에서 ChatGPT로 로그인하세요.");
|
|
27
|
-
return
|
|
27
|
+
return ko(`Generation failed — ${error?.message || "unknown error"}`, `생성 실패 — ${error?.message || "알 수 없는 오류"}`, `生成失败 — ${error?.message || "未知错误"}`);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
async function generatePoseImage(capturePoseImage) {
|
|
@@ -37,9 +37,9 @@ async function generatePoseImage(capturePoseImage) {
|
|
|
37
37
|
...store.refs.map((dataUrl, index) => ({ role: "reference", name: String(index + 1), dataUrl })),
|
|
38
38
|
];
|
|
39
39
|
if (references.length > REFERENCES_MAX) {
|
|
40
|
-
setPoseImageState({ shot, error:
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
setPoseImageState({ shot, error: ko(`At most ${REFERENCES_MAX} reference images (${shot.references.length} identity/environment included).`,
|
|
41
|
+
`참조 이미지는 최대 ${REFERENCES_MAX}장이에요 (인물·환경 참조 ${shot.references.length}장 포함).`,
|
|
42
|
+
`最多 ${REFERENCES_MAX} 张参考图(含 ${shot.references.length} 张角色/环境参考)。`) });
|
|
43
43
|
return;
|
|
44
44
|
}
|
|
45
45
|
setPoseImageState({ shot, busySince: Date.now(), error: "" });
|
|
@@ -78,7 +78,7 @@ export default function PoseImagePanel({ hidden, capturePoseImage }) {
|
|
|
78
78
|
const read = await Promise.all(picked.map((file) => readReferenceImage(file)));
|
|
79
79
|
setPoseImageState({ refs: [...store.refs, ...read].slice(0, USER_REFERENCES_MAX), error: "" });
|
|
80
80
|
} catch (failure) {
|
|
81
|
-
setPoseImageState({ error:
|
|
81
|
+
setPoseImageState({ error: ko(`Could not load that image — ${failure.message}`, `이미지를 불러오지 못했어요 — ${failure.message}`, `无法加载该图片 — ${failure.message}`) });
|
|
82
82
|
}
|
|
83
83
|
};
|
|
84
84
|
|
|
@@ -109,7 +109,7 @@ export default function PoseImagePanel({ hidden, capturePoseImage }) {
|
|
|
109
109
|
onDrop={(event) => { event.preventDefault(); addFiles(event.dataTransfer?.files ?? []); }}>
|
|
110
110
|
{refs.map((dataUrl, index) => (
|
|
111
111
|
<div key={index} className="pose-image-ref">
|
|
112
|
-
<img src={dataUrl} alt={
|
|
112
|
+
<img src={dataUrl} alt={ko(`Reference ${index + 1}`, `참조 ${index + 1}`, `参考图 ${index + 1}`)} />
|
|
113
113
|
<button type="button" className="pose-image-ref-remove" aria-label={ko("Remove reference", "참조 삭제")}
|
|
114
114
|
onClick={() => setPoseImageState({ refs: store.refs.filter((_, at) => at !== index) })}>×</button>
|
|
115
115
|
</div>
|
|
@@ -135,7 +135,7 @@ export default function PoseImagePanel({ hidden, capturePoseImage }) {
|
|
|
135
135
|
<button type="button" className="btn primary full" data-testid="pose-image-generate"
|
|
136
136
|
disabled={!prompt.trim() || Boolean(busySince)}
|
|
137
137
|
onClick={() => generatePoseImage(capturePoseImage)}>
|
|
138
|
-
{busySince ? (
|
|
138
|
+
{busySince ? ko(`Generating… ${elapsed}s`, `생성 중… ${elapsed}초`, `生成中… ${elapsed} 秒`) : ko("Generate image", "이미지 생성")}
|
|
139
139
|
</button>
|
|
140
140
|
{error && <p className="inspector-hint pose-image-error" role="status" data-testid="pose-image-error">{error}</p>}
|
|
141
141
|
|
package/src/panels/PosePanel.jsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Foldout from "./Foldout.jsx";
|
|
2
|
-
import { ko
|
|
2
|
+
import { ko } from "../locale.js";
|
|
3
3
|
import { PoseTileGrid } from "../posestudio.jsx";
|
|
4
4
|
import { DEFAULT_POSE } from "../poses.js";
|
|
5
5
|
import { poseLabelKo } from "../app-stage.jsx";
|
|
@@ -19,7 +19,7 @@ export default function PosePanel({
|
|
|
19
19
|
is the same grid the studio shows, applied to whichever
|
|
20
20
|
character the hierarchy has selected. */}
|
|
21
21
|
<p className="inspector-hint v2-details-hint">
|
|
22
|
-
{
|
|
22
|
+
{ko(`The pose on Subject ${activeCharIndex + 1}.`, `인물 ${activeCharIndex + 1}의 자세입니다.`, `角色 ${activeCharIndex + 1} 的姿势。`)}
|
|
23
23
|
</p>
|
|
24
24
|
<PoseTileGrid
|
|
25
25
|
poses={selectablePoses}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState, useRef } from "react";
|
|
2
|
-
import { ko
|
|
2
|
+
import { ko } from "../locale.js";
|
|
3
3
|
import { ASSET_IMAGE_TYPES, downscaleTarget } from "../scene-assets.js";
|
|
4
4
|
|
|
5
5
|
const REFERENCE_IMAGE_MAX_DIMENSION = 1024;
|
|
@@ -82,7 +82,7 @@ export default function ReferenceImageField({ label, hint, value, alt, onPick, o
|
|
|
82
82
|
try {
|
|
83
83
|
onPick(await readReferenceImage(file));
|
|
84
84
|
} catch (failure) {
|
|
85
|
-
setError(
|
|
85
|
+
setError(ko(`Could not load that image — ${failure.message}`, `이미지를 불러오지 못했어요 — ${failure.message}`, `无法加载该图片 — ${failure.message}`));
|
|
86
86
|
}
|
|
87
87
|
}}
|
|
88
88
|
/>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Foldout from "./Foldout.jsx";
|
|
2
|
-
import { ko
|
|
2
|
+
import { ko } from "../locale.js";
|
|
3
3
|
import { HIERARCHY_INSPECTOR_TITLES } from "../app-stage.jsx";
|
|
4
4
|
import { RangePinPanel } from "../range-pin-panel.jsx";
|
|
5
5
|
|
|
@@ -27,7 +27,7 @@ export default function RigControlPanel({
|
|
|
27
27
|
onChange={(event) => setPoseObjectOpacity(Number(event.target.value) / 100)} />
|
|
28
28
|
<b>{Math.round(poseObjectOpacity * 100)}%</b>
|
|
29
29
|
</div>}
|
|
30
|
-
<p className="inspector-hint">{rigSelection && rigSelection.token !== "rig" ? (
|
|
30
|
+
<p className="inspector-hint">{rigSelection && rigSelection.token !== "rig" ? ko(`${HIERARCHY_INSPECTOR_TITLES[rigSelection.token]} is the active control group.`, `${HIERARCHY_INSPECTOR_TITLES[rigSelection.token]}이 활성 제어 그룹입니다.`, `${HIERARCHY_INSPECTOR_TITLES[rigSelection.token]}是当前控制组。`) : ko("Select a body group to pose it in the viewport.", "몸 그룹을 선택해 뷰포트에서 포즈를 잡으세요.")}</p>
|
|
31
31
|
</section>
|
|
32
32
|
{ikMode && <section className="pose-section" data-testid="pose-active-tool">
|
|
33
33
|
<h4>{ko("Active tool", "활성 도구")} · {ko(activeTool, activeTool === "Pin" ? "고정" : "IK")}</h4>
|