cozyclay 2.0.0 → 2.1.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 (260) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +17 -2
  3. package/THIRD_PARTY_NOTICES.md +8 -0
  4. package/bin/agent/agent-routes.mjs +219 -38
  5. package/bin/agent/agent-runner.mjs +98 -4
  6. package/bin/agent/codex-client.mjs +2 -1
  7. package/bin/agent/fal-contract.mjs +0 -17
  8. package/bin/agent/h3-preservation.mjs +6 -8
  9. package/bin/agent/provider-keys.mjs +1 -1
  10. package/bin/agent/providers.mjs +124 -3
  11. package/bin/agent/script-tool.mjs +74 -0
  12. package/bin/agent/session-store.mjs +5 -5
  13. package/bin/agent/studio-prompt.mjs +2 -1
  14. package/bin/agent/studio-rebase.mjs +314 -0
  15. package/bin/agent/studio-tools.mjs +21 -14
  16. package/bin/agent/supervisor.mjs +311 -0
  17. package/bin/agent/video-adapters.mjs +0 -7
  18. package/bin/codex-auth.mjs +6 -5
  19. package/bin/cozyclay.mjs +12 -3
  20. package/bin/live/cli.mjs +2 -3
  21. package/bin/live/client.mjs +1 -1
  22. package/bin/live/studio.mjs +5 -2
  23. package/bin/mcp-runtime.mjs +12 -2
  24. package/bin/telemetry-state.mjs +3 -3
  25. package/dist/app/index.html +6 -6
  26. package/dist/assets/analytics-B41FmPYI.js +1 -0
  27. package/dist/assets/app-Do-Og5Pu.css +1 -0
  28. package/dist/assets/app-DwP-tl2x.js +4920 -0
  29. package/dist/assets/{demo-ByeYp-dY.js → demo-Dt0qZfS7.js} +1 -1
  30. package/dist/assets/{first-shot-handoff-DgT28ZiV.js → first-shot-handoff-BlT33CGf.js} +1 -1
  31. package/dist/assets/{landing-BaXkpFip.js → landing-BSesrCpL.js} +1 -1
  32. package/dist/assets/scene-asset-sync-BCWDJB1d.js +19 -0
  33. package/dist/assets/{shot-prompt-CbqWcWYi.css → scene-asset-sync-DjJ_cJ2R.css} +1 -1
  34. package/dist/assets/{ticket-BcmsnHn5.js → ticket-jqMhqL88.js} +1 -1
  35. package/dist/assets/workflow-Br-K7Srs.js +179 -0
  36. package/dist/cozyclay-package.json +1 -1
  37. package/dist/index.html +328 -222
  38. package/dist/sitemap.xml +5 -5
  39. package/dist/workflow/index.html +4 -4
  40. package/mcp/LIVE-PROTOCOL.md +23 -2
  41. package/mcp/README.md +37 -2
  42. package/mcp/ardy-prompts.mjs +1 -1
  43. package/mcp/image-file.mjs +132 -0
  44. package/mcp/live-hub.mjs +2 -2
  45. package/mcp/package.json +1 -0
  46. package/mcp/runtime/package-lock.json +7 -0
  47. package/mcp/runtime/package.json +1 -0
  48. package/mcp/server.mjs +0 -6
  49. package/mcp/tool-handlers.mjs +289 -87
  50. package/package.json +11 -2
  51. package/src/App.jsx +801 -259
  52. package/src/agent-image-client.js +94 -0
  53. package/src/analytics.js +47 -3
  54. package/src/app-stage.jsx +129 -2745
  55. package/src/ardy/auto-physics.js +6 -16
  56. package/src/ardy/collision-blockers.js +3 -7
  57. package/src/ardy/fix-collisions.js +5 -35
  58. package/src/ardy/ik-drag.js +86 -5
  59. package/src/ardy/ik-key-json.js +3 -1
  60. package/src/ardy/ik.js +15 -13
  61. package/src/ardy/motion-calibration.js +1 -1
  62. package/src/ardy/npz.js +0 -8
  63. package/src/ardy/physics-panel.jsx +3 -2
  64. package/src/ardy/physics-review.js +3 -2
  65. package/src/ardy/physics-support.js +1 -1
  66. package/src/ardy/physics-surface.js +1 -1
  67. package/src/ardy/platform-fit-panel.jsx +2 -1
  68. package/src/ardy/playback.js +1 -5
  69. package/src/ardy/range-pin.js +2 -2
  70. package/src/ardy/root-drop.js +0 -1
  71. package/src/ardy/timeline.css +196 -3
  72. package/src/ardy/timeline.jsx +117 -29
  73. package/src/ardy/to-cskel27.js +0 -2
  74. package/src/ardy/waypoints.js +1 -4
  75. package/src/ardy-bridge-contract.js +281 -0
  76. package/src/asset-pane.jsx +19 -9
  77. package/src/auto-color.js +1 -1
  78. package/src/burn-in.js +1 -1
  79. package/src/camera-block.js +1 -1
  80. package/src/camera-follow.js +75 -32
  81. package/src/camera-move.js +0 -6
  82. package/src/camera-tutorial.jsx +43 -24
  83. package/src/character-kind.js +79 -0
  84. package/src/command-bus.js +76 -17
  85. package/src/commands/cast.js +31 -7
  86. package/src/commands/elements/object.js +1 -1
  87. package/src/commands/elements/shot.js +1 -1
  88. package/src/commands/elements.js +1 -1
  89. package/src/commands/export.js +3 -3
  90. package/src/commands/motion.js +2 -2
  91. package/src/commands/objects.js +105 -10
  92. package/src/commands/project.js +16 -3
  93. package/src/commands/sculpt-images.js +53 -0
  94. package/src/commands/shot.js +88 -10
  95. package/src/commands/view.js +10 -4
  96. package/src/coplanar-depth.js +216 -0
  97. package/src/cuts.js +7 -6
  98. package/src/domains/cast.js +60 -43
  99. package/src/domains/motion/motion-intent-codec.js +50 -0
  100. package/src/domains/motion/motion-range-pin-rules.js +23 -0
  101. package/src/domains/motion/motion-video-capture.js +664 -0
  102. package/src/domains/motion.js +253 -742
  103. package/src/domains/objects.js +106 -5
  104. package/src/domains/scenes.js +89 -32
  105. package/src/domains/shots.js +33 -7
  106. package/src/error-boundary.jsx +1 -1
  107. package/src/execution-telemetry.js +5 -5
  108. package/src/facing-marks.js +14 -1
  109. package/src/first-success-guide.jsx +35 -4
  110. package/src/gizmo-claim.js +3 -1
  111. package/src/grid-floor.jsx +1 -4
  112. package/src/hierarchy-model.js +48 -35
  113. package/src/hierarchy-panel.css +1 -0
  114. package/src/hierarchy-panel.jsx +54 -102
  115. package/src/history.js +0 -2
  116. package/src/i2v-motion-client.js +1 -4
  117. package/src/ime.js +6 -0
  118. package/src/keyframe-pack.js +15 -6
  119. package/src/line-edit.js +4 -4
  120. package/src/live-control.js +6 -6
  121. package/src/matte-editor.js +2 -8
  122. package/src/matte.js +0 -19
  123. package/src/mesh-graph-clone.js +16 -2
  124. package/src/mesh-sniff.js +0 -1
  125. package/src/monotone-cubic.js +40 -0
  126. package/src/motion/generation.js +1 -1
  127. package/src/motion-store.js +4 -4
  128. package/src/motion-trail.js +5 -11
  129. package/src/multimodel-ingest.js +1 -3
  130. package/src/object-catalog.jsx +7 -5
  131. package/src/object-gizmo.jsx +45 -31
  132. package/src/object-path.js +310 -67
  133. package/src/object-travel.js +224 -0
  134. package/src/otio.js +8 -3
  135. package/src/panels/CharacterTransformPanel.jsx +24 -6
  136. package/src/panels/ObjectTransformPanel.jsx +4 -3
  137. package/src/panels/PoseImagePanel.jsx +205 -0
  138. package/src/panels/PromptBlocksPanel.jsx +3 -1
  139. package/src/panels/ProxyFigurePanel.jsx +56 -0
  140. package/src/panels/ReferenceImageField.jsx +1 -1
  141. package/src/panels/RigControlPanel.jsx +24 -3
  142. package/src/panels/RigPanel.jsx +2 -1
  143. package/src/panels/SculptPanel.jsx +405 -0
  144. package/src/panels/SubjectBox.jsx +1 -1
  145. package/src/panels/SubjectsPanel.jsx +0 -2
  146. package/src/panels/VideoCapturePanel.jsx +2 -1
  147. package/src/panels/pose-image.css +42 -0
  148. package/src/panels/pose.css +3 -0
  149. package/src/panels/sculpt.css +162 -0
  150. package/src/plan-diagram.js +184 -0
  151. package/src/planview.jsx +127 -80
  152. package/src/playground.js +3 -5
  153. package/src/pose-fade.js +77 -0
  154. package/src/pose-gizmo.css +23 -0
  155. package/src/pose-gizmo.js +96 -0
  156. package/src/posestudio.jsx +134 -83
  157. package/src/project-browser.css +53 -0
  158. package/src/project-browser.jsx +52 -3
  159. package/src/project.js +55 -24
  160. package/src/props.jsx +177 -36
  161. package/src/proxy-figure.jsx +69 -0
  162. package/src/range-pin-panel.jsx +0 -1
  163. package/src/receipt-findings.js +20 -0
  164. package/src/render-passes.js +0 -1
  165. package/src/resource-status.jsx +1 -1
  166. package/src/root-path.js +48 -0
  167. package/src/route-owner.js +52 -0
  168. package/src/scene-asset-cache.js +1 -2
  169. package/src/scene-assets.js +12 -4
  170. package/src/scene-fbx.js +1 -1
  171. package/src/scene-mesh-cache.js +0 -1
  172. package/src/scene-objects.js +319 -19
  173. package/src/scenes.js +62 -17
  174. package/src/sculpt-blueprint.js +65 -0
  175. package/src/sculpt-compare.js +384 -0
  176. package/src/sculpt-geometry.js +141 -0
  177. package/src/sculpt-mode.js +168 -0
  178. package/src/sculpt-recipe.js +385 -0
  179. package/src/sculpt-run.js +351 -0
  180. package/src/sculpt-stage.jsx +221 -0
  181. package/src/settings-menu.jsx +1 -1
  182. package/src/shell/BoardSlot.jsx +341 -0
  183. package/src/shell/BottomDock.jsx +58 -14
  184. package/src/shell/DetailsSlot.jsx +26 -6
  185. package/src/shell/MenuBar.jsx +78 -6
  186. package/src/shell/OutlinerSlot.jsx +3 -1
  187. package/src/shell/TopBar.jsx +17 -3
  188. package/src/shell/ViewportToolbar.jsx +18 -3
  189. package/src/shell/dock.css +239 -0
  190. package/src/shell/glass-regions.css +3 -2
  191. package/src/shell/log-store.js +4 -4
  192. package/src/shell/mode.css +20 -0
  193. package/src/shell/studio-shell-context.js +4 -1
  194. package/src/shot-authoring.js +69 -6
  195. package/src/stable-items.js +1 -1
  196. package/src/stage-attach-frames.js +169 -0
  197. package/src/stage-canvas-rigs.jsx +402 -0
  198. package/src/stage-capture.jsx +216 -0
  199. package/src/stage-characters.jsx +302 -0
  200. package/src/stage-editor-gizmos.jsx +1229 -0
  201. package/src/stage-labels-ko.js +204 -0
  202. package/src/stage-startup-defaults.js +182 -0
  203. package/src/starter-scenes.js +1 -1
  204. package/src/storyboard.js +24 -5
  205. package/src/studio-actions.js +42 -9
  206. package/src/studio-agent-commands.js +186 -23
  207. package/src/studio-agent-context.js +11 -9
  208. package/src/studio-agent-motion.js +2 -1
  209. package/src/studio-agent-protocol.js +56 -23
  210. package/src/studio-app-binding.js +52 -23
  211. package/src/studio-contact-sheet.js +3 -3
  212. package/src/studio-elements.js +11 -2
  213. package/src/studio-geometry-facts.js +114 -0
  214. package/src/styles.css +111 -10
  215. package/src/theme.js +1 -1
  216. package/src/timeline-fps.js +13 -0
  217. package/src/ui-scrub.js +4 -4
  218. package/src/ui.jsx +6 -4
  219. package/src/use-asset-shelf.js +60 -0
  220. package/src/use-camera-tutorial.js +50 -0
  221. package/src/use-embed-bridge.js +12 -0
  222. package/src/use-first-success-prompt.js +7 -0
  223. package/src/use-i2v-capture-modal.js +15 -0
  224. package/src/use-sculpt-mode.js +95 -0
  225. package/src/workflow/AgentPanel.jsx +110 -20
  226. package/src/workflow/WorkflowBuilder.jsx +1 -1
  227. package/src/workflow/agent-client.js +113 -45
  228. package/src/workflow/agent-panel.css +35 -0
  229. package/src/workflow/attachment-image.js +1 -1
  230. package/src/workflow/canvas-commands.js +3 -3
  231. package/src/workflow/cozy-scene-node.js +3 -3
  232. package/src/workflow/local-workflow.js +2 -2
  233. package/src/workflow/motion-input.js +3 -3
  234. package/src/workflow/node-schema.js +1 -1
  235. package/src/workflow/scene-asset-sync.js +2 -9
  236. package/src/workflow/shot-prompt-node.js +1 -1
  237. package/src/workflow/video-contract.js +1 -1
  238. package/src/workflow/workflow-resources.js +0 -8
  239. package/src/workflow-send.js +87 -0
  240. package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
  241. package/tools/check-eslint.mjs +57 -0
  242. package/tools/check-knip.mjs +60 -0
  243. package/tools/check-loc.mjs +54 -0
  244. package/tools/demo-worker/api-client.mjs +7 -13
  245. package/tools/demo-worker/generate.mjs +2 -4
  246. package/tools/eslint-baseline.json +23 -0
  247. package/tools/kimodo/effector-constraints.mjs +4 -4
  248. package/tools/kimodo/generate.mjs +1 -1
  249. package/tools/knip-baseline.json +331 -0
  250. package/tools/loc-baseline.json +126 -0
  251. package/tools/process-supervisor.mjs +1 -1
  252. package/tools/projflow/replay.mjs +4 -4
  253. package/tools/projflow/runner.mjs +3 -3
  254. package/tools/qa/studio-control-count.mjs +2 -0
  255. package/tools/run-tests.mjs +96 -7
  256. package/dist/assets/analytics-MVZOEW8O.js +0 -1
  257. package/dist/assets/app-C1sdVbg4.css +0 -1
  258. package/dist/assets/app-CA55tcI3.js +0 -4876
  259. package/dist/assets/shot-prompt-Yrt99wZx.js +0 -17
  260. package/dist/assets/workflow-dNPGY9xz.js +0 -179
