cozyclay 3.1.1 → 3.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +64 -0
- package/README.md +1 -1
- package/bin/agent/agent-routes.mjs +4 -4
- package/bin/agent/agent-runner.mjs +160 -151
- package/bin/agent/agent-stream.mjs +86 -0
- package/bin/agent/anthropic-subscription-events.mjs +120 -0
- package/bin/agent/anthropic-subscription-prompt.mjs +116 -0
- package/bin/agent/anthropic-subscription-stream.mjs +251 -0
- package/bin/agent/anthropic-subscription.mjs +73 -0
- package/bin/agent/claude-code-executable.mjs +93 -0
- package/bin/agent/cliproxy-provider.mjs +25 -11
- package/bin/agent/main-agent.mjs +21 -7
- package/bin/agent/main-prompt.mjs +7 -6
- package/bin/agent/model-roles.mjs +112 -0
- package/bin/agent/openai-compatible-provider.mjs +95 -0
- package/bin/agent/pi-tools.mjs +19 -10
- package/bin/agent/provider-login.mjs +3 -1
- package/bin/agent/provider-routes.mjs +2 -0
- package/bin/agent/providers.mjs +35 -130
- package/bin/agent/script-tool.mjs +2 -1
- package/bin/agent/studio-prompt.mjs +1 -1
- package/bin/agent/studio-rebase.mjs +5 -4
- package/bin/agent/studio-task-routes.mjs +1 -1
- package/bin/agent/studio-tasks.mjs +116 -25
- package/bin/agent/studio-tools.mjs +3 -2
- package/bin/cozyclay.mjs +4 -2
- package/bin/live-endpoint.mjs +2 -0
- package/dist/app/index.html +6 -6
- package/dist/assets/TaskBoard-U1Yl-4F1.js +1 -0
- package/dist/assets/app-BdRlMf4J.css +1 -0
- package/dist/assets/app-DwETmoQZ.js +5144 -0
- package/dist/assets/compile-worker-oy_P9HHh.js +6 -0
- package/dist/assets/motion3-compile-DDG3wklD.js +1 -0
- package/dist/assets/scene-asset-sync-BEjjhNEo.css +1 -0
- package/dist/assets/scene-asset-sync-Bz59Ghdz.js +13 -0
- package/dist/assets/task-board-DFfNa0xg.js +15 -0
- package/dist/assets/workflow-BMJGd_gZ.js +179 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/index.html +1 -1
- package/dist/sitemap.xml +1 -1
- package/dist/workflow/index.html +4 -4
- package/mcp/README.md +1 -1
- package/mcp/live-hub.mjs +5 -3
- package/mcp/tool-handlers.mjs +5 -5
- package/package.json +6 -4
- package/src/App.jsx +65 -87
- package/src/agent-image-client.js +2 -2
- package/src/asset-pane.jsx +14 -14
- package/src/bus-task-scopes.js +59 -11
- package/src/bus-task-targets.js +35 -9
- package/src/bus-tasks.js +23 -10
- package/src/character-kind.js +13 -12
- package/src/command-bus.js +7 -4
- package/src/commands/ai.js +1 -1
- package/src/commands/export.js +1 -1
- package/src/commands/motion3.js +27 -7
- package/src/commands/project.js +2 -1
- package/src/commands/shot.js +1 -1
- package/src/commands/view.js +2 -2
- package/src/domains/cast.js +15 -15
- package/src/domains/motion/motion-range-pin-rules.js +1 -1
- package/src/domains/motion.js +11 -11
- package/src/domains/objects.js +20 -30
- package/src/domains/scenes.js +14 -21
- package/src/domains/shots.js +3 -3
- package/src/export-workspace.js +244 -0
- package/src/first-success-guide.jsx +19 -3
- package/src/hierarchy-panel.jsx +6 -7
- package/src/keyframe-pack.js +5 -1
- package/src/live-control.js +14 -7
- package/src/locale-zh/app.js +124 -0
- package/src/locale-zh/domains.js +36 -0
- package/src/locale-zh/labels.js +148 -0
- package/src/locale-zh/panels.js +225 -0
- package/src/locale-zh/shell.js +230 -0
- package/src/locale-zh/timeline.js +145 -0
- package/src/locale-zh/workflow.js +104 -0
- package/src/locale-zh/workspace.js +190 -0
- package/src/locale-zh.js +21 -0
- package/src/locale.js +16 -6
- package/src/main.jsx +1 -1
- package/src/motion/beat-board-model.js +4 -4
- package/src/motion/beat-board.jsx +1 -1
- package/src/motion/ik-pick.js +24 -0
- package/src/motion/take-segments.jsx +5 -5
- package/src/motion/timeline.jsx +30 -40
- package/src/motion/waypoints.js +6 -6
- package/src/motion3/compile-job.js +15 -3
- package/src/motion3/compiler/channels.js +74 -24
- package/src/motion3/compiler/compile.js +2 -1
- package/src/motion3/compiler/idle.js +2 -0
- package/src/motion3/compiler/overlap.js +180 -0
- package/src/motion3/compiler/pose.js +6 -4
- package/src/motion3/compiler/validate-common.js +1 -0
- package/src/motion3/compiler/validate-parts.js +5 -1
- package/src/motion3/compiler/validate.js +30 -10
- package/src/motion3/correct.js +236 -0
- package/src/motion3/docs.js +8 -6
- package/src/motion3/face-lean.js +31 -0
- package/src/motion3/fix/index.js +5 -1
- package/src/motion3/fix/reach-body.js +172 -0
- package/src/motion3/hint-check.js +44 -0
- package/src/motion3/json-patch.js +120 -12
- package/src/motion3/lenient.js +46 -4
- package/src/motion3/object-keys.js +17 -5
- package/src/motion3/pipeline.js +46 -9
- package/src/motion3/qa/arm-checks.js +96 -0
- package/src/motion3/qa/asked.js +113 -0
- package/src/motion3/qa/goals.js +37 -8
- package/src/motion3/qa/hints-goal.js +6 -5
- package/src/motion3/qa/index.js +5 -2
- package/src/motion3/qa/key-checks.js +4 -3
- package/src/motion3/qa/motion-life.js +243 -0
- package/src/motion3/qa/spec-checks.js +15 -9
- package/src/motion3/qa/verdict.js +50 -0
- package/src/motion3/refs.js +38 -14
- package/src/motion3/repair-watch.js +132 -0
- package/src/motion3/repair.js +52 -19
- package/src/motion3/report.js +50 -9
- package/src/motion3/schema/topics.js +6 -4
- package/src/motion3/schema/types.js +1 -1
- package/src/motion3/skills/basic.js +15 -8
- package/src/motion3/skills/index.js +52 -18
- package/src/motion3/skills/info.js +1 -1
- package/src/motion3/skills/stance.js +6 -0
- package/src/motion3/skills/walk-fit.js +170 -0
- package/src/object-catalog.jsx +4 -5
- package/src/panels/CameraPanel.jsx +3 -3
- package/src/panels/HdrEnvironmentControls.jsx +2 -1
- package/src/panels/ObjectTransformPanel.jsx +19 -29
- package/src/panels/PoseImagePanel.jsx +11 -10
- package/src/panels/PosePanel.jsx +2 -2
- package/src/panels/ReferenceImageField.jsx +2 -2
- package/src/panels/RigControlPanel.jsx +2 -2
- package/src/panels/SculptPanel.jsx +11 -11
- package/src/panels/SubjectBox.jsx +3 -3
- package/src/panels/SubjectsPanel.jsx +1 -1
- package/src/posestudio.jsx +10 -8
- package/src/project-browser.css +7 -6
- package/src/project-browser.jsx +7 -6
- package/src/project-open-file.js +19 -0
- package/src/qa-label.js +1 -1
- package/src/range-pin-panel.jsx +14 -14
- package/src/resource-status.jsx +3 -3
- package/src/result-modal.jsx +13 -13
- package/src/settings-menu.jsx +4 -1
- package/src/shell/BoardSlot.jsx +5 -5
- package/src/shell/CompositionGuideButton.jsx +1 -1
- package/src/shell/ExportFolderPreference.jsx +103 -0
- package/src/shell/GhostsMenuGroup.jsx +1 -1
- package/src/shell/PreferencesDialog.jsx +3 -166
- package/src/shell/StatusBar.jsx +4 -4
- package/src/shell/ViewportToolbar.jsx +3 -3
- package/src/shell/add-camera-shot.js +2 -2
- package/src/shell/details-selection.js +1 -1
- package/src/shell/preference-controls.jsx +115 -0
- package/src/shell/preference-pages.jsx +72 -0
- package/src/shell/preferences.css +46 -0
- package/src/shot-guides.js +5 -5
- package/src/shot-prompt.js +29 -9
- package/src/stage-hdr.js +3 -3
- package/src/stage-labels-ko.js +2 -2
- package/src/stage-startup-defaults.js +3 -3
- package/src/studio-actions.js +4 -4
- package/src/studio-agent-motion.js +19 -9
- package/src/studio-agent-protocol.js +1 -1
- package/src/studio-app-binding.js +3 -2
- package/src/styles/tokens.css +1 -1
- package/src/styles.css +1 -1
- package/src/use-case-question.jsx +18 -18
- package/src/workflow/AgentPanel.jsx +6 -6
- package/src/workflow/AgentPlanCard.jsx +1 -1
- package/src/workflow/AgentProgressCard.jsx +4 -4
- package/src/workflow/AgentTurnSteps.jsx +1 -1
- package/src/workflow/CozySceneNode.jsx +1 -1
- package/src/workflow/LoginPanel.jsx +1 -1
- package/src/workflow/TaskBoard.jsx +1 -1
- package/src/workflow/TaskInline.jsx +1 -1
- package/src/workflow/WorkflowBuilder.jsx +2 -2
- package/src/workflow/agent-client.js +1 -1
- package/src/workflow/agent-turn.js +5 -5
- package/src/workflow/keyframe-pack-request.js +3 -10
- package/src/workflow/task-board.css +1 -1
- package/src/workflow/task-board.js +11 -9
- package/tools/loc-baseline.json +2 -3
- package/tools/run-tests.mjs +9 -9
- package/dist/assets/TaskBoard-ngdqiu7u.js +0 -1
- package/dist/assets/app-BlwOpest.css +0 -1
- package/dist/assets/app-DmR7jBkz.js +0 -5144
- package/dist/assets/compile-worker-D0pKioa0.js +0 -5
- package/dist/assets/motion3-compile-BB3_1-EB.js +0 -1
- package/dist/assets/scene-asset-sync-CBx7XRti.css +0 -1
- package/dist/assets/scene-asset-sync-Cgdtr1Yw.js +0 -13
- package/dist/assets/task-board-ErQvfoaz.js +0 -14
- package/dist/assets/workflow-zQM-EKW1.js +0 -179
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// walkTo's fit (C21): to with steps walks where the steps walk, so the steps are fitted to end at to in a natural
|
|
2
|
+
// gait for the rig (skills/basic.js walkTo calls fitTo when correct.js fitWalks asks), or the walk is refused.
|
|
3
|
+
import { len, sub } from "../compiler/math.js";
|
|
4
|
+
import { walk, stateAt } from "../compiler/walk.js";
|
|
5
|
+
import { partsOf } from "../object-pose.js";
|
|
6
|
+
import { standClearance } from "./stance.js";
|
|
7
|
+
|
|
8
|
+
/** The feet's midpoint [x, z] once the walk's keys have played out. */
|
|
9
|
+
export function walkEndAt(keys) {
|
|
10
|
+
const st = stateAt(keys, Infinity);
|
|
11
|
+
return [(st.foot_l[0] + st.foot_r[0]) / 2, (st.foot_l[2] + st.foot_r[2]) / 2];
|
|
12
|
+
}
|
|
13
|
+
export const r2 = v => Math.round(v * 100) / 100, xzText = p => `[${p.map(r2).join(", ")}]`;
|
|
14
|
+
// A natural gait for the rig (fitTo): a step STEP of the leg (hip to ankle: 0.25..0.7 m on the Y Bot), the first of a
|
|
15
|
+
// two-step walk FIRST of it (a short shuffle is a natural reposition), CADENCE steps a second, and the step over the cadence
|
|
16
|
+
// within RATIO of WALK_RATIO legs x s: a person's walk ratio (about 6.5 mm per step a minute, 0.39 m x s on a 0.83 m leg)
|
|
17
|
+
// holds from a slow walk to a brisk one, so longer steps come with quicker ones. A stride fitted at a fixed cadence went to
|
|
18
|
+
// 0.87 m at 1.2 steps a second in the agent sessions (a lunge); the walk generator slides or pops in most cadences from a
|
|
19
|
+
// 0.75 m step, and pops above 1.8 steps a second (31 of 47 fitted walks at period 12-13, none at 14-18; a person's brisk walk
|
|
20
|
+
// reaches 2.1).
|
|
21
|
+
const STEP = [0.3, 0.85], FIRST = [0.18, 0.85], CADENCE = [1, 1.8], WALK_RATIO = 0.47, RATIO = [0.7, 1.4], CHANGE = 0.1, HEADING_TOL = 20;
|
|
22
|
+
export const REACH_TOL = 0.1;
|
|
23
|
+
// A walk whose to lies within CLEAR legs (0.41 m on the Y Bot) of a body ahead of it (inside it, or its nearest point within
|
|
24
|
+
// 60 deg of the way the walk goes) stops before the body instead: a low body (top under LOW legs: a box on the floor, a crate)
|
|
25
|
+
// CLEAR from its near side, the feet's midpoint to its footprint, room to bend over it; a higher one (a table, a counter, a
|
|
26
|
+
// wall) where the standing body (stance.js: legs, feet, hips, arms down) clears it by GAP. The bench replay (n=692): steps
|
|
27
|
+
// fitted onto a to the agent had set 0.22-0.30 m from a box it then picked up walked the body into it (COLLIDE frames +58%,
|
|
28
|
+
// first-compile passes 28 -> 10); the build before the fit had stopped those walks 0.40-0.56 m from the box. One clearance
|
|
29
|
+
// for every body (0.41 m from a table too) cut COLLIDE frames as far but cost 59 goals and 127 REACH groups (a cup on the table
|
|
30
|
+
// out of reach); this split keeps the goals the fit gained (996, as 582d0501) with COLLIDE frames 80,853 -> 63,350 and
|
|
31
|
+
// first-compile passes 10 -> 19. A body taller than HEAD m at its foot (a shelf overhead), lower than SUPPORT (a mat), carried
|
|
32
|
+
// during the walk or sat on next is not in the way.
|
|
33
|
+
const CLEAR = 0.5, SUPPORT = 0.12, HEAD = 1.7, AHEAD = Math.cos(Math.PI / 3), LOW = 0.65, GAP = 0.05;
|
|
34
|
+
const legOf = sk => { const P = sk.restWorldPos, I = sk.index; return len(sub(P[I.LeftLeg], P[I.LeftUpLeg])) + len(sub(P[I.LeftFoot], P[I.LeftLeg])); };
|
|
35
|
+
// the bodies a walk may end against, as the still scene has them: what a hand carries during the walk (spec.objects with
|
|
36
|
+
// a grab before its end, not released before it starts), the seat a later sitInSeat sits on, its back, the car, and
|
|
37
|
+
// every part of their groups
|
|
38
|
+
function passable(ctx, end) {
|
|
39
|
+
const scene = ctx.scene, ids = new Set(), add = id => { const top = scene?.objects?.[id]?.ancestors?.at(-1) ?? id; for (const part of partsOf(scene, top)) ids.add(part); };
|
|
40
|
+
for (const u of (ctx.use ?? []).filter((x, j) => j !== ctx.index && x?.f0 >= ctx.f0 && x?.skill === "sitInSeat"))
|
|
41
|
+
for (const id of [u.args?.seat, u.args?.seatBack, u.args?.car].filter(x => typeof x === "string")) add(id);
|
|
42
|
+
for (const [id, o] of Object.entries(ctx.objects ?? {})) if (Number.isFinite(o?.grab) && o.grab <= end && !(o.release < ctx.f0)) for (const part of partsOf(scene, id)) ids.add(part);
|
|
43
|
+
return ids;
|
|
44
|
+
}
|
|
45
|
+
// the feet's midpoint p = [x, z] to body o's footprint: [distance, the nearest point]
|
|
46
|
+
function footprint(o, p) {
|
|
47
|
+
const c = o.R, dx = p[0] - o.pos[0], dz = p[1] - o.pos[2], lx = c[0][0] * dx + c[2][0] * dz, lz = c[0][2] * dx + c[2][2] * dz;
|
|
48
|
+
const qx = Math.max(-o.half[0], Math.min(o.half[0], lx)), qz = Math.max(-o.half[2], Math.min(o.half[2], lz));
|
|
49
|
+
const q = [o.pos[0] + c[0][0] * qx + c[0][2] * qz, o.pos[2] + c[2][0] * qx + c[2][2] * qz];
|
|
50
|
+
return [Math.hypot(p[0] - q[0], p[1] - q[1]), q];
|
|
51
|
+
}
|
|
52
|
+
// the point of the approach from c0 (unit u) where a walk toward a to (at d) stops clear of the bodies ahead: { t, at, near }, or null
|
|
53
|
+
function clearStop(ctx, c0, u, d, skip, clear, h) {
|
|
54
|
+
const to = [c0[0] + u[0] * d, c0[1] + u[1] * d], leg = legOf(ctx.sk);
|
|
55
|
+
const ahead = Object.entries(ctx.scene?.objects ?? {}).filter(([id, o]) => !skip.has(id) && o.half && o.pos && o.R && o.pos[1] < HEAD && o.pos[1] + 2 * o.half[1] * o.R[1][1] > SUPPORT)
|
|
56
|
+
.map(([id, o]) => { const [dist, q] = footprint(o, to); return { id, o, dist, q, low: o.pos[1] + 2 * o.half[1] * o.R[1][1] < LOW * leg }; })
|
|
57
|
+
.filter(b => b.dist < clear && (b.dist < 1e-6 || ((b.q[0] - to[0]) * u[0] + (b.q[1] - to[1]) * u[1]) >= AHEAD * b.dist)).sort((a, b) => a.dist - b.dist);
|
|
58
|
+
if (!ahead.length) return null;
|
|
59
|
+
const at = s => [c0[0] + u[0] * s, c0[1] + u[1] * s], high = new Set(Object.keys(ctx.scene.objects).filter(id => !ahead.some(b => b.id === id && !b.low)));
|
|
60
|
+
const blocked = s => ahead.some(b => b.low && footprint(b.o, at(s))[0] < clear) || (ahead.some(b => !b.low) && standClearance(ctx.sk, ctx.scene, at(s), h, high) < GAP);
|
|
61
|
+
if (!blocked(d)) return null;
|
|
62
|
+
let t = d;
|
|
63
|
+
while (t > 0 && blocked(t)) t -= 0.01;
|
|
64
|
+
return t > REACH_TOL ? { t, at: at(t), near: ahead[0].id, gap: footprint(ahead[0].o, at(t))[0] } : null;
|
|
65
|
+
}
|
|
66
|
+
// the last frame a walk whose stepping ends at `from` may end at: the first later frame another skill starts at, a key of the
|
|
67
|
+
// spec (or an object key, grab or release: ctx.fit.busy) after its stepping, or the take's last frame (ctx.fit.last)
|
|
68
|
+
const roomAfter = (ctx, from) => Math.min(ctx.fit.last ?? Infinity, ...(ctx.use ?? []).filter((x, j) => j !== ctx.index && x?.f0 > ctx.f0).map(x => x.f0),
|
|
69
|
+
...ctx.keys.filter(k => k.f > from && /^keys\[/.test(k.src ?? "")).map(k => k.f), ...(ctx.fit.busy ?? []).filter(f => f > from));
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* to with steps (C21): the steps walk where they walk, and to only turns the heading, so a walk with both is fitted
|
|
73
|
+
* when ctx.fit asks (correct.js fitWalks, before the spec is stored), in a natural gait (the bands above):
|
|
74
|
+
* the stride (steps >= 3, the first step keeping its share of it) or the first step (steps 2) that makes the steps end
|
|
75
|
+
* at to, when it stays in its band and in the walk ratio band at the walk's cadence;
|
|
76
|
+
* else the step count whose stride is nearest the walk ratio, its stride and cadence in their bands: in the walk's own
|
|
77
|
+
* stepping time (steps x period frames, the period the one that fits, never ending later; fewer steps stand at the end),
|
|
78
|
+
* or at the walk's own period when nothing comes after it in a frames "auto" spec (ctx.fit.stretch: it may end later);
|
|
79
|
+
* else more steps that end later, into frames nothing uses (roomAfter), at the walk's own cadence or a quicker natural one;
|
|
80
|
+
* else the walk's own steps at its band's edge when they still end within REACH_TOL of to (a walk that near is not fitted);
|
|
81
|
+
* else, or for a heading that points away from to, a line in ctx.fit.refusals with what the walk would do and needs.
|
|
82
|
+
* The last two were refusals until the bench (S7, S10: 3 of 7 walkTo refusals in 30 merged runs, 4 walks): each had one in
|
|
83
|
+
* the bands (step 0.7 m at 1.33-1.5 steps a second ending 0.05-0.07 m short, or 4 steps at 1.71 steps a second ending 2
|
|
84
|
+
* frames later into free frames) whose whole take had no foot, knee or float problem in the walk the in-band walks lacked.
|
|
85
|
+
* Deterministic: a walk at a candidate count is estimated (its end is linear in the stride) and the chosen one walked
|
|
86
|
+
* once more to set its stride. -> { gen, adjust } with the fitted gen.
|
|
87
|
+
*/
|
|
88
|
+
export function fitTo(ctx, gen, c0, d, bearing) {
|
|
89
|
+
const { args, tag, sk } = ctx, steps = gen.steps, h = Array.isArray(args.heading) ? args.heading[1] : args.heading;
|
|
90
|
+
const off = h === undefined ? 0 : Math.abs(((h - bearing) % 360 + 540) % 360 - 180);
|
|
91
|
+
if (off > HEADING_TOL) {
|
|
92
|
+
ctx.fit.refusals.push(`${tag}: heading ${h} points ${Math.round(off)} deg away from to ${xzText(args.to)} (it lies at heading ${Math.round(bearing)}), so the walk would not go there; with to the walk turns toward it by itself: leave heading out, or give heading without to to walk that way`);
|
|
93
|
+
return { gen };
|
|
94
|
+
}
|
|
95
|
+
const period = gen.period ?? 20, swing = gen.swing ?? 14, was = gen.step ?? 0.5, first = gen.firstStep ?? 0.4;
|
|
96
|
+
if (args.steps === undefined || args.arrive !== undefined || !(steps >= 2) || ![steps, period, swing, was, first].every(Number.isFinite)) return { gen };
|
|
97
|
+
const u = [Math.sin(bearing * Math.PI / 180), Math.cos(bearing * Math.PI / 180)];
|
|
98
|
+
// to against a body: the steps end at the nearest point of the approach where the body stands clear of it; when no
|
|
99
|
+
// natural gait ends there, the fit to to itself (and its refusal) as before
|
|
100
|
+
// (ctx.fit.noStop: the walks correct.js fitWalks keeps on to, as a stop would make a later walk refuse; ctx.fit.stopped: the walks that stopped)
|
|
101
|
+
const stop = !ctx.fit.noStop?.has(ctx.index) && clearStop(ctx, c0, u, d, passable(ctx, ctx.f0 + 16 + steps * period + 18), CLEAR * legOf(sk), bearing);
|
|
102
|
+
let r = stop && fitSteps(ctx, gen, c0, u, stop.t, stop);
|
|
103
|
+
if (r && !r.refuse) ctx.fit.stopped?.add(ctx.index);
|
|
104
|
+
if (!r || r.refuse) r = fitSteps(ctx, gen, c0, u, d, null);
|
|
105
|
+
if (r.refuse) ctx.fit.refusals.push(r.refuse);
|
|
106
|
+
return { gen: r.gen, ...(r.adjust ? { adjust: r.adjust } : {}) };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** fitTo's stride, step count and cadence for a walk ending d along u from c0 (stop: the clear point it ends at) -> { gen, adjust? } or { gen, refuse } */
|
|
110
|
+
function fitSteps(ctx, gen, c0, u, d, stop) {
|
|
111
|
+
const { args, tag, sk } = ctx, steps = gen.steps, period = gen.period ?? 20, swing = gen.swing ?? 14, was = gen.step ?? 0.5, first = gen.firstStep ?? 0.4;
|
|
112
|
+
const target = stop ? `${xzText(stop.at)}, ${r2(stop.gap)} m before ${stop.near} (to ${xzText(args.to)} is against it)` : `to ${xzText(args.to)}`;
|
|
113
|
+
const reach = g => { const e = walkEndAt(walk(g, ctx.keys, sk, tag)); return { end: e, along: (e[0] - c0[0]) * u[0] + (e[1] - c0[1]) * u[1] }; };
|
|
114
|
+
const { end, along } = reach(gen);
|
|
115
|
+
if (Math.abs(d - along) <= REACH_TOL) return { gen };
|
|
116
|
+
const fps = ctx.fps ?? 24, leg = legOf(sk), nat = WALK_RATIO * leg, span = steps * period;
|
|
117
|
+
const band = n => (n >= 3 ? STEP : FIRST).map(x => r2(x * leg)), pLo = Math.ceil(fps / CADENCE[1]), pHi = Math.floor(fps / CADENCE[0]);
|
|
118
|
+
const stretch = ctx.fit.stretch === true && !(ctx.use ?? []).some((x, j) => j !== ctx.index && x?.f0 > ctx.f0) && !ctx.keys.some(k => k.f > ctx.f0 && /^keys\[/.test(k.src ?? ""));
|
|
119
|
+
// n steps: the period (its own, or the one that keeps the stepping time), the stride that ends them at to (the end moves
|
|
120
|
+
// by the first step and a stride a middle step, rest the closing step's share; with steps 3 and up the first step keeps
|
|
121
|
+
// its share of the stride unless args give it, as a person's first step is a little shorter), whether they stay in their bands
|
|
122
|
+
const rest = along - first - (steps - 2) * was, share = args.firstStep === undefined ? first / was : 0;
|
|
123
|
+
const at = (n, pAt) => {
|
|
124
|
+
const p = pAt ?? (stretch || n === steps ? period : Math.min(Math.max(pHi, period), Math.floor(span / n))), name = n >= 3 ? "step" : "firstStep";
|
|
125
|
+
const value = n >= 3 ? (d - rest - (share ? 0 : first)) / (share + n - 2) : d - rest, lead = n >= 3 && share ? share * value : first;
|
|
126
|
+
const [lo, hi] = band(n), ratio = value * p / fps / nat, inBand = (x, [a, b]) => x >= a && x <= b;
|
|
127
|
+
return { n, p, name, value, ratio, ok: inBand(value, [lo, hi]) && inBand(lead, band(2)) && (p === period || inBand(p, [pLo, pHi])) };
|
|
128
|
+
};
|
|
129
|
+
const own = at(steps), most = Math.min(60, Math.ceil(d / band(3)[0]) + 2);
|
|
130
|
+
let pick = own.ok && own.ratio >= RATIO[0] && own.ratio <= RATIO[1] ? own : null, room, short;
|
|
131
|
+
// nearest the walk ratio, a step count further from the one asked costing CHANGE of it a step
|
|
132
|
+
const cost = c => Math.abs(Math.log(c.ratio)) + CHANGE * Math.abs(c.n - steps), nearest = list => list.filter(c => c.ok).sort((a, b) => cost(a) - cost(b) || a.n - b.n)[0];
|
|
133
|
+
const counts = Array.from({ length: most - 1 }, (_, i) => i + 2);
|
|
134
|
+
if (!pick) pick = nearest(counts.map(n => at(n)));
|
|
135
|
+
if (!pick && !stretch) {
|
|
136
|
+
// more steps, ending later into frames nothing uses: the walk's own period, or the slowest natural one that ends in time
|
|
137
|
+
room = roomAfter(ctx, ctx.f0 + 16 + span);
|
|
138
|
+
pick = nearest(counts.filter(n => n > steps).map(n => at(n, Math.min(period, Math.floor((room - ctx.f0 - 34) / n)))).filter(c => c.p >= pLo && c.n * c.p > span));
|
|
139
|
+
}
|
|
140
|
+
if (!pick) {
|
|
141
|
+
// the walk's own steps at its band's edge, when they end within REACH_TOL of to
|
|
142
|
+
const [lo, hi] = band(steps), value = Math.min(hi, Math.max(lo, own.value)), ratio = value * period / fps / nat, lead = steps >= 3 && share ? share * value : first;
|
|
143
|
+
const g = { ...gen, [own.name]: r2(value), ...(steps >= 3 && share ? { firstStep: r2(lead) } : {}) };
|
|
144
|
+
const left = ratio >= RATIO[0] && ratio <= RATIO[1] && lead >= band(2)[0] && lead <= band(2)[1] ? d - reach(g).along : Infinity;
|
|
145
|
+
if (Math.abs(left) <= REACH_TOL) ({ pick, short } = { pick: { ...own, value: r2(value), ratio }, short: left });
|
|
146
|
+
}
|
|
147
|
+
const [sLo, sHi] = band(3), gait = `a natural gait (step ${sLo}..${sHi} m, ${CADENCE[0]}..${CADENCE[1]} steps a second: period ${pLo}..${pHi} at ${fps} fps`;
|
|
148
|
+
const miss = `${r2(Math.abs(d - along))} m ${along < d ? "short of" : "past"} ${stop ? `the point ${xzText(stop.at)} clear of ${stop.near} before to ${xzText(args.to)}` : `to ${xzText(args.to)}`} (${r2(d)} m away)`;
|
|
149
|
+
if (!pick) {
|
|
150
|
+
// short: the fewest steps at the longest natural step, at the quickest natural cadence (the least stepping time that reaches)
|
|
151
|
+
const n = 2 + Math.ceil((d - rest - first) / sHi - 1e-9), e = ctx.f0 + 16 + n * pLo + 18;
|
|
152
|
+
return { gen, refuse: `${tag}: steps ${steps} at period ${period} (${span} frames of stepping) walk ${r2(along)} m and end at ${xzText(end)}, ${miss}; ${along < d
|
|
153
|
+
? `in ${gait}) that takes ${n} steps and ${n * pLo} frames of stepping or more (the walk would end at f${e} or later, not f${ctx.f0 + 16 + span + 18}): leave steps${args.step === undefined ? "" : " and step"} out (walkTo picks the count), or give steps: ${n} and room for the walk before what follows`
|
|
154
|
+
: `no natural step count ends there: leave steps out (walkTo picks the count)`}` };
|
|
155
|
+
}
|
|
156
|
+
const timed = pick.p === period ? {} : { period: pick.p, swing: Math.max(2, Math.min(pick.p - 1, Math.round(swing * pick.p / period))) };
|
|
157
|
+
const lead = pick.n >= 3 && share > 0, strides = v => ({ [pick.name]: r2(v), ...(lead ? { firstStep: r2(share * v) } : {}) });
|
|
158
|
+
let fitted = { ...gen, steps: pick.n, ...timed, ...strides(pick.value) };
|
|
159
|
+
const left = short ?? d - reach(fitted).along;
|
|
160
|
+
if (short === undefined && Math.abs(left) > 0.02) fitted = { ...fitted, ...strides(pick.value + left / (pick.n >= 3 ? share + pick.n - 2 : 1)) };
|
|
161
|
+
const v = fitted[pick.name], dflt = pick.name === "step" ? was : first, e0 = ctx.f0 + 16 + span + 18, e1 = ctx.f0 + 16 + pick.n * pick.p + 18;
|
|
162
|
+
const changes = [pick.n !== steps && `steps ${steps} -> ${pick.n}`, timed.period && `period ${period} -> ${timed.period} (swing ${swing} -> ${timed.swing})`, `${pick.name} ${dflt} -> ${v} m`,
|
|
163
|
+
lead && fitted.firstStep !== first && `firstStep ${first} -> ${fitted.firstStep} m`].filter(Boolean).join(", ");
|
|
164
|
+
const when = e1 === e0 ? "" : `; it ends f${e1}, not f${e0} (${stretch ? "nothing follows it" : e1 > e0 ? `nothing uses f${e0 + 1}-f${e1}` : `the pose holds to f${e0}`})`;
|
|
165
|
+
const needs = `steps ${steps} would need a ${own.name} of ${r2(own.value)} m at ${r2(fps / period)} steps a second`;
|
|
166
|
+
const why = short !== undefined ? ` (${gait}; ${needs})` : pick.n === steps && !timed.period ? ` (at ${pick.name} ${dflt} they walk ${r2(along)} of ${r2(d)} m)` : ` in ${gait}; ${needs})${when}`;
|
|
167
|
+
const ends = short === undefined ? `end at ${target}` : `end ${r2(Math.abs(short))} m ${short > 0 ? "short of" : "past"} ${target}, the ${short > 0 ? "longest" : "shortest"} natural step`;
|
|
168
|
+
const adjust = { args: { ...(pick.n === steps ? {} : { steps: pick.n }), ...timed, [pick.name]: v, ...(lead && fitted.firstStep !== first ? { firstStep: fitted.firstStep } : {}) }, note: `${tag}: ${changes}, so steps ${pick.n} ${ends}${why}` };
|
|
169
|
+
return { gen: fitted, adjust };
|
|
170
|
+
}
|
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,9 +1,10 @@
|
|
|
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
|
+
import { saveExport } from "../export-workspace.js";
|
|
7
8
|
import "./pose-image.css";
|
|
8
9
|
|
|
9
10
|
// The sidecar accepts at most 6 references per request, shared with the scene's identity and
|
|
@@ -23,7 +24,7 @@ function setPoseImageState(patch) {
|
|
|
23
24
|
|
|
24
25
|
function poseImageErrorMessage(error) {
|
|
25
26
|
if (error?.status === 401 || error?.code === "auth") return ko("Sign in with ChatGPT in the Agent panel first.", "먼저 Agent 패널에서 ChatGPT로 로그인하세요.");
|
|
26
|
-
return
|
|
27
|
+
return ko(`Generation failed — ${error?.message || "unknown error"}`, `생성 실패 — ${error?.message || "알 수 없는 오류"}`, `生成失败 — ${error?.message || "未知错误"}`);
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
async function generatePoseImage(capturePoseImage) {
|
|
@@ -36,9 +37,9 @@ async function generatePoseImage(capturePoseImage) {
|
|
|
36
37
|
...store.refs.map((dataUrl, index) => ({ role: "reference", name: String(index + 1), dataUrl })),
|
|
37
38
|
];
|
|
38
39
|
if (references.length > REFERENCES_MAX) {
|
|
39
|
-
setPoseImageState({ shot, error:
|
|
40
|
-
|
|
41
|
-
|
|
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} 张角色/环境参考)。`) });
|
|
42
43
|
return;
|
|
43
44
|
}
|
|
44
45
|
setPoseImageState({ shot, busySince: Date.now(), error: "" });
|
|
@@ -77,7 +78,7 @@ export default function PoseImagePanel({ hidden, capturePoseImage }) {
|
|
|
77
78
|
const read = await Promise.all(picked.map((file) => readReferenceImage(file)));
|
|
78
79
|
setPoseImageState({ refs: [...store.refs, ...read].slice(0, USER_REFERENCES_MAX), error: "" });
|
|
79
80
|
} catch (failure) {
|
|
80
|
-
setPoseImageState({ error:
|
|
81
|
+
setPoseImageState({ error: ko(`Could not load that image — ${failure.message}`, `이미지를 불러오지 못했어요 — ${failure.message}`, `无法加载该图片 — ${failure.message}`) });
|
|
81
82
|
}
|
|
82
83
|
};
|
|
83
84
|
|
|
@@ -108,7 +109,7 @@ export default function PoseImagePanel({ hidden, capturePoseImage }) {
|
|
|
108
109
|
onDrop={(event) => { event.preventDefault(); addFiles(event.dataTransfer?.files ?? []); }}>
|
|
109
110
|
{refs.map((dataUrl, index) => (
|
|
110
111
|
<div key={index} className="pose-image-ref">
|
|
111
|
-
<img src={dataUrl} alt={
|
|
112
|
+
<img src={dataUrl} alt={ko(`Reference ${index + 1}`, `참조 ${index + 1}`, `参考图 ${index + 1}`)} />
|
|
112
113
|
<button type="button" className="pose-image-ref-remove" aria-label={ko("Remove reference", "참조 삭제")}
|
|
113
114
|
onClick={() => setPoseImageState({ refs: store.refs.filter((_, at) => at !== index) })}>×</button>
|
|
114
115
|
</div>
|
|
@@ -134,7 +135,7 @@ export default function PoseImagePanel({ hidden, capturePoseImage }) {
|
|
|
134
135
|
<button type="button" className="btn primary full" data-testid="pose-image-generate"
|
|
135
136
|
disabled={!prompt.trim() || Boolean(busySince)}
|
|
136
137
|
onClick={() => generatePoseImage(capturePoseImage)}>
|
|
137
|
-
{busySince ? (
|
|
138
|
+
{busySince ? ko(`Generating… ${elapsed}s`, `생성 중… ${elapsed}초`, `生成中… ${elapsed} 秒`) : ko("Generate image", "이미지 생성")}
|
|
138
139
|
</button>
|
|
139
140
|
{error && <p className="inspector-hint pose-image-error" role="status" data-testid="pose-image-error">{error}</p>}
|
|
140
141
|
|
|
@@ -145,7 +146,7 @@ export default function PoseImagePanel({ hidden, capturePoseImage }) {
|
|
|
145
146
|
<img src={result.dataUrl} alt={result.prompt} />
|
|
146
147
|
</button>
|
|
147
148
|
<div className="pose-image-result-actions">
|
|
148
|
-
<a className="btn ghost small" href={result.dataUrl} download={`cozyclay-pose-${result.at}.png`}>{ko("Download", "다운로드")}</a>
|
|
149
|
+
<a className="btn ghost small" href={result.dataUrl} download={`cozyclay-pose-${result.at}.png`} onClick={(event) => { event.preventDefault(); void saveExport(result.dataUrl, `cozyclay-pose-${result.at}.png`); }}>{ko("Download", "다운로드")}</a>
|
|
149
150
|
<button type="button" className="btn ghost small" disabled={refs.length >= USER_REFERENCES_MAX}
|
|
150
151
|
onClick={() => setPoseImageState({ refs: [...store.refs, result.dataUrl].slice(0, USER_REFERENCES_MAX) })}>
|
|
151
152
|
{ko("Use as reference", "참조로 쓰기")}
|
|
@@ -194,7 +195,7 @@ function PoseImageViewer({ results, selected, onSelect, onClose }) {
|
|
|
194
195
|
<figcaption>
|
|
195
196
|
<span className="pose-image-viewer-prompt">{entry.prompt}</span>
|
|
196
197
|
{results.length > 1 && <span className="pose-image-viewer-count">{results.length - selected}/{results.length}</span>}
|
|
197
|
-
<a className="btn ghost small" href={entry.dataUrl} download={`cozyclay-pose-${entry.at}.png`}>{ko("Download", "다운로드")}</a>
|
|
198
|
+
<a className="btn ghost small" href={entry.dataUrl} download={`cozyclay-pose-${entry.at}.png`} onClick={(event) => { event.preventDefault(); void saveExport(entry.dataUrl, `cozyclay-pose-${entry.at}.png`); }}>{ko("Download", "다운로드")}</a>
|
|
198
199
|
<button type="button" className="btn ghost small" data-testid="pose-image-viewer-close" onClick={onClose}>{ko("Close", "닫기")}</button>
|
|
199
200
|
</figcaption>
|
|
200
201
|
</figure>
|
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>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from "react";
|
|
2
2
|
import Foldout from "./Foldout.jsx";
|
|
3
|
-
import { ko
|
|
4
|
-
import { effortOptions } from "../workflow/agent-client.js";
|
|
3
|
+
import { ko } from "../locale.js";
|
|
4
|
+
import { effortOptions } from "../workflow/agent-client.js"; import { saveExport } from "../export-workspace.js";
|
|
5
5
|
import "./sculpt.css";
|
|
6
6
|
|
|
7
7
|
// Sculpt mode side panel (#739). Pure view: the run controller owns the run state and the
|
|
@@ -59,7 +59,7 @@ function NowLine({ waiting, now = Date.now() }) {
|
|
|
59
59
|
<span className="sculpt-now-time" data-testid="sculpt-now-time">{Math.floor(waited / 1000)}s</span>
|
|
60
60
|
{expect !== null && (
|
|
61
61
|
<span className="sculpt-now-expect">
|
|
62
|
-
{over ? ko("taking longer than usual", "평소보다 오래 걸려요") :
|
|
62
|
+
{over ? ko("taking longer than usual", "평소보다 오래 걸려요") : ko(`usually ~${Math.round(expect / 1000)}s`, `보통 ~${Math.round(expect / 1000)}초`, `通常约 ${Math.round(expect / 1000)} 秒`)}
|
|
63
63
|
</span>
|
|
64
64
|
)}
|
|
65
65
|
{expect !== null && <span className="sculpt-now-bar" aria-hidden="true"><span style={{ "--sculpt-wait": Math.min(1, waited / expect) }} /></span>}
|
|
@@ -78,7 +78,7 @@ function IouTrail({ history, target }) {
|
|
|
78
78
|
const delta = history.length > 1 ? last - history.at(-2).iou : null;
|
|
79
79
|
return (
|
|
80
80
|
<div className="sculpt-trail" data-testid="sculpt-trail">
|
|
81
|
-
<svg viewBox="0 0 100 34" preserveAspectRatio="none" aria-label={
|
|
81
|
+
<svg viewBox="0 0 100 34" preserveAspectRatio="none" aria-label={ko(`IoU by round: ${values.map((v) => v.toFixed(3)).join(", ")}`, `라운드별 IoU: ${values.map((v) => v.toFixed(3)).join(", ")}`, `各轮 IoU:${values.map((v) => v.toFixed(3)).join(", ")}`)} role="img">
|
|
82
82
|
{target !== null && <line className="sculpt-trail-target" x1="0" x2="100" y1={y(target)} y2={y(target)} />}
|
|
83
83
|
{history.length > 1 && <polyline className="sculpt-trail-line" points={history.map((row, index) => `${x(index)},${y(row.iou)}`).join(" ")} />}
|
|
84
84
|
{history.map((row, index) => <circle key={index} className="sculpt-trail-dot" data-last={index === history.length - 1 ? "true" : undefined} cx={x(index)} cy={y(row.iou)} r="2.2" />)}
|
|
@@ -186,7 +186,7 @@ function ModelPicker({ picker, locked }) {
|
|
|
186
186
|
{efforts.length > 0 && (
|
|
187
187
|
<select className="sculpt-select sculpt-effort" data-testid="sculpt-effort-select" aria-label={ko("Effort", "추론 강도")} value={effort ?? ""} disabled={locked}
|
|
188
188
|
onChange={(event) => onEffort?.(event.target.value || null)}>
|
|
189
|
-
<option value="">{
|
|
189
|
+
<option value="">{ko(`Default (${efforts[0]})`, `기본 (${efforts[0]})`, `默认(${efforts[0]})`)}</option>
|
|
190
190
|
{efforts.map((value) => <option key={value} value={value}>{value}</option>)}
|
|
191
191
|
</select>
|
|
192
192
|
)}
|
|
@@ -241,7 +241,7 @@ function BlueprintSection({ blueprint, disabled }) {
|
|
|
241
241
|
</a>
|
|
242
242
|
<figcaption>
|
|
243
243
|
<span>{sheet.width}×{sheet.height}{Number.isFinite(sheet.ms) ? ` · ${Math.round(sheet.ms / 1000)}s` : ""}</span>
|
|
244
|
-
<a href={sheet.dataUrl} download="sculpt-blueprint.png" data-testid="sculpt-blueprint-download">{ko("Download", "내려받기")}</a>
|
|
244
|
+
<a href={sheet.dataUrl} download="sculpt-blueprint.png" data-testid="sculpt-blueprint-download" onClick={(event) => { event.preventDefault(); void saveExport(sheet.dataUrl, "sculpt-blueprint.png"); }}>{ko("Download", "내려받기")}</a>
|
|
245
245
|
</figcaption>
|
|
246
246
|
</figure>
|
|
247
247
|
)}
|
|
@@ -293,7 +293,7 @@ export default function SculptPanel({ run, reference, onReference, onStart, onCa
|
|
|
293
293
|
onDragOver={(event) => { if (event.dataTransfer?.types?.includes("Files")) event.preventDefault(); }}
|
|
294
294
|
onDrop={(event) => { event.preventDefault(); take(event.dataTransfer?.files); }}>
|
|
295
295
|
<div className="sculpt-ref-frame">
|
|
296
|
-
<img src={reference.dataUrl} alt={
|
|
296
|
+
<img src={reference.dataUrl} alt={ko(`Reference: ${reference.name}`, `참조 이미지: ${reference.name}`, `参考图:${reference.name}`)} />
|
|
297
297
|
<OutlineScan outline={run?.outline} scanning={stage === "measure" && !run?.outline} />
|
|
298
298
|
</div>
|
|
299
299
|
<button type="button" className="btn ghost small sculpt-well-replace" data-testid="sculpt-reference-replace"
|
|
@@ -351,7 +351,7 @@ export default function SculptPanel({ run, reference, onReference, onStart, onCa
|
|
|
351
351
|
{running && <NowLine waiting={run.waiting} />}
|
|
352
352
|
|
|
353
353
|
<div className="sculpt-stats">
|
|
354
|
-
<span data-testid="sculpt-round">{
|
|
354
|
+
<span data-testid="sculpt-round">{ko(`Round ${run.round} / ${run.maxRounds}`, `라운드 ${run.round} / ${run.maxRounds}`, `第 ${run.round} / ${run.maxRounds} 轮`)}</span>
|
|
355
355
|
<span data-testid="sculpt-elapsed">{formatElapsed(run.elapsedMs)}</span>
|
|
356
356
|
</div>
|
|
357
357
|
|
|
@@ -365,7 +365,7 @@ export default function SculptPanel({ run, reference, onReference, onStart, onCa
|
|
|
365
365
|
data-reached={iou !== null && target !== null && iou >= target ? "true" : undefined}
|
|
366
366
|
style={{ "--sculpt-iou": iou ?? 0, "--sculpt-target": target ?? 0 }}>
|
|
367
367
|
<span className="sculpt-iou-fill" />
|
|
368
|
-
{target !== null && <span className="sculpt-iou-marker" data-testid="sculpt-iou-target" title={
|
|
368
|
+
{target !== null && <span className="sculpt-iou-marker" data-testid="sculpt-iou-target" title={ko(`Target ${formatIou(target)}`, `목표 ${formatIou(target)}`, `目标 ${formatIou(target)}`)} />}
|
|
369
369
|
</div>
|
|
370
370
|
<IouTrail history={run.history} target={target} />
|
|
371
371
|
</div>
|
|
@@ -374,7 +374,7 @@ export default function SculptPanel({ run, reference, onReference, onStart, onCa
|
|
|
374
374
|
<ActivityLog activity={run.activity} />
|
|
375
375
|
|
|
376
376
|
{run.message && !hasLog && <p className="inspector-hint sculpt-message" role="status">{run.message}</p>}
|
|
377
|
-
{stage === "done" && <p className="inspector-hint sculpt-done" role="status" data-testid="sculpt-done">{
|
|
377
|
+
{stage === "done" && <p className="inspector-hint sculpt-done" role="status" data-testid="sculpt-done">{ko(`Done — IoU ${formatIou(iou)}`, `완료 — IoU ${formatIou(iou)}`, `完成 — IoU ${formatIou(iou)}`)}</p>}
|
|
378
378
|
{stage === "cancelled" && <p className="inspector-hint" role="status" data-testid="sculpt-cancelled">{ko("Cancelled.", "취소했어요.")}</p>}
|
|
379
379
|
{stage === "failed" && <p className="inspector-hint sculpt-error" role="alert" data-testid="sculpt-error">{run.error || ko("The sculpt run failed.", "조형에 실패했어요.")}</p>}
|
|
380
380
|
</div>
|
|
@@ -391,7 +391,7 @@ export default function SculptPanel({ run, reference, onReference, onStart, onCa
|
|
|
391
391
|
)}
|
|
392
392
|
|
|
393
393
|
<button type="button" className="btn full" data-testid="sculpt-place" disabled={!sculptName || running}
|
|
394
|
-
aria-label={sculptName ? (
|
|
394
|
+
aria-label={sculptName ? ko(`Place ${sculptName} in scene`, `${sculptName} 장면에 배치`, `将 ${sculptName} 放入场景`) : ko("Place in scene", "장면에 배치")}
|
|
395
395
|
onClick={() => onPlaceInScene?.()}>
|
|
396
396
|
{ko("Place in scene", "장면에 배치")}
|
|
397
397
|
</button>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ko
|
|
1
|
+
import { ko } from "../locale.js";
|
|
2
2
|
|
|
3
3
|
export default function SubjectBox({ label, onRemove, onPose, posing, color, onColorChange, onColorEditStart }) {
|
|
4
4
|
return (
|
|
@@ -28,8 +28,8 @@ export default function SubjectBox({ label, onRemove, onPose, posing, color, onC
|
|
|
28
28
|
<button
|
|
29
29
|
type="button"
|
|
30
30
|
className={"cam-toggle" + (posing ? " active" : "")}
|
|
31
|
-
aria-label={
|
|
32
|
-
title={
|
|
31
|
+
aria-label={ko(`Open pose studio for ${label}`, `${label} 포즈 열기`, `打开 ${label} 的姿势工作室`)}
|
|
32
|
+
title={ko(`Pose ${label}`, `${label} 포즈`, `${label} 姿势`)}
|
|
33
33
|
onClick={onPose}
|
|
34
34
|
>
|
|
35
35
|
{/* The Pose mode figure, not a keyboard glyph. */}
|
|
@@ -14,7 +14,7 @@ export default function SubjectsPanel({
|
|
|
14
14
|
{characters.map((entry, index) => entry.hidden ? null : (
|
|
15
15
|
<SubjectBox
|
|
16
16
|
key={entry.id}
|
|
17
|
-
label={ko(`Subject ${index + 1}`, `인물 ${index + 1}`)}
|
|
17
|
+
label={ko(`Subject ${index + 1}`, `인물 ${index + 1}`, `角色 ${index + 1}`)}
|
|
18
18
|
onPose={() => openStudio(entry.id)}
|
|
19
19
|
posing={posing === entry.id}
|
|
20
20
|
onRemove={index > 0 ? () => run('character.remove', { characterId: entry.id }) : undefined}
|