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.
Files changed (286) hide show
  1. package/CHANGELOG.md +171 -0
  2. package/bin/agent/agent-routes.mjs +74 -70
  3. package/bin/agent/agent-runner.mjs +10 -3
  4. package/bin/agent/motion-runtime.mjs +32 -260
  5. package/bin/agent/providers.mjs +46 -3
  6. package/bin/agent/session-store.mjs +41 -5
  7. package/bin/agent/studio-prompt.mjs +1 -1
  8. package/bin/agent/studio-tools.mjs +48 -11
  9. package/bin/cozyclay.mjs +26 -8
  10. package/bin/live/cli.mjs +22 -3
  11. package/bin/mcp-runtime.mjs +35 -20
  12. package/bin/telemetry-state.mjs +5 -2
  13. package/bin/update-check.mjs +15 -0
  14. package/dist/app/index.html +6 -6
  15. package/dist/assets/analytics-MVZOEW8O.js +1 -0
  16. package/dist/assets/app-C1sdVbg4.css +1 -0
  17. package/dist/assets/app-CA55tcI3.js +4876 -0
  18. package/dist/assets/{demo-B-eg1CT8.js → demo-ByeYp-dY.js} +1 -1
  19. package/dist/assets/{first-shot-handoff-CGgnvNu8.js → first-shot-handoff-DgT28ZiV.js} +1 -1
  20. package/dist/assets/{landing-IbRp3FnQ.js → landing-BaXkpFip.js} +1 -1
  21. package/dist/assets/shot-prompt-CbqWcWYi.css +1 -0
  22. package/dist/assets/shot-prompt-Yrt99wZx.js +17 -0
  23. package/dist/assets/{ticket-BgIzOBOf.js → ticket-BcmsnHn5.js} +1 -1
  24. package/dist/assets/workflow-DRdwoDBT.css +1 -0
  25. package/dist/assets/workflow-dNPGY9xz.js +179 -0
  26. package/dist/cozyclay-package.json +1 -1
  27. package/dist/fonts/IBM-Plex-Sans-OFL.txt +92 -0
  28. package/dist/fonts/JetBrains-Mono-OFL.txt +93 -0
  29. package/dist/fonts/README.md +13 -9
  30. package/dist/fonts/ibm-plex-sans-400-latin.woff2 +0 -0
  31. package/dist/fonts/ibm-plex-sans-500-latin.woff2 +0 -0
  32. package/dist/fonts/ibm-plex-sans-600-latin.woff2 +0 -0
  33. package/dist/fonts/jetbrains-mono-400-latin.woff2 +0 -0
  34. package/dist/fonts/jetbrains-mono-500-latin.woff2 +0 -0
  35. package/dist/index.html +7 -7
  36. package/dist/privacy/index.html +13 -8
  37. package/dist/sitemap.xml +6 -6
  38. package/dist/workflow/index.html +5 -5
  39. package/mcp/LIVE-PROTOCOL.md +1 -1
  40. package/mcp/live-hub.mjs +35 -93
  41. package/mcp/mesh-file.mjs +48 -0
  42. package/mcp/server.mjs +6 -77
  43. package/mcp/tool-handlers.mjs +224 -377
  44. package/package.json +2 -1
  45. package/src/App.jsx +1273 -9075
  46. package/src/analytics.js +393 -9
  47. package/src/app-context.js +211 -0
  48. package/src/app-stage.jsx +25 -24
  49. package/src/ardy/auto-fix-panel.css +108 -0
  50. package/src/ardy/collision-blockers.js +9 -3
  51. package/src/ardy/fix-collisions.js +10 -66
  52. package/src/ardy/ground.js +4 -4
  53. package/src/ardy/ik-drag.js +101 -0
  54. package/src/ardy/ik-key-json.js +43 -0
  55. package/src/ardy/ik.js +122 -76
  56. package/src/ardy/physics-panel.css +2 -0
  57. package/src/ardy/physics-panel.jsx +17 -5
  58. package/src/ardy/platform-fit-panel.css +64 -0
  59. package/src/ardy/platform-fit-panel.jsx +40 -0
  60. package/src/ardy/platform-fit.js +326 -0
  61. package/src/ardy/playback.js +166 -20
  62. package/src/ardy/range-pin.js +299 -0
  63. package/src/ardy/timeline-coordinates.js +26 -0
  64. package/src/ardy/timeline.css +1199 -0
  65. package/src/ardy/timeline.jsx +149 -41
  66. package/src/ardy/waypoints.js +2 -2
  67. package/src/asset-pane.css +462 -0
  68. package/src/asset-pane.jsx +461 -232
  69. package/src/command-bus.js +349 -0
  70. package/src/commands/ai.js +31 -0
  71. package/src/commands/cast.js +140 -0
  72. package/src/commands/elements/character.js +13 -0
  73. package/src/commands/elements/motion.js +14 -0
  74. package/src/commands/elements/object.js +22 -0
  75. package/src/commands/elements/scene.js +16 -0
  76. package/src/commands/elements/shot.js +9 -0
  77. package/src/commands/elements/stage.js +11 -0
  78. package/src/commands/elements.js +155 -0
  79. package/src/commands/export.js +20 -0
  80. package/src/commands/index.js +40 -0
  81. package/src/commands/motion.js +231 -0
  82. package/src/commands/objects.js +174 -0
  83. package/src/commands/project.js +53 -0
  84. package/src/commands/scene.js +81 -0
  85. package/src/commands/shared.js +19 -0
  86. package/src/commands/shot.js +103 -0
  87. package/src/commands/stage.js +17 -0
  88. package/src/commands/view.js +42 -0
  89. package/src/document-store.js +144 -0
  90. package/src/domains/cast.js +1007 -0
  91. package/src/domains/motion.js +3219 -0
  92. package/src/domains/objects.js +932 -0
  93. package/src/domains/scenes.js +817 -0
  94. package/src/domains/shots.js +404 -0
  95. package/src/domains/stage.js +93 -0
  96. package/src/dualview.jsx +113 -57
  97. package/src/facing-marks.js +3 -0
  98. package/src/first-success-guide.jsx +14 -14
  99. package/src/grid-view.js +18 -5
  100. package/src/hierarchy-model.js +24 -16
  101. package/src/hierarchy-panel.css +386 -0
  102. package/src/hierarchy-panel.jsx +150 -21
  103. package/src/{fal-motion-client.js → i2v-motion-client.js} +13 -13
  104. package/src/i2v-motion-studio.jsx +180 -0
  105. package/src/ik-camera.js +3 -0
  106. package/src/main.jsx +6 -0
  107. package/src/motion/generation.js +105 -0
  108. package/src/motion-readiness-ui.jsx +4 -4
  109. package/src/motion-readiness.js +11 -0
  110. package/src/motion-trail.js +366 -9
  111. package/src/object-gizmo.jsx +7 -1
  112. package/src/otio.js +2 -1
  113. package/src/panels/CameraPanel.jsx +60 -0
  114. package/src/panels/CharacterTransformPanel.jsx +48 -0
  115. package/src/panels/EnvironmentPanel.jsx +40 -0
  116. package/src/panels/Foldout.jsx +32 -0
  117. package/src/panels/LightPanel.jsx +24 -0
  118. package/src/panels/ObjectTransformPanel.jsx +554 -0
  119. package/src/panels/PosePanel.jsx +97 -0
  120. package/src/panels/ProjectPanel.jsx +33 -0
  121. package/src/panels/PromptBlocksPanel.jsx +442 -0
  122. package/src/panels/PropsPanel.jsx +70 -0
  123. package/src/panels/ReferenceImageField.jsx +91 -0
  124. package/src/panels/RigControlPanel.jsx +78 -0
  125. package/src/panels/RigPanel.jsx +37 -0
  126. package/src/panels/SubjectBox.jsx +47 -0
  127. package/src/panels/SubjectsPanel.jsx +39 -0
  128. package/src/panels/VideoCapturePanel.jsx +180 -0
  129. package/src/panels/details.css +1171 -0
  130. package/src/panels/motion.css +197 -0
  131. package/src/panels/pose.css +39 -0
  132. package/src/planview.jsx +60 -31
  133. package/src/posestudio.jsx +59 -4
  134. package/src/project-browser.css +1073 -0
  135. package/src/project-browser.jsx +284 -123
  136. package/src/range-pin-object-transform.js +19 -0
  137. package/src/range-pin-panel.css +518 -0
  138. package/src/range-pin-panel.jsx +344 -0
  139. package/src/result-modal.jsx +5 -5
  140. package/src/room.jsx +79 -51
  141. package/src/scene-objects.js +37 -1
  142. package/src/scenes.js +5 -0
  143. package/src/semantic-edit.js +2 -0
  144. package/src/settings-menu.jsx +39 -105
  145. package/src/shell/BottomDock.jsx +450 -0
  146. package/src/shell/DetailsSlot.jsx +513 -0
  147. package/src/shell/LibrarySlot.jsx +86 -0
  148. package/src/shell/MenuBar.jsx +601 -0
  149. package/src/shell/OutlinerSlot.jsx +50 -0
  150. package/src/shell/PreferencesDialog.jsx +538 -0
  151. package/src/shell/PreferencesSlot.jsx +40 -0
  152. package/src/shell/StatusBar.jsx +125 -0
  153. package/src/shell/StudioShell.jsx +92 -0
  154. package/src/shell/TopBar.jsx +129 -0
  155. package/src/shell/ViewportToolbar.jsx +656 -0
  156. package/src/shell/agent-glass.css +296 -0
  157. package/src/shell/dock.css +93 -0
  158. package/src/shell/glass-regions.css +985 -0
  159. package/src/shell/glass.css +687 -0
  160. package/src/shell/log-store.js +52 -0
  161. package/src/shell/mode.css +9 -0
  162. package/src/shell/preferences.css +651 -0
  163. package/src/shell/shell.css +282 -0
  164. package/src/shell/studio-shell-context.js +19 -0
  165. package/src/shell/topbar.css +435 -0
  166. package/src/shell/viewport.css +451 -0
  167. package/src/store/authored-intent.js +22 -0
  168. package/src/store/runtime-adapters.js +92 -0
  169. package/src/store/scene-stage.js +21 -0
  170. package/src/store/use-document-store.js +14 -0
  171. package/src/studio-actions.js +210 -0
  172. package/src/studio-agent-commands.js +41 -271
  173. package/src/studio-agent-context.js +39 -10
  174. package/src/studio-agent-motion.js +122 -373
  175. package/src/studio-agent-protocol.js +67 -31
  176. package/src/studio-app-binding.js +379 -0
  177. package/src/studio-contact-sheet.js +73 -0
  178. package/src/studio-elements.js +58 -51
  179. package/src/styles/themes.css +200 -0
  180. package/src/styles/tokens.css +69 -0
  181. package/src/styles.css +460 -1742
  182. package/src/theme.js +44 -0
  183. package/src/timeline-extent.js +16 -0
  184. package/src/trail-key-conflicts.js +39 -0
  185. package/src/trail-pick.js +59 -0
  186. package/src/ui.jsx +8 -8
  187. package/src/use-case-question.jsx +66 -0
  188. package/src/workflow/AgentPanel.jsx +82 -69
  189. package/src/workflow/agent-client.js +12 -2
  190. package/src/workflow/agent-panel.css +34 -32
  191. package/src/workflow/cozy-scene-node.css +2 -0
  192. package/src/workflow/workflow.css +2 -0
  193. package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
  194. package/tools/ardy/bridge.mjs +164 -146
  195. package/tools/ardy/visual-qa.mjs +5 -5
  196. package/tools/bench/EXP3.md +61 -0
  197. package/tools/bench/cclay_bench_extract_incam.py +125 -0
  198. package/tools/bench/cclay_bench_extract_obs.py +204 -0
  199. package/tools/bench/cclay_bench_runner.py +42 -0
  200. package/tools/bench/cube-contact.mjs +162 -0
  201. package/tools/bench/exp3.mjs +152 -0
  202. package/tools/bench/extract-bench-lib.mjs +216 -0
  203. package/tools/bench/extract-bench.mjs +302 -0
  204. package/tools/bench/fal-generate.mjs +88 -0
  205. package/tools/bench/fit/README.md +189 -0
  206. package/tools/bench/fit/camera.mjs +59 -0
  207. package/tools/bench/fit/contact.mjs +419 -0
  208. package/tools/bench/fit/footlock.mjs +234 -0
  209. package/tools/bench/fit/motion.mjs +85 -0
  210. package/tools/bench/fit/pin.mjs +27 -0
  211. package/tools/bench/fit/remote.mjs +105 -0
  212. package/tools/bench/fit-bench.mjs +126 -0
  213. package/tools/bench/fit-sanity.mjs +45 -0
  214. package/tools/bench/metrics.mjs +313 -0
  215. package/tools/bench/obs/depth.mjs +258 -0
  216. package/tools/bench/obs/extrinsics.mjs +175 -0
  217. package/tools/bench/obs/fit_mannequin_betas.py +340 -0
  218. package/tools/bench/obs/ground.mjs +294 -0
  219. package/tools/bench/obs/heading.mjs +151 -0
  220. package/tools/bench/obs/ladder.mjs +555 -0
  221. package/tools/bench/obs/mannequin-betas.json +124 -0
  222. package/tools/bench/obs/remote.mjs +83 -0
  223. package/tools/bench/obs/rest_joints.py +52 -0
  224. package/tools/bench/obs/ybot-targets.mjs +49 -0
  225. package/tools/bench/obs-bench.mjs +520 -0
  226. package/tools/bench/score.mjs +343 -0
  227. package/tools/bench/summarize.mjs +86 -0
  228. package/tools/dev/pages/privacy.html +13 -8
  229. package/tools/gt-render/browser.mjs +159 -0
  230. package/tools/gt-render/camera-math.mjs +276 -0
  231. package/tools/gt-render/page.mjs +267 -0
  232. package/tools/gt-render/render.mjs +514 -0
  233. package/tools/gt-render/scene-box.mjs +31 -0
  234. package/tools/gt-render/take-transform.mjs +87 -0
  235. package/tools/morphgs/assets/gen_truth.py +39 -0
  236. package/tools/morphgs/assets/mesh_ori_rig.txt +27 -0
  237. package/tools/morphgs/assets/playback-check.mjs +30 -0
  238. package/tools/morphgs/demo-gate.sh +26 -0
  239. package/tools/morphgs/fbx2morphgs.mjs +68 -0
  240. package/tools/morphgs/morphgs-to-cskel27.mjs +105 -0
  241. package/tools/morphgs/patches/preprocess_src-none-mode.patch +76 -0
  242. package/tools/morphgs/setup-on-cluster.sh +126 -0
  243. package/tools/qa/css-rule-usage.mjs +283 -0
  244. package/tools/qa/studio-control-count.mjs +70 -32
  245. package/tools/run-tests.mjs +134 -12
  246. package/tools/track/DESIGN.md +513 -0
  247. package/tools/track/backfill-provenance.mjs +116 -0
  248. package/tools/track/budget.mjs +8 -0
  249. package/tools/track/check-rig.mjs +209 -0
  250. package/tools/track/diagnostics.schema.json +60 -0
  251. package/tools/track/export-rig.mjs +256 -0
  252. package/tools/track/fallback.mjs +7 -0
  253. package/tools/track/fk-parity-fixture.mjs +161 -0
  254. package/tools/track/gate.mjs +349 -0
  255. package/tools/track/masks.mjs +318 -0
  256. package/tools/track/metrics.mjs +201 -0
  257. package/tools/track/publish-obs.mjs +108 -0
  258. package/tools/track/py/check_env.py +61 -0
  259. package/tools/track/py/eval_lr.py +229 -0
  260. package/tools/track/py/lr_viterbi.py +294 -0
  261. package/tools/track/py/masks.py +405 -0
  262. package/tools/track/py/objective.py +382 -0
  263. package/tools/track/py/rig.py +508 -0
  264. package/tools/track/py/scene.py +326 -0
  265. package/tools/track/py/test_joint_indices.py +123 -0
  266. package/tools/track/py/test_lr_viterbi.py +195 -0
  267. package/tools/track/py/test_masks.py +229 -0
  268. package/tools/track/py/test_rig.py +288 -0
  269. package/tools/track/py/test_scene.py +354 -0
  270. package/tools/track/py/test_track.py +328 -0
  271. package/tools/track/py/track.py +411 -0
  272. package/tools/track/remote.mjs +140 -0
  273. package/tools/track/rig-dump.mjs +227 -0
  274. package/tools/track/run-box-tests.mjs +14 -0
  275. package/tools/track/run-box.mjs +168 -0
  276. package/tools/track/setup-box.sh +59 -0
  277. package/tools/track/study-2d.mjs +728 -0
  278. package/dist/assets/analytics-B1hnH66c.js +0 -1
  279. package/dist/assets/app-BWusbqgO.js +0 -4861
  280. package/dist/assets/app-qKDo4PBX.css +0 -1
  281. package/dist/assets/shot-prompt-CHUtw6af.js +0 -17
  282. package/dist/assets/shot-prompt-C_g2BHVc.css +0 -1
  283. package/dist/assets/workflow-CVwzMhz2.js +0 -179
  284. package/dist/assets/workflow-DsudxKHF.css +0 -1
  285. package/src/fal-motion-studio.jsx +0 -180
  286. 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
+ }