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,78 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import Foldout from "./Foldout.jsx";
|
|
3
|
+
import { useStudioShell } from "../shell/studio-shell-context.js";
|
|
4
|
+
import { useMotionCommands } from "../domains/motion.js";
|
|
5
|
+
import { ko, isKo } from "../locale.js";
|
|
6
|
+
import { HIERARCHY_INSPECTOR_TITLES } from "../app-stage.jsx";
|
|
7
|
+
import { PhysicsPanel } from "../ardy/physics-panel.jsx";
|
|
8
|
+
import { Field } from "../ui.jsx";
|
|
9
|
+
import { MotionReadiness } from "../motion-readiness-ui.jsx";
|
|
10
|
+
import { RangePinPanel } from "../range-pin-panel.jsx";
|
|
11
|
+
import { PlatformFitPanel } from "../ardy/platform-fit-panel.jsx";
|
|
12
|
+
import "../ardy/auto-fix-panel.css";
|
|
13
|
+
import "./pose.css";
|
|
14
|
+
|
|
15
|
+
export default function RigControlPanel({
|
|
16
|
+
isRigSelection, rigSelection, ikChains, ikFocus, footSnap, ikMode,
|
|
17
|
+
collisionCleanupSupported, motion, autoPhysicsRunning, physicsProgress, physicsPreview,
|
|
18
|
+
physicsShow, physicsOptions, tlFrame, changePhysicsOptions, runAutoPhysics, showPhysicsPreview,
|
|
19
|
+
applyPhysicsPreview, cancelPhysicsPreview, setTlFrame, platformFitRunning, platformFitProgress,
|
|
20
|
+
platformFitLast, platformFitApplied, ikEditTool, setIkEditTool, showTrails, setShowTrails,
|
|
21
|
+
trailFalloffS, setTrailFalloffS, trailEdit, generationBusy, bridgeChecking, bridge,
|
|
22
|
+
runTrailRegeneration, trailReadinessState, openMotionSetup, recheckMotionHealth,
|
|
23
|
+
rangePins = [], rangePinResiduals = new Map(), rangePinSelection = null, rangePinPartPick = null,
|
|
24
|
+
rangePinPreview = null, objects = [], setRangePinSelection, setRangePinPartPick,
|
|
25
|
+
previewRangePinDraft, applyRangePinDraft, deleteRangePin,
|
|
26
|
+
}) {
|
|
27
|
+
const { run } = useMotionCommands();
|
|
28
|
+
const shell = useStudioShell();
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const preview = platformFitLast && !platformFitApplied && platformFitLast.candidate;
|
|
31
|
+
const physics = physicsPreview && !preview;
|
|
32
|
+
if (preview) {
|
|
33
|
+
shell?.setStatusText?.(ko("Platform fit preview — review before applying", "발판 맞춤 미리보기 — 적용 전에 확인하세요"));
|
|
34
|
+
const next = (platformFitLast.steps ?? []).map((step) => ({ start: step.start, end: step.end, kind: "body" }));
|
|
35
|
+
shell?.setPreviewRanges?.((current) => JSON.stringify(current) === JSON.stringify(next) ? current : next);
|
|
36
|
+
} else if (physics) {
|
|
37
|
+
shell?.setStatusText?.(ko("Physics cleanup preview — review before applying", "물리 정리 미리보기 — 적용 전에 확인하세요"));
|
|
38
|
+
const next = (physicsPreview.changedFrames ?? []).map((frame) => ({ start: frame, end: frame, kind: "body" }));
|
|
39
|
+
shell?.setPreviewRanges?.((current) => JSON.stringify(current) === JSON.stringify(next) ? current : next);
|
|
40
|
+
} else if (!platformFitRunning) {
|
|
41
|
+
shell?.setStatusText?.(""); shell?.setPreviewRanges?.((current) => current.length ? [] : current);
|
|
42
|
+
}
|
|
43
|
+
}, [platformFitLast, platformFitApplied, platformFitRunning, physicsPreview, shell]);
|
|
44
|
+
const activeTool = ikEditTool === "trail" ? "Path fix" : ikEditTool === "pin" ? "Pin" : "IK";
|
|
45
|
+
return <Foldout hidden={!isRigSelection} title={ko("Pose", "포즈")}>
|
|
46
|
+
<div className="pose-details" data-testid="pose-details">
|
|
47
|
+
<section className="pose-section">
|
|
48
|
+
<h4>{ko("Pose", "포즈")}</h4>
|
|
49
|
+
<div className="pose-row"><span>{ko("Foot lock", "발 고정")}</span><b>{footSnap ? ko("On", "켜짐") : ko("Off", "꺼짐")}</b></div>
|
|
50
|
+
<div className="pose-row"><span>{ko("Body contact", "몸 접촉")}</span><b>{ikChains ? ko("Ready", "준비됨") : ko("Unavailable", "사용 불가")}</b></div>
|
|
51
|
+
<div className="pose-row"><span>{ko("Influence", "영향")}</span><b>{ikFocus ?? ko("All", "전체")}</b></div>
|
|
52
|
+
<p className="inspector-hint">{rigSelection && rigSelection.token !== "rig" ? (isKo ? `${HIERARCHY_INSPECTOR_TITLES[rigSelection.token]}이 활성 제어 그룹입니다.` : `${HIERARCHY_INSPECTOR_TITLES[rigSelection.token]} is the active control group.`) : ko("Select a body group to pose it in the viewport.", "몸 그룹을 선택해 뷰포트에서 포즈를 잡으세요.")}</p>
|
|
53
|
+
</section>
|
|
54
|
+
{ikMode && <section className="pose-section" data-testid="pose-active-tool">
|
|
55
|
+
<h4>{ko("Active tool", "활성 도구")} · {ko(activeTool, activeTool === "Path fix" ? "경로 수정" : activeTool === "Pin" ? "고정" : "IK")}</h4>
|
|
56
|
+
<div className="pose-actions">
|
|
57
|
+
<button type="button" className={"btn" + (ikEditTool === "ik" ? " primary" : "")} aria-pressed={ikEditTool === "ik"} onClick={() => setIkEditTool("ik")}>{ko("IK", "IK")}</button>
|
|
58
|
+
<button type="button" className={"btn" + (ikEditTool === "trail" ? " primary" : "")} aria-pressed={ikEditTool === "trail"} disabled={!showTrails} onClick={() => setIkEditTool("trail")}>{ko("Path fix", "경로 수정")}</button>
|
|
59
|
+
<button type="button" data-testid="range-pin-tool" className={"btn" + (ikEditTool === "pin" ? " primary" : "")} aria-pressed={ikEditTool === "pin"} disabled={!motion} onClick={() => { setIkEditTool("pin"); setRangePinPartPick?.(null); }}>{ko("Pin", "고정")}</button>
|
|
60
|
+
</div>
|
|
61
|
+
{ikEditTool === "pin" && <RangePinPanel active motion={motion} frame={tlFrame} frameCount={motion?.frames ?? 1} fps={motion?.fps ?? 24} pins={rangePins} residuals={rangePinResiduals} objects={objects} selectedPinId={rangePinSelection} partPick={rangePinPartPick} conflictFrames={rangePinPreview?.conflictFrames ?? []} overlapError={rangePinPreview?.overlapError ?? ""} onSelectPin={(id) => { setRangePinSelection?.(id); if (id) setIkEditTool("pin"); }} onApply={applyRangePinDraft} onCancel={() => { setRangePinSelection?.(null); setIkEditTool("ik"); }} onDelete={deleteRangePin} onPreviewTarget={previewRangePinDraft} />}
|
|
62
|
+
{ikEditTool === "trail" && <>
|
|
63
|
+
<button type="button" className="btn full" aria-pressed={showTrails} onClick={() => setShowTrails((value) => !value)}>{ko(`Trails ${showTrails ? "on" : "off"}`, `궤적선 ${showTrails ? "표시" : "숨김"}`)}</button>
|
|
64
|
+
<Field label={ko("Falloff", "영향 범위")}><div className="trail-falloff-row"><input type="range" min={0.1} max={2} step={0.1} value={trailFalloffS} onChange={(event) => setTrailFalloffS(Number(event.target.value))} /><span className="trail-falloff-value">{trailFalloffS.toFixed(1)}s</span></div></Field>
|
|
65
|
+
<button type="button" className="btn primary full" disabled={!trailEdit || !motion?.url || generationBusy || bridgeChecking || bridge === null} onClick={runTrailRegeneration}>{ko("Regenerate", "재생성")}</button>
|
|
66
|
+
<MotionReadiness state={trailReadinessState} checking={bridgeChecking} onSetup={() => openMotionSetup("trail")} onRetry={recheckMotionHealth} />
|
|
67
|
+
</>}
|
|
68
|
+
{ikEditTool === "ik" && <p className="inspector-hint">{ko("Grab a hand, foot, elbow or knee handle in the viewport.", "뷰포트에서 손·발·팔꿈치·무릎 핸들을 잡으세요.")}</p>}
|
|
69
|
+
</section>}
|
|
70
|
+
<section className="pose-section" data-testid="pose-auto-fix">
|
|
71
|
+
<h4>{ko("Auto-fix", "자동 수정")}</h4>
|
|
72
|
+
{collisionCleanupSupported && <div className="pose-actions"><button type="button" className="btn" disabled={!ikChains} onClick={() => run("motion.fixCollisions", { scope: "frame" })}>{ko("Body collisions · Frame", "신체 충돌 · 프레임")}</button><button type="button" className="btn" disabled={!ikChains || !motion} onClick={() => run("motion.fixCollisions", { scope: "clip" })}>{ko("Body collisions · Clip", "신체 충돌 · 클립")}</button></div>}
|
|
73
|
+
<PlatformFitPanel ko={ko} disabled={!ikChains || !motion || autoPhysicsRunning} running={platformFitRunning} progress={platformFitProgress} last={platformFitLast} applied={platformFitApplied} onRun={() => run("motion.platformFit.run", { preview: true })} onApply={() => run("motion.platformFit.remove", { apply: true })} onCancel={() => run("motion.platformFit.remove")} onRemove={() => run("motion.platformFit.remove")} onFrame={setTlFrame} />
|
|
74
|
+
<PhysicsPanel ko={ko} disabled={!ikChains || !motion} running={autoPhysicsRunning} progress={physicsProgress} preview={physicsPreview} show={physicsShow} options={physicsOptions} frame={tlFrame} frames={motion?.frames ?? 1} onOptions={changePhysicsOptions} onRun={runAutoPhysics} onShow={showPhysicsPreview} onApply={applyPhysicsPreview} onCancel={cancelPhysicsPreview} onFrame={setTlFrame} />
|
|
75
|
+
</section>
|
|
76
|
+
</div>
|
|
77
|
+
</Foldout>;
|
|
78
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import Foldout from "./Foldout.jsx";
|
|
2
|
+
import { ko } from "../locale.js";
|
|
3
|
+
import { CHARACTER_MODEL_IDS } from "../scenes.js";
|
|
4
|
+
import { PoseThumbPreview } from "../posestudio.jsx";
|
|
5
|
+
import { DEFAULT_POSE } from "../poses.js";
|
|
6
|
+
import { CHARACTER_MODEL_LABELS } from "../app-stage.jsx";
|
|
7
|
+
import { useBus } from '../app-context.js';
|
|
8
|
+
|
|
9
|
+
export default function RigPanel({ isCharacterSelection, activeChar }) {
|
|
10
|
+
const { run } = useBus();
|
|
11
|
+
return (
|
|
12
|
+
<Foldout hidden={!isCharacterSelection} defaultOpen={false} title={ko("Rig", "리그")}>
|
|
13
|
+
{/* The rig is a property of the character, and swapping it is a
|
|
14
|
+
look decision made while blocking — so it belongs beside the
|
|
15
|
+
subject, not buried in the project file. */}
|
|
16
|
+
<div className="rig-picker v2-details-rig-picker" role="radiogroup" aria-label={ko("Character rig", "캐릭터 리그")}>
|
|
17
|
+
{CHARACTER_MODEL_IDS.map((id) => (
|
|
18
|
+
<button
|
|
19
|
+
type="button"
|
|
20
|
+
key={id}
|
|
21
|
+
role="radio"
|
|
22
|
+
aria-checked={activeChar.model === id}
|
|
23
|
+
className={"rig-option" + (activeChar.model === id ? " active" : "")}
|
|
24
|
+
data-rig-id={id}
|
|
25
|
+
onClick={() => {
|
|
26
|
+
if (activeChar.model === id) return;
|
|
27
|
+
run('character.update', { characterId: activeChar.id, patch: { model: id } });
|
|
28
|
+
}}
|
|
29
|
+
>
|
|
30
|
+
<PoseThumbPreview model={id} pose={activeChar.pose ?? DEFAULT_POSE} alt={CHARACTER_MODEL_LABELS[id]} />
|
|
31
|
+
<span>{CHARACTER_MODEL_LABELS[id]}</span>
|
|
32
|
+
</button>
|
|
33
|
+
))}
|
|
34
|
+
</div>
|
|
35
|
+
</Foldout>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { ko, isKo } from "../locale.js";
|
|
2
|
+
|
|
3
|
+
export default function SubjectBox({ label, value, onChange, onRemove, onPose, posing, color, onColorChange, onColorEditStart }) {
|
|
4
|
+
return (
|
|
5
|
+
<div className="subject-box v2-subject-box">
|
|
6
|
+
<div className="subject-box-head">
|
|
7
|
+
<span className="sb-name">{label}</span>
|
|
8
|
+
<div className="sb-actions">
|
|
9
|
+
{onColorChange && (
|
|
10
|
+
<input
|
|
11
|
+
type="color"
|
|
12
|
+
className="sb-color"
|
|
13
|
+
title={ko("Character color", "인물 색상")}
|
|
14
|
+
aria-label={ko("Character color", "인물 색상")}
|
|
15
|
+
value={color}
|
|
16
|
+
/* Focus opens the session for keyboard/eyedropper use; the
|
|
17
|
+
native swatch dialog can drive onChange without focus, so the
|
|
18
|
+
first change of a session opens it too (the handler is
|
|
19
|
+
session-idempotent). */
|
|
20
|
+
onFocus={onColorEditStart}
|
|
21
|
+
onChange={(e) => {
|
|
22
|
+
onColorEditStart?.();
|
|
23
|
+
onColorChange(e.target.value);
|
|
24
|
+
}}
|
|
25
|
+
/>
|
|
26
|
+
)}
|
|
27
|
+
{onPose && (
|
|
28
|
+
<button
|
|
29
|
+
type="button"
|
|
30
|
+
className={"cam-toggle" + (posing ? " active" : "")}
|
|
31
|
+
aria-label={isKo ? `${label} 포즈 열기` : `Open pose studio for ${label}`}
|
|
32
|
+
title={isKo ? `${label} 포즈` : `Pose ${label}`}
|
|
33
|
+
onClick={onPose}
|
|
34
|
+
>
|
|
35
|
+
⌘
|
|
36
|
+
</button>
|
|
37
|
+
)}
|
|
38
|
+
{onRemove && (
|
|
39
|
+
<button type="button" className="sb-remove" title={ko("Remove subject", "인물 제거")} onClick={onRemove}>
|
|
40
|
+
✕
|
|
41
|
+
</button>
|
|
42
|
+
)}
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import Foldout from "./Foldout.jsx";
|
|
2
|
+
import { ko } from "../locale.js";
|
|
3
|
+
import { defaultCharacterTint } from "../app-stage.jsx";
|
|
4
|
+
import SubjectBox from "./SubjectBox.jsx";
|
|
5
|
+
import { useCastTransaction } from '../domains/cast.js';
|
|
6
|
+
|
|
7
|
+
export default function SubjectsPanel({
|
|
8
|
+
isCharacterSelection, showB, characters, openStudio, posing,
|
|
9
|
+
}) {
|
|
10
|
+
const { run, begin, commit, cancel } = useCastTransaction();
|
|
11
|
+
return (
|
|
12
|
+
<Foldout hidden={!isCharacterSelection} title={showB ? ko("Subjects", "인물들") : ko("Subject", "인물")}>
|
|
13
|
+
<div className={"subjects-row v2-details-subjects" + (showB ? "" : " single")} onBlur={commit} onPointerUp={commit} onPointerCancel={cancel}>
|
|
14
|
+
{characters.map((entry, index) => entry.hidden ? null : (
|
|
15
|
+
<SubjectBox
|
|
16
|
+
key={entry.id}
|
|
17
|
+
label={ko(`Subject ${index + 1}`, `인물 ${index + 1}`)}
|
|
18
|
+
value={entry}
|
|
19
|
+
onChange={(patch) => run('character.update', { characterId: entry.id, patch })}
|
|
20
|
+
onPose={() => openStudio(entry.id)}
|
|
21
|
+
posing={posing === entry.id}
|
|
22
|
+
onRemove={index > 0 ? () => run('character.remove', { characterId: entry.id }) : undefined}
|
|
23
|
+
color={entry.tint ?? defaultCharacterTint(entry, index)}
|
|
24
|
+
/* A colour picker streams values while it is open, so the
|
|
25
|
+
whole picking session is one Ctrl+Z entry. */
|
|
26
|
+
onColorEditStart={begin}
|
|
27
|
+
onColorChange={(tint) => run('character.update', { characterId: entry.id, patch: { tint } })}
|
|
28
|
+
/>
|
|
29
|
+
))}
|
|
30
|
+
</div>
|
|
31
|
+
{!showB && (
|
|
32
|
+
<button type="button" className="add-subject" onClick={() => run('cast.showExtras', { show: true })}>
|
|
33
|
+
<span className="as-plus">+</span>
|
|
34
|
+
<span>{ko("Add second subject", "두 번째 인물 추가")}</span>
|
|
35
|
+
</button>
|
|
36
|
+
)}
|
|
37
|
+
</Foldout>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import Foldout from "./Foldout.jsx";
|
|
2
|
+
import { ko, isKo } from "../locale.js";
|
|
3
|
+
import { footageSummary, trajectoryReceipt, segmentationReceipt } from "../multimodel-ingest.js";
|
|
4
|
+
|
|
5
|
+
export default function VideoCapturePanel({
|
|
6
|
+
isCharacterSelection, multiModelStatus, multiModelStage, multiModelFileRef, chooseMultiModelFile,
|
|
7
|
+
multiModelSource, multiModelUrl, setMultiModelUrl, useMultiModelUrl, pasteMultiModelUrl,
|
|
8
|
+
multiModelProgress, multiModelError, multiModelFootage, extractMultiModelMotion, multiModelExtract,
|
|
9
|
+
multiModelTake, multiModelExtractProgress, multiModelExtractError, activeChar, bridge,
|
|
10
|
+
}) {
|
|
11
|
+
return (
|
|
12
|
+
<Foldout hidden={!isCharacterSelection} defaultOpen={false} title={ko("Video capture", "영상 모캡")}>
|
|
13
|
+
<div className="multimodel-card v2-details-video-card">
|
|
14
|
+
<div className="multimodel-card-head">
|
|
15
|
+
<div>
|
|
16
|
+
<strong>{ko("Footage → motion", "영상 → 모션")}</strong>
|
|
17
|
+
<span>{ko("Prepare a source inside the Motion workspace.", "모션 작업공간에서 입력 영상을 준비하세요.")}</span>
|
|
18
|
+
</div>
|
|
19
|
+
<span className={"multimodel-status " + multiModelStatus}>
|
|
20
|
+
{multiModelStatus === "ready"
|
|
21
|
+
? ko("READY", "준비됨")
|
|
22
|
+
: multiModelStatus === "error"
|
|
23
|
+
? ko("CHECK", "확인 필요")
|
|
24
|
+
: multiModelStatus === "busy"
|
|
25
|
+
? (multiModelStage === "fetching" ? ko("FETCHING", "받는 중") : ko("PROBING", "분석 중"))
|
|
26
|
+
: ko("IDLE", "대기")}
|
|
27
|
+
</span>
|
|
28
|
+
</div>
|
|
29
|
+
<div className="multimodel-input-block">
|
|
30
|
+
<div className="multimodel-input-label">
|
|
31
|
+
<strong>{ko("Local video file", "로컬 비디오 파일")}</strong>
|
|
32
|
+
<span>{ko("Upload from this computer", "이 컴퓨터에서 업로드")}</span>
|
|
33
|
+
</div>
|
|
34
|
+
<div className="multimodel-source-row">
|
|
35
|
+
<button type="button" className="btn ghost" onClick={() => multiModelFileRef.current?.click()}>
|
|
36
|
+
{ko("Choose video", "영상 선택")}
|
|
37
|
+
</button>
|
|
38
|
+
<input ref={multiModelFileRef} className="multimodel-file-input" type="file" accept="video/*" onChange={chooseMultiModelFile} />
|
|
39
|
+
<span className="multimodel-file-name">{multiModelSource?.kind === "file" ? multiModelSource.name : ko("No file selected", "파일이 선택되지 않음")}</span>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
<div className="multimodel-input-block">
|
|
43
|
+
<div className="multimodel-input-label">
|
|
44
|
+
<strong>{ko("Video URL", "비디오 URL")}</strong>
|
|
45
|
+
<span>{ko("Use a hosted or local route", "호스팅 또는 로컬 경로 사용")}</span>
|
|
46
|
+
</div>
|
|
47
|
+
<input
|
|
48
|
+
className="multimodel-url-input"
|
|
49
|
+
type="text"
|
|
50
|
+
value={multiModelUrl}
|
|
51
|
+
onChange={(event) => setMultiModelUrl(event.target.value)}
|
|
52
|
+
onKeyDown={(event) => { if (event.key === "Enter") useMultiModelUrl(); }}
|
|
53
|
+
placeholder={ko("https://…/boxing.mp4", "https://…/boxing.mp4")}
|
|
54
|
+
aria-label={ko("Multi-Model video URL", "멀티 모델 영상 URL")}
|
|
55
|
+
spellCheck={false}
|
|
56
|
+
/>
|
|
57
|
+
<div className="multimodel-url-actions">
|
|
58
|
+
<button type="button" className="btn ghost" onClick={pasteMultiModelUrl}>
|
|
59
|
+
{ko("Paste", "붙여넣기")}
|
|
60
|
+
</button>
|
|
61
|
+
<button type="button" className="btn ghost" onClick={() => setMultiModelUrl("")} disabled={!multiModelUrl}>
|
|
62
|
+
{ko("Clear", "지우기")}
|
|
63
|
+
</button>
|
|
64
|
+
<button type="button" className="btn primary" onClick={useMultiModelUrl} disabled={!multiModelUrl.trim()}>
|
|
65
|
+
{ko("Use URL", "URL 사용")}
|
|
66
|
+
</button>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
{multiModelStatus === "busy" && (
|
|
70
|
+
<div className="multimodel-progress">
|
|
71
|
+
<div className="multimodel-progress-track">
|
|
72
|
+
<div
|
|
73
|
+
className={"multimodel-progress-bar" + (multiModelProgress === null ? " indeterminate" : "")}
|
|
74
|
+
style={multiModelProgress === null ? undefined : { width: `${Math.round(multiModelProgress * 100)}%` }}
|
|
75
|
+
/>
|
|
76
|
+
</div>
|
|
77
|
+
<span>
|
|
78
|
+
{multiModelStage === "fetching"
|
|
79
|
+
? (multiModelProgress === null
|
|
80
|
+
? ko("Downloading…", "다운로드 중…")
|
|
81
|
+
: `${Math.round(multiModelProgress * 100)}%`)
|
|
82
|
+
: ko("Decoding…", "디코딩 중…")}
|
|
83
|
+
</span>
|
|
84
|
+
</div>
|
|
85
|
+
)}
|
|
86
|
+
{multiModelError && <p className="multimodel-error">{multiModelError}</p>}
|
|
87
|
+
{multiModelFootage && (
|
|
88
|
+
<div className="multimodel-receipt">
|
|
89
|
+
<video className="multimodel-preview" src={multiModelFootage.objectUrl} muted playsInline preload="metadata" />
|
|
90
|
+
<div className="multimodel-receipt-body">
|
|
91
|
+
<strong>{multiModelSource?.name}</strong>
|
|
92
|
+
<span>{footageSummary(multiModelFootage)}</span>
|
|
93
|
+
<span className="multimodel-receipt-timeline">
|
|
94
|
+
{isKo
|
|
95
|
+
? `타임라인 0–${multiModelFootage.frames - 1} 프레임으로 맞춤`
|
|
96
|
+
: `Timeline sized to frames 0–${multiModelFootage.frames - 1}`}
|
|
97
|
+
</span>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
)}
|
|
101
|
+
{multiModelFootage && (
|
|
102
|
+
<div className="multimodel-extract">
|
|
103
|
+
<button
|
|
104
|
+
type="button"
|
|
105
|
+
className="btn primary"
|
|
106
|
+
onClick={extractMultiModelMotion}
|
|
107
|
+
disabled={multiModelExtract === "running"}
|
|
108
|
+
>
|
|
109
|
+
{multiModelExtract === "running"
|
|
110
|
+
? ko("Extracting…", "추출 중…")
|
|
111
|
+
: multiModelTake
|
|
112
|
+
? ko("Extract again", "다시 추출")
|
|
113
|
+
: ko("Extract motion", "모션 추출")}
|
|
114
|
+
</button>
|
|
115
|
+
{multiModelExtract === "running" && (
|
|
116
|
+
<div className="multimodel-progress">
|
|
117
|
+
<div className="multimodel-progress-track">
|
|
118
|
+
<div
|
|
119
|
+
className={"multimodel-progress-bar" + (multiModelExtractProgress === null ? " indeterminate" : "")}
|
|
120
|
+
style={multiModelExtractProgress === null ? undefined : { width: `${Math.round(multiModelExtractProgress * 100)}%` }}
|
|
121
|
+
/>
|
|
122
|
+
</div>
|
|
123
|
+
<span>
|
|
124
|
+
{multiModelExtractProgress === null
|
|
125
|
+
? ko("Engine…", "엔진 준비…")
|
|
126
|
+
: `${Math.round(multiModelExtractProgress * 100)}%`}
|
|
127
|
+
</span>
|
|
128
|
+
</div>
|
|
129
|
+
)}
|
|
130
|
+
{multiModelExtract === "error" && <p className="multimodel-error">{multiModelExtractError}</p>}
|
|
131
|
+
{multiModelTake && (
|
|
132
|
+
<p className="multimodel-extract-receipt">
|
|
133
|
+
{multiModelTake.gpu
|
|
134
|
+
? (isKo
|
|
135
|
+
? `GVHMR 테이크 ${multiModelTake.frames}프레임 추출됨 — 타임라인에서 재생하세요`
|
|
136
|
+
: `GVHMR take extracted, ${multiModelTake.frames} frames — press play on the timeline`)
|
|
137
|
+
: (isKo
|
|
138
|
+
? `${multiModelTake.frames}프레임 테이크 구움 (실측 ${multiModelTake.fitted} · 유지 ${multiModelTake.held}) — 타임라인에서 재생하세요`
|
|
139
|
+
: `Baked a ${multiModelTake.frames}-frame take (${multiModelTake.fitted} measured · ${multiModelTake.held} held) — press play on the timeline`)}
|
|
140
|
+
</p>
|
|
141
|
+
)}
|
|
142
|
+
{multiModelTake?.trajectory && <p className="multimodel-note" data-testid="trajectory-receipt">{trajectoryReceipt(multiModelTake.trajectory, isKo)}</p>}
|
|
143
|
+
{multiModelTake?.segmentation && <p className="multimodel-note" data-testid="segmentation-receipt">{segmentationReceipt(multiModelTake.segmentation, isKo)}</p>}
|
|
144
|
+
{multiModelTake?.quality && <p className="multimodel-note" data-testid="mocap-quality-receipt">
|
|
145
|
+
{multiModelTake.quality.pass
|
|
146
|
+
? ko("Mocap quality gate passed", "모캡 품질 게이트 통과")
|
|
147
|
+
: ko(`Mocap quality warning: ${multiModelTake.quality.checks?.filter((check) => !check.pass).map((check) => check.name).join(", ") || "review required"}`, `모캡 품질 게이트 경고: ${multiModelTake.quality.checks?.filter((check) => !check.pass).map((check) => check.name).join(", ") || "확인 필요"}`)}
|
|
148
|
+
</p>}
|
|
149
|
+
{multiModelTake?.gpu && Math.abs(activeChar.y ?? 0) > .001 && <p className="multimodel-note" data-testid="trajectory-stage-offset">{isKo ? `씬 높이 ${(activeChar.y * 100).toFixed(1)}cm가 모션에 추가돼요 (Subject → Y)` : `Scene height ${(activeChar.y * 100).toFixed(1)}cm is added to the motion (Subject → Y)`}</p>}
|
|
150
|
+
{multiModelTake?.persons > 1 && (
|
|
151
|
+
<p className="multimodel-extract-receipt">
|
|
152
|
+
{isKo
|
|
153
|
+
? `${multiModelTake.persons}명의 테이크를 각 인물 레이어에 배치했어요`
|
|
154
|
+
: `${multiModelTake.persons} performers landed on their own subject layers`}
|
|
155
|
+
</p>
|
|
156
|
+
)}
|
|
157
|
+
{multiModelExtract === "idle" && !multiModelTake && (
|
|
158
|
+
<p className="multimodel-note">
|
|
159
|
+
{bridge === null
|
|
160
|
+
? ko("Checking for the dev bridge…", "개발 브리지를 확인하는 중…")
|
|
161
|
+
: bridge.ok && bridge.extractionBackend === "gvhmr"
|
|
162
|
+
? ko(
|
|
163
|
+
"GVHMR extraction runs on the GPU box (about a minute per 15 s of footage).",
|
|
164
|
+
"GVHMR 추출은 GPU 박스에서 돌아갑니다(영상 15초당 약 1분)."
|
|
165
|
+
)
|
|
166
|
+
: ko(
|
|
167
|
+
"GVHMR extraction is unavailable until the local GPU bridge is connected.",
|
|
168
|
+
"로컬 GPU 브리지가 연결될 때까지 GVHMR 추출을 사용할 수 없어요."
|
|
169
|
+
)}
|
|
170
|
+
</p>
|
|
171
|
+
)}
|
|
172
|
+
</div>
|
|
173
|
+
)}
|
|
174
|
+
<p className="multimodel-note">
|
|
175
|
+
{ko("Locked-off footage with both performers in frame is best.", "두 사람이 함께 보이는 고정 카메라 영상이 가장 적합합니다.")}
|
|
176
|
+
</p>
|
|
177
|
+
</div>
|
|
178
|
+
</Foldout>
|
|
179
|
+
);
|
|
180
|
+
}
|