@@ -0,0 +1,341 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import { useStudioShell } from "./studio-shell-context.js";
3
+ import { ko } from "../locale.js";
4
+ import { assetRecord, rememberAsset } from "../scene-asset-cache.js";
5
+ import { ASSET_MAX_SOURCE_BYTES, importImageFile } from "../scene-assets.js";
6
+ import { STILL_HOLD_MAX } from "../shot-authoring.js";
7
+ import { startExportAttempt, track } from "../analytics.js";
8
+ import { bytesToDataUrl, panelVideoPrompt, sendPanelToWorkflow } from "../workflow-send.js";
9
+ import { panelStylizeRequest, requestAgentImage, stylizedPngBytes, stylizeErrorMessage } from "../agent-image-client.js";
10
+
11
+ // A card's greybox is re-captured this long after the last document edit, so
12
+ // a drag or a typing burst costs one capture pass, not one per change.
13
+ const THUMB_DEBOUNCE_MS = 500;
14
+ const PANEL_DRAG_TYPE = "application/x-cozyclay-panel";
15
+
16
+ /** The stylized picture behind a panel, as an object URL for its lifetime. */
17
+ function useStylizedUrl(assetId) {
18
+ const [url, setUrl] = useState(null);
19
+ useEffect(() => {
20
+ setUrl(null);
21
+ if (!assetId) return undefined;
22
+ let live = true;
23
+ let objectUrl = null;
24
+ assetRecord(assetId).then((record) => {
25
+ if (!live || !record) return;
26
+ objectUrl = URL.createObjectURL(new Blob([record.bytes], { type: record.type }));
27
+ setUrl(objectUrl);
28
+ }, (error) => console.warn(`[cozyclay] stylized panel ${assetId} could not be read`, error));
29
+ return () => {
30
+ live = false;
31
+ if (objectUrl) URL.revokeObjectURL(objectUrl);
32
+ };
33
+ }, [assetId]);
34
+ return url;
35
+ }
36
+
37
+ /** Board: the storyboard's stills as a strip of panel cards, in still order.
38
+ * Every edit goes through the shot commands, so the Sequencer, undo and the
39
+ * Agent see the same document. */
40
+ export default function BoardSlot({ active = true }) {
41
+ const { shots, activeShot, tlFps, runStudioAction, selectTimelineShot, capturePanelThumbnail, packMetaForShot, setToast, sceneRevision, exportPanelPacks, recState } = useStudioShell();
42
+ const [stylizing, setStylizing] = useState({});
43
+ const stills = shots.filter((entry) => entry.kind === "still").sort((a, b) => a.startFrame - b.startFrame);
44
+ const [thumbs, setThumbs] = useState({});
45
+ const [dragId, setDragId] = useState(null);
46
+ const [drop, setDrop] = useState(null);
47
+ const stillsRef = useRef(stills);
48
+ stillsRef.current = stills;
49
+ const captureRef = useRef(capturePanelThumbnail);
50
+ captureRef.current = capturePanelThumbnail;
51
+ const stripRef = useRef(null);
52
+ const countRef = useRef(stills.length);
53
+
54
+ const stillsKey = stills.map((entry) => `${entry.id}:${entry.startFrame}`).join(",");
55
+ useEffect(() => {
56
+ if (!active || !stillsKey) return undefined;
57
+ const timer = setTimeout(() => {
58
+ const next = {};
59
+ for (const entry of stillsRef.current) {
60
+ try {
61
+ next[entry.id] = captureRef.current(entry);
62
+ } catch (error) {
63
+ console.warn(`[cozyclay] panel ${entry.id} thumbnail failed`, error);
64
+ next[entry.id] = null;
65
+ }
66
+ }
67
+ setThumbs(next);
68
+ }, THUMB_DEBOUNCE_MS);
69
+ return () => clearTimeout(timer);
70
+ }, [active, sceneRevision, stillsKey]);
71
+
72
+ // A new panel lands at the end of the strip: bring it into view.
73
+ useEffect(() => {
74
+ if (stills.length > countRef.current) stripRef.current?.lastElementChild?.scrollIntoView?.({ block: "nearest", inline: "nearest" });
75
+ countRef.current = stills.length;
76
+ }, [stills.length]);
77
+
78
+ const fps = tlFps || 24;
79
+ const addPanel = () => runStudioAction("shot.createStill", {});
80
+
81
+ // Append the panel picture (greybox, or the stylized image) and a Video
82
+ // node wired to it to the Workflow draft, then open the canvas. Nothing runs.
83
+ async function sendToWorkflow(entry, source) {
84
+ const attempt = startExportAttempt({ export_kind: "workflow_send", format: "png", surface: "studio" });
85
+ try {
86
+ let dataUrl;
87
+ let mimeType = "image/png";
88
+ if (source === "stylized") {
89
+ const record = await assetRecord(entry.stylizedAssetId);
90
+ if (!record) throw new Error(ko("the stylized image is missing", "스타일화 이미지를 찾을 수 없어요"));
91
+ mimeType = record.type || "image/png";
92
+ dataUrl = bytesToDataUrl(record.bytes, mimeType);
93
+ } else {
94
+ dataUrl = capturePanelThumbnail(entry);
95
+ }
96
+ if (!dataUrl) throw new Error(ko("the shot renderer is not ready", "샷 렌더러가 아직 준비되지 않았어요"));
97
+ const index = shots.findIndex((candidate) => candidate.id === entry.id);
98
+ const prompt = panelVideoPrompt(packMetaForShot(entry, index), entry.caption);
99
+ sendPanelToWorkflow({ dataUrl, mimeType, fileName: `panel-${stills.findIndex((candidate) => candidate.id === entry.id) + 1}.${mimeType.split("/")[1] || "png"}`, prompt });
100
+ attempt.succeed();
101
+ setToast(ko("Panel sent to Workflow", "패널을 워크플로로 보냈어요"));
102
+ window.open("/workflow/", "_blank", "noopener,noreferrer");
103
+ } catch (error) {
104
+ attempt.fail(error);
105
+ console.warn(`[cozyclay] panel ${entry.id} could not be sent to Workflow`, error);
106
+ setToast(ko(`Could not send the panel to Workflow: ${error?.message || error}`, `패널을 워크플로로 보내지 못했어요: ${error?.message || error}`));
107
+ }
108
+ }
109
+
110
+ // Stylize: the panel's full-size frame through /agent/image. The picture is
111
+ // stored as an image asset first, then one shot.setStylized names it, so a
112
+ // failure anywhere leaves the panel as it was. Never runs on its own.
113
+ async function stylize(entry) {
114
+ if (stylizing[entry.id]) return;
115
+ setStylizing((current) => ({ ...current, [entry.id]: true }));
116
+ try {
117
+ // The thumbnail capture is the full-size export frame at the shot camera.
118
+ const imageDataUrl = capturePanelThumbnail(entry);
119
+ if (!imageDataUrl) throw new Error(ko("the shot renderer is not ready yet", "샷 렌더러가 아직 준비되지 않았어요"));
120
+ const meta = packMetaForShot(entry, shots.findIndex((candidate) => candidate.id === entry.id));
121
+ const result = await requestAgentImage(panelStylizeRequest({ meta, caption: entry.caption, imageDataUrl }));
122
+ const bytes = stylizedPngBytes(result?.dataUrl, ASSET_MAX_SOURCE_BYTES);
123
+ const asset = await rememberAsset(await importImageFile(new File([bytes], `${entry.name || "panel"} stylized.png`, { type: "image/png" })));
124
+ if (!runStudioAction("shot.setStylized", { shotId: entry.id, assetId: asset.id })) throw new Error(ko("the panel is no longer on the board", "패널이 보드에 더 이상 없어요"));
125
+ track("storyboard:panel_stylized", { outcome: "ok" });
126
+ } catch (error) {
127
+ track("storyboard:panel_stylized", { outcome: "error" });
128
+ console.warn(`[cozyclay] panel ${entry.id} stylize failed`, error);
129
+ setToast(stylizeErrorMessage(error));
130
+ } finally {
131
+ setStylizing(({ [entry.id]: _done, ...rest }) => rest);
132
+ }
133
+ }
134
+
135
+ function dropTarget(event, entry) {
136
+ const box = event.currentTarget.getBoundingClientRect();
137
+ return { id: entry.id, after: event.clientX > box.left + box.width / 2 };
138
+ }
139
+
140
+ function reorderTo(target) {
141
+ const moving = dragId;
142
+ setDragId(null);
143
+ setDrop(null);
144
+ if (!moving || !target || moving === target.id) return;
145
+ const others = stills.filter((entry) => entry.id !== moving);
146
+ const index = others.findIndex((entry) => entry.id === target.id) + (target.after ? 1 : 0);
147
+ if (stills.findIndex((entry) => entry.id === moving) === index) return;
148
+ runStudioAction("shot.reorder", { shotId: moving, index });
149
+ }
150
+
151
+ return (
152
+ <section className="dock-board" aria-label={ko("Board", "보드")} data-testid="dock-board" hidden={!active}>
153
+ <header className="dock-board-head">
154
+ <span className="dock-board-count">{ko(`${stills.length} panels`, `패널 ${stills.length}개`)}</span>
155
+ {/* An empty board offers + Panel once, in its empty state. */}
156
+ {stills.length > 0 && (
157
+ <button type="button" className="dock-board-add" data-testid="board-add-panel" title={ko("Add a panel after the last one, framed by the current camera", "현재 카메라 구도로 마지막 패널 뒤에 패널 추가")} onClick={addPanel}>
158
+ {ko("+ Panel", "+ 패널")}
159
+ </button>
160
+ )}
161
+ </header>
162
+ {stills.length === 0 ? (
163
+ <div className="dock-board-empty" data-testid="board-empty">
164
+ <p>{ko("No panels yet. Each panel is one held picture of the scene through the shot camera.", "아직 패널이 없어요. 패널 하나는 샷 카메라로 본 장면 한 장을 일정 시간 보여 줍니다.")}</p>
165
+ <button type="button" className="dock-board-add" data-testid="board-empty-add" title={ko("Add the first panel, framed by the current camera", "현재 카메라 구도로 첫 패널 추가")} onClick={addPanel}>{ko("+ Panel", "+ 패널")}</button>
166
+ </div>
167
+ ) : (
168
+ <ol className="dock-board-strip" ref={stripRef} data-testid="board-strip">
169
+ {stills.map((entry, index) => (
170
+ <PanelCard
171
+ key={entry.id}
172
+ shot={entry}
173
+ index={index}
174
+ fps={fps}
175
+ thumb={thumbs[entry.id] ?? null}
176
+ stylizing={Boolean(stylizing[entry.id])}
177
+ onStylize={() => stylize(entry)}
178
+ onUnstylize={() => runStudioAction("shot.setStylized", { shotId: entry.id, assetId: null })}
179
+ selected={activeShot?.id === entry.id}
180
+ dragging={dragId === entry.id}
181
+ dropSide={drop?.id === entry.id ? (drop.after ? "after" : "before") : null}
182
+ onSelect={() => selectTimelineShot(entry.id)}
183
+ onRemove={() => runStudioAction("shot.remove", { shotId: entry.id })}
184
+ onDuplicate={() => runStudioAction("shot.duplicate", { shotId: entry.id })}
185
+ onCaption={(caption) => runStudioAction("shot.setCaption", { shotId: entry.id, caption })}
186
+ onHold={(hold) => runStudioAction("shot.setHold", { shotId: entry.id, hold })}
187
+ onSendToWorkflow={(source) => sendToWorkflow(entry, source)}
188
+ exporting={recState === "recording"}
189
+ onExport={() => exportPanelPacks(false, entry.id)}
190
+ onDragStart={(event) => {
191
+ event.dataTransfer.effectAllowed = "move";
192
+ event.dataTransfer.setData(PANEL_DRAG_TYPE, entry.id);
193
+ setDragId(entry.id);
194
+ }}
195
+ onDragOver={(event) => {
196
+ if (!dragId) return;
197
+ event.preventDefault();
198
+ event.dataTransfer.dropEffect = "move";
199
+ const next = dropTarget(event, entry);
200
+ if (drop?.id !== next.id || drop?.after !== next.after) setDrop(next);
201
+ }}
202
+ onDrop={(event) => {
203
+ event.preventDefault();
204
+ reorderTo(dropTarget(event, entry));
205
+ }}
206
+ onDragEnd={() => { setDragId(null); setDrop(null); }}
207
+ />
208
+ ))}
209
+ </ol>
210
+ )}
211
+ </section>
212
+ );
213
+ }
214
+
215
+ function PanelCard({ shot, index, fps, thumb, stylizing, onStylize, onUnstylize, selected, dragging, dropSide, onSelect, onRemove, onDuplicate, onCaption, onHold, onSendToWorkflow, exporting, onExport, onDragStart, onDragOver, onDrop, onDragEnd }) {
216
+ const stylized = useStylizedUrl(shot.stylizedAssetId);
217
+ const [sendMenu, setSendMenu] = useState(false);
218
+ const hold = shot.endFrame - shot.startFrame + 1;
219
+ const seconds = Number((hold / fps).toFixed(2));
220
+ const stop = (event) => event.stopPropagation();
221
+
222
+ function commitCaption(event) {
223
+ const next = event.currentTarget.value.trim();
224
+ if (next !== (shot.caption ?? "")) onCaption(next);
225
+ }
226
+
227
+ function commitHold(event) {
228
+ const value = Number(event.currentTarget.value);
229
+ const frames = Math.round(value * fps);
230
+ if (!Number.isFinite(value) || frames < 1 || frames > STILL_HOLD_MAX) {
231
+ event.currentTarget.value = String(seconds);
232
+ return;
233
+ }
234
+ if (frames !== hold) onHold(frames);
235
+ }
236
+
237
+ return (
238
+ <li
239
+ className="dock-board-card"
240
+ data-testid="board-card"
241
+ data-shot-id={shot.id}
242
+ data-selected={selected || undefined}
243
+ data-dragging={dragging || undefined}
244
+ data-drop={dropSide ?? undefined}
245
+ data-stylizing={stylizing || undefined}
246
+ tabIndex={0}
247
+ draggable
248
+ aria-label={ko(`Panel ${index + 1}`, `패널 ${index + 1}`)}
249
+ aria-current={selected ? "true" : undefined}
250
+ onClick={onSelect}
251
+ onKeyDown={(event) => {
252
+ if (event.target !== event.currentTarget) return;
253
+ if (event.key === "Delete") {
254
+ event.preventDefault();
255
+ event.stopPropagation();
256
+ onRemove();
257
+ } else if (event.key === "Enter") {
258
+ onSelect();
259
+ }
260
+ }}
261
+ onDragStart={onDragStart}
262
+ onDragOver={onDragOver}
263
+ onDrop={onDrop}
264
+ onDragEnd={onDragEnd}
265
+ >
266
+ <div className="dock-board-actions">
267
+ <button type="button" data-action="duplicate" title={ko("Duplicate this panel after itself", "이 패널을 바로 뒤에 복제")} onClick={(event) => { stop(event); onDuplicate(); }}>{ko("Duplicate", "복제")}</button>
268
+ <button type="button" data-action="delete" className="danger" title={ko("Delete this panel (Delete)", "이 패널 삭제 (Delete)")} onClick={(event) => { stop(event); onRemove(); }}>{ko("Delete", "삭제")}</button>
269
+ <button type="button" data-action="stylize" disabled={stylizing}
270
+ title={shot.stylizedAssetId
271
+ ? ko("Generate a new stylized picture from this panel's frame and caption (replaces the current one)", "이 패널의 프레임과 캡션으로 스타일화 그림을 새로 생성 (지금 그림을 바꿔요)")
272
+ : ko("Generate a stylized picture from this panel's frame and caption", "이 패널의 프레임과 캡션으로 스타일화 그림 생성")}
273
+ onClick={(event) => { stop(event); onStylize(); }}>
274
+ {stylizing ? ko("Stylizing...", "스타일화 중...") : shot.stylizedAssetId ? ko("Re-stylize", "다시 스타일화") : ko("Stylize", "스타일화")}
275
+ </button>
276
+ {shot.stylizedAssetId && !stylizing && (
277
+ <button type="button" data-action="unstylize" title={ko("Remove the stylized picture; the greybox stays", "스타일화 그림 제거 (그레이박스는 남아요)")} onClick={(event) => { stop(event); onUnstylize(); }}>{ko("Remove", "제거")}</button>
278
+ )}
279
+ <button
280
+ type="button"
281
+ data-action="workflow"
282
+ title={ko("Send this panel to a Workflow video node", "이 패널을 워크플로 비디오 노드로 보내기")}
283
+ aria-haspopup={shot.stylizedAssetId ? "menu" : undefined}
284
+ aria-expanded={shot.stylizedAssetId ? sendMenu : undefined}
285
+ onClick={(event) => {
286
+ stop(event);
287
+ // Only a stylized panel has a choice to make.
288
+ if (shot.stylizedAssetId) setSendMenu((open) => !open);
289
+ else onSendToWorkflow("greybox");
290
+ }}
291
+ >{ko("Send to Workflow", "워크플로로")}</button>
292
+ {sendMenu && shot.stylizedAssetId && (
293
+ <div className="dock-board-menu" role="menu" aria-label={ko("Send to Workflow", "워크플로로 보내기")} onKeyDown={(event) => { if (event.key === "Escape") { event.stopPropagation(); setSendMenu(false); } }}>
294
+ <button type="button" role="menuitem" data-send-source="stylized" autoFocus onClick={(event) => { stop(event); setSendMenu(false); onSendToWorkflow("stylized"); }}>{ko("Stylized image", "스타일화 이미지")}</button>
295
+ <button type="button" role="menuitem" data-send-source="greybox" onClick={(event) => { stop(event); setSendMenu(false); onSendToWorkflow("greybox"); }}>{ko("Greybox", "그레이박스")}</button>
296
+ </div>
297
+ )}
298
+ <button type="button" data-action="export" disabled={exporting} title={exporting ? ko("Export — another export is running", "내보내기 — 다른 내보내기가 진행 중입니다") : ko("Export this panel as a pack (zip)", "이 패널을 팩(zip)으로 내보내기")} data-disabled-reason={exporting ? "export-running" : undefined} onClick={(event) => { stop(event); onExport(); }}>{ko("Export", "내보내기")}</button>
299
+ </div>
300
+ <div className="dock-board-thumb" data-stylized={stylized ? "true" : undefined}>
301
+ {stylized ? <img className="dock-board-image" src={stylized} alt="" draggable={false} /> : thumb && <img className="dock-board-image" src={thumb} alt="" draggable={false} />}
302
+ {stylized && thumb && <img className="dock-board-inset" src={thumb} alt="" draggable={false} />}
303
+ <span className="dock-board-index" data-testid="board-card-index">{index + 1}</span>
304
+ {stylizing && (
305
+ <span data-testid="board-card-stylizing" role="status"
306
+ style={{ position: "absolute", inset: 0, display: "grid", placeItems: "center", background: "rgba(0, 0, 0, .45)", color: "#fff", font: "500 11px var(--sans)" }}>
307
+ {ko("Stylizing...", "스타일화 중...")}
308
+ </span>
309
+ )}
310
+ </div>
311
+ <textarea
312
+ key={`caption:${shot.caption ?? ""}`}
313
+ className="dock-board-caption"
314
+ data-testid="board-card-caption"
315
+ aria-label={ko(`Panel ${index + 1} caption`, `패널 ${index + 1} 캡션`)}
316
+ placeholder={ko("Action or dialogue", "동작 또는 대사")}
317
+ defaultValue={shot.caption ?? ""}
318
+ maxLength={500}
319
+ rows={2}
320
+ onClick={stop}
321
+ onBlur={commitCaption}
322
+ />
323
+ <label className="dock-board-hold" onClick={stop}>
324
+ <span>{ko("Hold", "홀드")}</span>
325
+ <input
326
+ key={`hold:${hold}`}
327
+ type="number"
328
+ data-testid="board-card-hold"
329
+ min={1 / fps}
330
+ max={STILL_HOLD_MAX / fps}
331
+ step={0.5}
332
+ defaultValue={seconds}
333
+ aria-label={ko(`Panel ${index + 1} hold in seconds`, `패널 ${index + 1} 홀드 (초)`)}
334
+ onBlur={commitHold}
335
+ onKeyDown={(event) => { if (event.key === "Enter") event.currentTarget.blur(); }}
336
+ />
337
+ <span>{ko("s", "초")}</span>
338
+ </label>
339
+ </li>
340
+ );
341
+ }
@@ -3,19 +3,21 @@ import { useStudioShell } from "./studio-shell-context.js";
3
3
  import { logStore } from "./log-store.js";
