cozyclay 2.0.2 → 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 (256) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +17 -2
  3. package/THIRD_PARTY_NOTICES.md +8 -0
  4. package/bin/agent/agent-routes.mjs +211 -36
  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 +4 -4
  19. package/bin/live/cli.mjs +2 -3
  20. package/bin/live/client.mjs +1 -1
  21. package/bin/live/studio.mjs +5 -2
  22. package/bin/mcp-runtime.mjs +2 -2
  23. package/bin/telemetry-state.mjs +3 -3
  24. package/dist/app/index.html +6 -6
  25. package/dist/assets/analytics-B41FmPYI.js +1 -0
  26. package/dist/assets/app-Do-Og5Pu.css +1 -0
  27. package/dist/assets/app-DwP-tl2x.js +4920 -0
  28. package/dist/assets/{demo-ByeYp-dY.js → demo-Dt0qZfS7.js} +1 -1
  29. package/dist/assets/{first-shot-handoff-DgT28ZiV.js → first-shot-handoff-BlT33CGf.js} +1 -1
  30. package/dist/assets/{landing-BaXkpFip.js → landing-BSesrCpL.js} +1 -1
  31. package/dist/assets/scene-asset-sync-BCWDJB1d.js +19 -0
  32. package/dist/assets/{shot-prompt-CbqWcWYi.css → scene-asset-sync-DjJ_cJ2R.css} +1 -1
  33. package/dist/assets/{ticket-BcmsnHn5.js → ticket-jqMhqL88.js} +1 -1
  34. package/dist/assets/workflow-Br-K7Srs.js +179 -0
  35. package/dist/cozyclay-package.json +1 -1
  36. package/dist/index.html +328 -222
  37. package/dist/sitemap.xml +1 -1
  38. package/dist/workflow/index.html +4 -4
  39. package/mcp/LIVE-PROTOCOL.md +23 -2
  40. package/mcp/README.md +37 -2
  41. package/mcp/ardy-prompts.mjs +1 -1
  42. package/mcp/image-file.mjs +132 -0
  43. package/mcp/live-hub.mjs +2 -2
  44. package/mcp/package.json +1 -0
  45. package/mcp/runtime/package-lock.json +7 -0
  46. package/mcp/runtime/package.json +1 -0
  47. package/mcp/server.mjs +0 -6
  48. package/mcp/tool-handlers.mjs +289 -87
  49. package/package.json +11 -2
  50. package/src/App.jsx +789 -263
  51. package/src/agent-image-client.js +94 -0
  52. package/src/analytics.js +47 -3
  53. package/src/app-stage.jsx +129 -2745
  54. package/src/ardy/auto-physics.js +6 -16
  55. package/src/ardy/collision-blockers.js +3 -7
  56. package/src/ardy/fix-collisions.js +5 -35
  57. package/src/ardy/ik-drag.js +86 -5
  58. package/src/ardy/ik-key-json.js +3 -1
  59. package/src/ardy/ik.js +15 -13
  60. package/src/ardy/motion-calibration.js +1 -1
  61. package/src/ardy/npz.js +0 -8
  62. package/src/ardy/physics-panel.jsx +3 -2
  63. package/src/ardy/physics-review.js +3 -2
  64. package/src/ardy/physics-support.js +1 -1
  65. package/src/ardy/physics-surface.js +1 -1
  66. package/src/ardy/platform-fit-panel.jsx +2 -1
  67. package/src/ardy/playback.js +1 -5
  68. package/src/ardy/range-pin.js +2 -2
  69. package/src/ardy/root-drop.js +0 -1
  70. package/src/ardy/timeline.css +184 -6
  71. package/src/ardy/timeline.jsx +113 -28
  72. package/src/ardy/to-cskel27.js +0 -2
  73. package/src/ardy/waypoints.js +0 -3
  74. package/src/ardy-bridge-contract.js +281 -0
  75. package/src/asset-pane.jsx +19 -9
  76. package/src/auto-color.js +1 -1
  77. package/src/burn-in.js +1 -1
  78. package/src/camera-block.js +1 -1
  79. package/src/camera-follow.js +75 -32
  80. package/src/camera-move.js +0 -6
  81. package/src/camera-tutorial.jsx +6 -7
  82. package/src/character-kind.js +79 -0
  83. package/src/command-bus.js +76 -17
  84. package/src/commands/cast.js +31 -7
  85. package/src/commands/elements/object.js +1 -1
  86. package/src/commands/elements/shot.js +1 -1
  87. package/src/commands/elements.js +1 -1
  88. package/src/commands/export.js +3 -3
  89. package/src/commands/motion.js +2 -2
  90. package/src/commands/objects.js +105 -10
  91. package/src/commands/project.js +15 -2
  92. package/src/commands/sculpt-images.js +53 -0
  93. package/src/commands/shot.js +88 -10
  94. package/src/commands/view.js +10 -4
  95. package/src/coplanar-depth.js +216 -0
  96. package/src/cuts.js +7 -6
  97. package/src/domains/cast.js +60 -43
  98. package/src/domains/motion/motion-intent-codec.js +50 -0
  99. package/src/domains/motion/motion-range-pin-rules.js +23 -0
  100. package/src/domains/motion/motion-video-capture.js +664 -0
  101. package/src/domains/motion.js +253 -742
  102. package/src/domains/objects.js +106 -5
  103. package/src/domains/scenes.js +79 -28
  104. package/src/domains/shots.js +33 -7
  105. package/src/error-boundary.jsx +1 -1
  106. package/src/execution-telemetry.js +5 -5
  107. package/src/facing-marks.js +14 -1
  108. package/src/first-success-guide.jsx +35 -4
  109. package/src/gizmo-claim.js +3 -1
  110. package/src/grid-floor.jsx +1 -4
  111. package/src/hierarchy-model.js +48 -35
  112. package/src/hierarchy-panel.css +1 -0
  113. package/src/hierarchy-panel.jsx +54 -102
  114. package/src/history.js +0 -2
  115. package/src/i2v-motion-client.js +1 -4
  116. package/src/ime.js +6 -0
  117. package/src/keyframe-pack.js +15 -6
  118. package/src/line-edit.js +4 -4
  119. package/src/live-control.js +6 -6
  120. package/src/matte-editor.js +2 -8
  121. package/src/matte.js +0 -19
  122. package/src/mesh-graph-clone.js +16 -2
  123. package/src/mesh-sniff.js +0 -1
  124. package/src/monotone-cubic.js +40 -0
  125. package/src/motion-store.js +4 -4
  126. package/src/motion-trail.js +5 -11
  127. package/src/multimodel-ingest.js +1 -3
  128. package/src/object-catalog.jsx +7 -5
  129. package/src/object-gizmo.jsx +45 -31
  130. package/src/object-path.js +310 -67
  131. package/src/object-travel.js +224 -0
  132. package/src/otio.js +8 -3
  133. package/src/panels/CharacterTransformPanel.jsx +24 -6
  134. package/src/panels/ObjectTransformPanel.jsx +4 -3
  135. package/src/panels/PoseImagePanel.jsx +205 -0
  136. package/src/panels/PromptBlocksPanel.jsx +3 -1
  137. package/src/panels/ProxyFigurePanel.jsx +56 -0
  138. package/src/panels/ReferenceImageField.jsx +1 -1
  139. package/src/panels/RigControlPanel.jsx +24 -3
  140. package/src/panels/RigPanel.jsx +2 -1
  141. package/src/panels/SculptPanel.jsx +405 -0
  142. package/src/panels/SubjectBox.jsx +1 -1
  143. package/src/panels/SubjectsPanel.jsx +0 -2
  144. package/src/panels/VideoCapturePanel.jsx +2 -1
  145. package/src/panels/pose-image.css +42 -0
  146. package/src/panels/pose.css +3 -0
  147. package/src/panels/sculpt.css +162 -0
  148. package/src/plan-diagram.js +184 -0
  149. package/src/planview.jsx +127 -80
  150. package/src/playground.js +3 -5
  151. package/src/pose-fade.js +77 -0
  152. package/src/pose-gizmo.css +23 -0
  153. package/src/pose-gizmo.js +96 -0
  154. package/src/posestudio.jsx +134 -83
  155. package/src/project-browser.css +53 -0
  156. package/src/project-browser.jsx +49 -4
  157. package/src/project.js +55 -24
  158. package/src/props.jsx +177 -36
  159. package/src/proxy-figure.jsx +69 -0
  160. package/src/range-pin-panel.jsx +0 -1
  161. package/src/receipt-findings.js +20 -0
  162. package/src/render-passes.js +0 -1
  163. package/src/resource-status.jsx +1 -1
  164. package/src/root-path.js +48 -0
  165. package/src/route-owner.js +52 -0
  166. package/src/scene-asset-cache.js +1 -2
  167. package/src/scene-assets.js +12 -4
  168. package/src/scene-fbx.js +1 -1
  169. package/src/scene-mesh-cache.js +0 -1
  170. package/src/scene-objects.js +319 -19
  171. package/src/scenes.js +58 -9
  172. package/src/sculpt-blueprint.js +65 -0
  173. package/src/sculpt-compare.js +384 -0
  174. package/src/sculpt-geometry.js +141 -0
  175. package/src/sculpt-mode.js +168 -0
  176. package/src/sculpt-recipe.js +385 -0
  177. package/src/sculpt-run.js +351 -0
  178. package/src/sculpt-stage.jsx +221 -0
  179. package/src/settings-menu.jsx +1 -1
  180. package/src/shell/BoardSlot.jsx +341 -0
  181. package/src/shell/BottomDock.jsx +58 -14
  182. package/src/shell/DetailsSlot.jsx +26 -6
  183. package/src/shell/MenuBar.jsx +78 -6
  184. package/src/shell/OutlinerSlot.jsx +3 -1
  185. package/src/shell/TopBar.jsx +17 -3
  186. package/src/shell/ViewportToolbar.jsx +18 -3
  187. package/src/shell/dock.css +239 -0
  188. package/src/shell/glass-regions.css +3 -2
  189. package/src/shell/log-store.js +4 -4
  190. package/src/shell/mode.css +20 -0
  191. package/src/shell/studio-shell-context.js +4 -1
  192. package/src/shot-authoring.js +69 -6
  193. package/src/stable-items.js +1 -1
  194. package/src/stage-attach-frames.js +169 -0
  195. package/src/stage-canvas-rigs.jsx +402 -0
  196. package/src/stage-capture.jsx +216 -0
  197. package/src/stage-characters.jsx +302 -0
  198. package/src/stage-editor-gizmos.jsx +1229 -0
  199. package/src/stage-labels-ko.js +204 -0
  200. package/src/stage-startup-defaults.js +182 -0
  201. package/src/starter-scenes.js +1 -1
  202. package/src/storyboard.js +24 -5
  203. package/src/studio-actions.js +42 -9
  204. package/src/studio-agent-commands.js +186 -23
  205. package/src/studio-agent-context.js +11 -9
  206. package/src/studio-agent-motion.js +2 -1
  207. package/src/studio-agent-protocol.js +56 -23
  208. package/src/studio-app-binding.js +52 -23
  209. package/src/studio-contact-sheet.js +3 -3
  210. package/src/studio-elements.js +11 -2
  211. package/src/studio-geometry-facts.js +114 -0
  212. package/src/styles.css +92 -9
  213. package/src/ui-scrub.js +4 -4
  214. package/src/ui.jsx +6 -4
  215. package/src/use-asset-shelf.js +60 -0
  216. package/src/use-camera-tutorial.js +50 -0
  217. package/src/use-embed-bridge.js +12 -0
  218. package/src/use-first-success-prompt.js +7 -0
  219. package/src/use-i2v-capture-modal.js +15 -0
  220. package/src/use-sculpt-mode.js +95 -0
  221. package/src/workflow/AgentPanel.jsx +106 -18
  222. package/src/workflow/WorkflowBuilder.jsx +1 -1
  223. package/src/workflow/agent-client.js +113 -45
  224. package/src/workflow/agent-panel.css +35 -0
  225. package/src/workflow/attachment-image.js +1 -1
  226. package/src/workflow/canvas-commands.js +3 -3
  227. package/src/workflow/cozy-scene-node.js +3 -3
  228. package/src/workflow/local-workflow.js +2 -2
  229. package/src/workflow/motion-input.js +3 -3
  230. package/src/workflow/node-schema.js +1 -1
  231. package/src/workflow/scene-asset-sync.js +2 -9
  232. package/src/workflow/shot-prompt-node.js +1 -1
  233. package/src/workflow/video-contract.js +1 -1
  234. package/src/workflow/workflow-resources.js +0 -8
  235. package/src/workflow-send.js +87 -0
  236. package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
  237. package/tools/check-eslint.mjs +57 -0
  238. package/tools/check-knip.mjs +60 -0
  239. package/tools/check-loc.mjs +54 -0
  240. package/tools/demo-worker/api-client.mjs +7 -13
  241. package/tools/demo-worker/generate.mjs +2 -4
  242. package/tools/eslint-baseline.json +23 -0
  243. package/tools/kimodo/effector-constraints.mjs +4 -4
  244. package/tools/kimodo/generate.mjs +1 -1
  245. package/tools/knip-baseline.json +331 -0
  246. package/tools/loc-baseline.json +126 -0
  247. package/tools/process-supervisor.mjs +1 -1
  248. package/tools/projflow/replay.mjs +4 -4
  249. package/tools/projflow/runner.mjs +3 -3
  250. package/tools/qa/studio-control-count.mjs +2 -0
  251. package/tools/run-tests.mjs +94 -7
  252. package/dist/assets/analytics-MVZOEW8O.js +0 -1
  253. package/dist/assets/app-BxBLC6GT.js +0 -4876
  254. package/dist/assets/app-CJdxvnrl.css +0 -1
  255. package/dist/assets/shot-prompt-hN0j_O-p.js +0 -17
  256. package/dist/assets/workflow-B5XZwVfe.js +0 -179
