cozyclay 1.9.0 → 2.0.0
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 +171 -0
- package/bin/agent/agent-routes.mjs +74 -70
- package/bin/agent/agent-runner.mjs +10 -3
- package/bin/agent/motion-runtime.mjs +32 -260
- package/bin/agent/providers.mjs +46 -3
- package/bin/agent/session-store.mjs +41 -5
- package/bin/agent/studio-prompt.mjs +1 -1
- package/bin/agent/studio-tools.mjs +48 -11
- package/bin/cozyclay.mjs +26 -8
- package/bin/live/cli.mjs +22 -3
- package/bin/mcp-runtime.mjs +35 -20
- package/bin/telemetry-state.mjs +5 -2
- package/bin/update-check.mjs +15 -0
- package/dist/app/index.html +6 -6
- package/dist/assets/analytics-MVZOEW8O.js +1 -0
- package/dist/assets/app-C1sdVbg4.css +1 -0
- package/dist/assets/app-CA55tcI3.js +4876 -0
- package/dist/assets/{demo-B-eg1CT8.js → demo-ByeYp-dY.js} +1 -1
- package/dist/assets/{first-shot-handoff-CGgnvNu8.js → first-shot-handoff-DgT28ZiV.js} +1 -1
- package/dist/assets/{landing-IbRp3FnQ.js → landing-BaXkpFip.js} +1 -1
- package/dist/assets/shot-prompt-CbqWcWYi.css +1 -0
- package/dist/assets/shot-prompt-Yrt99wZx.js +17 -0
- package/dist/assets/{ticket-BgIzOBOf.js → ticket-BcmsnHn5.js} +1 -1
- package/dist/assets/workflow-DRdwoDBT.css +1 -0
- package/dist/assets/workflow-dNPGY9xz.js +179 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/fonts/IBM-Plex-Sans-OFL.txt +92 -0
- package/dist/fonts/JetBrains-Mono-OFL.txt +93 -0
- package/dist/fonts/README.md +13 -9
- package/dist/fonts/ibm-plex-sans-400-latin.woff2 +0 -0
- package/dist/fonts/ibm-plex-sans-500-latin.woff2 +0 -0
- package/dist/fonts/ibm-plex-sans-600-latin.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-400-latin.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-500-latin.woff2 +0 -0
- package/dist/index.html +7 -7
- package/dist/privacy/index.html +13 -8
- package/dist/sitemap.xml +6 -6
- package/dist/workflow/index.html +5 -5
- package/mcp/LIVE-PROTOCOL.md +1 -1
- package/mcp/live-hub.mjs +35 -93
- package/mcp/mesh-file.mjs +48 -0
- package/mcp/server.mjs +6 -77
- package/mcp/tool-handlers.mjs +224 -377
- package/package.json +2 -1
- package/src/App.jsx +1273 -9075
- package/src/analytics.js +393 -9
- package/src/app-context.js +211 -0
- package/src/app-stage.jsx +25 -24
- package/src/ardy/auto-fix-panel.css +108 -0
- package/src/ardy/collision-blockers.js +9 -3
- package/src/ardy/fix-collisions.js +10 -66
- package/src/ardy/ground.js +4 -4
- package/src/ardy/ik-drag.js +101 -0
- package/src/ardy/ik-key-json.js +43 -0
- package/src/ardy/ik.js +122 -76
- package/src/ardy/physics-panel.css +2 -0
- package/src/ardy/physics-panel.jsx +17 -5
- package/src/ardy/platform-fit-panel.css +64 -0
- package/src/ardy/platform-fit-panel.jsx +40 -0
- package/src/ardy/platform-fit.js +326 -0
- package/src/ardy/playback.js +166 -20
- package/src/ardy/range-pin.js +299 -0
- package/src/ardy/timeline-coordinates.js +26 -0
- package/src/ardy/timeline.css +1199 -0
- package/src/ardy/timeline.jsx +149 -41
- package/src/ardy/waypoints.js +2 -2
- package/src/asset-pane.css +462 -0
- package/src/asset-pane.jsx +461 -232
- package/src/command-bus.js +349 -0
- package/src/commands/ai.js +31 -0
- package/src/commands/cast.js +140 -0
- package/src/commands/elements/character.js +13 -0
- package/src/commands/elements/motion.js +14 -0
- package/src/commands/elements/object.js +22 -0
- package/src/commands/elements/scene.js +16 -0
- package/src/commands/elements/shot.js +9 -0
- package/src/commands/elements/stage.js +11 -0
- package/src/commands/elements.js +155 -0
- package/src/commands/export.js +20 -0
- package/src/commands/index.js +40 -0
- package/src/commands/motion.js +231 -0
- package/src/commands/objects.js +174 -0
- package/src/commands/project.js +53 -0
- package/src/commands/scene.js +81 -0
- package/src/commands/shared.js +19 -0
- package/src/commands/shot.js +103 -0
- package/src/commands/stage.js +17 -0
- package/src/commands/view.js +42 -0
- package/src/document-store.js +144 -0
- package/src/domains/cast.js +1007 -0
- package/src/domains/motion.js +3219 -0
- package/src/domains/objects.js +932 -0
- package/src/domains/scenes.js +817 -0
- package/src/domains/shots.js +404 -0
- package/src/domains/stage.js +93 -0
- package/src/dualview.jsx +113 -57
- package/src/facing-marks.js +3 -0
- package/src/first-success-guide.jsx +14 -14
- package/src/grid-view.js +18 -5
- package/src/hierarchy-model.js +24 -16
- package/src/hierarchy-panel.css +386 -0
- package/src/hierarchy-panel.jsx +150 -21
- package/src/{fal-motion-client.js → i2v-motion-client.js} +13 -13
- package/src/i2v-motion-studio.jsx +180 -0
- package/src/ik-camera.js +3 -0
- package/src/main.jsx +6 -0
- package/src/motion/generation.js +105 -0
- package/src/motion-readiness-ui.jsx +4 -4
- package/src/motion-readiness.js +11 -0
- package/src/motion-trail.js +366 -9
- package/src/object-gizmo.jsx +7 -1
- package/src/otio.js +2 -1
- package/src/panels/CameraPanel.jsx +60 -0
- package/src/panels/CharacterTransformPanel.jsx +48 -0
- package/src/panels/EnvironmentPanel.jsx +40 -0
- package/src/panels/Foldout.jsx +32 -0
- package/src/panels/LightPanel.jsx +24 -0
- package/src/panels/ObjectTransformPanel.jsx +554 -0
- package/src/panels/PosePanel.jsx +97 -0
- package/src/panels/ProjectPanel.jsx +33 -0
- package/src/panels/PromptBlocksPanel.jsx +442 -0
- package/src/panels/PropsPanel.jsx +70 -0
- package/src/panels/ReferenceImageField.jsx +91 -0
- package/src/panels/RigControlPanel.jsx +78 -0
- package/src/panels/RigPanel.jsx +37 -0
- package/src/panels/SubjectBox.jsx +47 -0
- package/src/panels/SubjectsPanel.jsx +39 -0
- package/src/panels/VideoCapturePanel.jsx +180 -0
- package/src/panels/details.css +1171 -0
- package/src/panels/motion.css +197 -0
- package/src/panels/pose.css +39 -0
- package/src/planview.jsx +60 -31
- package/src/posestudio.jsx +59 -4
- package/src/project-browser.css +1073 -0
- package/src/project-browser.jsx +284 -123
- package/src/range-pin-object-transform.js +19 -0
- package/src/range-pin-panel.css +518 -0
- package/src/range-pin-panel.jsx +344 -0
- package/src/result-modal.jsx +5 -5
- package/src/room.jsx +79 -51
- package/src/scene-objects.js +37 -1
- package/src/scenes.js +5 -0
- package/src/semantic-edit.js +2 -0
- package/src/settings-menu.jsx +39 -105
- package/src/shell/BottomDock.jsx +450 -0
- package/src/shell/DetailsSlot.jsx +513 -0
- package/src/shell/LibrarySlot.jsx +86 -0
- package/src/shell/MenuBar.jsx +601 -0
- package/src/shell/OutlinerSlot.jsx +50 -0
- package/src/shell/PreferencesDialog.jsx +538 -0
- package/src/shell/PreferencesSlot.jsx +40 -0
- package/src/shell/StatusBar.jsx +125 -0
- package/src/shell/StudioShell.jsx +92 -0
- package/src/shell/TopBar.jsx +129 -0
- package/src/shell/ViewportToolbar.jsx +656 -0
- package/src/shell/agent-glass.css +296 -0
- package/src/shell/dock.css +93 -0
- package/src/shell/glass-regions.css +985 -0
- package/src/shell/glass.css +687 -0
- package/src/shell/log-store.js +52 -0
- package/src/shell/mode.css +9 -0
- package/src/shell/preferences.css +651 -0
- package/src/shell/shell.css +282 -0
- package/src/shell/studio-shell-context.js +19 -0
- package/src/shell/topbar.css +435 -0
- package/src/shell/viewport.css +451 -0
- package/src/store/authored-intent.js +22 -0
- package/src/store/runtime-adapters.js +92 -0
- package/src/store/scene-stage.js +21 -0
- package/src/store/use-document-store.js +14 -0
- package/src/studio-actions.js +210 -0
- package/src/studio-agent-commands.js +41 -271
- package/src/studio-agent-context.js +39 -10
- package/src/studio-agent-motion.js +122 -373
- package/src/studio-agent-protocol.js +67 -31
- package/src/studio-app-binding.js +379 -0
- package/src/studio-contact-sheet.js +73 -0
- package/src/studio-elements.js +58 -51
- package/src/styles/themes.css +200 -0
- package/src/styles/tokens.css +69 -0
- package/src/styles.css +460 -1742
- package/src/theme.js +44 -0
- package/src/timeline-extent.js +16 -0
- package/src/trail-key-conflicts.js +39 -0
- package/src/trail-pick.js +59 -0
- package/src/ui.jsx +8 -8
- package/src/use-case-question.jsx +66 -0
- package/src/workflow/AgentPanel.jsx +82 -69
- package/src/workflow/agent-client.js +12 -2
- package/src/workflow/agent-panel.css +34 -32
- package/src/workflow/cozy-scene-node.css +2 -0
- package/src/workflow/workflow.css +2 -0
- package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
- package/tools/ardy/bridge.mjs +164 -146
- package/tools/ardy/visual-qa.mjs +5 -5
- package/tools/bench/EXP3.md +61 -0
- package/tools/bench/cclay_bench_extract_incam.py +125 -0
- package/tools/bench/cclay_bench_extract_obs.py +204 -0
- package/tools/bench/cclay_bench_runner.py +42 -0
- package/tools/bench/cube-contact.mjs +162 -0
- package/tools/bench/exp3.mjs +152 -0
- package/tools/bench/extract-bench-lib.mjs +216 -0
- package/tools/bench/extract-bench.mjs +302 -0
- package/tools/bench/fal-generate.mjs +88 -0
- package/tools/bench/fit/README.md +189 -0
- package/tools/bench/fit/camera.mjs +59 -0
- package/tools/bench/fit/contact.mjs +419 -0
- package/tools/bench/fit/footlock.mjs +234 -0
- package/tools/bench/fit/motion.mjs +85 -0
- package/tools/bench/fit/pin.mjs +27 -0
- package/tools/bench/fit/remote.mjs +105 -0
- package/tools/bench/fit-bench.mjs +126 -0
- package/tools/bench/fit-sanity.mjs +45 -0
- package/tools/bench/metrics.mjs +313 -0
- package/tools/bench/obs/depth.mjs +258 -0
- package/tools/bench/obs/extrinsics.mjs +175 -0
- package/tools/bench/obs/fit_mannequin_betas.py +340 -0
- package/tools/bench/obs/ground.mjs +294 -0
- package/tools/bench/obs/heading.mjs +151 -0
- package/tools/bench/obs/ladder.mjs +555 -0
- package/tools/bench/obs/mannequin-betas.json +124 -0
- package/tools/bench/obs/remote.mjs +83 -0
- package/tools/bench/obs/rest_joints.py +52 -0
- package/tools/bench/obs/ybot-targets.mjs +49 -0
- package/tools/bench/obs-bench.mjs +520 -0
- package/tools/bench/score.mjs +343 -0
- package/tools/bench/summarize.mjs +86 -0
- package/tools/dev/pages/privacy.html +13 -8
- package/tools/gt-render/browser.mjs +159 -0
- package/tools/gt-render/camera-math.mjs +276 -0
- package/tools/gt-render/page.mjs +267 -0
- package/tools/gt-render/render.mjs +514 -0
- package/tools/gt-render/scene-box.mjs +31 -0
- package/tools/gt-render/take-transform.mjs +87 -0
- package/tools/morphgs/assets/gen_truth.py +39 -0
- package/tools/morphgs/assets/mesh_ori_rig.txt +27 -0
- package/tools/morphgs/assets/playback-check.mjs +30 -0
- package/tools/morphgs/demo-gate.sh +26 -0
- package/tools/morphgs/fbx2morphgs.mjs +68 -0
- package/tools/morphgs/morphgs-to-cskel27.mjs +105 -0
- package/tools/morphgs/patches/preprocess_src-none-mode.patch +76 -0
- package/tools/morphgs/setup-on-cluster.sh +126 -0
- package/tools/qa/css-rule-usage.mjs +283 -0
- package/tools/qa/studio-control-count.mjs +70 -32
- package/tools/run-tests.mjs +134 -12
- package/tools/track/DESIGN.md +513 -0
- package/tools/track/backfill-provenance.mjs +116 -0
- package/tools/track/budget.mjs +8 -0
- package/tools/track/check-rig.mjs +209 -0
- package/tools/track/diagnostics.schema.json +60 -0
- package/tools/track/export-rig.mjs +256 -0
- package/tools/track/fallback.mjs +7 -0
- package/tools/track/fk-parity-fixture.mjs +161 -0
- package/tools/track/gate.mjs +349 -0
- package/tools/track/masks.mjs +318 -0
- package/tools/track/metrics.mjs +201 -0
- package/tools/track/publish-obs.mjs +108 -0
- package/tools/track/py/check_env.py +61 -0
- package/tools/track/py/eval_lr.py +229 -0
- package/tools/track/py/lr_viterbi.py +294 -0
- package/tools/track/py/masks.py +405 -0
- package/tools/track/py/objective.py +382 -0
- package/tools/track/py/rig.py +508 -0
- package/tools/track/py/scene.py +326 -0
- package/tools/track/py/test_joint_indices.py +123 -0
- package/tools/track/py/test_lr_viterbi.py +195 -0
- package/tools/track/py/test_masks.py +229 -0
- package/tools/track/py/test_rig.py +288 -0
- package/tools/track/py/test_scene.py +354 -0
- package/tools/track/py/test_track.py +328 -0
- package/tools/track/py/track.py +411 -0
- package/tools/track/remote.mjs +140 -0
- package/tools/track/rig-dump.mjs +227 -0
- package/tools/track/run-box-tests.mjs +14 -0
- package/tools/track/run-box.mjs +168 -0
- package/tools/track/setup-box.sh +59 -0
- package/tools/track/study-2d.mjs +728 -0
- package/dist/assets/analytics-B1hnH66c.js +0 -1
- package/dist/assets/app-BWusbqgO.js +0 -4861
- package/dist/assets/app-qKDo4PBX.css +0 -1
- package/dist/assets/shot-prompt-CHUtw6af.js +0 -17
- package/dist/assets/shot-prompt-C_g2BHVc.css +0 -1
- package/dist/assets/workflow-CVwzMhz2.js +0 -179
- package/dist/assets/workflow-DsudxKHF.css +0 -1
- package/src/fal-motion-studio.jsx +0 -180
- package/src/scene-history.js +0 -129
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ko } from "../locale.js";
|
|
2
|
+
import Foldout from "./Foldout.jsx";
|
|
3
|
+
import { Slider } from "../ui.jsx";
|
|
4
|
+
import { createKeyLight } from "../scenes.js";
|
|
5
|
+
import { useStageTransaction } from "../domains/stage.js";
|
|
6
|
+
|
|
7
|
+
export default function LightPanel({ keyLightSelected, keyLight }) {
|
|
8
|
+
const { run, begin, commit, cancel } = useStageTransaction();
|
|
9
|
+
return (
|
|
10
|
+
<Foldout hidden={!keyLightSelected} title={ko("Light", "조명")}>
|
|
11
|
+
<p className="hint v2-details-hint">{ko("Drag the sun in the scene to move the light. Shadows and warmth follow it.", "씬의 해를 드래그해 조명을 옮깁니다. 그림자와 빛의 방향이 따라옵니다.")}</p>
|
|
12
|
+
<div onPointerUp={commit} onPointerCancel={cancel} onKeyUp={commit} onBlur={commit}>
|
|
13
|
+
<Slider label={ko("Brightness", "밝기")} min={0} max={4} step={0.05} value={keyLight.intensity} onChange={(value) => run("run.update", { txId: begin("stage.setKeyLight"), args: { keyLight: { intensity: value } } })} />
|
|
14
|
+
<Slider label={ko("Warm ↔ Cool", "따뜻함 ↔ 차가움")} min={0} max={1} step={0.05} value={keyLight.warmth ?? 0.5} onChange={(value) => run("run.update", { txId: begin("stage.setKeyLight"), args: { keyLight: { warmth: value } } })} />
|
|
15
|
+
</div>
|
|
16
|
+
<div className="readout v2-details-readout">
|
|
17
|
+
<span title={ko("light position", "조명 위치")}>{`x ${keyLight.x.toFixed(1)} y ${keyLight.y.toFixed(1)} z ${keyLight.z.toFixed(1)}`}</span>
|
|
18
|
+
</div>
|
|
19
|
+
<button className="btn ghost" onClick={() => run("stage.setKeyLight", { keyLight: createKeyLight(null) })}>
|
|
20
|
+
{ko("Reset light", "조명 초기화")}
|
|
21
|
+
</button>
|
|
22
|
+
</Foldout>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
import { useContext, useEffect, useRef } from "react";
|
|
2
|
+
import { AppContext } from "../app-context.js";
|
|
3
|
+
import Foldout from "./Foldout.jsx";
|
|
4
|
+
import { ko, isKo } from "../locale.js";
|
|
5
|
+
import { Field, Vector3Row } from "../ui.jsx";
|
|
6
|
+
import { sceneObjectNameDisplayKo } from "../app-stage.jsx";
|
|
7
|
+
import { MESH_KIND, CUTOUT_KIND, CUTOUT_DEFAULT_HEIGHT, OBJECT_COLORS, normalizeObjectColor } from "../scene-objects.js";
|
|
8
|
+
import { MESH_HEIGHT_MIN } from "../scene-mesh.js";
|
|
9
|
+
import { autoColorHex } from "../auto-color.js";
|
|
10
|
+
|
|
11
|
+
export default function ObjectTransformPanel({
|
|
12
|
+
selectedSceneObject, snapEnabled, setSnapEnabled, attachTargetLabel,
|
|
13
|
+
sceneObjects, beginSceneTransaction, endSceneTransaction, matteCanvasRef, matteStats, matteMode,
|
|
14
|
+
setMatteMode, matteEditorRef, matteTolerance, setMatteTolerance, matteBrush, setMatteBrush, matteShrink,
|
|
15
|
+
setMatteShrink, matteFeather, setMatteFeather, matteBusy, autoColor,
|
|
16
|
+
recentObjectColors, rememberSceneObjectColor, objectColorDraft, setObjectColorDraft,
|
|
17
|
+
}) {
|
|
18
|
+
const app = useContext(AppContext), session = useRef(null);
|
|
19
|
+
const run = (...args) => {
|
|
20
|
+
const result = app.bus.run(...args);
|
|
21
|
+
const show = receipt => { if (!receipt.ok) app.notify(receipt.message); return receipt; };
|
|
22
|
+
return result?.then ? result.then(show) : show(result);
|
|
23
|
+
};
|
|
24
|
+
function finish(commit = true) {
|
|
25
|
+
const active = session.current;
|
|
26
|
+
if (!active) return;
|
|
27
|
+
session.current = null;
|
|
28
|
+
return endSceneTransaction(active.txId, { commit });
|
|
29
|
+
}
|
|
30
|
+
function begin(field) {
|
|
31
|
+
if (session.current?.field !== field) finish();
|
|
32
|
+
if (!session.current) {
|
|
33
|
+
const txId = beginSceneTransaction({ owner: `inspector:${field}`, cancel: () => { session.current = null; } });
|
|
34
|
+
session.current = { field, txId };
|
|
35
|
+
}
|
|
36
|
+
return session.current.txId;
|
|
37
|
+
}
|
|
38
|
+
const updateArgs = (patch, txId) => txId
|
|
39
|
+
? ['run.update', { txId, args: { id: selectedSceneObject.id, patch } }]
|
|
40
|
+
: ['object.update', { id: selectedSceneObject.id, patch }];
|
|
41
|
+
const endKey = event => {
|
|
42
|
+
if (event.key === 'Enter' || event.key === 'Escape') {
|
|
43
|
+
event.stopPropagation(); finish(event.key === 'Enter'); setObjectColorDraft(null);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
useEffect(() => () => finish(false), [selectedSceneObject?.id]);
|
|
47
|
+
return (
|
|
48
|
+
<Foldout hidden={!selectedSceneObject} title={ko("Transform", "변환")}>
|
|
49
|
+
{selectedSceneObject && (
|
|
50
|
+
<>
|
|
51
|
+
<p className="inspector-hint v2-details-hint">
|
|
52
|
+
{ko("Type a value and press Enter, or drag a number sideways to scrub (Shift for fine).", "값을 입력하고 Enter를 누르거나 숫자를 좌우로 끌어 조절하세요(Shift는 미세 조정).")}
|
|
53
|
+
</p>
|
|
54
|
+
<label className="check snap-toggle details-toggle-row">
|
|
55
|
+
<span>
|
|
56
|
+
{isKo ? (
|
|
57
|
+
<>
|
|
58
|
+
그리드 스냅 — <kbd>Ctrl</kbd>을 누르면 반대로 작동
|
|
59
|
+
</>
|
|
60
|
+
) : (
|
|
61
|
+
<>
|
|
62
|
+
Snap to grid — hold <kbd>Ctrl</kbd> to invert
|
|
63
|
+
</>
|
|
64
|
+
)}
|
|
65
|
+
</span>
|
|
66
|
+
<input type="checkbox" checked={snapEnabled} onChange={(event) => setSnapEnabled(event.target.checked)} />
|
|
67
|
+
</label>
|
|
68
|
+
<Field label={ko("Name", "이름")}>
|
|
69
|
+
<input
|
|
70
|
+
type="text"
|
|
71
|
+
value={sceneObjectNameDisplayKo(selectedSceneObject.name)}
|
|
72
|
+
onChange={(event) => run(...updateArgs({ name: event.target.value }, begin('name')))}
|
|
73
|
+
onBlur={() => finish()}
|
|
74
|
+
onKeyDown={endKey}
|
|
75
|
+
/>
|
|
76
|
+
</Field>
|
|
77
|
+
{/* A carried prop has no grouping parent to pick — the character
|
|
78
|
+
IS its parent — so the dropdown gives way to what it is
|
|
79
|
+
riding and the way off it. Detaching here is the Props drop,
|
|
80
|
+
numbers and all. */}
|
|
81
|
+
{selectedSceneObject.attach ? (
|
|
82
|
+
<Field label={ko("Attached to", "부착 대상")}>
|
|
83
|
+
<div className="attach-target">
|
|
84
|
+
<span>{attachTargetLabel(selectedSceneObject.attach)}</span>
|
|
85
|
+
<button
|
|
86
|
+
type="button"
|
|
87
|
+
className="btn ghost"
|
|
88
|
+
onClick={() => run('object.detach', { objectId: selectedSceneObject.id })}
|
|
89
|
+
title={ko("Put it back in the set, where it is now", "지금 있는 자리에 그대로 세트로 되돌립니다")}
|
|
90
|
+
>
|
|
91
|
+
{ko("Detach", "분리")}
|
|
92
|
+
</button>
|
|
93
|
+
</div>
|
|
94
|
+
</Field>
|
|
95
|
+
) : (
|
|
96
|
+
<Field label={ko("Parent", "상위 그룹")}>
|
|
97
|
+
<select
|
|
98
|
+
value={selectedSceneObject.parent ?? ""}
|
|
99
|
+
onChange={(event) => {
|
|
100
|
+
const parent = event.target.value || null;
|
|
101
|
+
run(parent ? 'object.group' : 'object.ungroup', { ...(parent ? { parent } : {}), children: [selectedSceneObject.id] });
|
|
102
|
+
}}
|
|
103
|
+
>
|
|
104
|
+
<option value="">{ko("(none)", "(없음)")}</option>
|
|
105
|
+
{sceneObjects
|
|
106
|
+
.filter((object) => object.id !== selectedSceneObject.id)
|
|
107
|
+
.map((object) => (
|
|
108
|
+
<option key={object.id} value={object.id}>{sceneObjectNameDisplayKo(object.name)}</option>
|
|
109
|
+
))}
|
|
110
|
+
</select>
|
|
111
|
+
</Field>
|
|
112
|
+
)}
|
|
113
|
+
<Vector3Row
|
|
114
|
+
label={ko("Position", "위치")}
|
|
115
|
+
fields={[
|
|
116
|
+
{ axis: "X", value: selectedSceneObject.x, step: 0.05, precision: 2, scrubRange: 5, onChange: (x, token) => run(...updateArgs({ x }, token)), onScrubStart: beginSceneTransaction, onScrubEnd: endSceneTransaction },
|
|
117
|
+
{ axis: "Y", value: selectedSceneObject.y ?? 0, step: 0.05, precision: 2, scrubRange: 5, onChange: (y, token) => run(...updateArgs({ y }, token)), onScrubStart: beginSceneTransaction, onScrubEnd: endSceneTransaction },
|
|
118
|
+
{ axis: "Z", value: selectedSceneObject.z, step: 0.05, precision: 2, scrubRange: 5, onChange: (z, token) => run(...updateArgs({ z }, token)), onScrubStart: beginSceneTransaction, onScrubEnd: endSceneTransaction },
|
|
119
|
+
]}
|
|
120
|
+
/>
|
|
121
|
+
<Vector3Row
|
|
122
|
+
label={ko("Rotation", "회전")}
|
|
123
|
+
fields={[
|
|
124
|
+
{ axis: "X", value: selectedSceneObject.rotX ?? 0, step: 1, precision: 1, scrubRange: 180, onChange: (rotX, token) => run(...updateArgs({ rotX }, token)), onScrubStart: beginSceneTransaction, onScrubEnd: endSceneTransaction },
|
|
125
|
+
{ axis: "Y", value: selectedSceneObject.rot, step: 1, precision: 1, scrubRange: 180, onChange: (rot, token) => run(...updateArgs({ rot }, token)), onScrubStart: beginSceneTransaction, onScrubEnd: endSceneTransaction },
|
|
126
|
+
{ axis: "Z", value: selectedSceneObject.rotZ ?? 0, step: 1, precision: 1, scrubRange: 180, onChange: (rotZ, token) => run(...updateArgs({ rotZ }, token)), onScrubStart: beginSceneTransaction, onScrubEnd: endSceneTransaction },
|
|
127
|
+
]}
|
|
128
|
+
/>
|
|
129
|
+
<Vector3Row
|
|
130
|
+
label={ko("Scale", "크기")}
|
|
131
|
+
fields={[
|
|
132
|
+
{ axis: "X", value: selectedSceneObject.scaleX ?? 1, step: 0.05, precision: 2, scrubRange: 4, onChange: (scaleX, token) => run(...updateArgs({ scaleX }, token)), onScrubStart: beginSceneTransaction, onScrubEnd: endSceneTransaction },
|
|
133
|
+
{ axis: "Y", value: selectedSceneObject.scaleY ?? 1, step: 0.05, precision: 2, scrubRange: 4, onChange: (scaleY, token) => run(...updateArgs({ scaleY }, token)), onScrubStart: beginSceneTransaction, onScrubEnd: endSceneTransaction },
|
|
134
|
+
{ axis: "Z", value: selectedSceneObject.scaleZ ?? 1, step: 0.05, precision: 2, scrubRange: 4, onChange: (scaleZ, token) => run(...updateArgs({ scaleZ }, token)), onScrubStart: beginSceneTransaction, onScrubEnd: endSceneTransaction },
|
|
135
|
+
]}
|
|
136
|
+
/>
|
|
137
|
+
{selectedSceneObject.renderer === MESH_KIND && (
|
|
138
|
+
<>
|
|
139
|
+
<Field label={ko("Height (m)", "높이 (m)")}>
|
|
140
|
+
<input
|
|
141
|
+
type="number"
|
|
142
|
+
data-field="mesh-height"
|
|
143
|
+
min={MESH_HEIGHT_MIN}
|
|
144
|
+
step="0.05"
|
|
145
|
+
value={selectedSceneObject.height ?? 1}
|
|
146
|
+
onChange={(event) => run(...updateArgs({ height: Number(event.target.value) }, begin('height')))}
|
|
147
|
+
onBlur={() => finish()} onKeyDown={endKey}
|
|
148
|
+
/>
|
|
149
|
+
</Field>
|
|
150
|
+
<label className="check details-toggle-row">
|
|
151
|
+
<span>{ko("Clay", "클레이")}</span>
|
|
152
|
+
<input
|
|
153
|
+
type="checkbox"
|
|
154
|
+
data-field="mesh-clay"
|
|
155
|
+
checked={selectedSceneObject.clay === true}
|
|
156
|
+
onChange={(event) => run(...updateArgs({ clay: event.target.checked }))}
|
|
157
|
+
/>
|
|
158
|
+
</label>
|
|
159
|
+
</>
|
|
160
|
+
)}
|
|
161
|
+
{selectedSceneObject.renderer === CUTOUT_KIND && (
|
|
162
|
+
<>
|
|
163
|
+
<Field label={ko("Card height (m)", "판 높이 (m)")}>
|
|
164
|
+
<input
|
|
165
|
+
type="number"
|
|
166
|
+
data-field="cutout-height"
|
|
167
|
+
min="0.05"
|
|
168
|
+
step="0.05"
|
|
169
|
+
value={selectedSceneObject.height ?? CUTOUT_DEFAULT_HEIGHT}
|
|
170
|
+
onChange={(event) => run(...updateArgs({ height: Number(event.target.value) }, begin('height')))}
|
|
171
|
+
onBlur={() => finish()} onKeyDown={endKey}
|
|
172
|
+
/>
|
|
173
|
+
</Field>
|
|
174
|
+
<Field label={ko("Card width (m)", "판 너비 (m)")}>
|
|
175
|
+
<input
|
|
176
|
+
type="number"
|
|
177
|
+
data-field="cutout-width"
|
|
178
|
+
min="0.05"
|
|
179
|
+
step="0.05"
|
|
180
|
+
value={Number((selectedSceneObject.footprint?.width ?? 0).toFixed(2))}
|
|
181
|
+
onChange={(event) => run(...updateArgs({ width: Number(event.target.value) }, begin('width')))}
|
|
182
|
+
onBlur={() => finish()} onKeyDown={endKey}
|
|
183
|
+
/>
|
|
184
|
+
</Field>
|
|
185
|
+
<p className="inspector-hint">
|
|
186
|
+
{isKo
|
|
187
|
+
? `높이를 바꾸면 너비는 사진 비율(${(selectedSceneObject.aspect ?? 1).toFixed(2)})을 따라갑니다. 너비만 따로 정하거나 기즈모의 가로축을 끌면 사진이 늘어납니다. 사진 속에서 크기를 알 수 있는 것(문 2 m, 사람 1.8 m)에 맞추세요.`
|
|
188
|
+
: `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.`}
|
|
189
|
+
</p>
|
|
190
|
+
{Math.abs((selectedSceneObject.stretch ?? 1) - 1) > 0.005 && (
|
|
191
|
+
<p className="inspector-hint">
|
|
192
|
+
<button
|
|
193
|
+
type="button"
|
|
194
|
+
className="ghost"
|
|
195
|
+
data-field="cutout-unstretch"
|
|
196
|
+
onClick={() => run(...updateArgs({ stretch: 1 }))}
|
|
197
|
+
>
|
|
198
|
+
{isKo
|
|
199
|
+
? `사진 비율로 되돌리기 (지금 ${((selectedSceneObject.stretch ?? 1) * 100).toFixed(0)}%)`
|
|
200
|
+
: `Back to the picture's proportions (now ${((selectedSceneObject.stretch ?? 1) * 100).toFixed(0)}%)`}
|
|
201
|
+
</button>
|
|
202
|
+
</p>
|
|
203
|
+
)}
|
|
204
|
+
<div className="matte-editor">
|
|
205
|
+
{!!selectedSceneObject.matteAssetId && (
|
|
206
|
+
<p className="inspector-hint matte-state">
|
|
207
|
+
{ko(
|
|
208
|
+
"This card's background is removed. You are editing the original photograph — apply again to change what goes.",
|
|
209
|
+
"이 카드는 배경이 지워진 상태입니다. 지금 보이는 것은 원본 사진이며, 다시 적용하면 지워지는 범위가 바뀝니다.",
|
|
210
|
+
)}
|
|
211
|
+
</p>
|
|
212
|
+
)}
|
|
213
|
+
<canvas
|
|
214
|
+
ref={matteCanvasRef}
|
|
215
|
+
className="matte-canvas"
|
|
216
|
+
// Focusable for the space-drag pan, not for a shortcut: undo
|
|
217
|
+
// belongs to the buttons here. Ctrl+Z is the scene's, and one
|
|
218
|
+
// key meaning two different undos in two different panels is
|
|
219
|
+
// worse than a key that means one thing everywhere.
|
|
220
|
+
tabIndex={0}
|
|
221
|
+
aria-label={ko("Background editor — drag over the background to cut it out", "배경 편집기 — 배경 위를 드래그하면 그 영역이 잘려 나갑니다")}
|
|
222
|
+
/>
|
|
223
|
+
<p className="inspector-hint">
|
|
224
|
+
{matteStats.painted
|
|
225
|
+
? isKo
|
|
226
|
+
? `사진의 ${Math.round(matteStats.coverage * 100)}%가 선택됨 — 보라색이 지워집니다.`
|
|
227
|
+
: `${Math.round(matteStats.coverage * 100)}% of the picture marked — purple is what goes.`
|
|
228
|
+
: ko(
|
|
229
|
+
"Drag over the background — the cut grows out from wherever the brush touches.",
|
|
230
|
+
"배경 위를 드래그하세요 — 브러시가 닿은 곳에서 같은 배경으로 번져 나가며 잘립니다.",
|
|
231
|
+
)}
|
|
232
|
+
</p>
|
|
233
|
+
</div>
|
|
234
|
+
{/* Two hands: what the brush does on the left, what to do
|
|
235
|
+
about what it did on the right. Clear sits under Undo and
|
|
236
|
+
Redo because it is the same kind of act — taking work
|
|
237
|
+
back — only all of it. */}
|
|
238
|
+
<div className="matte-tools">
|
|
239
|
+
<div className="presets matte-modes">
|
|
240
|
+
<button
|
|
241
|
+
type="button"
|
|
242
|
+
className={matteMode === "paint" ? "active" : ""}
|
|
243
|
+
onClick={() => {
|
|
244
|
+
setMatteMode("paint");
|
|
245
|
+
matteEditorRef.current?.setMode("paint");
|
|
246
|
+
}}
|
|
247
|
+
>
|
|
248
|
+
{ko("Cut out", "누끼 따기")}
|
|
249
|
+
</button>
|
|
250
|
+
<button
|
|
251
|
+
type="button"
|
|
252
|
+
className={matteMode === "erase" ? "active" : ""}
|
|
253
|
+
onClick={() => {
|
|
254
|
+
setMatteMode("erase");
|
|
255
|
+
matteEditorRef.current?.setMode("erase");
|
|
256
|
+
}}
|
|
257
|
+
>
|
|
258
|
+
{ko("Bring back", "되살리기")}
|
|
259
|
+
</button>
|
|
260
|
+
</div>
|
|
261
|
+
{/* Icons, not words: undo, redo and clear are the same three
|
|
262
|
+
acts in every tool anyone has used, and spelling them out
|
|
263
|
+
took more width than the two that actually name what this
|
|
264
|
+
brush does. The label lives in the tooltip and in the
|
|
265
|
+
accessible name. */}
|
|
266
|
+
<div className="matte-history">
|
|
267
|
+
<div className="presets matte-modes matte-icons">
|
|
268
|
+
<button
|
|
269
|
+
type="button"
|
|
270
|
+
disabled={!matteStats.canUndo}
|
|
271
|
+
title={ko("Undo", "실행 취소")}
|
|
272
|
+
aria-label={ko("Undo", "실행 취소")}
|
|
273
|
+
onClick={() => matteEditorRef.current?.undo()}
|
|
274
|
+
>
|
|
275
|
+
<span aria-hidden="true">↩️</span>
|
|
276
|
+
</button>
|
|
277
|
+
<button
|
|
278
|
+
type="button"
|
|
279
|
+
disabled={!matteStats.canRedo}
|
|
280
|
+
title={ko("Redo", "다시 실행")}
|
|
281
|
+
aria-label={ko("Redo", "다시 실행")}
|
|
282
|
+
onClick={() => matteEditorRef.current?.redo()}
|
|
283
|
+
>
|
|
284
|
+
<span aria-hidden="true">↪️</span>
|
|
285
|
+
</button>
|
|
286
|
+
</div>
|
|
287
|
+
<div className="presets matte-modes matte-icons matte-clear">
|
|
288
|
+
<button
|
|
289
|
+
type="button"
|
|
290
|
+
title={ko("Clear the selection", "선택 모두 지우기")}
|
|
291
|
+
aria-label={ko("Clear the selection", "선택 모두 지우기")}
|
|
292
|
+
onClick={() => matteEditorRef.current?.clear()}
|
|
293
|
+
>
|
|
294
|
+
<span aria-hidden="true">🗑️</span>
|
|
295
|
+
</button>
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
<div className="matte-slider">
|
|
300
|
+
<label htmlFor="matte-tolerance">{ko("Tolerance", "허용치")}</label>
|
|
301
|
+
<input
|
|
302
|
+
id="matte-tolerance"
|
|
303
|
+
type="range"
|
|
304
|
+
min="0.02"
|
|
305
|
+
max="0.6"
|
|
306
|
+
step="0.01"
|
|
307
|
+
value={matteTolerance}
|
|
308
|
+
onChange={(event) => {
|
|
309
|
+
const value = Number(event.target.value);
|
|
310
|
+
setMatteTolerance(value);
|
|
311
|
+
matteEditorRef.current?.setTolerance(value);
|
|
312
|
+
}}
|
|
313
|
+
/>
|
|
314
|
+
<input
|
|
315
|
+
type="number"
|
|
316
|
+
data-field="matte-tolerance"
|
|
317
|
+
min="0.02"
|
|
318
|
+
max="0.6"
|
|
319
|
+
step="0.01"
|
|
320
|
+
value={matteTolerance}
|
|
321
|
+
aria-label={ko("Tolerance", "허용치")}
|
|
322
|
+
onChange={(event) => {
|
|
323
|
+
const value = Number(event.target.value);
|
|
324
|
+
if (!Number.isFinite(value)) return;
|
|
325
|
+
setMatteTolerance(value);
|
|
326
|
+
matteEditorRef.current?.setTolerance(value);
|
|
327
|
+
}}
|
|
328
|
+
/>
|
|
329
|
+
<div className="matte-slider">
|
|
330
|
+
<label htmlFor="matte-brush">{ko("Brush", "붓 크기")}</label>
|
|
331
|
+
<input
|
|
332
|
+
id="matte-brush"
|
|
333
|
+
type="range"
|
|
334
|
+
min="2"
|
|
335
|
+
max="200"
|
|
336
|
+
step="1"
|
|
337
|
+
value={matteBrush}
|
|
338
|
+
onChange={(event) => {
|
|
339
|
+
const value = Number(event.target.value);
|
|
340
|
+
setMatteBrush(value);
|
|
341
|
+
matteEditorRef.current?.setBrush(value);
|
|
342
|
+
}}
|
|
343
|
+
/>
|
|
344
|
+
<input
|
|
345
|
+
type="number"
|
|
346
|
+
data-field="matte-brush"
|
|
347
|
+
min="2"
|
|
348
|
+
max="200"
|
|
349
|
+
step="1"
|
|
350
|
+
value={matteBrush}
|
|
351
|
+
aria-label={ko("Brush size", "붓 크기")}
|
|
352
|
+
onChange={(event) => {
|
|
353
|
+
const value = Number(event.target.value);
|
|
354
|
+
if (!Number.isFinite(value)) return;
|
|
355
|
+
setMatteBrush(value);
|
|
356
|
+
matteEditorRef.current?.setBrush(value);
|
|
357
|
+
}}
|
|
358
|
+
/>
|
|
359
|
+
</div>
|
|
360
|
+
<div className="matte-slider">
|
|
361
|
+
<label htmlFor="matte-shrink">{ko("Edge shrink", "가장자리 먹기")}</label>
|
|
362
|
+
<input
|
|
363
|
+
id="matte-shrink"
|
|
364
|
+
type="range"
|
|
365
|
+
min="0"
|
|
366
|
+
max="3"
|
|
367
|
+
step="0.5"
|
|
368
|
+
value={matteShrink}
|
|
369
|
+
onChange={(event) => setMatteShrink(Number(event.target.value))}
|
|
370
|
+
/>
|
|
371
|
+
<input
|
|
372
|
+
type="number"
|
|
373
|
+
data-field="matte-shrink"
|
|
374
|
+
min="0"
|
|
375
|
+
max="3"
|
|
376
|
+
step="0.5"
|
|
377
|
+
value={matteShrink}
|
|
378
|
+
aria-label={ko("Edge shrink", "가장자리 먹기")}
|
|
379
|
+
onChange={(event) => {
|
|
380
|
+
const value = Number(event.target.value);
|
|
381
|
+
if (Number.isFinite(value)) setMatteShrink(value);
|
|
382
|
+
}}
|
|
383
|
+
/>
|
|
384
|
+
</div>
|
|
385
|
+
<div className="matte-slider">
|
|
386
|
+
<label htmlFor="matte-feather">{ko("Edge feather", "가장자리 부드럽게")}</label>
|
|
387
|
+
<input
|
|
388
|
+
id="matte-feather"
|
|
389
|
+
type="range"
|
|
390
|
+
min="0"
|
|
391
|
+
max="3"
|
|
392
|
+
step="0.5"
|
|
393
|
+
value={matteFeather}
|
|
394
|
+
onChange={(event) => setMatteFeather(Number(event.target.value))}
|
|
395
|
+
/>
|
|
396
|
+
<input
|
|
397
|
+
type="number"
|
|
398
|
+
data-field="matte-feather"
|
|
399
|
+
min="0"
|
|
400
|
+
max="3"
|
|
401
|
+
step="0.5"
|
|
402
|
+
value={matteFeather}
|
|
403
|
+
aria-label={ko("Edge feather", "가장자리 부드럽게")}
|
|
404
|
+
onChange={(event) => {
|
|
405
|
+
const value = Number(event.target.value);
|
|
406
|
+
if (Number.isFinite(value)) setMatteFeather(value);
|
|
407
|
+
}}
|
|
408
|
+
/>
|
|
409
|
+
</div>
|
|
410
|
+
</div>
|
|
411
|
+
<p className="inspector-hint">
|
|
412
|
+
{ko(
|
|
413
|
+
"Tolerance is how far a drag spreads: low keeps to one flat colour, high walks across a shaded wall. It applies to the next drag and to Auto-detect, not to what is already purple.",
|
|
414
|
+
"허용치는 드래그가 얼마나 번질지입니다. 낮으면 한 가지 색에 머무르고, 높으면 명암이 변하는 벽까지 따라갑니다. 이미 칠한 보라가 아니라 다음 드래그와 자동 인식에 적용됩니다.",
|
|
415
|
+
)}
|
|
416
|
+
</p>
|
|
417
|
+
<button
|
|
418
|
+
type="button"
|
|
419
|
+
className="btn ghost full"
|
|
420
|
+
onClick={() => {
|
|
421
|
+
const added = matteEditorRef.current?.autoDetect(matteTolerance) ?? 0;
|
|
422
|
+
if (!added) {
|
|
423
|
+
app.notify(
|
|
424
|
+
isKo
|
|
425
|
+
? "자동 인식이 더 칠할 곳을 찾지 못했어요 — 허용치를 높이거나 직접 칠하세요"
|
|
426
|
+
: "Auto-detect found nothing new to paint — raise the tolerance, or paint it by hand",
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
}}
|
|
430
|
+
>
|
|
431
|
+
{ko("Auto-detect background", "배경 자동 인식")}
|
|
432
|
+
</button>
|
|
433
|
+
<button
|
|
434
|
+
type="button"
|
|
435
|
+
className="btn primary full matte-apply"
|
|
436
|
+
disabled={matteBusy || !matteStats.painted}
|
|
437
|
+
onClick={() => run('object.matte', { objectId: selectedSceneObject.id })}
|
|
438
|
+
>
|
|
439
|
+
{matteBusy
|
|
440
|
+
? ko("Removing…", "지우는 중…")
|
|
441
|
+
: matteStats.painted
|
|
442
|
+
? isKo
|
|
443
|
+
? `보라색 부분 지우기 — 사진의 ${Math.round(matteStats.coverage * 100)}%`
|
|
444
|
+
: `Remove what is purple — ${Math.round(matteStats.coverage * 100)}% of the picture`
|
|
445
|
+
: ko("Nothing is marked yet", "아직 선택된 부분이 없습니다")}
|
|
446
|
+
</button>
|
|
447
|
+
<p className="inspector-hint">
|
|
448
|
+
{ko(
|
|
449
|
+
"Cut out grows the selection from wherever you drag; Bring back is the same growth fenced to what is already selected, so one drag returns a wrongly-cut region whole. Applying removes exactly what is purple and trims the empty margin — the card keeps the original photograph and this selection, so you can come back and change your mind.",
|
|
450
|
+
"누끼 따기는 드래그한 자리에서 선택 영역을 키우고, 되살리기는 그 성장을 이미 선택된 범위 안으로 가둔 것이라 잘못 잘린 부분이 드래그 한 번에 통째로 돌아옵니다. 적용하면 보라색 부분만 지우고 여백을 잘라냅니다 — 원본 사진과 지금 선택한 영역은 카드에 남아 있어 언제든 다시 열어 고칠 수 있습니다.",
|
|
451
|
+
)}
|
|
452
|
+
</p>
|
|
453
|
+
</>
|
|
454
|
+
)}
|
|
455
|
+
{selectedSceneObject.renderer !== CUTOUT_KIND && (selectedSceneObject.renderer !== MESH_KIND || selectedSceneObject.clay) && (
|
|
456
|
+
// One swatch shows the colour; the row opens only when you want
|
|
457
|
+
// to change it, instead of six chips sitting there all day.
|
|
458
|
+
<details className="object-colors-pop">
|
|
459
|
+
<summary
|
|
460
|
+
className="object-color current"
|
|
461
|
+
style={{ background: selectedSceneObject.color }}
|
|
462
|
+
aria-label={ko("Object colour", "오브젝트 색상")}
|
|
463
|
+
title={ko("Object colour", "오브젝트 색상")}
|
|
464
|
+
/>
|
|
465
|
+
{/* The displayed color while auto-color mode is on — the "hex"
|
|
466
|
+
made visible. Computed inline off the RAW object; the swatch
|
|
467
|
+
above keeps showing the authored color it returns to. */}
|
|
468
|
+
{autoColor && (
|
|
469
|
+
<span className="auto-color-hex">{ko("auto ", "자동 ")}{autoColorHex(selectedSceneObject.id)}</span>
|
|
470
|
+
)}
|
|
471
|
+
<div className="object-colors" role="group" aria-label={ko("Object colour", "오브젝트 색상")}>
|
|
472
|
+
{OBJECT_COLORS.map((color) => (
|
|
473
|
+
<button
|
|
474
|
+
type="button"
|
|
475
|
+
key={color}
|
|
476
|
+
className={"object-color" + (selectedSceneObject.color === color ? " active" : "")}
|
|
477
|
+
style={{ background: color }}
|
|
478
|
+
aria-label={isKo ? `색상 ${color}` : `Colour ${color}`}
|
|
479
|
+
aria-pressed={selectedSceneObject.color === color}
|
|
480
|
+
onClick={(event) => {
|
|
481
|
+
run(...updateArgs({ color }));
|
|
482
|
+
event.currentTarget.closest("details")?.removeAttribute("open");
|
|
483
|
+
}}
|
|
484
|
+
/>
|
|
485
|
+
))}
|
|
486
|
+
{/* Recently mixed tints, fenced off from the presets by a
|
|
487
|
+
rule: they are this browser's memory, not the palette,
|
|
488
|
+
and MCP's update_object can put any hex on a prop — an
|
|
489
|
+
author has to be able to reach the same colour back. */}
|
|
490
|
+
{recentObjectColors.length > 0 && <span className="object-colors-split" aria-hidden="true" />}
|
|
491
|
+
{recentObjectColors.map((color) => (
|
|
492
|
+
<button
|
|
493
|
+
type="button"
|
|
494
|
+
key={color}
|
|
495
|
+
className={"object-color" + (selectedSceneObject.color === color ? " active" : "")}
|
|
496
|
+
style={{ background: color }}
|
|
497
|
+
aria-label={isKo ? `최근 색상 ${color}` : `Recent colour ${color}`}
|
|
498
|
+
aria-pressed={selectedSceneObject.color === color}
|
|
499
|
+
onClick={(event) => {
|
|
500
|
+
run(...updateArgs({ color }));
|
|
501
|
+
rememberSceneObjectColor(color);
|
|
502
|
+
event.currentTarget.closest("details")?.removeAttribute("open");
|
|
503
|
+
}}
|
|
504
|
+
/>
|
|
505
|
+
))}
|
|
506
|
+
{/* The free colour: the native picker for choosing one, the
|
|
507
|
+
hex field for typing or reading back an exact value. Neither
|
|
508
|
+
closes the popover the way a preset does — a picker drag and
|
|
509
|
+
a half-typed hex both fire change after change, and a row
|
|
510
|
+
that vanished mid-edit would be unusable. */}
|
|
511
|
+
<input
|
|
512
|
+
type="color"
|
|
513
|
+
className="object-color object-color-free"
|
|
514
|
+
value={normalizeObjectColor(selectedSceneObject.color) ?? "#ffffff"}
|
|
515
|
+
title={ko("Custom colour", "직접 고른 색상")}
|
|
516
|
+
aria-label={ko("Custom colour", "직접 고른 색상")}
|
|
517
|
+
onChange={(event) => {
|
|
518
|
+
const color = normalizeObjectColor(event.target.value);
|
|
519
|
+
if (!color) return;
|
|
520
|
+
run(...updateArgs({ color }, begin('color')));
|
|
521
|
+
rememberSceneObjectColor(color);
|
|
522
|
+
}}
|
|
523
|
+
onBlur={() => finish()} onKeyDown={endKey}
|
|
524
|
+
/>
|
|
525
|
+
<input
|
|
526
|
+
type="text"
|
|
527
|
+
className="object-color-hex"
|
|
528
|
+
// Idle, the field IS the object's colour — including one an
|
|
529
|
+
// agent set through MCP's update_object, which the palette
|
|
530
|
+
// alone could never show. Mid-edit the draft takes over.
|
|
531
|
+
value={objectColorDraft ?? selectedSceneObject.color}
|
|
532
|
+
spellCheck={false}
|
|
533
|
+
maxLength={7}
|
|
534
|
+
placeholder="#rrggbb"
|
|
535
|
+
title={ko("Colour hex", "색상 hex")}
|
|
536
|
+
aria-label={ko("Colour hex", "색상 hex")}
|
|
537
|
+
onChange={(event) => {
|
|
538
|
+
setObjectColorDraft(event.target.value);
|
|
539
|
+
const color = normalizeObjectColor(event.target.value);
|
|
540
|
+
if (!color) return; // a typo is a keystroke, not a repaint
|
|
541
|
+
run(...updateArgs({ color }, begin('hex')));
|
|
542
|
+
rememberSceneObjectColor(color);
|
|
543
|
+
}}
|
|
544
|
+
onBlur={() => { finish(); setObjectColorDraft(null); }}
|
|
545
|
+
onKeyDown={endKey}
|
|
546
|
+
/>
|
|
547
|
+
</div>
|
|
548
|
+
</details>
|
|
549
|
+
)}
|
|
550
|
+
</>
|
|
551
|
+
)}
|
|
552
|
+
</Foldout>
|
|
553
|
+
);
|
|
554
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import Foldout from "./Foldout.jsx";
|
|
2
|
+
import { ko, isKo } from "../locale.js";
|
|
3
|
+
import { I2vMotionCaptureCard } from "../i2v-motion-studio.jsx";
|
|
4
|
+
import { PoseTileGrid } from "../posestudio.jsx";
|
|
5
|
+
import { DEFAULT_POSE } from "../poses.js";
|
|
6
|
+
import { poseLabelKo } from "../app-stage.jsx";
|
|
7
|
+
import ReferenceImageField from "./ReferenceImageField.jsx";
|
|
8
|
+
import { useBus } from '../app-context.js';
|
|
9
|
+
import { trackFeature } from "../analytics.js";
|
|
10
|
+
|
|
11
|
+
export default function PosePanel({
|
|
12
|
+
isCharacterSelection, activeCharIndex, i2vMotionModel, i2vMotionActions, setI2vMotionStudioOpen,
|
|
13
|
+
selectablePoses, activeChar, ikMode, ikApplyPoseAsKey, motion,
|
|
14
|
+
setStudioPick, setToast, removePose, setPhotoPoseError, photoPoseFileRef,
|
|
15
|
+
photoPoseState, photoPoseError, activeRig, saveCurrentPose,
|
|
16
|
+
}) {
|
|
17
|
+
const { run } = useBus();
|
|
18
|
+
return (
|
|
19
|
+
<Foldout hidden={!isCharacterSelection} defaultOpen={false} title={ko("Pose", "포즈")}>
|
|
20
|
+
{/* Tiles, not a dropdown: a pose read out of a photograph has no
|
|
21
|
+
name worth reading — it is recognisable only as a shape. This
|
|
22
|
+
is the same grid the studio shows, applied to whichever
|
|
23
|
+
character the hierarchy has selected. */}
|
|
24
|
+
<p className="inspector-hint v2-details-hint">
|
|
25
|
+
{isKo ? `인물 ${activeCharIndex + 1}의 자세입니다.` : `The pose on Subject ${activeCharIndex + 1}.`}
|
|
26
|
+
</p>
|
|
27
|
+
<I2vMotionCaptureCard model={i2vMotionModel} actions={i2vMotionActions} onOpen={() => {
|
|
28
|
+
// While the route is locked its Generate button is disabled, so opening
|
|
29
|
+
// the authoring modal is the card's demand signal (#466).
|
|
30
|
+
trackFeature("fal_motion_open");
|
|
31
|
+
setI2vMotionStudioOpen(true);
|
|
32
|
+
}} />
|
|
33
|
+
<PoseTileGrid
|
|
34
|
+
poses={selectablePoses}
|
|
35
|
+
model={activeChar.model}
|
|
36
|
+
selectedId={(activeChar.pose ?? DEFAULT_POSE)?.id}
|
|
37
|
+
onSelect={(id) => {
|
|
38
|
+
const pose = selectablePoses.find((entry) => entry.id === id);
|
|
39
|
+
if (!pose) return;
|
|
40
|
+
// IK mode over a take: the pick is a mid-clip correction, so it
|
|
41
|
+
// keys onto the Full-Body lane instead of erasing the motion.
|
|
42
|
+
if (ikMode && ikApplyPoseAsKey(pose)) return;
|
|
43
|
+
// A running take drives the same bones a pose writes, so the
|
|
44
|
+
// pick would otherwise land invisibly underneath it.
|
|
45
|
+
const hadMotion = Boolean(motion);
|
|
46
|
+
if (!run('character.setPose', { characterId: activeChar.id, pose: pose.id, clearMotion: hadMotion }).ok) return;
|
|
47
|
+
setStudioPick(pose.id);
|
|
48
|
+
setToast(hadMotion
|
|
49
|
+
? ko("Cleared the current motion and applied the pose", "현재 모션을 지우고 포즈를 적용했어요")
|
|
50
|
+
: ko("Pose applied", "포즈를 적용했어요"));
|
|
51
|
+
}}
|
|
52
|
+
onDelete={removePose}
|
|
53
|
+
onPhoto={() => {
|
|
54
|
+
setPhotoPoseError("");
|
|
55
|
+
photoPoseFileRef.current?.click();
|
|
56
|
+
}}
|
|
57
|
+
photoState={photoPoseState}
|
|
58
|
+
labelOf={poseLabelKo}
|
|
59
|
+
/>
|
|
60
|
+
{photoPoseError && <p className="studio-hint error" data-pose-photo-error role="status">{photoPoseError}</p>}
|
|
61
|
+
{/* Bottles whatever the viewport shows right now — a take frame,
|
|
62
|
+
IK corrections included — without touching the character, so a
|
|
63
|
+
good mid-clip moment becomes a reusable library pose. */}
|
|
64
|
+
<button
|
|
65
|
+
type="button"
|
|
66
|
+
className="btn full v2-details-pose-action"
|
|
67
|
+
data-save-current-pose
|
|
68
|
+
disabled={!activeRig}
|
|
69
|
+
title={ko(
|
|
70
|
+
"Save the pose the character is in right now — with a motion loaded, that is the current frame plus IK corrections",
|
|
71
|
+
"캐릭터의 지금 자세를 저장해요 — 모션이 실려 있으면 현재 프레임에 IK 보정까지 합친 자세예요",
|
|
72
|
+
)}
|
|
73
|
+
onClick={saveCurrentPose}
|
|
74
|
+
>
|
|
75
|
+
{ko("Save current pose", "지금 자세 저장")}
|
|
76
|
+
</button>
|
|
77
|
+
{/* Identity sits beside "Pose from photo" on purpose: both take a
|
|
78
|
+
picture of a person, but that one reads a SHAPE off it while
|
|
79
|
+
this one keeps the picture itself as who the character is. */}
|
|
80
|
+
<ReferenceImageField
|
|
81
|
+
label={ko("Identity image", "인물 이미지")}
|
|
82
|
+
hint={ko(
|
|
83
|
+
"A character sheet or photo of this person. It travels with every framing capture so a render keeps the same face, hair and wardrobe.",
|
|
84
|
+
"이 인물의 캐릭터 시트나 사진입니다. 모든 프레이밍 캐프처에 함께 실려 얼굴·머리·의상을 유지합니다.",
|
|
85
|
+
)}
|
|
86
|
+
value={activeChar.identityImage ?? null}
|
|
87
|
+
alt={ko("Identity reference", "인물 참고 이미지")}
|
|
88
|
+
inputProps={{ "data-identity-image-input": "" }}
|
|
89
|
+
onPick={(dataUrl) => {
|
|
90
|
+
run('character.update', { characterId: activeChar.id, patch: { identityImage: dataUrl } });
|
|
91
|
+
setToast(ko("Identity image set", "인물 이미지를 설정했어요"));
|
|
92
|
+
}}
|
|
93
|
+
onClear={() => run('character.update', { characterId: activeChar.id, patch: { identityImage: null } })}
|
|
94
|
+
/>
|
|
95
|
+
</Foldout>
|
|
96
|
+
);
|
|
97
|
+
}
|