4
4
  import "./dock.css";
5
5
  import LibrarySlot from "./LibrarySlot.jsx";
6
+ import BoardSlot from "./BoardSlot.jsx";
6
7
  import { ko } from "../locale.js";
7
8
  import Timeline from "../ardy/timeline.jsx";
8
9
  import { DEFAULT_PLAYBACK_SPEED, SHOT_ASPECT_PRESETS, sceneObjectNameDisplayKo } from "../app-stage.jsx";
9
10
  import { motionEditLayout, createMotionEdit } from "../ardy/motion-edit.js";
10
11
  import { trackFeature } from "../analytics.js";
11
12
  import { defaultRailRange } from "../camera-rail-schedule.js";
13
+ import { insertPathMark, pathMarks } from "../object-path.js";
12
14
 
13
15
  // G11: the dock resizes within [220, 480] px, and never so far that the 3D
14
16
  // viewport drops under 480 px (top bar 44 + status bar 24 + three 1 px gaps).
15
17
  // Never below the sequencer's own grid (shell.css --shell-dock-default, 276
16
18
  // px): a shorter dock would have to scroll its lanes.
17
- export const DOCK_MIN_HEIGHT = 276;
18
- export const DOCK_MAX_HEIGHT = 480;
19
+ const DOCK_MIN_HEIGHT = 276;
20
+ const DOCK_MAX_HEIGHT = 480;
19
21
  const VIEWPORT_MIN_HEIGHT = 480;
