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.
- package/CHANGELOG.md +193 -0
- package/bin/agent/agent-routes.mjs +82 -72
- package/bin/agent/agent-runner.mjs +10 -3
- package/bin/agent/motion-runtime.mjs +32 -260
- package/bin/agent/providers.mjs +46 -3
- package/bin/agent/session-store.mjs +41 -5
- package/bin/agent/studio-prompt.mjs +1 -1
- package/bin/agent/studio-tools.mjs +48 -11
- package/bin/codex-auth.mjs +2 -1
- package/bin/cozyclay.mjs +38 -11
- package/bin/live/cli.mjs +22 -3
- package/bin/mcp-runtime.mjs +45 -20
- package/bin/telemetry-state.mjs +5 -2
- package/bin/update-check.mjs +15 -0
- package/dist/app/index.html +6 -6
- package/dist/assets/analytics-MVZOEW8O.js +1 -0
- package/dist/assets/app-BxBLC6GT.js +4876 -0
- package/dist/assets/app-CJdxvnrl.css +1 -0
- package/dist/assets/{demo-B-eg1CT8.js → demo-ByeYp-dY.js} +1 -1
- package/dist/assets/{first-shot-handoff-CGgnvNu8.js → first-shot-handoff-DgT28ZiV.js} +1 -1
- package/dist/assets/{landing-IbRp3FnQ.js → landing-BaXkpFip.js} +1 -1
- package/dist/assets/shot-prompt-CbqWcWYi.css +1 -0
- package/dist/assets/shot-prompt-hN0j_O-p.js +17 -0
- package/dist/assets/{ticket-BgIzOBOf.js → ticket-BcmsnHn5.js} +1 -1
- package/dist/assets/workflow-B5XZwVfe.js +179 -0
- package/dist/assets/workflow-DRdwoDBT.css +1 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/fonts/IBM-Plex-Sans-OFL.txt +92 -0
- package/dist/fonts/JetBrains-Mono-OFL.txt +93 -0
- package/dist/fonts/README.md +13 -9
- package/dist/fonts/ibm-plex-sans-400-latin.woff2 +0 -0
- package/dist/fonts/ibm-plex-sans-500-latin.woff2 +0 -0
- package/dist/fonts/ibm-plex-sans-600-latin.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-400-latin.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-500-latin.woff2 +0 -0
- package/dist/index.html +7 -7
- package/dist/privacy/index.html +13 -8
- package/dist/sitemap.xml +2 -2
- package/dist/workflow/index.html +5 -5
- package/mcp/live-hub.mjs +35 -93
- package/mcp/mesh-file.mjs +48 -0
- package/mcp/server.mjs +6 -77
- package/mcp/tool-handlers.mjs +219 -376
- package/package.json +2 -1
- package/src/App.jsx +1275 -9086
- package/src/analytics.js +393 -9
- package/src/app-context.js +211 -0
- package/src/app-stage.jsx +25 -24
- package/src/ardy/auto-fix-panel.css +108 -0
- package/src/ardy/fix-collisions.js +10 -66
- package/src/ardy/ik-drag.js +101 -0
- package/src/ardy/ik-key-json.js +43 -0
- package/src/ardy/ik.js +97 -67
- package/src/ardy/physics-panel.css +2 -0
- package/src/ardy/physics-panel.jsx +17 -5
- package/src/ardy/platform-fit-panel.css +64 -0
- package/src/ardy/platform-fit-panel.jsx +40 -0
- package/src/ardy/platform-fit.js +326 -0
- package/src/ardy/playback.js +166 -20
- package/src/ardy/range-pin.js +299 -0
- package/src/ardy/timeline-coordinates.js +26 -0
- package/src/ardy/timeline.css +1214 -0
- package/src/ardy/timeline.jsx +152 -41
- package/src/ardy/waypoints.js +3 -3
- package/src/asset-pane.css +462 -0
- package/src/asset-pane.jsx +461 -232
- package/src/camera-tutorial.jsx +38 -18
- package/src/command-bus.js +349 -0
- package/src/commands/ai.js +31 -0
- package/src/commands/cast.js +140 -0
- package/src/commands/elements/character.js +13 -0
- package/src/commands/elements/motion.js +14 -0
- package/src/commands/elements/object.js +22 -0
- package/src/commands/elements/scene.js +16 -0
- package/src/commands/elements/shot.js +9 -0
- package/src/commands/elements/stage.js +11 -0
- package/src/commands/elements.js +155 -0
- package/src/commands/export.js +20 -0
- package/src/commands/index.js +40 -0
- package/src/commands/motion.js +231 -0
- package/src/commands/objects.js +174 -0
- package/src/commands/project.js +53 -0
- package/src/commands/scene.js +81 -0
- package/src/commands/shared.js +19 -0
- package/src/commands/shot.js +103 -0
- package/src/commands/stage.js +17 -0
- package/src/commands/view.js +42 -0
- package/src/document-store.js +144 -0
- package/src/domains/cast.js +1007 -0
- package/src/domains/motion.js +3219 -0
- package/src/domains/objects.js +932 -0
- package/src/domains/scenes.js +823 -0
- package/src/domains/shots.js +404 -0
- package/src/domains/stage.js +93 -0
- package/src/dualview.jsx +113 -57
- package/src/facing-marks.js +3 -0
- package/src/first-success-guide.jsx +14 -14
- package/src/grid-view.js +18 -5
- package/src/hierarchy-panel.css +386 -0
- package/src/hierarchy-panel.jsx +88 -19
- package/src/{fal-motion-client.js → i2v-motion-client.js} +13 -13
- package/src/i2v-motion-studio.jsx +180 -0
- package/src/ik-camera.js +3 -0
- package/src/main.jsx +6 -0
- package/src/motion/generation.js +105 -0
- package/src/motion-readiness-ui.jsx +4 -4
- package/src/motion-readiness.js +11 -0
- package/src/motion-trail.js +366 -9
- package/src/object-gizmo.jsx +7 -1
- package/src/panels/CameraPanel.jsx +60 -0
- package/src/panels/CharacterTransformPanel.jsx +48 -0
- package/src/panels/EnvironmentPanel.jsx +40 -0
- package/src/panels/Foldout.jsx +32 -0
- package/src/panels/LightPanel.jsx +24 -0
- package/src/panels/ObjectTransformPanel.jsx +554 -0
- package/src/panels/PosePanel.jsx +97 -0
- package/src/panels/ProjectPanel.jsx +33 -0
- package/src/panels/PromptBlocksPanel.jsx +442 -0
- package/src/panels/PropsPanel.jsx +70 -0
- package/src/panels/ReferenceImageField.jsx +91 -0
- package/src/panels/RigControlPanel.jsx +78 -0
- package/src/panels/RigPanel.jsx +37 -0
- package/src/panels/SubjectBox.jsx +47 -0
- package/src/panels/SubjectsPanel.jsx +39 -0
- package/src/panels/VideoCapturePanel.jsx +180 -0
- package/src/panels/details.css +1171 -0
- package/src/panels/motion.css +197 -0
- package/src/panels/pose.css +39 -0
- package/src/planview.jsx +60 -31
- package/src/posestudio.jsx +59 -4
- package/src/project-browser.css +1073 -0
- package/src/project-browser.jsx +288 -123
- package/src/range-pin-object-transform.js +19 -0
- package/src/range-pin-panel.css +518 -0
- package/src/range-pin-panel.jsx +344 -0
- package/src/result-modal.jsx +5 -5
- package/src/room.jsx +79 -51
- package/src/scenes.js +9 -8
- package/src/semantic-edit.js +2 -0
- package/src/settings-menu.jsx +39 -105
- package/src/shell/BottomDock.jsx +450 -0
- package/src/shell/DetailsSlot.jsx +513 -0
- package/src/shell/LibrarySlot.jsx +86 -0
- package/src/shell/MenuBar.jsx +601 -0
- package/src/shell/OutlinerSlot.jsx +50 -0
- package/src/shell/PreferencesDialog.jsx +538 -0
- package/src/shell/PreferencesSlot.jsx +40 -0
- package/src/shell/StatusBar.jsx +125 -0
- package/src/shell/StudioShell.jsx +92 -0
- package/src/shell/TopBar.jsx +129 -0
- package/src/shell/ViewportToolbar.jsx +656 -0
- package/src/shell/agent-glass.css +296 -0
- package/src/shell/dock.css +93 -0
- package/src/shell/glass-regions.css +985 -0
- package/src/shell/glass.css +687 -0
- package/src/shell/log-store.js +52 -0
- package/src/shell/mode.css +9 -0
- package/src/shell/preferences.css +651 -0
- package/src/shell/shell.css +282 -0
- package/src/shell/studio-shell-context.js +19 -0
- package/src/shell/topbar.css +435 -0
- package/src/shell/viewport.css +451 -0
- package/src/store/authored-intent.js +22 -0
- package/src/store/runtime-adapters.js +92 -0
- package/src/store/scene-stage.js +21 -0
- package/src/store/use-document-store.js +14 -0
- package/src/studio-actions.js +210 -0
- package/src/studio-agent-commands.js +38 -268
- package/src/studio-agent-context.js +38 -9
- package/src/studio-agent-motion.js +122 -378
- package/src/studio-agent-protocol.js +66 -30
- package/src/studio-app-binding.js +379 -0
- package/src/studio-contact-sheet.js +73 -0
- package/src/studio-elements.js +58 -51
- package/src/styles/themes.css +200 -0
- package/src/styles/tokens.css +69 -0
- package/src/styles.css +456 -1748
- package/src/theme.js +44 -0
- package/src/timeline-extent.js +16 -0
- package/src/timeline-fps.js +13 -0
- package/src/trail-key-conflicts.js +39 -0
- package/src/trail-pick.js +59 -0
- package/src/ui.jsx +8 -8
- package/src/use-case-question.jsx +66 -0
- package/src/workflow/AgentPanel.jsx +86 -71
- package/src/workflow/agent-client.js +12 -2
- package/src/workflow/agent-panel.css +34 -32
- package/src/workflow/cozy-scene-node.css +2 -0
- package/src/workflow/workflow.css +2 -0
- package/tools/ardy/bridge.mjs +164 -146
- package/tools/ardy/visual-qa.mjs +5 -5
- package/tools/bench/EXP3.md +61 -0
- package/tools/bench/cclay_bench_extract_incam.py +125 -0
- package/tools/bench/cclay_bench_extract_obs.py +204 -0
- package/tools/bench/cclay_bench_runner.py +42 -0
- package/tools/bench/cube-contact.mjs +162 -0
- package/tools/bench/exp3.mjs +152 -0
- package/tools/bench/extract-bench-lib.mjs +216 -0
- package/tools/bench/extract-bench.mjs +302 -0
- package/tools/bench/fal-generate.mjs +88 -0
- package/tools/bench/fit/README.md +189 -0
- package/tools/bench/fit/camera.mjs +59 -0
- package/tools/bench/fit/contact.mjs +419 -0
- package/tools/bench/fit/footlock.mjs +234 -0
- package/tools/bench/fit/motion.mjs +85 -0
- package/tools/bench/fit/pin.mjs +27 -0
- package/tools/bench/fit/remote.mjs +105 -0
- package/tools/bench/fit-bench.mjs +126 -0
- package/tools/bench/fit-sanity.mjs +45 -0
- package/tools/bench/metrics.mjs +313 -0
- package/tools/bench/obs/depth.mjs +258 -0
- package/tools/bench/obs/extrinsics.mjs +175 -0
- package/tools/bench/obs/fit_mannequin_betas.py +340 -0
- package/tools/bench/obs/ground.mjs +294 -0
- package/tools/bench/obs/heading.mjs +151 -0
- package/tools/bench/obs/ladder.mjs +555 -0
- package/tools/bench/obs/mannequin-betas.json +124 -0
- package/tools/bench/obs/remote.mjs +83 -0
- package/tools/bench/obs/rest_joints.py +52 -0
- package/tools/bench/obs/ybot-targets.mjs +49 -0
- package/tools/bench/obs-bench.mjs +520 -0
- package/tools/bench/score.mjs +343 -0
- package/tools/bench/summarize.mjs +86 -0
- package/tools/dev/pages/privacy.html +13 -8
- package/tools/gt-render/browser.mjs +159 -0
- package/tools/gt-render/camera-math.mjs +276 -0
- package/tools/gt-render/page.mjs +267 -0
- package/tools/gt-render/render.mjs +514 -0
- package/tools/gt-render/scene-box.mjs +31 -0
- package/tools/gt-render/take-transform.mjs +87 -0
- package/tools/qa/css-rule-usage.mjs +283 -0
- package/tools/qa/studio-control-count.mjs +70 -32
- package/tools/run-tests.mjs +60 -3
- package/tools/track/DESIGN.md +513 -0
- package/tools/track/backfill-provenance.mjs +116 -0
- package/tools/track/budget.mjs +8 -0
- package/tools/track/check-rig.mjs +209 -0
- package/tools/track/diagnostics.schema.json +60 -0
- package/tools/track/export-rig.mjs +256 -0
- package/tools/track/fallback.mjs +7 -0
- package/tools/track/fk-parity-fixture.mjs +161 -0
- package/tools/track/gate.mjs +349 -0
- package/tools/track/masks.mjs +318 -0
- package/tools/track/metrics.mjs +201 -0
- package/tools/track/publish-obs.mjs +108 -0
- package/tools/track/py/check_env.py +61 -0
- package/tools/track/py/eval_lr.py +229 -0
- package/tools/track/py/lr_viterbi.py +294 -0
- package/tools/track/py/masks.py +405 -0
- package/tools/track/py/objective.py +382 -0
- package/tools/track/py/rig.py +508 -0
- package/tools/track/py/scene.py +326 -0
- package/tools/track/py/test_joint_indices.py +123 -0
- package/tools/track/py/test_lr_viterbi.py +195 -0
- package/tools/track/py/test_masks.py +229 -0
- package/tools/track/py/test_rig.py +288 -0
- package/tools/track/py/test_scene.py +354 -0
- package/tools/track/py/test_track.py +328 -0
- package/tools/track/py/track.py +411 -0
- package/tools/track/remote.mjs +140 -0
- package/tools/track/rig-dump.mjs +227 -0
- package/tools/track/run-box-tests.mjs +14 -0
- package/tools/track/run-box.mjs +168 -0
- package/tools/track/setup-box.sh +59 -0
- package/tools/track/study-2d.mjs +728 -0
- package/dist/assets/analytics-B1hnH66c.js +0 -1
- package/dist/assets/app-Cu1cexZR.css +0 -1
- package/dist/assets/app-Dxdh8kds.js +0 -4861
- package/dist/assets/shot-prompt-C_g2BHVc.css +0 -1
- package/dist/assets/shot-prompt-HVJRP4Gr.js +0 -17
- package/dist/assets/workflow-CJd-PDtV.js +0 -179
- package/dist/assets/workflow-DsudxKHF.css +0 -1
- package/src/fal-motion-studio.jsx +0 -180
- package/src/scene-history.js +0 -129
- package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// One semantic entry point for timeline, camera controls and agent framing.
|
|
2
|
+
import { createCameraBlock, updateCameraBlock, removeCameraRail } from '../camera-block.js';
|
|
3
|
+
import { addShotAtFrame, cutAtFrame, duplicateShot, removeShot, reorderShot, resizeShot, renameShot, moveCameraKey, removeCameraKey } from '../cuts.js';
|
|
4
|
+
import { createStableItemId, updateStableItem } from '../stable-items.js';
|
|
5
|
+
import { railFollowForNewGeometry } from '../camera-rail-schedule.js';
|
|
6
|
+
import { studioActionDeclaration } from '../studio-actions.js';
|
|
7
|
+
import { STUDIO_TOOL_SCHEMAS, StudioSchemas } from '../studio-agent-protocol.js';
|
|
8
|
+
import { elementSetSchema, registerElementSet } from './elements.js';
|
|
9
|
+
import './elements/shot.js';
|
|
10
|
+
import { changedIds, fail, shotLabel } from './shared.js';
|
|
11
|
+
|
|
12
|
+
const id = StudioSchemas.TargetGuard.properties.targetId;
|
|
13
|
+
const number = { type: 'number' };
|
|
14
|
+
const frame = { type: 'integer', minimum: 0 };
|
|
15
|
+
const input = (properties, required = Object.keys(properties)) => ({ type: 'object', properties, required, additionalProperties: false });
|
|
16
|
+
const mutation = (id, label, schema) => ({ id, label, description: label, kind: 'mutation', undoDomain: 'shot', input: schema });
|
|
17
|
+
const existing = ['shot.create', 'shot.split', 'shot.duplicate', 'shot.remove', 'shot.setRange', 'shot.setCameraRail', 'shot.clearCameraRail', 'shot.reorder'].map(studioActionDeclaration);
|
|
18
|
+
const extra = [
|
|
19
|
+
mutation('shot.set', 'Set shot fields', elementSetSchema('shot')),
|
|
20
|
+
mutation('shot.rename', 'Rename shot', input({ shotId: id, name: { type: 'string', maxLength: 240 } })),
|
|
21
|
+
mutation('shot.setCamera', 'Set camera block', input({ shotId: id, patch: { type: 'object', properties: {}, additionalProperties: true } })),
|
|
22
|
+
mutation('shot.addKey', 'Add camera key', input({ shotId: id, frame })),
|
|
23
|
+
mutation('shot.moveKey', 'Move camera key', input({ shotId: id, keyId: id, frame })),
|
|
24
|
+
mutation('shot.removeKey', 'Remove camera key', input({ shotId: id, keyId: id })),
|
|
25
|
+
mutation('shot.clearKeys', 'Clear camera keys', input({ shotId: id })),
|
|
26
|
+
mutation('shot.setTimeline', 'Set shot timeline', input({ frameCount: { type: 'integer', minimum: 24, maximum: 28800 } })),
|
|
27
|
+
mutation('shot.setLens', 'Set camera lens', input({ fovDeg: { ...number, minimum: 14, maximum: 90 } })),
|
|
28
|
+
mutation('shot.frame', 'Frame the shot', { ...input({ ...STUDIO_TOOL_SCHEMAS.frame_shot.properties, preset: { type: 'string' } }, []), oneOf: [STUDIO_TOOL_SCHEMAS.frame_shot, input({ preset: { type: 'string' } })] }),
|
|
29
|
+
{ ...mutation('shot.replace', 'Replace shot authoring', input({ shots: { type: 'array', items: { type: 'object', properties: {}, additionalProperties: true } } })), exposure: 'ui-only' },
|
|
30
|
+
{ ...mutation('shot.captureCamera', 'Capture camera framing', input({ shotId: id }, [])), exposure: 'ui-only' },
|
|
31
|
+
{ ...mutation('shot.placeCamera', 'Place camera', input(Object.fromEntries(['x', 'y', 'z', 'lookAtX', 'lookAtY', 'lookAtZ', 'focalMm'].map(key => [key, number])), [])), exposure: 'ui-only' },
|
|
32
|
+
];
|
|
33
|
+
export const declarations = Object.freeze([extra[0], ...existing, ...extra.slice(1).map(entry => entry.id === 'shot.frame' ? entry : { ...entry, exposure: 'ui-only' })]);
|
|
34
|
+
|
|
35
|
+
export function register(registry, ports) {
|
|
36
|
+
const owner = () => ports.storeDomain('shot');
|
|
37
|
+
const mounted = () => Boolean(ports.storeDomain?.('shot')) || 'The shots document owner is not mounted.';
|
|
38
|
+
const shotOf = shotId => owner().read().find(shot => shot.id === shotId) ?? fail('STALE_TARGET', `Shot ${shotId} is not in this scene.`);
|
|
39
|
+
const patchShot = (shotId, update) => { shotOf(shotId); owner().write(current => updateStableItem(current, shotId, update, 'shots')); };
|
|
40
|
+
const methods = {
|
|
41
|
+
'shot.create': () => owner().write(current => addShotAtFrame(current, ports.state().frame, owner().state().frameCount, owner().capture())),
|
|
42
|
+
'shot.split': ({ shotId }) => {
|
|
43
|
+
const shot = shotOf(shotId), { frame } = ports.state();
|
|
44
|
+
if (frame <= shot.startFrame || frame > shot.endFrame) fail('TARGET_NOT_READY', `The playhead (frame ${frame}) is not inside ${shot.name} after its first frame.`);
|
|
45
|
+
owner().write(current => cutAtFrame(current, shotId, frame, owner().capture()));
|
|
46
|
+
},
|
|
47
|
+
'shot.duplicate': ({ shotId }) => { shotOf(shotId); owner().write(current => duplicateShot(current, shotId, owner().state().frameCount)); },
|
|
48
|
+
'shot.remove': ({ shotId }) => { shotOf(shotId); owner().write(current => removeShot(current, shotId)); },
|
|
49
|
+
'shot.reorder': ({ shotId, startFrame }) => { shotOf(shotId); owner().write(current => reorderShot(current, shotId, startFrame, owner().state().frameCount)); },
|
|
50
|
+
'shot.setRange': ({ shotId, range }) => {
|
|
51
|
+
const shot = shotOf(shotId), startFrame = range.startFrame, endFrame = range.endFrameExclusive - 1;
|
|
52
|
+
const edges = startFrame > shot.endFrame ? [['end', endFrame], ['start', startFrame]] : [['start', startFrame], ['end', endFrame]];
|
|
53
|
+
owner().write(current => edges.reduce((rows, [edge, frame]) => resizeShot(rows, shotId, edge, frame, owner().state().frameCount), current));
|
|
54
|
+
},
|
|
55
|
+
'shot.setCameraRail': ({ shotId, points }) => patchShot(shotId, shot => ({ ...shot, camera: updateCameraBlock(shot.camera, {
|
|
56
|
+
cameraRail: points, mode: 'rail', railFollow: railFollowForNewGeometry(shot.camera?.railFollow, shot.endFrame - shot.startFrame + 1),
|
|
57
|
+
}) })),
|
|
58
|
+
'shot.clearCameraRail': ({ shotId }) => {
|
|
59
|
+
const shot = shotOf(shotId);
|
|
60
|
+
if (!createCameraBlock(shot.camera).cameraRail) fail('TARGET_NOT_READY', `${shot.name || shotId} has no camera rail.`);
|
|
61
|
+
patchShot(shotId, shot => ({ ...shot, camera: removeCameraRail(shot.camera) }));
|
|
62
|
+
},
|
|
63
|
+
'shot.rename': ({ shotId, name }) => { shotOf(shotId); owner().write(current => renameShot(current, shotId, name)); },
|
|
64
|
+
'shot.setCamera': ({ shotId, patch }) => patchShot(shotId, shot => ({ ...shot, camera: updateCameraBlock(shot.camera, patch) })),
|
|
65
|
+
'shot.addKey': ({ shotId, frame }) => {
|
|
66
|
+
const shot = shotOf(shotId), target = Math.min(owner().state().frameCount - 1, frame);
|
|
67
|
+
if (target < shot.startFrame || target > shot.endFrame) return;
|
|
68
|
+
const framing = owner().capture();
|
|
69
|
+
patchShot(shotId, shot => ({ ...shot, cameraKeys: [...shot.cameraKeys.filter(key => key.frame !== target),
|
|
70
|
+
{ id: shot.cameraKeys.find(key => key.frame === target)?.id ?? createStableItemId('camera-key'), frame: target, framing }].sort((a, b) => a.frame - b.frame) }));
|
|
71
|
+
},
|
|
72
|
+
'shot.moveKey': ({ shotId, keyId, frame }) => patchShot(shotId, shot => ({ ...shot,
|
|
73
|
+
cameraKeys: moveCameraKey(shot.cameraKeys, keyId, Math.max(shot.startFrame, Math.min(shot.endFrame, frame))) })),
|
|
74
|
+
'shot.removeKey': ({ shotId, keyId }) => patchShot(shotId, shot => ({ ...shot, cameraKeys: removeCameraKey(shot.cameraKeys, keyId) })),
|
|
75
|
+
'shot.clearKeys': ({ shotId }) => patchShot(shotId, shot => ({ ...shot, cameraKeys: [] })),
|
|
76
|
+
'shot.setTimeline': ({ frameCount }) => owner().writeState(before => ({ ...before, frameCount })),
|
|
77
|
+
'shot.setLens': ({ fovDeg }) => owner().setLens(fovDeg),
|
|
78
|
+
'shot.replace': ({ shots }) => owner().write(shots),
|
|
79
|
+
'shot.captureCamera': args => owner().captureCamera(args.shotId),
|
|
80
|
+
'shot.placeCamera': args => owner().placeCamera(args),
|
|
81
|
+
};
|
|
82
|
+
const hasShots = state => state.shots.length > 0 || 'There are no shots yet; add one with shot.create.';
|
|
83
|
+
const availability = {
|
|
84
|
+
'shot.create': state => addShotAtFrame(state.shots, state.frame, state.frameCount, null) !== state.shots || `There is no free room for a new shot at the playhead (frame ${state.frame}); move it with operate_studio { frame } or shorten a shot.`,
|
|
85
|
+
'shot.split': state => state.shots.some(shot => state.frame > shot.startFrame && state.frame <= shot.endFrame) || `The playhead (frame ${state.frame}) is not inside a shot after its first frame; move it with operate_studio { frame }.`,
|
|
86
|
+
'shot.clearCameraRail': state => state.shots.some(shot => createCameraBlock(shot.camera).cameraRail) || 'No shot has a camera rail; lay one with shot.setCameraRail.',
|
|
87
|
+
};
|
|
88
|
+
registerElementSet(registry, ports, extra[0]);
|
|
89
|
+
for (const declaration of declarations.filter(row => row.id !== 'shot.set')) registry.register({ ...declaration,
|
|
90
|
+
available: state => mounted() === true ? (availability[declaration.id] ?? (existing.includes(declaration) ? hasShots : () => true))(state) : mounted(),
|
|
91
|
+
run(args) {
|
|
92
|
+
if (declaration.id === 'shot.frame') {
|
|
93
|
+
const plan = owner().frame(args);
|
|
94
|
+
return { affectedIds: plan.affectedIds, summary: 'Framed the shot.' };
|
|
95
|
+
}
|
|
96
|
+
const before = owner().read(); methods[declaration.id](args);
|
|
97
|
+
const after = owner().read(), changed = changedIds(before, after);
|
|
98
|
+
const affectedIds = changed.length ? changed : [args.shotId ?? ports.state().activeSceneId];
|
|
99
|
+
return { affectedIds, summary: changed.length ? `${declaration.label}: ${changed.map(id => after.find(row => row.id === id)).filter(Boolean).map(shotLabel).join('; ')}.` : `${declaration.label}.` };
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
if (ports.storeDomain?.('shot')) registry.registerToolAlias('frame_shot', 'shot.frame');
|
|
103
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { elementSetSchema, registerElementSet } from './elements.js';
|
|
2
|
+
export { STAGE_FIELDS, normalizeStage } from './elements/stage.js';
|
|
3
|
+
|
|
4
|
+
const input = elementSetSchema('stage');
|
|
5
|
+
const declaration = (id, properties = input.properties) => ({ id, label: 'Set stage', kind: 'mutation', undoDomain: 'stage',
|
|
6
|
+
description: 'Update authored stage fields, normalized by scene persistence.',
|
|
7
|
+
input: { ...input, properties } });
|
|
8
|
+
export const declarations = Object.freeze([
|
|
9
|
+
declaration('stage.set'),
|
|
10
|
+
declaration('stage.setKeyLight', { keyLight: input.properties.keyLight }),
|
|
11
|
+
declaration('stage.setEnvironment', Object.fromEntries(['environment', 'environmentImage', 'hasEnvSheet'].map(key => [key, input.properties[key]]))),
|
|
12
|
+
declaration('stage.setStyle', { style: input.properties.style }),
|
|
13
|
+
declaration('stage.setFilmback', Object.fromEntries(['shotAspect', 'cameraPresetId', 'sensorId'].filter(key => input.properties[key]).map(key => [key, input.properties[key]]))),
|
|
14
|
+
]);
|
|
15
|
+
export function register(registry, ports) {
|
|
16
|
+
for (const declaration of declarations) registerElementSet(registry, ports, declaration);
|
|
17
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Viewer preferences: transient, like the View menu they mirror.
|
|
2
|
+
import { studioActionDeclaration } from "../studio-actions.js";
|
|
3
|
+
import { StudioProtocolError, STUDIO_TOOL_SCHEMAS } from '../studio-agent-protocol.js';
|
|
4
|
+
import { shotAtFrame } from '../cuts.js';
|
|
5
|
+
|
|
6
|
+
const fields = STUDIO_TOOL_SCHEMAS.operate_studio.properties;
|
|
7
|
+
const groups = { 'view.select': ['selection', 'shotId'], 'timeline.seek': ['frame'], 'timeline.play': ['playing'], 'view.setMode': ['mode', 'view'] };
|
|
8
|
+
const operations = Object.entries({ ...groups, 'view.update': Object.keys(fields) }).map(([id, keys]) => ({
|
|
9
|
+
id, label: id, description: 'Change selection, timeline or viewer state without authoring history.', kind: 'transient',
|
|
10
|
+
input: { type: 'object', properties: Object.fromEntries(keys.map(key => [key, fields[key]])), required: [], additionalProperties: false },
|
|
11
|
+
}));
|
|
12
|
+
export const declarations = Object.freeze([...operations, ...["view.setPartColours", "view.setGuideMode", "view.setInset"].map(studioActionDeclaration)]);
|
|
13
|
+
export const viewCommand = args => Object.entries(groups).find(([, keys]) => Object.keys(args).every(key => keys.includes(key)))?.[0] ?? 'view.update';
|
|
14
|
+
|
|
15
|
+
export function register(registry, ports) {
|
|
16
|
+
registry.registerToolAlias('operate_studio', viewCommand);
|
|
17
|
+
for (const declaration of operations) registry.register({ ...declaration, available: () => true, run(args) {
|
|
18
|
+
const fail = (code, message) => { throw new StudioProtocolError(code, message); };
|
|
19
|
+
if (!Object.keys(args).length || (args.view && !Object.keys(args.view).length)) fail('INVALID_ARGUMENT', 'Specify a view operation.');
|
|
20
|
+
const state = ports.readView();
|
|
21
|
+
// Pose mode is IK editing: without a rig to solve there is nothing to pose.
|
|
22
|
+
if (args.mode === 'pose' && args.mode !== state.view.mode && !ports.canPose()) fail('TARGET_NOT_READY', 'Pose mode needs a loaded character rig to edit with IK.');
|
|
23
|
+
const selection = args.selection === undefined ? state.selection : args.selection;
|
|
24
|
+
if (selection) {
|
|
25
|
+
const found = selection.kind === 'scene' ? selection.id === state.host.sceneId : selection.kind === 'camera' ? selection.id === 'camera'
|
|
26
|
+
: (selection.kind === 'object' ? state.objects : state.characters).some(row => row.id === selection.id);
|
|
27
|
+
if (!found) fail('STALE_TARGET', 'Selection is not present in this document.');
|
|
28
|
+
}
|
|
29
|
+
const shot = args.shotId === undefined ? null : state.shots.find(row => row.id === args.shotId);
|
|
30
|
+
if (args.shotId !== undefined && !shot) fail('STALE_TARGET', 'Shot is not present in this document.');
|
|
31
|
+
const frame = args.frame ?? shot?.startFrame ?? state.view.frame;
|
|
32
|
+
if (frame >= Math.max(1, state.frameCount)) fail('INVALID_RANGE', 'Frame is outside the timeline.');
|
|
33
|
+
const view = { ...state.view, ...args.view, frame, mode: args.mode ?? state.view.mode, playing: args.playing ?? state.view.playing };
|
|
34
|
+
ports.publishView({ selection, view, shotId: args.shotId ?? shotAtFrame(state.shots, frame)?.id ?? null });
|
|
35
|
+
return { affectedIds: [state.host.sceneId], summary: 'Updated editor view.' };
|
|
36
|
+
} });
|
|
37
|
+
const viewAction = (id, run) => registry.register({ ...studioActionDeclaration(id), available: () => true,
|
|
38
|
+
run: args => ({ affectedIds: [], summary: run(args) }) });
|
|
39
|
+
viewAction("view.setPartColours", ({ mode }) => { ports.choosePartColours(mode); return `Part colours: ${mode}.`; });
|
|
40
|
+
viewAction("view.setGuideMode", ({ mode }) => { ports.setGuideMode(mode); return `Composition guide: ${mode}.`; });
|
|
41
|
+
viewAction("view.setInset", ({ collapsed }) => { ports.setInsetCollapsed(collapsed); return `Top-View inset ${collapsed ? "folded" : "unfolded"}.`; });
|
|
42
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// Each domain owns immutable authored slices here. The facade composes sessions
|
|
2
|
+
// across stores; neither this store nor its callers restore native snapshots.
|
|
3
|
+
import { createHistory, pushHistory, undoHistory, redoHistory } from './history.js';
|
|
4
|
+
import { StudioProtocolError } from './studio-agent-protocol.js';
|
|
5
|
+
import { copyAuthoredIntent, deepFreeze } from './store/authored-intent.js';
|
|
6
|
+
|
|
7
|
+
const fail = (code, message) => { throw new StudioProtocolError(code, message); };
|
|
8
|
+
|
|
9
|
+
export function createDocumentStore({ owned = {}, dev = import.meta.env?.DEV ?? true, copyIntent = copyAuthoredIntent } = {}) {
|
|
10
|
+
const freeze = value => dev ? deepFreeze(value) : value;
|
|
11
|
+
const copy = value => freeze(copyIntent(value));
|
|
12
|
+
let slices = copy(owned);
|
|
13
|
+
let snapshot = freeze({ revision: 0, domainRevisions: Object.fromEntries(Object.keys(owned).map(domain => [domain, 0])), slices });
|
|
14
|
+
let history = createHistory({ historyEntryId: null, snapshot: slices });
|
|
15
|
+
let active = null, running = 0;
|
|
16
|
+
const listeners = new Set();
|
|
17
|
+
const owns = domain => Object.hasOwn(slices, domain);
|
|
18
|
+
function requireOwner(domain) {
|
|
19
|
+
if (!owns(domain)) fail('CAPABILITY_MISSING', `No document slice is owned for ${domain}.`);
|
|
20
|
+
}
|
|
21
|
+
function publish(next) {
|
|
22
|
+
const changed = Object.keys(slices).filter(domain => slices[domain] !== next[domain]);
|
|
23
|
+
if (!changed.length) return;
|
|
24
|
+
const domainRevisions = { ...snapshot.domainRevisions };
|
|
25
|
+
for (const domain of changed) domainRevisions[domain]++;
|
|
26
|
+
slices = freeze(next);
|
|
27
|
+
snapshot = freeze({ revision: snapshot.revision + 1, domainRevisions, slices });
|
|
28
|
+
for (const listener of listeners) listener();
|
|
29
|
+
}
|
|
30
|
+
function beginAction(domain) {
|
|
31
|
+
requireOwner(domain);
|
|
32
|
+
if (active) fail('TARGET_BUSY', 'A document transaction is already open.');
|
|
33
|
+
const before = slices;
|
|
34
|
+
const check = () => { if (active !== session) fail('STALE_TARGET', 'Document transaction is no longer current.'); };
|
|
35
|
+
const session = {
|
|
36
|
+
// Permission ends at the synchronous publication boundary, not when an
|
|
37
|
+
// async preparation finishes. Jobs explicitly re-enter through commit.
|
|
38
|
+
run(fn) { check(); running++; try { return fn(); } finally { running--; } },
|
|
39
|
+
touch(domain) { check(); requireOwner(domain); },
|
|
40
|
+
update(domain, update) { return session.run(() => write(domain, update)); },
|
|
41
|
+
cancel({ restore = true } = {}) {
|
|
42
|
+
if (active !== session) return false;
|
|
43
|
+
active = null;
|
|
44
|
+
if (restore) publish(before);
|
|
45
|
+
return true;
|
|
46
|
+
},
|
|
47
|
+
commit() {
|
|
48
|
+
check(); active = null;
|
|
49
|
+
if (!Object.keys(slices).some(key => slices[key] !== before[key])) return { historyEntryId: null };
|
|
50
|
+
const historyEntryId = crypto.randomUUID();
|
|
51
|
+
history = pushHistory(history, { historyEntryId, snapshot: slices });
|
|
52
|
+
return { historyEntryId };
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
active = session;
|
|
56
|
+
return session;
|
|
57
|
+
}
|
|
58
|
+
function recordAction(domain, fn, targetId = null, nested = false) {
|
|
59
|
+
if (nested && active) {
|
|
60
|
+
active.touch(domain);
|
|
61
|
+
const result = active.run(fn);
|
|
62
|
+
return result?.then ? result.then(result => ({ result, historyEntryId: null })) : { result, historyEntryId: null };
|
|
63
|
+
}
|
|
64
|
+
const session = beginAction(domain);
|
|
65
|
+
const done = result => ({ result, ...session.commit() });
|
|
66
|
+
const failed = error => { session.cancel(); throw error; };
|
|
67
|
+
try { const result = session.run(fn); return result?.then ? result.then(done, failed) : done(result); }
|
|
68
|
+
catch (error) { return failed(error); }
|
|
69
|
+
}
|
|
70
|
+
function write(domain, update) {
|
|
71
|
+
requireOwner(domain);
|
|
72
|
+
if (dev && !running) throw new TypeError(`A store-owned ${domain} write requires a bus run.`);
|
|
73
|
+
if (!active) return recordAction(domain, () => write(domain, update)).result;
|
|
74
|
+
if (!running) fail('TARGET_BUSY', 'A document transaction owns this write.');
|
|
75
|
+
const next = typeof update === 'function' ? update(slices[domain]) : update;
|
|
76
|
+
if (next !== slices[domain]) publish({ ...slices, [domain]: copy(next) });
|
|
77
|
+
return slices[domain];
|
|
78
|
+
}
|
|
79
|
+
// The oldest snapshot has no retained pre-image. Its id cannot promise Undo.
|
|
80
|
+
const retainedEntries = () => [...(history.past.length ? [...history.past.slice(1), history.present] : []), ...history.future];
|
|
81
|
+
function step(redo) {
|
|
82
|
+
if (active) fail('TARGET_BUSY', 'Finish the document transaction before traversing history.');
|
|
83
|
+
const entry = redo ? history.future[0] : history.present;
|
|
84
|
+
const next = (redo ? redoHistory : undoHistory)(history);
|
|
85
|
+
if (!next) return null;
|
|
86
|
+
history = next; publish(history.present.snapshot);
|
|
87
|
+
return entry;
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
owns, read: domain => { requireOwner(domain); return slices[domain]; }, write, beginAction, recordAction,
|
|
91
|
+
dispose() { active?.cancel(); listeners.clear(); },
|
|
92
|
+
undo: () => step(false), redo: () => step(true),
|
|
93
|
+
canUndo: id => !active && history.past.length > 0 && (id === undefined || history.present.historyEntryId === id),
|
|
94
|
+
canRedo: () => !active && history.future.length > 0,
|
|
95
|
+
getSnapshot: () => snapshot,
|
|
96
|
+
subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener); },
|
|
97
|
+
isRetained: id => Boolean(id && retainedEntries().some(entry => entry.historyEntryId === id)),
|
|
98
|
+
history: () => freeze(history),
|
|
99
|
+
depths: () => ({ past: history.past.length, future: history.future.length }),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Compatibility for object producers with token-based drags. There is no
|
|
104
|
+
// separate object history: every preview, cancellation and traversal uses the
|
|
105
|
+
// document store. Immutable producer arrays retain their reference contract.
|
|
106
|
+
export function createSceneHistoryStore(initialObjects, { onObjects, onCommit } = {}) {
|
|
107
|
+
const store = createDocumentStore({ owned: { objects: initialObjects }, dev: false, copyIntent: value => value });
|
|
108
|
+
const read = () => store.read('objects');
|
|
109
|
+
store.subscribe(() => onObjects?.(read()));
|
|
110
|
+
let active = null, sequence = 0;
|
|
111
|
+
function close(commit, notify = true) {
|
|
112
|
+
const current = active; active = null;
|
|
113
|
+
if (commit) {
|
|
114
|
+
current.session.commit();
|
|
115
|
+
if (notify) onCommit?.(current.before, read());
|
|
116
|
+
} else current.session.cancel();
|
|
117
|
+
}
|
|
118
|
+
function settle(notify = true) {
|
|
119
|
+
if (!active) return;
|
|
120
|
+
const cancel = active.cancel;
|
|
121
|
+
close(true, notify); cancel?.();
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
get objects() { return read(); },
|
|
125
|
+
present: () => store.history().present.snapshot.objects,
|
|
126
|
+
applyAtomic(fn) {
|
|
127
|
+
settle();
|
|
128
|
+
const before = read();
|
|
129
|
+
store.recordAction('objects', () => store.write('objects', fn));
|
|
130
|
+
if (before !== read()) onCommit?.(before, read());
|
|
131
|
+
},
|
|
132
|
+
begin(owner, cancel) {
|
|
133
|
+
settle();
|
|
134
|
+
active = { token: ++sequence, owner, cancel, before: read(), session: store.beginAction('objects') };
|
|
135
|
+
return active.token;
|
|
136
|
+
},
|
|
137
|
+
applyIn(token, fn) { if (active?.token === token) active.session.update('objects', fn); },
|
|
138
|
+
end(token, { commit }) { if (active?.token !== token) return false; close(commit); return true; },
|
|
139
|
+
settle,
|
|
140
|
+
undo() { settle(false); return store.undo() ? read() : null; },
|
|
141
|
+
redo() { settle(false); return store.redo() ? read() : null; },
|
|
142
|
+
canUndo: store.canUndo, canRedo: store.canRedo, depths: store.depths,
|
|
143
|
+
};
|
|
144
|
+
}
|