@@ -12,7 +12,8 @@ import {
12
12
  captureHipsOffset,
13
13
  saveCustomPoses,
14
14
  } from "../poses.js";
15
- import { createCharacterEntry, createCharacterLayer } from "../scenes.js";
15
+ import { createCharacterEntry, createCharacterLayer, isProxyFigure, PROXY_FIGURE_MODEL } from "../scenes.js";
16
+ import { kindRefusal } from "../character-kind.js";
16
17
  import {
17
18
  DEFAULT_SUBJECT,
18
19
  DEFAULT_SUBJECT2,
@@ -28,12 +29,12 @@ import { judgeNextWaypoint } from "../ardy/waypoints.js";
28
29
  import { StudioProtocolError } from "../studio-agent-protocol.js";
29
30
  import { studioActionRefusal } from "../studio-actions.js";
30
31
  import { createStableItemId, removeStableItem } from "../stable-items.js";
31
- import { trackFeature } from "../analytics.js";
32
+ import { track, trackFeature } from "../analytics.js";
32
33
  import { requestBridgeExtract } from "../multimodel-ingest.js";
33
34
  import { loadMotionFromUrl } from "../ardy/npz.js";
34
35
  import { snapshotPlaybackBones, applyMotionFrame, restorePlaybackBones } from "../ardy/playback.js";
35
36
 
36
- import { HISTORY_LIMIT } from "../history.js";
37
+ import { shotAtFrame } from "../cuts.js";
37
38
 
38
39
  const sameCastValue = (a, b) => JSON.stringify(a) === JSON.stringify(b);
39
40
 
@@ -43,7 +44,7 @@ export function createCastDomain(appContext, initial, customPoses = []) {
43
44
  let native = createDocumentStore({ owned: { cast: normalize(initial) } });
44
45
  const listeners = new Set(), motions = new Map();
45
46
  const notify = () => { for (const listener of listeners) listener(); };
46
- let release = native.subscribe(notify), loading = false;
47
+ let release = native.subscribe(notify);
47
48
  const documentStore = { ...Object.fromEntries(Object.keys(native).map(key => [key, (...args) => native[key](...args)])),
48
49
  subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener); } };
