cozyclay 1.10.0 → 2.0.2

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 (275) hide show
  1. package/CHANGELOG.md +193 -0
  2. package/bin/agent/agent-routes.mjs +82 -72
  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/codex-auth.mjs +2 -1
  10. package/bin/cozyclay.mjs +38 -11
  11. package/bin/live/cli.mjs +22 -3
  12. package/bin/mcp-runtime.mjs +45 -20
  13. package/bin/telemetry-state.mjs +5 -2
  14. package/bin/update-check.mjs +15 -0
  15. package/dist/app/index.html +6 -6
  16. package/dist/assets/analytics-MVZOEW8O.js +1 -0
  17. package/dist/assets/app-BxBLC6GT.js +4876 -0
  18. package/dist/assets/app-CJdxvnrl.css +1 -0
  19. package/dist/assets/{demo-B-eg1CT8.js → demo-ByeYp-dY.js} +1 -1
  20. package/dist/assets/{first-shot-handoff-CGgnvNu8.js → first-shot-handoff-DgT28ZiV.js} +1 -1
  21. package/dist/assets/{landing-IbRp3FnQ.js → landing-BaXkpFip.js} +1 -1
  22. package/dist/assets/shot-prompt-CbqWcWYi.css +1 -0
  23. package/dist/assets/shot-prompt-hN0j_O-p.js +17 -0
  24. package/dist/assets/{ticket-BgIzOBOf.js → ticket-BcmsnHn5.js} +1 -1
  25. package/dist/assets/workflow-B5XZwVfe.js +179 -0
  26. package/dist/assets/workflow-DRdwoDBT.css +1 -0
  27. package/dist/cozyclay-package.json +1 -1
  28. package/dist/fonts/IBM-Plex-Sans-OFL.txt +92 -0
  29. package/dist/fonts/JetBrains-Mono-OFL.txt +93 -0
  30. package/dist/fonts/README.md +13 -9
  31. package/dist/fonts/ibm-plex-sans-400-latin.woff2 +0 -0
  32. package/dist/fonts/ibm-plex-sans-500-latin.woff2 +0 -0
  33. package/dist/fonts/ibm-plex-sans-600-latin.woff2 +0 -0
  34. package/dist/fonts/jetbrains-mono-400-latin.woff2 +0 -0
  35. package/dist/fonts/jetbrains-mono-500-latin.woff2 +0 -0
  36. package/dist/index.html +7 -7
  37. package/dist/privacy/index.html +13 -8
  38. package/dist/sitemap.xml +2 -2
  39. package/dist/workflow/index.html +5 -5
  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 +219 -376
  44. package/package.json +2 -1
  45. package/src/App.jsx +1275 -9086
  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/fix-collisions.js +10 -66
  51. package/src/ardy/ik-drag.js +101 -0
  52. package/src/ardy/ik-key-json.js +43 -0
  53. package/src/ardy/ik.js +97 -67
  54. package/src/ardy/physics-panel.css +2 -0
  55. package/src/ardy/physics-panel.jsx +17 -5
  56. package/src/ardy/platform-fit-panel.css +64 -0
  57. package/src/ardy/platform-fit-panel.jsx +40 -0
  58. package/src/ardy/platform-fit.js +326 -0
  59. package/src/ardy/playback.js +166 -20
  60. package/src/ardy/range-pin.js +299 -0
  61. package/src/ardy/timeline-coordinates.js +26 -0
  62. package/src/ardy/timeline.css +1214 -0
  63. package/src/ardy/timeline.jsx +152 -41
  64. package/src/ardy/waypoints.js +3 -3
  65. package/src/asset-pane.css +462 -0
  66. package/src/asset-pane.jsx +461 -232
  67. package/src/camera-tutorial.jsx +38 -18
  68. package/src/command-bus.js +349 -0
  69. package/src/commands/ai.js +31 -0
  70. package/src/commands/cast.js +140 -0
  71. package/src/commands/elements/character.js +13 -0
  72. package/src/commands/elements/motion.js +14 -0
  73. package/src/commands/elements/object.js +22 -0
  74. package/src/commands/elements/scene.js +16 -0
  75. package/src/commands/elements/shot.js +9 -0
  76. package/src/commands/elements/stage.js +11 -0
  77. package/src/commands/elements.js +155 -0
  78. package/src/commands/export.js +20 -0
  79. package/src/commands/index.js +40 -0
  80. package/src/commands/motion.js +231 -0
  81. package/src/commands/objects.js +174 -0
  82. package/src/commands/project.js +53 -0
  83. package/src/commands/scene.js +81 -0
  84. package/src/commands/shared.js +19 -0
  85. package/src/commands/shot.js +103 -0
  86. package/src/commands/stage.js +17 -0
  87. package/src/commands/view.js +42 -0
  88. package/src/document-store.js +144 -0
  89. package/src/domains/cast.js +1007 -0
  90. package/src/domains/motion.js +3219 -0
  91. package/src/domains/objects.js +932 -0
  92. package/src/domains/scenes.js +823 -0
  93. package/src/domains/shots.js +404 -0
  94. package/src/domains/stage.js +93 -0
  95. package/src/dualview.jsx +113 -57
  96. package/src/facing-marks.js +3 -0
  97. package/src/first-success-guide.jsx +14 -14
  98. package/src/grid-view.js +18 -5
  99. package/src/hierarchy-panel.css +386 -0
  100. package/src/hierarchy-panel.jsx +88 -19
  101. package/src/{fal-motion-client.js → i2v-motion-client.js} +13 -13
  102. package/src/i2v-motion-studio.jsx +180 -0
  103. package/src/ik-camera.js +3 -0
  104. package/src/main.jsx +6 -0
  105. package/src/motion/generation.js +105 -0
  106. package/src/motion-readiness-ui.jsx +4 -4
  107. package/src/motion-readiness.js +11 -0
  108. package/src/motion-trail.js +366 -9
  109. package/src/object-gizmo.jsx +7 -1
  110. package/src/panels/CameraPanel.jsx +60 -0
  111. package/src/panels/CharacterTransformPanel.jsx +48 -0
  112. package/src/panels/EnvironmentPanel.jsx +40 -0
  113. package/src/panels/Foldout.jsx +32 -0
  114. package/src/panels/LightPanel.jsx +24 -0
  115. package/src/panels/ObjectTransformPanel.jsx +554 -0
  116. package/src/panels/PosePanel.jsx +97 -0
  117. package/src/panels/ProjectPanel.jsx +33 -0
  118. package/src/panels/PromptBlocksPanel.jsx +442 -0
  119. package/src/panels/PropsPanel.jsx +70 -0
  120. package/src/panels/ReferenceImageField.jsx +91 -0
  121. package/src/panels/RigControlPanel.jsx +78 -0
  122. package/src/panels/RigPanel.jsx +37 -0
  123. package/src/panels/SubjectBox.jsx +47 -0
  124. package/src/panels/SubjectsPanel.jsx +39 -0
  125. package/src/panels/VideoCapturePanel.jsx +180 -0
  126. package/src/panels/details.css +1171 -0
  127. package/src/panels/motion.css +197 -0
  128. package/src/panels/pose.css +39 -0
  129. package/src/planview.jsx +60 -31
  130. package/src/posestudio.jsx +59 -4
  131. package/src/project-browser.css +1073 -0
  132. package/src/project-browser.jsx +288 -123
  133. package/src/range-pin-object-transform.js +19 -0
  134. package/src/range-pin-panel.css +518 -0
  135. package/src/range-pin-panel.jsx +344 -0
  136. package/src/result-modal.jsx +5 -5
  137. package/src/room.jsx +79 -51
  138. package/src/scenes.js +9 -8
  139. package/src/semantic-edit.js +2 -0
  140. package/src/settings-menu.jsx +39 -105
  141. package/src/shell/BottomDock.jsx +450 -0
  142. package/src/shell/DetailsSlot.jsx +513 -0
  143. package/src/shell/LibrarySlot.jsx +86 -0
  144. package/src/shell/MenuBar.jsx +601 -0
  145. package/src/shell/OutlinerSlot.jsx +50 -0
  146. package/src/shell/PreferencesDialog.jsx +538 -0
  147. package/src/shell/PreferencesSlot.jsx +40 -0
  148. package/src/shell/StatusBar.jsx +125 -0
  149. package/src/shell/StudioShell.jsx +92 -0
  150. package/src/shell/TopBar.jsx +129 -0
  151. package/src/shell/ViewportToolbar.jsx +656 -0
  152. package/src/shell/agent-glass.css +296 -0
  153. package/src/shell/dock.css +93 -0
  154. package/src/shell/glass-regions.css +985 -0
  155. package/src/shell/glass.css +687 -0
  156. package/src/shell/log-store.js +52 -0
  157. package/src/shell/mode.css +9 -0
  158. package/src/shell/preferences.css +651 -0
  159. package/src/shell/shell.css +282 -0
  160. package/src/shell/studio-shell-context.js +19 -0
  161. package/src/shell/topbar.css +435 -0
  162. package/src/shell/viewport.css +451 -0
  163. package/src/store/authored-intent.js +22 -0
  164. package/src/store/runtime-adapters.js +92 -0
  165. package/src/store/scene-stage.js +21 -0
  166. package/src/store/use-document-store.js +14 -0
  167. package/src/studio-actions.js +210 -0
  168. package/src/studio-agent-commands.js +38 -268
  169. package/src/studio-agent-context.js +38 -9
  170. package/src/studio-agent-motion.js +122 -378
  171. package/src/studio-agent-protocol.js +66 -30
  172. package/src/studio-app-binding.js +379 -0
  173. package/src/studio-contact-sheet.js +73 -0
  174. package/src/studio-elements.js +58 -51
  175. package/src/styles/themes.css +200 -0
  176. package/src/styles/tokens.css +69 -0
  177. package/src/styles.css +456 -1748
  178. package/src/theme.js +44 -0
  179. package/src/timeline-extent.js +16 -0
  180. package/src/timeline-fps.js +13 -0
  181. package/src/trail-key-conflicts.js +39 -0
  182. package/src/trail-pick.js +59 -0
  183. package/src/ui.jsx +8 -8
  184. package/src/use-case-question.jsx +66 -0
  185. package/src/workflow/AgentPanel.jsx +86 -71
  186. package/src/workflow/agent-client.js +12 -2
  187. package/src/workflow/agent-panel.css +34 -32
  188. package/src/workflow/cozy-scene-node.css +2 -0
  189. package/src/workflow/workflow.css +2 -0
  190. package/tools/ardy/bridge.mjs +164 -146
  191. package/tools/ardy/visual-qa.mjs +5 -5
  192. package/tools/bench/EXP3.md +61 -0
  193. package/tools/bench/cclay_bench_extract_incam.py +125 -0
  194. package/tools/bench/cclay_bench_extract_obs.py +204 -0
  195. package/tools/bench/cclay_bench_runner.py +42 -0
  196. package/tools/bench/cube-contact.mjs +162 -0
  197. package/tools/bench/exp3.mjs +152 -0
  198. package/tools/bench/extract-bench-lib.mjs +216 -0
  199. package/tools/bench/extract-bench.mjs +302 -0
  200. package/tools/bench/fal-generate.mjs +88 -0
  201. package/tools/bench/fit/README.md +189 -0
  202. package/tools/bench/fit/camera.mjs +59 -0
  203. package/tools/bench/fit/contact.mjs +419 -0
  204. package/tools/bench/fit/footlock.mjs +234 -0
  205. package/tools/bench/fit/motion.mjs +85 -0
  206. package/tools/bench/fit/pin.mjs +27 -0
  207. package/tools/bench/fit/remote.mjs +105 -0
  208. package/tools/bench/fit-bench.mjs +126 -0
  209. package/tools/bench/fit-sanity.mjs +45 -0
  210. package/tools/bench/metrics.mjs +313 -0
  211. package/tools/bench/obs/depth.mjs +258 -0
  212. package/tools/bench/obs/extrinsics.mjs +175 -0
  213. package/tools/bench/obs/fit_mannequin_betas.py +340 -0
  214. package/tools/bench/obs/ground.mjs +294 -0
  215. package/tools/bench/obs/heading.mjs +151 -0
  216. package/tools/bench/obs/ladder.mjs +555 -0
  217. package/tools/bench/obs/mannequin-betas.json +124 -0
  218. package/tools/bench/obs/remote.mjs +83 -0
  219. package/tools/bench/obs/rest_joints.py +52 -0
  220. package/tools/bench/obs/ybot-targets.mjs +49 -0
  221. package/tools/bench/obs-bench.mjs +520 -0
  222. package/tools/bench/score.mjs +343 -0
  223. package/tools/bench/summarize.mjs +86 -0
  224. package/tools/dev/pages/privacy.html +13 -8
  225. package/tools/gt-render/browser.mjs +159 -0
  226. package/tools/gt-render/camera-math.mjs +276 -0
  227. package/tools/gt-render/page.mjs +267 -0
  228. package/tools/gt-render/render.mjs +514 -0
  229. package/tools/gt-render/scene-box.mjs +31 -0
  230. package/tools/gt-render/take-transform.mjs +87 -0
  231. package/tools/qa/css-rule-usage.mjs +283 -0
  232. package/tools/qa/studio-control-count.mjs +70 -32
  233. package/tools/run-tests.mjs +60 -3
  234. package/tools/track/DESIGN.md +513 -0
  235. package/tools/track/backfill-provenance.mjs +116 -0
  236. package/tools/track/budget.mjs +8 -0
  237. package/tools/track/check-rig.mjs +209 -0
  238. package/tools/track/diagnostics.schema.json +60 -0
  239. package/tools/track/export-rig.mjs +256 -0
  240. package/tools/track/fallback.mjs +7 -0
  241. package/tools/track/fk-parity-fixture.mjs +161 -0
  242. package/tools/track/gate.mjs +349 -0
  243. package/tools/track/masks.mjs +318 -0
  244. package/tools/track/metrics.mjs +201 -0
  245. package/tools/track/publish-obs.mjs +108 -0
  246. package/tools/track/py/check_env.py +61 -0
  247. package/tools/track/py/eval_lr.py +229 -0
  248. package/tools/track/py/lr_viterbi.py +294 -0
  249. package/tools/track/py/masks.py +405 -0
  250. package/tools/track/py/objective.py +382 -0
  251. package/tools/track/py/rig.py +508 -0
  252. package/tools/track/py/scene.py +326 -0
  253. package/tools/track/py/test_joint_indices.py +123 -0
  254. package/tools/track/py/test_lr_viterbi.py +195 -0
  255. package/tools/track/py/test_masks.py +229 -0
  256. package/tools/track/py/test_rig.py +288 -0
  257. package/tools/track/py/test_scene.py +354 -0
  258. package/tools/track/py/test_track.py +328 -0
  259. package/tools/track/py/track.py +411 -0
  260. package/tools/track/remote.mjs +140 -0
  261. package/tools/track/rig-dump.mjs +227 -0
  262. package/tools/track/run-box-tests.mjs +14 -0
  263. package/tools/track/run-box.mjs +168 -0
  264. package/tools/track/setup-box.sh +59 -0
  265. package/tools/track/study-2d.mjs +728 -0
  266. package/dist/assets/analytics-B1hnH66c.js +0 -1
  267. package/dist/assets/app-Cu1cexZR.css +0 -1
  268. package/dist/assets/app-Dxdh8kds.js +0 -4861
  269. package/dist/assets/shot-prompt-C_g2BHVc.css +0 -1
  270. package/dist/assets/shot-prompt-HVJRP4Gr.js +0 -17
  271. package/dist/assets/workflow-CJd-PDtV.js +0 -179
  272. package/dist/assets/workflow-DsudxKHF.css +0 -1
  273. package/src/fal-motion-studio.jsx +0 -180
  274. package/src/scene-history.js +0 -129
  275. package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