20
22
  const SHELL_FIXED_ROWS = 44 + 24 + 3;
21
23
  // v2: v1 heights were clamped to a 324px floor that left an empty band under
@@ -49,13 +51,25 @@ function readDockHeight() {
49
51
  }
50
52
 
51
53
  const DOCK_TABS = [
54
+ { key: "board", label: () => ko("Board", "보드") },
52
55
  { key: "animation", label: () => ko("Animation", "애니메이션") },
53
56
  { key: "assets", label: () => ko("Assets", "에셋") },
54
57
  ];
55
58
 
56
- /** Animation | Assets: both stay mounted, so switching keeps the Sequencer's
57
- * scroll and zoom and the Library's folder and search. */
58
- export default function BottomDock({ tab = "animation", onTabChange, embedded = false }) {
59
+ /** The dock's tabs for a project mode: a storyboard works in panels
60
+ * (Board | Assets), an animation in the Sequencer (Animation | Assets). */
61
+ function dockTabsFor(previsMode) {
62
+ const keys = previsMode === "storyboard" ? ["board", "assets"] : ["animation", "assets"];
63
+ return keys.map((key) => DOCK_TABS.find((entry) => entry.key === key));
64
+ }
65
+
66
+ /** Animation | Assets (Board | Assets in a storyboard): all stay mounted, so
67
+ * switching keeps the Sequencer's scroll and zoom and the Library's folder
68
+ * and search. Any stored tab the mode does not offer opens its first tab. */
69
+ export default function BottomDock({ tab: requestedTab = "animation", onTabChange, embedded = false }) {
70
+ const { previsMode } = useStudioShell();
71
+ const tabs = embedded ? dockTabsFor(null) : dockTabsFor(previsMode);
72
+ const tab = tabs.some((entry) => entry.key === requestedTab) ? requestedTab : tabs[0].key;
59
73
  const {
60
74
  tlFrame, motion, waypointMode, craneSelectedIndex,
61
75
  isCameraSelection, addActiveCranePoint, deleteSelectedCranePoint, setCraneSelectedIndex, tlFrameCount,
@@ -65,7 +79,7 @@ export default function BottomDock({ tab = "animation", onTabChange, embedded =
65
79
  resetMotionTrim, cutMotionAtPlayhead, changeMotionSegmentSpeed, removeMotionSegmentById, ikFrames,
66
80
  rangePins, rangePinSelection, ikEditTool, rangePinPreview, footSnap,
67
81
  bodyContact, shots, shotAspectKey, activeShotIdx, railDraw,
68
- pathDraw, selectedSceneObject, setPathDraw, setRailDraw, setWorkspaceLayout,
82
+ pathDraw, pathTarget, setPathDraw, setRailDraw, setWorkspaceLayout,
69
83
  changeSceneObject, timingTokenRef, beginSceneTransaction, endSceneTransaction, railCurve,
70
84
  posing, ikAddKeyframe, ikDeleteKeyframe, setRangePinSelection, setIkEditTool,
71
85
  setBodyContact, setToast, setFootSnap, setTlFrame, advanceFrame,
@@ -77,7 +91,7 @@ export default function BottomDock({ tab = "animation", onTabChange, embedded =
77
91
  activeCamera, activeShotDuration, changeActiveCamera, cameraRail, previewCameraShot,
78
92
  toggleCameraRailDraw, deleteCameraRail, selectTimelineShot, shotsDomain, runStudioAction,
79
93
  clearMotion, subscribeToasts, exportStatus, exportPhaseLabel, ardyRunning, ardyStatus,
80
- ardyOutcome,
94
+ ardyOutcome, pathPointIndex, setPathPointIndex,
81
95
  } = useStudioShell();
82
96
  const dockRef = useRef(null);
83
97
  const [dockHeight, setDockHeight] = useState(readDockHeight);
@@ -190,7 +204,7 @@ export default function BottomDock({ tab = "animation", onTabChange, embedded =
190
204
  onKeyDown={onDockResizeKey}
191
205
  />
192
206
  {!embedded && <div className="dock-tabs" role="tablist" aria-label={ko("Dock", "도크")}>
193
- {DOCK_TABS.map((entry) => (
207
+ {tabs.map((entry) => (
194
208
  <button
195
209
  type="button"
196
210
  role="tab"
@@ -204,6 +218,7 @@ export default function BottomDock({ tab = "animation", onTabChange, embedded =
204
218
  ))}
205
219
  </div>}
206
220
  {!embedded && <LibrarySlot active={tab === "assets"} />}
221
+ {!embedded && previsMode === "storyboard" && <BoardSlot active={tab === "board"} />}
207
222
  <div className="bottom-timeline" hidden={tab !== "animation"}>
208
223
  <Timeline
209
224
  frame={tlFrame}
@@ -222,8 +237,8 @@ export default function BottomDock({ tab = "animation", onTabChange, embedded =
222
237
  workflowMode={workflowMode === "pose" ? "motion" : workflowMode}
223
238
  waypointMode={waypointMode}
224
239
  waypoints={waypoints}
225
- pathSpeed={pathSpeed}
226
240
  pendingWaypointFrame={pendingWaypointFrame}
241
+ previsMode={previsMode}
227
242
  promptClips={promptClips}
228
243
  selectedPromptId={selectedPromptId}
229
244
  badge={stateBadge}
@@ -250,19 +265,46 @@ export default function BottomDock({ tab = "animation", onTabChange, embedded =
250
265
  activeShotIdx={activeShotIdx}
251
266
  railDraw={railDraw}
252
267
  pathDraw={pathDraw}
253
- pathObject={selectedSceneObject ? { id: selectedSceneObject.id, name: sceneObjectNameDisplayKo(selectedSceneObject.name), path: selectedSceneObject.path } : null}
268
+ pathObject={pathTarget ? { id: pathTarget.id, name: sceneObjectNameDisplayKo(pathTarget.name), path: pathTarget.path } : null}
254
269
  onObjectPathDrawToggle={() => {
255
270
  setPathDraw((current) => !current);
256
271
  if (!pathDraw) setRailDraw(false);
257
272
  setWorkspaceLayout((current) => ({ ...current, insetCollapsed: false }));
258
273
  }}
259
274
  onObjectPathChange={(path) => {
260
- if (selectedSceneObject) changeSceneObject(selectedSceneObject.id, { path }, timingTokenRef.current ?? undefined);
275
+ if (pathTarget) changeSceneObject(pathTarget.id, { path }, timingTokenRef.current ?? undefined);
276
+ }}
277
+ objectPathPointIndex={pathPointIndex}
278
+ onObjectMarkAdd={(t) => {
279
+ const path = pathTarget?.path;
280
+ const added = path ? insertPathMark(path, t) : null;
281
+ if (!added) return;
282
+ const token = beginSceneTransaction({ owner: "object-path", cancel: () => {} });
283
+ changeSceneObject(pathTarget.id, { path: { ...path, marks: added.marks } }, token);
284
+ endSceneTransaction(token, { commit: true });
285
+ setPathPointIndex(added.index);
286
+ }}
287
+ onObjectMarkLean={(key, value, token) => {
288
+ const path = pathTarget?.path;
289
+ if (!path || pathPointIndex == null || (key !== "bank" && key !== "pitch")) return;
290
+ const marks = pathMarks(path);
291
+ if (pathPointIndex < 1 || pathPointIndex > marks.length) return;
292
+ const next = { ...path, marks: marks.map((mark, i) => (i + 1 === pathPointIndex ? { ...mark, [key]: value } : mark)) };
293
+ // A scrub hands its own transaction; a typed value is one entry.
294
+ if (token != null) {
295
+ changeSceneObject(pathTarget.id, { path: next }, token);
296
+ return;
297
+ }
298
+ const own = beginSceneTransaction({ owner: "object-path", cancel: () => {} });
299
+ changeSceneObject(pathTarget.id, { path: next }, own);
300
+ endSceneTransaction(own, { commit: true });
261
301
  }}
302
+ onObjectMarkScrubStart={beginSceneTransaction}
303
+ onObjectMarkScrubEnd={endSceneTransaction}
262
304
  onObjectPathClear={() => {
263
- if (!selectedSceneObject) return;
305
+ if (!pathTarget) return;
264
306
  const token = beginSceneTransaction({ owner: "object-path", cancel: () => {} });
265
- changeSceneObject(selectedSceneObject.id, { path: null }, token);
307
+ changeSceneObject(pathTarget.id, { path: null }, token);
266
308
  endSceneTransaction(token, { commit: true });
267
309
  }}
268
310
  onObjectTimingGestureStart={() => {
@@ -372,7 +414,9 @@ export default function BottomDock({ tab = "animation", onTabChange, embedded =
372
414
  onClearMotion={motion ? clearMotion : null}
373
415
  />
374
416
  </div>
375
- {tab === "animation" && <ShotCard />}
417
+ {/* Embeds keep their own shot monitor in the viewport (.vp-shot-preview);
418
+ the card is styled for the full studio only. */}
419
+ {(tab === "animation" || tab === "board") && !embedded && <ShotCard />}
376
420
  </div>
377
421
  );
378
422
  }
@@ -1,16 +1,18 @@
1
1
  import { useStudioShell } from "./studio-shell-context.js";
2
2
  import { ko } from "../locale.js";
3
3
  import { sceneObjectNameDisplayKo, HIERARCHY_INSPECTOR_TITLES, CHARACTER_MODEL_LABELS } from "../app-stage.jsx";
4
- import { CUTOUT_KIND } from "../scene-objects.js";
4
+ import { CUTOUT_KIND, EMPTY_KIND, SCULPT_KIND } from "../scene-objects.js";
5
5
  import LightPanel from "../panels/LightPanel.jsx";
6
6
  import CameraPanel from "../panels/CameraPanel.jsx";
7
7
  import SubjectsPanel from "../panels/SubjectsPanel.jsx";
8
8
  import CharacterTransformPanel from "../panels/CharacterTransformPanel.jsx";
9
9
  import RigPanel from "../panels/RigPanel.jsx";
10
+ import ProxyFigurePanel from "../panels/ProxyFigurePanel.jsx";
10
11
  import PosePanel from "../panels/PosePanel.jsx";
11
12
  import VideoCapturePanel from "../panels/VideoCapturePanel.jsx";
12
13
  import PromptBlocksPanel from "../panels/PromptBlocksPanel.jsx";
13
14
  import RigControlPanel from "../panels/RigControlPanel.jsx";
15
+ import PoseImagePanel from "../panels/PoseImagePanel.jsx";
14
16
  import EnvironmentPanel from "../panels/EnvironmentPanel.jsx";
15
17
  import PropsPanel from "../panels/PropsPanel.jsx";
16
18
  import ObjectTransformPanel from "../panels/ObjectTransformPanel.jsx";
@@ -29,8 +31,8 @@ const characterTitle = (index) => ko(`Character ${index + 1}`, `인물 ${index +
29
31
  function detailsSelection({ selectedHierarchyId: id, selectedSceneObject, rigSelection, characters, shot, inspectorHasContent }) {
30
32
  if (!inspectorHasContent) return null;
31
33
  if (selectedSceneObject) {
32
- const kind = selectedSceneObject.renderer === CUTOUT_KIND ? ko("Cutout", "컷아웃") : ko("Mesh", "메시");
33
- return { name: sceneObjectNameDisplayKo(selectedSceneObject.name), type: `${ko("Prop", "소품")} · ${kind}` };
34
+ const kind = selectedSceneObject.renderer === CUTOUT_KIND ? ko("Cutout", "컷아웃") : selectedSceneObject.renderer === EMPTY_KIND ? ko("Empty", "빈 오브젝트") : selectedSceneObject.renderer === SCULPT_KIND ? ko("Sculpt", "조형") : ko("Mesh", "메시");
35
+ return { name: sceneObjectNameDisplayKo(selectedSceneObject.name), type: `${selectedSceneObject.renderer === EMPTY_KIND ? ko("Node", "노드") : ko("Prop", "소품")} · ${kind}` };
34
36
  }
35
37
  if (rigSelection) {
36
38
  const index = characterIndexForRow(rigSelection.rowId, characters);
@@ -55,7 +57,7 @@ function detailsSelection({ selectedHierarchyId: id, selectedSceneObject, rigSel
55
57
 
56
58
  export default function DetailsSlot() {
57
59
  const {
58
- selectedHierarchyId, sceneSaveError, studioAgentError, embedMode, studioAgentMode,
60
+ sculptPanel, selectedHierarchyId, sceneSaveError, studioAgentError, embedMode, studioAgentMode,
59
61
  setStudioAgentMode, AgentPanel, setAgentCollapsed, scenes, activeSceneId,
60
62
  buildStudioAgentContext, highlightAgentTargets, generateI2vMotionFromUi, selectedSceneObject, rigSelection,
61
63
  inspectorActionsOpen, setInspectorActionsOpen, runStudioAction, deleteSelectedSceneObject, inspectorHasContent,
@@ -80,12 +82,13 @@ export default function DetailsSlot() {
80
82
  generationBusy, bridgeChecking, lineReadinessState, runLineEdit, openMotionSetup,
81
83
  recheckMotionHealth, resetLineCurve, exitLineEditMode, readinessState, ardyRunning,
82
84
  cancelArdy, ardyStatus, ardyOutcome, tlFrame,
83
- isRigSelection, ikChains, ikFocus, footSnap, collisionCleanupSupported,
85
+ isRigSelection, ikChains, ikFocus, footSnap, poseObjectOpacity, setPoseObjectOpacity, capturePoseImage, collisionCleanupSupported,
84
86
  autoPhysicsRunning, physicsProgress, physicsPreview,
85
87
  physicsShow, physicsOptions, platformFitRunning, platformFitProgress, platformFitLast,
86
88
  platformFitApplied, changePhysicsOptions, runAutoPhysics, showPhysicsPreview, applyPhysicsPreview,
87
89
  cancelPhysicsPreview, ikEditTool, setIkEditTool, showTrails, setShowTrails,
88
- trailFalloffS, setTrailFalloffS, trailEdit, runTrailRegeneration, trailReadinessState,
90
+ trailFalloffS, setTrailFalloffS, trailEdit, trailTrackFocus, selectTrailTrack, clearTrailTrackFocus, runTrailRegeneration, trailReadinessState,
91
+ pendingIkEdit, applyPendingIkEdit, cancelPendingIkEdit,
89
92
  rangePins, rangePinResiduals, rangePinSelection, rangePinPartPick, rangePinPreview,
90
93
  sceneObjects, setRangePinSelection, setRangePinPartPick, previewRangePinDraft, applyRangePinDraft,
91
94
  deleteRangePin, hasEnvSheet, environment, style, environmentImage,
@@ -154,6 +157,8 @@ export default function DetailsSlot() {
154
157
  )}
155
158
  </div>
156
159
  <div className="inspector-scroll">
160
+ {/* Sculpt mode owns the column: its panel replaces the selection's settings. */}
161
+ {sculptPanel ?? (<>
157
162
  {/* Nothing is selected that owns settings — say so rather than
158
163
  showing an empty column the user has to interpret. */}
159
164
  {!inspectorHasContent && (
@@ -164,6 +169,7 @@ export default function DetailsSlot() {
164
169
  )}
165
170
  </p>
166
171
  )}
172
+ <PoseImagePanel hidden={workflowMode !== "pose"} capturePoseImage={capturePoseImage} />
167
173
  {/* Shot TYPE presets live in the viewport toolbar dropdown — not
168
174
  duplicated here. */}
169
175
 
@@ -204,6 +210,11 @@ export default function DetailsSlot() {
204
210
  activeChar={activeChar}
205
211
  />
206
212
 
213
+ <ProxyFigurePanel
214
+ isCharacterSelection={isCharacterSelection}
215
+ activeChar={activeChar}
216
+ />
217
+
207
218
  <PosePanel
208
219
  isCharacterSelection={isCharacterSelection}
209
220
  activeCharIndex={activeCharIndex}
@@ -306,6 +317,8 @@ export default function DetailsSlot() {
306
317
  ikChains={ikChains}
307
318
  ikFocus={ikFocus}
308
319
  footSnap={footSnap}
320
+ poseObjectOpacity={poseObjectOpacity}
321
+ setPoseObjectOpacity={setPoseObjectOpacity}
309
322
  ikMode={ikMode}
310
323
  collisionCleanupSupported={collisionCleanupSupported}
311
324
  motion={motion}
@@ -332,6 +345,12 @@ export default function DetailsSlot() {
332
345
  trailFalloffS={trailFalloffS}
333
346
  setTrailFalloffS={setTrailFalloffS}
334
347
  trailEdit={trailEdit}
348
+ trailTrackFocus={trailTrackFocus}
349
+ selectTrailTrack={selectTrailTrack}
350
+ clearTrailTrackFocus={clearTrailTrackFocus}
351
+ pendingIkEdit={pendingIkEdit}
352
+ applyPendingIkEdit={applyPendingIkEdit}
353
+ cancelPendingIkEdit={cancelPendingIkEdit}
335
354
  generationBusy={generationBusy}
336
355
  bridgeChecking={bridgeChecking}
337
356
  bridge={bridge}
@@ -400,6 +419,7 @@ export default function DetailsSlot() {
400
419
  objectColorDraft={objectColorDraft}
401
420
  setObjectColorDraft={setObjectColorDraft}
402
421
  />
422
+ </>)}
403
423
  </div>
404
424
  {selectedSceneObject && (
405
425
  <div className="inspector-footer">