49
50
  const state = () => documentStore.read('cast'), read = () => state().characters;
@@ -74,6 +75,25 @@ export function createCastDomain(appContext, initial, customPoses = []) {
74
75
  function write(update) {
75
76
  return writeState(before => ({ ...before, characters: typeof update === 'function' ? update(before.characters) : update }));
76
77
  }
78
+ function update(characterId, patch) {
79
+ const entry = read().find(entry => entry.id === characterId);
80
+ if (!entry) throw new StudioProtocolError('STALE_TARGET', `Character ${characterId} is not in this scene.`);
81
+ const raw = appContext.ports.read(), shot = shotAtFrame(raw.shots, raw.view.frame);
82
+ const fields = ['x', 'z', 'rot', 'posture', 'pose'];
83
+ const panelPatch = Object.fromEntries(Object.entries(patch).filter(([key, value]) => fields.includes(key) && value !== undefined));
84
+ if (shot?.kind === 'still' && !raw.targets?.get?.(characterId)?.motion && Object.keys(panelPatch).length) {
85
+ const current = { ...entry, ...shot.cast[characterId] };
86
+ const normalized = createCharacterEntry({ ...current, ...panelPatch });
87
+ const override = { x: normalized.x, z: normalized.z, rot: normalized.rot };
88
+ for (const key of ['posture', 'pose']) if (normalized[key] != null) override[key] = normalized[key];
89
+ // Enlist the shot owner in the already-open command/drag session:
90
+ // even a mixed scale + placement edit is one shared Undo entry.
91
+ const shots = appContext.storeDomain('shot');
92
+ appContext.recordAction('shot', () => shots.setCastOverride(shot.id, characterId, override), null, true);
93
+ patch = Object.fromEntries(Object.entries(patch).filter(([key]) => !fields.includes(key)));
94
+ }
95
+ if (Object.keys(patch).length) write(rows => rows.map(row => row.id === characterId ? { ...row, ...patch } : row));
96
+ }
77
97
  function publishMotion(characterId, motion) {
78
98
  if (motion) motions.set(characterId, motion); else motions.delete(characterId);
79
99
  publish(); notify();
@@ -109,7 +129,7 @@ export function createCastDomain(appContext, initial, customPoses = []) {
109
129
  }
110
130
  return run('run.update', { txId: gesture.txId, args });
111
131
  }
112
- const domain = { documentStore, state, read, write, writeState, run, edit, beginGesture, finishGesture,
132
+ const domain = { documentStore, state, read, write, update, writeState, run, edit, beginGesture, finishGesture,
113
133
  activeId: read()[0]?.id ?? null, projection: () => projection, publishMotion, normalizeCharacters: rows => rows.map(createCharacterEntry),
114
134
  bindRender(context) { appContext = context; },
115
135
  beginAction: () => documentStore.beginAction('cast'), canUndo: id => documentStore.canUndo(id),
@@ -121,13 +141,11 @@ export function createCastDomain(appContext, initial, customPoses = []) {
121
141
  write(plan.draft); return plan;
122
142
  },
123
143
  load(value) {
124
- finishGesture(true); loading = true;
125
- try {
126
- release(); native.dispose(); motions.clear();
127
- const next = normalize(Array.isArray(value) ? { characters: value, customPoses: state().customPoses } : value);
128
- native = createDocumentStore({ owned: { cast: next } }); release = native.subscribe(notify);
129
- domain.activeId = read()[0]?.id ?? null; domain.loadView?.(); notify();
130
- } finally { loading = false; }
144
+ finishGesture(true);
145
+ release(); native.dispose(); motions.clear();
146
+ const next = normalize(Array.isArray(value) ? { characters: value, customPoses: state().customPoses } : value);
147
+ native = createDocumentStore({ owned: { cast: next } }); release = native.subscribe(notify);
148
+ domain.activeId = read()[0]?.id ?? null; domain.loadView?.(); notify();
131
149
  },
132
150
  dispose() { finishGesture(true); unregister(); unsubscribe(); release(); native.dispose(); listeners.clear(); },
133
151
  };
@@ -164,7 +182,6 @@ export function useCast(appContext) {
164
182
 
165
183
  const [poseRevision, setPoseTick] = useState(0);
166
184
 
167
- /* --------------------- derived cast view + shims ---------------------- */
168
185
  // Fallback second slot mirrors the old charB defaults so preset math and
169
186
  // the two-subject inspector never see a hole before B exists.
170
187
  const charA = characters[0] ?? createCharacterEntry(null, 0);
@@ -181,29 +198,12 @@ export function useCast(appContext) {
181
198
 
182
199
  const subject2 = charB.subject ?? DEFAULT_SUBJECT2;
183
200
 
184
- const rigA = rigs[charA.id] ?? null;
185
-
186
- const rigB = (characters[1] ? rigs[charB.id] : null) ?? null;
187
-
188
201
  function updateCharacterAt(index, next) {
189
- const entry = domain.read()[index];
202
+ const base = domain.read()[index], raw = appContext.ports.read();
203
+ const entry = base && { ...base, ...shotAtFrame(raw.shots, raw.view.frame)?.cast[base.id] };
190
204
  if (entry) return domain.edit('character.update', { characterId: entry.id, patch: typeof next === 'function' ? next(entry) : next });
191
205
  }
192
206
 
193
- // The shims keep the legacy call sites (inspector sliders, presets, pose
194
- // studio, prompts) untouched while the list stays the source of truth.
195
- const setCharA = (next) => updateCharacterAt(0, next);
196
-
197
- const setCharB = (next) => updateCharacterAt(1, next);
198
-
199
- const setPoseA = (pose) => updateCharacterAt(0, (entry) => ({ pose: typeof pose === "function" ? pose(entry.pose ?? DEFAULT_POSE) : pose }));
200
-
201
- const setPoseB = (pose) => updateCharacterAt(1, (entry) => ({ pose: typeof pose === "function" ? pose(entry.pose ?? DEFAULT_POSE) : pose }));
202
-
203
- const setSubject = (value) => updateCharacterAt(0, (entry) => ({ subject: typeof value === "function" ? value(entry.subject) : value }));
204
-
205
- const setSubject2 = (value) => updateCharacterAt(1, (entry) => ({ subject: typeof value === "function" ? value(entry.subject) : value }));
206
-
207
207
  function setShowB(next) {
208
208
  return domain.run('cast.showExtras', { show: typeof next === 'function' ? next(domain.read().some((entry, i) => i > 0 && !entry.hidden)) : next });
209
209
  }
@@ -221,7 +221,8 @@ export function useCast(appContext) {
221
221
  }
222
222
 
223
223
  function moveCharacter(charId, next) {
224
- const entry = domain.read().find(entry => entry.id === charId);
224
+ const base = domain.read().find(entry => entry.id === charId), raw = appContext.ports.read();
225
+ const entry = { ...base, ...shotAtFrame(raw.shots, raw.view.frame)?.cast[charId] };
225
226
  return domain.edit('character.update', { characterId: charId, patch: typeof next === 'function' ? next(entry) : next });
226
227
  }
227
228
 
@@ -258,6 +259,7 @@ export function useCast(appContext) {
258
259
  const spawnCharacter = (model, x, z) => {
259
260
  const id = nextCharacterId(domain.read());
260
261
  domain.run('character.add', { character: { id, model, x, z, pose: DEFAULT_POSE, subject: 'a person' } });
262
+ if (model === PROXY_FIGURE_MODEL) track("cast:proxy_added", { surface: "assets" });
261
263
  appContext.shared.setSelectedHierarchyId(`character:${id}`);
262
264
  appContext.notify(ko("Character added to the scene", "인물을 씬에 추가했어요"));
263
265
  };
@@ -270,7 +272,6 @@ export function useCast(appContext) {
270
272
  return `character:${key.startsWith("char:") ? key.slice(5) : key}`;
271
273
  };
272
274
 
273
- /* ------------------- active character (motion layer) ------------------- */
274
275
  // Every character owns an animation layer (root path, prompt blocks,
275
276
  // generated clip, IK keys). The studio's motion machinery edits ONE layer
276
277
  // at a time — the ACTIVE character's — and selection decides who that is.
@@ -332,9 +333,6 @@ export function useCast(appContext) {
332
333
  waypointFrames: (entry.layer?.waypoints ?? []).map((waypoint) => waypoint.frame),
333
334
  }]), [characters, activeChar.id]);
334
335
 
335
- // Undo/redo (plan §6.5). The store settles any open drag first, so a
336
- // mid-drag press commits that drag as one entry and then steps past it.
337
- // After a step the selection can point at a deleted object — drop it to
338
336
  /* Cast authoring uses the registered store. The native compatibility
339
337
  * snapshot remains for motion/IK until its owner migrates; it must not
340
338
  * restore the owned cast over newer character edits. */
@@ -424,10 +422,9 @@ export function useCast(appContext) {
424
422
  const posedRig = () => rigs[posing] ?? null;
425
423
 
426
424
  const setPosed = (pose) => {
427
- if (posingIndex >= 0) updateCharacterAt(posingIndex, { pose: typeof pose === "function" ? pose(posingChar?.pose ?? DEFAULT_POSE) : pose });
425
+ if (posingIndex >= 0) updateCharacterAt(posingIndex, entry => ({ pose: typeof pose === "function" ? pose(entry.pose ?? DEFAULT_POSE) : pose }));
428
426
  };
429
427
 
430
- /* ------------------------- waypoint workspace --------------------------- */
431
428
  // A walking pace turns clicked distance into clip time, so pins land at
432
429
  // frames the character can actually reach without ice-skating.
433
430
  const WALK_SPEED_MPS = 1.4;
@@ -681,6 +678,23 @@ export function useCast(appContext) {
681
678
  : ko(`Saved the current pose to the library as “${pose.label}”`, `지금 자세를 “${pose.label}”로 라이브러리에 저장했어요`));
682
679
  }
683
680
 
681
+ /** A handle drag is an authored pose. When it ends, the rig as it now
682
+ * stands becomes the posed character's own pose — so the scene autosaves
683
+ * it and undo can step it back — instead of living only on the rig until
684
+ * a reload re-applies the stored pose over it. The library is untouched;
685
+ * Save pose still bottles a pose there. */
686
+ function commitPosedPose() {
687
+ const rig = posedRig();
688
+ if (!rig || !posingChar) return;
689
+ domain.run('character.setPose', { characterId: posingChar.id, clearMotion: false, pose: {
690
+ id: `edited:${posingChar.id}`,
691
+ label: ko("Edited pose", "편집한 포즈"),
692
+ prompt: "in the exact body pose shown in the blocking frame",
693
+ bones: capturePose(rig),
694
+ rootY: captureHipsOffset(rig),
695
+ } });
696
+ }
697
+
684
698
  function savePose() {
685
699
  const rig = posedRig();
686
700
  if (!rig) return;
@@ -720,6 +734,8 @@ export function useCast(appContext) {
720
734
  setPhotoPoseState("running");
721
735
  setPhotoPoseError("");
722
736
  try {
737
+ // A capsule figure has no bones for a photo to pose.
738
+ if (isProxyFigure(posingChar ?? activeChar)) throw new StudioProtocolError("TARGET_NOT_READY", kindRefusal("photoPose", isKo));
723
739
  if (!rig) throw new Error("rig-not-loaded");
724
740
  objectUrl = URL.createObjectURL(file);
725
741
  let bones = null;
@@ -907,7 +923,9 @@ export function useCast(appContext) {
907
923
  const live = appContext.live.state;
908
924
  const patch = finitePatch(args, ["x", "z", "rot"]);
909
925
  const id = nextCharacterId(live.characters);
910
- replaceCharacters([...live.characters, createCharacterEntry({ id, model: args.model, subject: args.subject, pose: DEFAULT_POSE, ...patch }, live.characters.length)]);
926
+ const entry = createCharacterEntry({ id, model: args.model, subject: args.subject, pose: DEFAULT_POSE, ...patch }, live.characters.length);
927
+ replaceCharacters([...live.characters, entry]);
928
+ if (isProxyFigure(entry)) track("cast:proxy_added", { surface: "mcp" });
911
929
  return { id };
912
930
  }
913
931
  function updateLiveCharacter(args) {
@@ -947,7 +965,6 @@ export function useCast(appContext) {
947
965
  if (typeof block.text !== "string" || !block.text.trim()) throw new Error(`Block ${i + 1} needs text`);
948
966
  return { id: `prompt-${stamp}-${i}`, startFrame, endFrame, text: block.text.trim() };
949
967
  });
950
- const live = appContext.live.state;
951
968
  domain.run('character.setPromptBlocks', { characterId: domain.activeId, blocks: clips });
952
969
  return { blocks: clips.length };
953
970
  }
@@ -982,14 +999,14 @@ export function useCast(appContext) {
982
999
  const motion = appContext.storeDomain('motion');
983
1000
  if (motion) motion.clear(characterId); else appContext.shared.motionDomain.clearMotionNative();
984
1001
  }, characterId, true);
985
- domain.write(rows => rows.map(entry => entry.id === characterId ? { ...entry, pose } : entry));
1002
+ domain.update(characterId, { pose });
986
1003
  };
987
1004
  appContext.updateActionPorts({ addCharacterWaypoint, moveCharacterWaypoint, removeCharacterWaypoint, clearCharacterWaypoints, setWaypointMode });
988
1005
  return {
989
1006
  ...domain,
990
1007
  applyExternalCharacters, publishStudioCharacters, syncStudioLayerBuffer, undoScene, redoScene, snapshotStudioDomain, removeLegacyRootWaypoint, switchActiveCharacterLayer, createLegacyCastHandlers, toggleCharacterHidden,
991
1008
  characters, editCharacters, customPoses, posing, setPosing, posingClosing,
992
- studioPick, setStudioPick, rigs, rigMountEpoch, setRigMountEpoch, setPoseTick, charA, charB, showB,
1009
+ studioPick, setStudioPick, commitPosedPose, rigs, rigMountEpoch, setRigMountEpoch, setPoseTick, charA, charB, showB,
993
1010
  poseA, poseB, subject, subject2, updateCharacterAt, setShowB, moveCharacter, removeCharacter, reportRig,
994
1011
  spawnCharacter, charKeyToHierarchyId, charIdFromHierarchyId, activeCharacterId, setActiveCharacterId,
995
1012
  rowIdForCharIndex, activeChar, selectActiveCharacterInHierarchy, activeCharIndex, activeRig, waitForRig,
@@ -0,0 +1,50 @@
1
+ import * as THREE from "three";
2
+
3
+ const sameMotionIntent = (a, b) => JSON.stringify(a) === JSON.stringify(b);
4
+ const emptyMotionLayer = id => ({ id, take: null, fullTake: null, ikKeys: [], ikPins: [], ikPinResiduals: [], committedIkEdits: [], takeRecipe: null, takeVersions: [] });
5
+ const keyVector = p => ({ x: p.x, y: p.y, z: p.z });
6
+ const keyQuaternion = q => ({ ...keyVector(q), w: q.w });
7
+ function encodeMotionKeys(keys) {
8
+ return [...keys].sort(([a], [b]) => a - b).map(([frame, tracks]) => ({ frame, tracks: Object.fromEntries([...tracks].map(([track, key]) => [track, {
9
+ ...(key.q ? { q: key.q.map(keyQuaternion) } : {}), ...(key.p ? { p: keyVector(key.p) } : {}),
10
+ ...(key.baseQ ? { baseQ: key.baseQ.map(keyQuaternion) } : {}), ...(key.basePos ? { basePos: keyVector(key.basePos) } : {}),
11
+ ...(key.chainP ? { chainP: key.chainP.map(keyVector) } : {}), ...(key.keepTranslations ? { keepTranslations: true } : {}),
12
+ ...(key.correctionRange ? { correctionRange: { start: key.correctionRange.start, end: key.correctionRange.end } } : {}),
13
+ ...(key.blend != null ? { blend: key.blend } : {}),
14
+ ...(key.pin ? { pin: key.pin } : {}),
15
+ }])) }));
16
+ }
17
+ function decodeMotionKeys(rows) {
18
+ const q = value => new THREE.Quaternion(value.x, value.y, value.z, value.w).normalize();
19
+ const p = value => new THREE.Vector3(value.x, value.y, value.z);
20
+ return new Map(rows.map(row => [row.frame, new Map(Object.entries(row.tracks).map(([track, key]) => [track, {
21
+ q: key.q?.map(q) ?? null, p: key.p ? p(key.p) : null,
22
+ ...(key.baseQ ? { baseQ: key.baseQ.map(q) } : {}), ...(key.basePos ? { basePos: p(key.basePos) } : {}),
23
+ ...(key.chainP ? { chainP: key.chainP.map(p) } : {}), ...(key.keepTranslations ? { keepTranslations: true } : {}),
24
+ ...(key.correctionRange ? { correctionRange: { start: key.correctionRange.start, end: key.correctionRange.end } } : {}),
25
+ ...(key.blend != null ? { blend: key.blend } : {}),
26
+ ...(key.pin ? { pin: key.pin } : {}),
27
+ }]))]));
28
+ }
29
+ function cloneRangePin(pin) {
30
+ return {
31
+ ...pin,
32
+ target: pin.target.space === "world"
33
+ ? { space: "world", position: [...pin.target.position] }
34
+ : { space: "object", objectId: pin.target.objectId, local: [...pin.target.local] },
35
+ };
36
+ }
37
+ function encodeRangePins(pins) {
38
+ return [...(pins instanceof Map ? pins.values() : pins ?? [])].map(cloneRangePin);
39
+ }
40
+ function decodeRangePins(rows) {
41
+ return new Map((rows ?? []).map((pin) => [pin.id, cloneRangePin(pin)]));
42
+ }
43
+ function encodeRangePinResiduals(values) {
44
+ return [...(values instanceof Map ? values.entries() : values ?? [])].map(([id, entries]) => [id, (entries ?? []).map((entry) => ({ ...entry }))]);
45
+ }
46
+ function decodeRangePinResiduals(rows) {
47
+ return new Map((rows ?? []).map(([id, entries]) => [id, (entries ?? []).map((entry) => ({ ...entry }))]));
48
+ }
49
+
50
+ export { sameMotionIntent, emptyMotionLayer, encodeMotionKeys, decodeMotionKeys, cloneRangePin, encodeRangePins, decodeRangePins, encodeRangePinResiduals, decodeRangePinResiduals };
@@ -0,0 +1,23 @@
1
+ import { rangePinTracks } from "../../ardy/range-pin.js";
2
+ import { ko } from "../../locale.js";
3
+
4
+ export function ensureRangePinState(state) {
5
+ if (!state) return state;
6
+ if (!(state.pins instanceof Map)) state.pins = new Map();
7
+ if (!(state.pinResiduals instanceof Map)) state.pinResiduals = new Map();
8
+ return state;
9
+ }
10
+ export function rangePinConflictFrames(draft, state) {
11
+ if (!Number.isInteger(draft?.startFrame) || !Number.isInteger(draft?.endFrame) || draft.endFrame < draft.startFrame) return [];
12
+ const tracks = rangePinTracks(draft), conflicts = [];
13
+ for (let frame = draft.startFrame; frame <= draft.endFrame; frame += 1) {
14
+ if (tracks.some((track) => { const key = state.keys.get(frame)?.get(track); return key && key.pin !== draft.id; })) conflicts.push(frame);
15
+ }
16
+ return conflicts;
17
+ }
18
+ export function overlappingRangePin(draft, state) {
19
+ return [...ensureRangePinState(state).pins.values()].find((pin) => pin.id !== draft.id && rangePinTracks(pin).some((track) => rangePinTracks(draft).includes(track)) && Math.max(pin.startFrame, draft.startFrame) <= Math.min(pin.endFrame, draft.endFrame));
20
+ }
21
+ export function rangePinOverlapMessage(pin) {
22
+ return pin ? ko(`A required body or limb track already has an overlapping pin (${pin.startFrame}–${pin.endFrame}).`, `필요한 몸통 또는 팔다리 파츠에 겹치는 고정이 있어요 (${pin.startFrame}–${pin.endFrame}).`) : "";
23
+ }