@@ -0,0 +1,140 @@
1
+ // Cast commands: a named character's root waypoints and IK key layer. They
2
+ // name their character explicitly, so they run the same way whichever
3
+ // character is active and whatever mode the editor is in.
4
+ import { studioActionDeclaration } from "../studio-actions.js";
5
+ import { characterOf, fail, changedIds } from "./shared.js";
6
+ import { elementSetSchema, registerElementSet } from './elements.js';
7
+ import './elements/character.js';
8
+ import { createCharacterEntry } from '../scenes.js';
9
+ import { DEFAULT_POSE } from '../poses.js';
10
+ import { createStableItemId, updateStableItem, removeStableItem } from '../stable-items.js';
11
+ import { movePromptClipFrames } from '../ardy/prompt-clips.js';
12
+ import { STUDIO_TOOL_SCHEMAS, StudioSchemas } from '../studio-agent-protocol.js';
13
+
14
+ const id = StudioSchemas.TargetGuard.properties.targetId;
15
+ const input = (properties, required = Object.keys(properties)) => ({ type: 'object', properties, required, additionalProperties: false });
16
+ const record = { type: 'object', properties: {}, additionalProperties: true };
17
+ const mutation = (id, label, schema, exposure = 'open') => ({ id, label, description: label, kind: 'mutation', undoDomain: 'cast', input: schema, exposure });
18
+ const blocks = elementSetSchema('character').properties.set.properties.layer.properties.promptClips;
19
+ const semantic = [
20
+ mutation('character.set', 'Set character fields', elementSetSchema('character')),
21
+ mutation('character.add', 'Add character', input({ character: record })),
22
+ mutation('character.remove', 'Remove character', input({ characterId: id })),
23
+ mutation('character.update', 'Update character', input({ characterId: id, patch: record })),
24
+ mutation('character.setPose', 'Set character pose', input({ characterId: id, pose: { oneOf: [id, record, { type: 'null' }] }, clearMotion: { type: 'boolean' } }, ['characterId', 'pose'])),
25
+ mutation('character.setPromptBlocks', 'Set prompt blocks', input({ characterId: id, blocks })),
26
+ mutation('characters.arrange', 'Arrange characters', STUDIO_TOOL_SCHEMAS.arrange_characters),
27
+ mutation('cast.replace', 'Replace cast', input({ characters: { type: 'array', items: record, minItems: 1 } }), 'ui-only'),
28
+ mutation('cast.setCustomPoses', 'Set pose library', input({ poses: { type: 'array', items: record } }), 'ui-only'),
29
+ mutation('cast.savePose', 'Save pose', input({ pose: record, characterId: id, clearMotion: { type: 'boolean' } }, ['pose']), 'ui-only'),
30
+ mutation('cast.removePose', 'Remove saved pose', input({ id }), 'ui-only'),
31
+ mutation('cast.showExtras', 'Show extra subjects', input({ show: { type: 'boolean' } }), 'ui-only'),
32
+ mutation('cast.setLayer', 'Set character layer', input({ characterId: id, layer: record }), 'ui-only'),
33
+ ...['add', 'move', 'resize', 'change', 'remove'].map(verb => mutation(`character.${verb}PromptBlock`, `${verb} prompt block`,
34
+ input({ characterId: id, id, frame: { type: 'number' }, edge: { type: 'string', enum: ['start', 'end'] }, text: { type: 'string', maxLength: 2000 } },
35
+ verb === 'add' ? ['characterId', 'frame'] : verb === 'remove' ? ['characterId', 'id'] : verb === 'change' ? ['characterId', 'id', 'text'] : verb === 'resize' ? ['characterId', 'id', 'edge', 'frame'] : ['characterId', 'id', 'frame']), 'ui-only')),
36
+ ];
37
+ export const declarations = Object.freeze([...semantic, ...["character.addWaypoint", "character.moveWaypoint", "character.removeWaypoint", "character.clearWaypoints"].map(studioActionDeclaration)]);
38
+
39
+ export function register(registry, ports) {
40
+ const owner = () => ports.storeDomain('cast');
41
+ const mounted = () => Boolean(ports.storeDomain?.('cast')) || 'The cast document owner is not mounted.';
42
+ const character = characterId => owner().read().find(row => row.id === characterId) ?? fail('STALE_TARGET', `Character ${characterId} is not in this scene.`);
43
+ const pose = value => typeof value === 'string' ? owner().poses().find(row => row.id === value) ?? fail('STALE_TARGET', `Pose ${value} is not in the library.`) : value;
44
+ const patch = (characterId, update) => { character(characterId); owner().write(rows => rows.map(row => row.id === characterId ? { ...row, ...update } : row)); };
45
+ function promptEdit(command, args) {
46
+ const before = character(args.characterId), clips = before.layer.promptClips;
47
+ let next;
48
+ if (command === 'character.addPromptBlock') {
49
+ const snapped = Math.max(0, Math.round(args.frame / 48) * 48);
50
+ const blocked = clips.some(clip => snapped < clip.endFrame && snapped + 48 > clip.startFrame);
51
+ const startFrame = blocked ? Math.max(snapped, ...clips.map(clip => clip.endFrame)) : snapped;
52
+ next = [...clips, { id: createStableItemId('prompt-clip'), startFrame, endFrame: startFrame + 48, text: '' }];
53
+ } else if (command === 'character.removePromptBlock') next = removeStableItem(clips, args.id, 'promptClips');
54
+ else if (command === 'character.changePromptBlock') next = updateStableItem(clips, args.id, clip => ({ ...clip, text: args.text }), 'promptClips');
55
+ else if (command === 'character.movePromptBlock') next = movePromptClipFrames(clips, args.id, args.frame, 48);
56
+ else {
57
+ next = updateStableItem(clips, args.id, clip => {
58
+ const snapped = Math.max(0, Math.round(args.frame / 48) * 48);
59
+ return args.edge === 'start' ? { ...clip, startFrame: Math.min(Math.max(snapped, clip.endFrame - 120), clip.endFrame - 48) }
60
+ : { ...clip, endFrame: Math.min(Math.max(clip.startFrame + 48, snapped), clip.startFrame + 120) };
61
+ }, 'promptClips');
62
+ const resized = next.find(clip => clip.id === args.id);
63
+ if (next.some(clip => clip.id !== args.id && resized.startFrame < clip.endFrame && resized.endFrame > clip.startFrame)) return;
64
+ }
65
+ patch(args.characterId, { layer: { ...before.layer, promptClips: next } });
66
+ owner().extendTimeline(next.reduce((end, clip) => Math.max(end, clip.endFrame), 24));
67
+ }
68
+ const methods = {
69
+ 'character.add': ({ character: value }) => {
70
+ const row = createCharacterEntry({ ...value, id: value.id ?? createStableItemId('character') }, owner().read().length);
71
+ if (owner().read().some(entry => entry.id === row.id)) fail('INVALID_ARGUMENT', 'Character id already exists.');
72
+ owner().write(rows => [...rows, row]);
73
+ },
74
+ 'character.remove': ({ characterId }) => { character(characterId); if (owner().read().length <= 1) fail('INVALID_ARGUMENT', 'Cannot remove the final character.'); owner().write(rows => rows.filter(row => row.id !== characterId)); },
75
+ 'character.update': ({ characterId, patch: value }) => patch(characterId, value),
76
+ 'character.setPose': ({ characterId, pose: value, clearMotion }) => { character(characterId); owner().applyPose(characterId, pose(value), clearMotion); },
77
+ 'character.setPromptBlocks': ({ characterId, blocks }) => {
78
+ const entry = character(characterId); patch(characterId, { layer: { ...entry.layer, promptClips: blocks } });
79
+ },
80
+ 'cast.replace': ({ characters }) => owner().write(characters),
81
+ 'cast.setCustomPoses': ({ poses }) => owner().writeState(before => ({ ...before, customPoses: poses })),
82
+ 'cast.savePose': ({ pose, characterId, clearMotion }) => {
83
+ if (characterId) character(characterId);
84
+ owner().writeState(before => ({ ...before, customPoses: [...before.customPoses.filter(entry => entry.id !== pose.id), pose] }));
85
+ if (characterId) owner().applyPose(characterId, pose, clearMotion);
86
+ },
87
+ 'cast.removePose': ({ id }) => owner().writeState(before => ({
88
+ characters: before.characters.map(entry => entry.pose?.id === id ? { ...entry, pose: DEFAULT_POSE } : entry),
89
+ customPoses: before.customPoses.filter(entry => entry.id !== id),
90
+ })),
91
+
92
+ 'cast.showExtras': ({ show }) => owner().showExtras(show),
93
+ 'cast.setLayer': ({ characterId, layer }) => patch(characterId, { layer: { ...character(characterId).layer, ...layer } }),
94
+ };
95
+ registerElementSet({ register(entry) {
96
+ registry.register({ ...entry, available: mounted, run(args) {
97
+ // A pose id selects a complete library entry, not a deep bone patch.
98
+ const ops = args.ops ?? [args];
99
+ const poses = ops.filter(op => Object.hasOwn(op.set, 'pose')).map(op => ({ id: op.id, value: pose(op.set.pose) }));
100
+ const fields = ops.map(({ id, set: { pose: _pose, ...set } }) => ({ id, set }));
101
+ const result = entry.run(args.ops ? { ops: fields } : fields[0]);
102
+ for (const selected of poses) owner().applyPose(selected.id, selected.value, false);
103
+ return result;
104
+ } });
105
+ } }, ports, semantic[0]);
106
+ for (const declaration of semantic.slice(1)) registry.register({ ...declaration, available: mounted, run(args) {
107
+ if (declaration.id === 'characters.arrange') { const plan = owner().arrange(args); return { affectedIds: plan.affectedIds, summary: 'Arranged characters.' }; }
108
+ const before = owner().read();
109
+ if (declaration.id.endsWith('PromptBlock')) promptEdit(declaration.id, args); else methods[declaration.id](args);
110
+ const changed = changedIds(before, owner().read());
111
+ return { affectedIds: changed.length ? changed : [args.characterId ?? ports.state().activeSceneId], summary: declaration.label };
112
+ } });
113
+ if (ports.storeDomain?.('cast')) registry.registerToolAlias('arrange_characters', 'characters.arrange');
114
+ const castAction = (id, run) => registry.register({ ...studioActionDeclaration(id),
115
+ available: state => state.characters.length > 0 || "There are no characters in this scene; add one with arrange_characters.",
116
+ run: args => {
117
+ const character = characterOf(ports, args.characterId);
118
+ return { affectedIds: [character.id], summary: run(args, character.subject || character.id) };
119
+ } });
120
+ const pin = waypoint => `frame ${waypoint.frame} (x ${waypoint.x}, z ${waypoint.z})`;
121
+ const warned = warnings => warnings.length ? `; warning: ${warnings[0]}` : "";
122
+ castAction("character.addWaypoint", ({ characterId, position, frame }, name) => {
123
+ const { waypoint, index, warnings } = ports.addCharacterWaypoint(characterId, position, frame ?? null);
124
+ ports.setWaypointMode(true);
125
+ return `Added ${name}'s root waypoint ${index + 1} at ${pin(waypoint)}${warned(warnings)}.`;
126
+ });
127
+ castAction("character.moveWaypoint", ({ characterId, frame, position }, name) => {
128
+ const { waypoint, warnings } = ports.moveCharacterWaypoint(characterId, frame, position);
129
+ ports.setWaypointMode(true);
130
+ return `Moved ${name}'s root waypoint to ${pin(waypoint)}${warned(warnings)}.`;
131
+ });
132
+ castAction("character.removeWaypoint", ({ characterId, frame }, name) => {
133
+ ports.removeCharacterWaypoint(characterId, frame);
134
+ return `Removed ${name}'s root waypoint at frame ${frame}.`;
135
+ });
136
+ castAction("character.clearWaypoints", ({ characterId }, name) => {
137
+ const count = ports.clearCharacterWaypoints(characterId);
138
+ return count ? `Cleared ${name}'s root path (${count} waypoint${count === 1 ? "" : "s"}).` : `${name} has no root waypoints; nothing changed.`;
139
+ });
140
+ }
@@ -0,0 +1,13 @@
1
+ import { createCharacterEntry } from '../../scenes.js';
2
+ import { STUDIO_ELEMENTS } from '../../studio-elements.js';
3
+ import { registerElementKind } from '../elements.js';
4
+
5
+ const elements = STUDIO_ELEMENTS.filter(row => row.path.startsWith('character.')).map(row => {
6
+ const { min, max, gizmo, ...element } = row;
7
+ if (row.path === 'character.position') return { ...element, documentPath: { x: 'x', y: 'y', z: 'z' } };
8
+ if (row.path === 'character.promptBlocks') return { ...element, documentPath: 'layer.promptClips' };
9
+ if (row.path === 'character.pose') return { ...element, readback: item => ({ text: item?.pose?.id ?? null }) };
10
+ return element;
11
+ });
12
+ registerElementKind('character', { collection: true, documentKey: 'characters', elements, normalize: createCharacterEntry });
13
+ registerElementKind('poseLibrary', { collection: true, documentKey: 'customPoses', elements: [], normalize: value => value });
@@ -0,0 +1,14 @@
1
+ import { registerElementKind } from '../elements.js';
2
+ import { wrapAngle } from '../../scene-objects.js';
3
+
4
+ const elements = [
5
+ ...['anchorX', 'anchorZ', 'rotationDeg'].map(field => ({ path: `motion.take.${field}`, type: 'number' })),
6
+ { path: 'motion.take.prompt', type: 'string' },
7
+ { path: 'motion.ikKeys', type: 'array', readOnly: true },
8
+ { path: 'motion.takeRecipe', type: 'object', readOnly: true },
9
+ { path: 'motion.takeVersions', type: 'array', readOnly: true },
10
+ ];
11
+ registerElementKind('motion', { collection: true, elements, normalize: row => ({ ...row, take: row.take && { ...row.take,
12
+ anchorX: Math.max(-240, Math.min(240, row.take.anchorX ?? 0)), anchorZ: Math.max(-240, Math.min(240, row.take.anchorZ ?? 0)),
13
+ rotationDeg: wrapAngle(row.take.rotationDeg ?? 0),
14
+ } }) });
@@ -0,0 +1,22 @@
1
+ import { normalizeSceneObject } from '../../scene-objects.js';
2
+ import { STUDIO_ELEMENTS } from '../../studio-elements.js';
3
+ import { registerElementKind } from '../elements.js';
4
+
5
+ const components = {
6
+ position: { x: 'x', y: 'y', z: 'z' },
7
+ rotation: { x: 'rotX', y: 'rot', z: 'rotZ' },
8
+ scale: { x: 'scaleX', y: 'scaleY', z: 'scaleZ' },
9
+ };
10
+ const elements = STUDIO_ELEMENTS.filter(row => row.path.startsWith('object.')).map(row => {
11
+ // Wire validation checks finite numbers; persistence owns the transform clamps.
12
+ const { min, max, ...element } = row;
13
+ const field = row.path.slice(7);
14
+ return { ...element, ...(components[field] ? { documentPath: components[field] } : {}),
15
+ ...(field === 'remove' ? { readback: item => ({ flag: item === undefined }) } : {}) };
16
+ });
17
+ export function normalizeObjectSet(record) {
18
+ const item = normalizeSceneObject(record);
19
+ // The owning collection consumes this lifecycle intent before persistence.
20
+ return record.remove === true ? { ...item, remove: true } : item;
21
+ }
22
+ registerElementKind('object', { collection: true, documentKey: 'objects', elements, normalize: normalizeObjectSet });
@@ -0,0 +1,16 @@
1
+ import { registerElementKind } from '../elements.js';
2
+
3
+ // Array position is the authoritative order. The owner refreshes this persisted
4
+ // field after every move, so generic set/read and the project file agree.
5
+ registerElementKind('scene', {
6
+ collection: true, documentKey: 'scenes',
7
+ elements: [
8
+ { path: 'scene.name', type: 'string' },
9
+ { path: 'scene.order', type: 'number' },
10
+ ],
11
+ normalize: scene => ({ ...scene, name: scene.name.trim(), order: Math.max(0, Math.round(scene.order)) }),
12
+ });
13
+ registerElementKind('project', {
14
+ elements: [{ path: 'project.name', type: 'string' }],
15
+ normalize: project => project,
16
+ });
@@ -0,0 +1,9 @@
1
+ import { createShotAuthoringDocument } from '../../shot-authoring.js';
2
+ import { STUDIO_ELEMENTS } from '../../studio-elements.js';
3
+ import { registerElementKind } from '../elements.js';
4
+
5
+ export function normalizeShot(record) {
6
+ return createShotAuthoringDocument({ shots: [record], frameCount: Math.max(24, record.endFrame + 1) }).shots[0];
7
+ }
8
+ registerElementKind('shot', { collection: true, documentKey: 'shots',
9
+ elements: STUDIO_ELEMENTS.filter(row => row.path.startsWith('shot.')), normalize: normalizeShot });
@@ -0,0 +1,11 @@
1
+ import { createSceneStage } from '../../scenes.js';
2
+ import { STUDIO_ELEMENTS } from '../../studio-elements.js';
3
+ import { registerElementKind } from '../elements.js';
4
+
5
+ const elements = STUDIO_ELEMENTS.filter(row => row.path.startsWith('stage.'));
6
+ export const STAGE_FIELDS = [...new Set(elements.map(element => (element.documentPath ?? element.path.slice(6)).split('.')[0]))];
7
+ export function normalizeStage(source) {
8
+ const normalized = createSceneStage(source);
9
+ return Object.fromEntries(STAGE_FIELDS.map(key => [key, normalized[key]]));
10
+ }
11
+ registerElementKind('stage', { elements, normalize: normalizeStage });
@@ -0,0 +1,155 @@
1
+ // Shared set/read machinery. Domain modules supply only their kind, declared
2
+ // fields and persistence normalizer; no UI or runtime resources live here.
3
+ import { patchValueSchema, validateStudioSchema, StudioProtocolError, StudioSchemas, utf8ByteLength } from '../studio-agent-protocol.js';
4
+
5
+ import { isSettableElement } from '../studio-elements.js';
6
+
7
+ const kinds = new Map();
8
+ // Each kind registers its declared elements and persistence normalizer once.
9
+ // Its command port supplies read()/write(); all schemas and path aliases use
10
+ // this same spec so a migration adds a module, not another generic switch.
11
+ export function registerElementKind(kind, spec) {
12
+ if (kinds.has(kind)) throw new Error(`Element kind already registered: ${kind}`);
13
+ kinds.set(kind, { documentKey: kind, ...spec });
14
+ }
15
+ const projectionKind = key => [...kinds].find(([, spec]) => spec.documentKey === key)?.[0] ?? key;
16
+ const patchElements = kind => kinds.get(kind).elements.filter(isSettableElement);
17
+ const object = () => ({ type: 'object', properties: {}, required: [], additionalProperties: false });
18
+ // String mappings retain the single-document schema. Component mappings use
19
+ // the logical field in set args and fan out only at the storage boundary.
20
+ const setPath = element => typeof element.documentPath === 'string' ? element.documentPath : element.path.slice(element.path.indexOf('.') + 1);
21
+ const atPath = (value, path) => path.split('.').reduce((value, key) => value?.[key], value);
22
+ const nestedValue = (path, value) => path.split('.').reduceRight((value, key) => ({ [key]: value }), value);
23
+ export function elementSetSchema(kind) {
24
+ const schema = object();
25
+ for (const element of patchElements(kind)) {
26
+ const keys = setPath(element).split('.');
27
+ let parent = schema;
28
+ for (const key of keys.slice(0, -1)) parent = parent.properties[key] ??= object();
29
+ // Validate the wire type here; persistence owns numeric clamps. Empty
30
+ // strings are valid authored text, including intermediate typing states.
31
+ const value = element.type === 'number' ? { type: 'number' }
32
+ : element.type === 'string' ? { oneOf: [{ const: '' }, { type: 'string', maxLength: 240 }] } : patchValueSchema(element);
33
+ parent.properties[keys.at(-1)] = element.nullable ? { oneOf: [value, { type: 'null' }] } : value;
34
+ }
35
+ if (!kinds.get(kind).collection) return schema;
36
+ // A single item is { id, set }; a batch is { ops: [{ id, set }] }.
37
+ // Both forms publish once and therefore own exactly one history entry.
38
+ const item = { ...object(), properties: { id: StudioSchemas.TargetGuard.properties.targetId, set: schema }, required: ['id', 'set'] };
39
+ const batch = { ...object(), properties: { ops: { type: 'array', items: item, minItems: 1, maxItems: 32 } }, required: ['ops'] };
40
+ return { ...object(), properties: { ...item.properties, ...batch.properties }, oneOf: [item, batch] };
41
+ }
42
+ export function elementTarget(kind, value, id, sceneId) {
43
+ return kinds.get(projectionKind(kind)).collection ? value.find(row => row.id === id) : id === sceneId ? value : undefined;
44
+ }
45
+ export function readElementDocument(projection, { ids, select } = {}, sceneId) {
46
+ const entries = Object.entries(projection).flatMap(([key, value]) => {
47
+ const kind = projectionKind(key);
48
+ if (select && !select.includes(key) && !select.includes(kind)) return [];
49
+ if (!ids || ids.includes(key) || ids.includes(kind) || ids.includes(sceneId)) return [[key, value]];
50
+ const selected = kinds.get(kind).collection ? value.filter(row => ids.includes(row.id)) : [];
51
+ return selected.length ? [[key, selected]] : [];
52
+ });
53
+ return { document: structuredClone(Object.fromEntries(entries)), schema: Object.fromEntries(entries.map(([key]) => [key, elementSetSchema(projectionKind(key))])) };
54
+ }
55
+ export function readElement(document, path) {
56
+ const element = kinds.get(path.slice(0, path.indexOf('.'))).elements.find(row => row.path === path);
57
+ return element.documentPath && typeof element.documentPath === 'object'
58
+ ? Object.fromEntries(Object.entries(element.documentPath).map(([component, stored]) => [component, atPath(document, stored)]))
59
+ : atPath(document, setPath(element));
60
+ }
61
+ export function mergeElementSet(document = {}, patch) {
62
+ document = document ?? {};
63
+ const next = { ...document };
64
+ for (const [key, value] of Object.entries(patch)) next[key] = value && typeof value === 'object' && !Array.isArray(value)
65
+ ? mergeElementSet(document[key], value) : value;
66
+ return next;
67
+ }
68
+ function storedElementSet(kind, patch) {
69
+ let stored = mergeElementSet({}, patch);
70
+ for (const element of patchElements(kind)) {
71
+ if (!element.documentPath || typeof element.documentPath !== 'object') continue;
72
+ const value = atPath(patch, setPath(element));
73
+ if (value === undefined) continue;
74
+ const keys = setPath(element).split('.');
75
+ const parent = keys.slice(0, -1).reduce((value, key) => value[key], stored);
76
+ delete parent[keys.at(-1)];
77
+ for (const [component, path] of Object.entries(element.documentPath)) stored = mergeElementSet(stored, nestedValue(path, value[component]));
78
+ }
79
+ return stored;
80
+ }
81
+ export function elementPatchArgs(kind, args) {
82
+ const set = { ...object(), additionalProperties: true };
83
+ const collection = kinds.get(kind).collection;
84
+ const target = { ...object(), properties: { kind: { const: kind }, ...(collection ? { id: StudioSchemas.TargetGuard.properties.targetId } : {}) }, required: collection ? ['kind', 'id'] : ['kind'] };
85
+ const op = { ...object(), properties: { target, set }, required: ['target', 'set'] };
86
+ const schema = { ...object(), properties: { ops: { type: 'array', items: op, minItems: 1, maxItems: 32 } }, required: ['ops'] };
87
+ const validated = validateStudioSchema(schema, args);
88
+ const ops = validated.ops.map(({ target, set }) => {
89
+ let patch = {};
90
+ for (const [key, value] of Object.entries(set)) {
91
+ const element = patchElements(kind).find(row => row.path === `${kind}.${key}`);
92
+ if (!element) throw new StudioProtocolError('INVALID_ARGUMENT', `Unknown ${kind} path: ${key}`);
93
+ const nested = nestedValue(setPath(element), value);
94
+ patch = mergeElementSet(patch, nested);
95
+ }
96
+ return { id: target.id, set: patch };
97
+ });
98
+ return collection ? ops.length === 1 ? ops[0] : { ops } : ops.reduce((patch, op) => mergeElementSet(patch, op.set), {});
99
+ }
100
+ export function elementReadback(kind, item, paths, document) {
101
+ return patchElements(projectionKind(kind)).filter(element => !paths || paths.includes(element.path)).map(element => {
102
+ const path = element.path;
103
+ if (element.readback) return { path, ...element.readback(item, document) };
104
+ const value = readElement(item, path);
105
+ if (value === null || value === undefined || value === '') return { path, text: null };
106
+ if (element.type === 'image') return { path, bytes: utf8ByteLength(value) };
107
+ if (element.type === 'vec3') return { path, vec: value };
108
+ if (element.type === 'array') return { path, count: Array.isArray(value) ? value.length : value.points?.length ?? 0 };
109
+ return { path, [typeof value === 'number' ? 'number' : typeof value === 'boolean' ? 'flag' : 'text']: value };
110
+ });
111
+ }
112
+ // Normalizer-added defaults are allowed; changed/clamped requested members
113
+ // are reported as dropped, just like the native patch planners.
114
+ function survives(requested, actual) {
115
+ if (requested === null || typeof requested !== 'object') return requested === actual;
116
+ if (Array.isArray(requested)) return Array.isArray(actual) && requested.length === actual.length && requested.every((value, index) => survives(value, actual[index]));
117
+ return actual !== null && typeof actual === 'object' && !Array.isArray(actual) && Object.entries(requested).every(([key, value]) => survives(value, actual[key]));
118
+ }
119
+ export function elementPatchReceipt(receipt, request, projection) {
120
+ const kind = request?.args.ops[0].target.kind;
121
+ if (!receipt.ok || !kind || !kinds.get(kind).collection) return receipt;
122
+ const value = projection[kinds.get(kind).documentKey];
123
+ const ops = request.args.ops.map(({ target, set }, index) => {
124
+ const item = elementTarget(kind, value, target.id, receipt.host.sceneId);
125
+ const droppedPaths = Object.entries(set).filter(([key, requested]) => {
126
+ const element = patchElements(kind).find(element => element.path === `${kind}.${key}`);
127
+ return !element.readback && !survives(requested, readElement(item, element.path));
128
+ }).map(([key]) => `${kind}.${key}`);
129
+ return { index, status: droppedPaths.length ? 'partial' : receipt.authored ? 'applied' : 'noop', ...(droppedPaths.length ? { droppedPaths } : {}) };
130
+ });
131
+ const partial = receipt.authored && ops.some(op => op.status === 'partial');
132
+ // The protocol's partial variant is a patch receipt, not an action receipt.
133
+ const { action, summary, ...base } = receipt;
134
+ return { ...(partial ? base : receipt), ops, status: partial ? 'partial' : receipt.status,
135
+ delta: !receipt.authored ? [] : request.args.ops.slice(0, 8).map(({ target, set }) => ({ id: target.id, after: {
136
+ patched: elementReadback(kind, elementTarget(kind, value, target.id, receipt.host.sceneId), Object.keys(set).map(key => `${kind}.${key}`), value),
137
+ } })) };
138
+ }
139
+ export function registerElementSet(registry, ports, declaration) {
140
+ const kind = declaration.id.slice(0, declaration.id.indexOf('.'));
141
+ const { normalize, collection } = kinds.get(kind);
142
+ registry.register({ ...declaration, available: () => true, run(args) {
143
+ const domain = ports.storeDomain(declaration.undoDomain ?? kind);
144
+ let next = domain.read();
145
+ const ops = collection ? args.ops ?? [args] : [{ id: ports.state().activeSceneId, set: args }];
146
+ for (const { id, set } of ops) {
147
+ const before = collection ? next.find(row => row.id === id) : next;
148
+ if (!before) throw new StudioProtocolError('TARGET_NOT_READY', `Unknown ${kind} id: ${id}`);
149
+ const after = normalize(mergeElementSet(before, storedElementSet(kind, set)));
150
+ next = collection ? next.map(row => row.id === id ? { ...after, id } : row) : after;
151
+ }
152
+ if (JSON.stringify(next) !== JSON.stringify(domain.read())) domain.write(next);
153
+ return { affectedIds: [...new Set(ops.map(op => op.id))], summary: `Updated ${kind}.` };
154
+ } });
155
+ }
@@ -0,0 +1,20 @@
1
+ // Export commands: the Export menu's Video (mp4), the same export, awaited to
2
+ // its file. A failure was already shown in the export panel, so it refuses
3
+ // silently.
4
+ import { studioActionDeclaration } from "../studio-actions.js";
5
+ import { fail, shotLabel, shotOf } from "./shared.js";
6
+
7
+ export const declarations = Object.freeze(["export.shotVideo"].map(studioActionDeclaration));
8
+
9
+ export function register(registry, ports) {
10
+ registry.register({ ...studioActionDeclaration("export.shotVideo"),
11
+ available: state => state.exporting ? "An export is already running; wait for it to finish."
12
+ : state.canExportVideo || "There is nothing to record yet: add a shot (shot.create), camera keys or a motion take first.",
13
+ run: async ({ shotId }, context) => {
14
+ const shot = shotId ? shotOf(ports, shotId) : null;
15
+ const result = await ports.exportShotVideo({ shotId: shotId ?? null }, context);
16
+ if (!result?.fileName) fail("TARGET_NOT_READY", "The video export did not finish; the editor's export panel shows why and offers Retry.");
17
+ return { affectedIds: shot ? [shot.id] : [], output: { fileName: result.fileName, frameCount: result.frameCount },
18
+ summary: `Recorded ${shot ? shotLabel(shot) : "the shot"} to ${result.fileName} (${result.frameCount} frames); the browser was asked to download it.` };
19
+ } });
20
+ }
@@ -0,0 +1,40 @@
1
+ // The editor's Studio actions: ONE registry whose entries call the same
2
+ // handlers the UI controls call, so a timeline button and the agent's
3
+ // run_action share one code path. Each command module registers its own
4
+ // actions over `ports`, the one generic port object App refreshes on every
5
+ // render; `ports.state()` reads the synchronously published document, so an
6
+ // action sees its own edit at once.
7
+ import { createStudioActionRegistry, STUDIO_ACTIONS } from "../studio-actions.js";
8
+ import * as shot from "./shot.js";
9
+ import * as cast from "./cast.js";
10
+ import * as motion from "./motion.js";
11
+ import * as objects from "./objects.js";
12
+ import * as view from "./view.js";
13
+ import * as scene from "./scene.js";
14
+ import * as project from "./project.js";
15
+ import * as exporting from "./export.js";
16
+ import * as ai from "./ai.js";
17
+ import * as stage from "./stage.js";
18
+
19
+ export const COMMAND_MODULES = Object.freeze({ shot, cast, motion, objects, view, scene, project, export: exporting, ai, stage });
20
+
21
+ export function commandDeclarations(modules = COMMAND_MODULES) {
22
+ return Object.values(modules).flatMap(module => module.declarations.map(declaration => ({
23
+ ...STUDIO_ACTIONS.find(entry => entry.id === declaration.id), ...declaration,
24
+ })));
25
+ }
26
+
27
+ export function createStudioAppActions(ports, modules = COMMAND_MODULES) {
28
+ const registry = createStudioActionRegistry({ readState: () => ports.state() });
29
+ const declarations = new Map(commandDeclarations(modules).map(entry => [entry.id, entry]));
30
+ const register = registry.register;
31
+ registry.register = entry => register({ ...entry, ...declarations.get(entry.id) });
32
+ const aliases = new Map();
33
+ registry.registerToolAlias = (tool, action, args = value => value) => {
34
+ if (aliases.has(tool)) throw new Error(`Tool alias already registered: ${tool}`);
35
+ aliases.set(tool, { action, args });
36
+ };
37
+ registry.toolAlias = tool => aliases.get(tool);
38
+ for (const module of Object.values(modules)) module.register(registry, ports);
39
+ return registry;
40
+ }