@dailies-studio/shot-editor 0.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.
- package/dist/assets/index-B7notge7.css +1 -0
- package/dist/assets/index-BBE6Ta9L.js +52 -0
- package/dist/index.html +13 -0
- package/package.json +45 -0
- package/src/api.ts +289 -0
- package/src/app/App.tsx +394 -0
- package/src/audio/index.tsx +74 -0
- package/src/audio/preview.ts +364 -0
- package/src/bridge.ts +45 -0
- package/src/bridgeTypes.ts +582 -0
- package/src/channels.ts +40 -0
- package/src/main.tsx +15 -0
- package/src/panels/ResizeHandle.tsx +113 -0
- package/src/panels/agent/AgentPanel.tsx +478 -0
- package/src/panels/agent/agent.css +322 -0
- package/src/panels/export/ExportDialog.tsx +284 -0
- package/src/panels/files/FilePanel.tsx +564 -0
- package/src/panels/files/McpSection.tsx +66 -0
- package/src/panels/files/icons.tsx +118 -0
- package/src/panels/header/DocSwitcher.tsx +85 -0
- package/src/panels/header/Header.tsx +132 -0
- package/src/panels/header/HistoryControls.tsx +20 -0
- package/src/panels/header/StatusBadges.tsx +49 -0
- package/src/panels/header/TransportBar.tsx +86 -0
- package/src/panels/header/ZoomControls.tsx +38 -0
- package/src/panels/help/HelpSection.tsx +190 -0
- package/src/panels/inspector/FramingRows.tsx +69 -0
- package/src/panels/inspector/InspectorPanel.tsx +178 -0
- package/src/panels/inspector/MarkerSection.tsx +63 -0
- package/src/panels/inspector/ParamRow.tsx +71 -0
- package/src/panels/inspector/animate/BlockTiming.tsx +341 -0
- package/src/panels/inspector/animate/DuringBlocks.tsx +85 -0
- package/src/panels/inspector/animate/InOutPresets.tsx +57 -0
- package/src/panels/inspector/animate/ResolvedChannels.tsx +150 -0
- package/src/panels/inspector/animate/ShotSection.tsx +126 -0
- package/src/panels/inspector/animate/useBlocks.ts +43 -0
- package/src/panels/inspector/design/AlignRow.tsx +63 -0
- package/src/panels/inspector/design/AppearanceSection.tsx +49 -0
- package/src/panels/inspector/design/ClonerSection.tsx +142 -0
- package/src/panels/inspector/design/ComponentSection.tsx +94 -0
- package/src/panels/inspector/design/CornerSection.tsx +52 -0
- package/src/panels/inspector/design/FillSection.tsx +89 -0
- package/src/panels/inspector/design/HtmlSection.tsx +50 -0
- package/src/panels/inspector/design/InstanceSection.tsx +302 -0
- package/src/panels/inspector/design/MediaSection.tsx +251 -0
- package/src/panels/inspector/design/MeshSection.tsx +172 -0
- package/src/panels/inspector/design/ShadowSection.tsx +73 -0
- package/src/panels/inspector/design/StrokeSection.tsx +85 -0
- package/src/panels/inspector/design/TransformSection.tsx +91 -0
- package/src/panels/inspector/design/TypographySection.tsx +170 -0
- package/src/panels/inspector/scene/CameraSection.tsx +59 -0
- package/src/panels/inspector/scene/EffectsSection.tsx +123 -0
- package/src/panels/inspector/scene/SceneSection.tsx +71 -0
- package/src/panels/inspector/scene/TransitionSection.tsx +136 -0
- package/src/panels/inspector/useDocWrite.ts +268 -0
- package/src/panels/material/MaterialPicker.tsx +172 -0
- package/src/panels/stage/StageMenu.tsx +38 -0
- package/src/panels/stage/StagePanel.tsx +131 -0
- package/src/panels/timeline/AudioLane.tsx +340 -0
- package/src/panels/timeline/BlockRow.tsx +242 -0
- package/src/panels/timeline/ChannelRow.tsx +266 -0
- package/src/panels/timeline/LifespanRow.tsx +165 -0
- package/src/panels/timeline/MarkerLane.tsx +146 -0
- package/src/panels/timeline/SceneStrip.tsx +145 -0
- package/src/panels/timeline/TimelineDock.tsx +488 -0
- package/src/panels/timeline/model.ts +641 -0
- package/src/panels/timeline/snap.ts +191 -0
- package/src/panels/toolbar/AddMenu.tsx +230 -0
- package/src/panels/toolbar/CameraViewBar.tsx +70 -0
- package/src/panels/toolbar/ShadingBar.tsx +42 -0
- package/src/panels/toolbar/ToolDockBar.tsx +110 -0
- package/src/panels/toolbar/ViewModeBar.tsx +49 -0
- package/src/shotTokens.css +131 -0
- package/src/stage/FrustumGizmo.tsx +184 -0
- package/src/stage/GridPlane.tsx +274 -0
- package/src/stage/MoveGizmo.tsx +411 -0
- package/src/stage/PerfHud.tsx +206 -0
- package/src/stage/SplitRenderer.tsx +511 -0
- package/src/stage/StageApp.tsx +3423 -0
- package/src/stage/StageTools.tsx +581 -0
- package/src/stage/bridgeClient.ts +278 -0
- package/src/stage/feed.ts +243 -0
- package/src/stage/frustumGeometry.ts +172 -0
- package/src/stage/gizmoMath.ts +494 -0
- package/src/stage/handles.ts +151 -0
- package/src/stage/instanceModules.ts +152 -0
- package/src/stage/interactions/move.ts +172 -0
- package/src/stage/interactions/panZoom.ts +60 -0
- package/src/stage/isolation.ts +155 -0
- package/src/stage/main.tsx +110 -0
- package/src/stage/modalState.ts +427 -0
- package/src/stage/overlays/Overlays.tsx +1046 -0
- package/src/stage/panes.ts +121 -0
- package/src/stage/penState.ts +112 -0
- package/src/stage/project.ts +402 -0
- package/src/stage/shading.ts +55 -0
- package/src/stage/tokenColor.ts +26 -0
- package/src/stage/view.ts +182 -0
- package/src/stage/workingView.ts +636 -0
- package/src/state/agentActions.ts +203 -0
- package/src/state/clientId.ts +25 -0
- package/src/state/context.tsx +33 -0
- package/src/state/insert.ts +507 -0
- package/src/state/keymap.ts +560 -0
- package/src/state/menu.ts +141 -0
- package/src/state/panelSize.ts +26 -0
- package/src/state/renderEvents.ts +100 -0
- package/src/state/store.ts +4094 -0
- package/src/state/triangles.ts +70 -0
- package/src/state/types.ts +384 -0
- package/src/styles.css +2948 -0
- package/src/tools.ts +181 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// What a right-click offers, as DATA.
|
|
2
|
+
//
|
|
3
|
+
// ── WHY IT IS A PURE FUNCTION AND NOT A COMPONENT ───────────────────────
|
|
4
|
+
//
|
|
5
|
+
// Two surfaces open this menu — the stage and the outliner's rows — and they
|
|
6
|
+
// have to agree completely: a Delete that means one thing on the canvas and
|
|
7
|
+
// another in the list is worse than having only one of them. A function from
|
|
8
|
+
// (state, targets) to entries is the smallest thing both can call, and the
|
|
9
|
+
// only thing a test can check without rendering either.
|
|
10
|
+
//
|
|
11
|
+
// ── AND WHY EVERY ENTRY IS AN EXISTING VERB ─────────────────────────────
|
|
12
|
+
//
|
|
13
|
+
// Not one of these does its own work. They are the store verbs the keymap
|
|
14
|
+
// already binds, listed under names an author can read — so the menu cannot
|
|
15
|
+
// drift from the shortcuts, and a fix to `duplicateSelection` fixes it in three
|
|
16
|
+
// places at once. A menu entry that reached into the document itself would be a
|
|
17
|
+
// second implementation of an edit that already exists.
|
|
18
|
+
import type { MenuEntry } from "@dailies-studio/ui";
|
|
19
|
+
import type { ShotEditorStore } from "./store.js";
|
|
20
|
+
import type { ShotEditorState } from "./types.js";
|
|
21
|
+
|
|
22
|
+
/** The layers a menu acts on, and what they are. Computed by the caller,
|
|
23
|
+
* because the stage knows the raw hit stack and the outliner knows the row. */
|
|
24
|
+
export interface MenuTargets {
|
|
25
|
+
/** Everything the verbs act on — the selection, in practice. */
|
|
26
|
+
ids: readonly string[];
|
|
27
|
+
/** Which of those are mesh layers, for the material entries. Empty means the
|
|
28
|
+
* material rows are absent rather than disabled: a disabled row invites a
|
|
29
|
+
* click and then explains, and there is nothing to explain here. */
|
|
30
|
+
meshIds: readonly string[];
|
|
31
|
+
/** Do any of the mesh targets already carry a material? Drives "Remove". */
|
|
32
|
+
hasMaterial: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function layerMenuEntries(
|
|
36
|
+
store: ShotEditorStore,
|
|
37
|
+
state: ShotEditorState,
|
|
38
|
+
targets: MenuTargets,
|
|
39
|
+
): MenuEntry[] {
|
|
40
|
+
if (targets.ids.length === 0) return [];
|
|
41
|
+
const layers = state.sceneId === null ? undefined : state.doc?.scenesById[state.sceneId]?.layersById;
|
|
42
|
+
const first = layers?.[targets.ids[0]!];
|
|
43
|
+
// The label follows what the FIRST target is, not what the whole set is. A
|
|
44
|
+
// mixed selection has no honest single word, and "Hide/Show" reading off the
|
|
45
|
+
// subject the author right-clicked is the behaviour every outliner has.
|
|
46
|
+
const hidden = first?.hidden === true;
|
|
47
|
+
const locked = first?.locked === true;
|
|
48
|
+
|
|
49
|
+
// The `id`s are STABLE and independent of the label, because the label flips
|
|
50
|
+
// (Hide/Show, Lock/Unlock) and the menu keys its rows on the id. A key that
|
|
51
|
+
// changed with the word would remount the row every time the flag did.
|
|
52
|
+
const entries: MenuEntry[] = [
|
|
53
|
+
{ kind: "action", id: "duplicate", label: "Duplicate", onSelect: () => store.duplicateSelection() },
|
|
54
|
+
{ kind: "separator" },
|
|
55
|
+
{
|
|
56
|
+
kind: "action",
|
|
57
|
+
id: "hidden",
|
|
58
|
+
label: hidden ? "Show" : "Hide",
|
|
59
|
+
onSelect: () => store.toggleFlag("hidden"),
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
kind: "action",
|
|
63
|
+
id: "locked",
|
|
64
|
+
label: locked ? "Unlock" : "Lock",
|
|
65
|
+
onSelect: () => store.toggleFlag("locked"),
|
|
66
|
+
},
|
|
67
|
+
{ kind: "separator" },
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
if (state.isolation !== null) {
|
|
71
|
+
entries.push({
|
|
72
|
+
kind: "action",
|
|
73
|
+
id: "isolation",
|
|
74
|
+
label: "Exit isolation",
|
|
75
|
+
onSelect: () => store.exitIsolation(),
|
|
76
|
+
});
|
|
77
|
+
} else {
|
|
78
|
+
entries.push({
|
|
79
|
+
kind: "action",
|
|
80
|
+
id: "isolation",
|
|
81
|
+
label: "Isolate",
|
|
82
|
+
onSelect: () => {
|
|
83
|
+
const id = targets.ids[0];
|
|
84
|
+
if (id !== undefined) store.enterIsolation(id);
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (targets.meshIds.length > 0) {
|
|
90
|
+
entries.push({ kind: "separator" });
|
|
91
|
+
entries.push({
|
|
92
|
+
kind: "action",
|
|
93
|
+
id: "material",
|
|
94
|
+
label: "Apply material…",
|
|
95
|
+
onSelect: () => store.openMaterialPicker(targets.meshIds),
|
|
96
|
+
});
|
|
97
|
+
if (targets.hasMaterial) {
|
|
98
|
+
entries.push({
|
|
99
|
+
kind: "action",
|
|
100
|
+
id: "material-remove",
|
|
101
|
+
label: "Remove material",
|
|
102
|
+
onSelect: () => store.removeMeshMaterial(targets.meshIds),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
entries.push({ kind: "separator" });
|
|
108
|
+
entries.push({
|
|
109
|
+
kind: "action",
|
|
110
|
+
id: "delete",
|
|
111
|
+
label: "Delete",
|
|
112
|
+
danger: true,
|
|
113
|
+
onSelect: () => store.deleteSelection(),
|
|
114
|
+
});
|
|
115
|
+
return entries;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The targets for a set of layer ids, read off the open document.
|
|
120
|
+
*
|
|
121
|
+
* Shared by both callers so "which of these can take a material" is answered
|
|
122
|
+
* once. A component definition's interior is deliberately not searched: the
|
|
123
|
+
* menu acts on what the outline is showing, which `editingDef` already scopes.
|
|
124
|
+
*/
|
|
125
|
+
export function menuTargetsFor(state: ShotEditorState, ids: readonly string[]): MenuTargets {
|
|
126
|
+
const layers =
|
|
127
|
+
state.editingDef !== null
|
|
128
|
+
? state.doc?.components?.[state.editingDef]?.layersById
|
|
129
|
+
: state.sceneId === null
|
|
130
|
+
? undefined
|
|
131
|
+
: state.doc?.scenesById[state.sceneId]?.layersById;
|
|
132
|
+
const meshIds: string[] = [];
|
|
133
|
+
let hasMaterial = false;
|
|
134
|
+
for (const id of ids) {
|
|
135
|
+
const layer = layers?.[id];
|
|
136
|
+
if (layer === undefined || layer.kind !== "mesh") continue;
|
|
137
|
+
meshIds.push(id);
|
|
138
|
+
if (layer.mesh.material !== undefined) hasMaterial = true;
|
|
139
|
+
}
|
|
140
|
+
return { ids, meshIds, hasMaterial };
|
|
141
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// How big the two resizable panels may be. Pure, so the bounds are one fact
|
|
2
|
+
// the store, the tests and the reset-to-default all read from the same place.
|
|
3
|
+
//
|
|
4
|
+
// Both are stored in PIXELS even though the dock's old default was `42vh`.
|
|
5
|
+
// A viewport-relative height cannot be dragged: the author sets a size in
|
|
6
|
+
// pixels with their pointer, and re-deriving a percentage from it would make
|
|
7
|
+
// the panel change size on the next window resize for no reason they asked
|
|
8
|
+
// for. The one place the viewport still gets a say is the dock's UPPER bound,
|
|
9
|
+
// which is passed in per drag — a dock taller than the window is a dock with
|
|
10
|
+
// no stage behind it.
|
|
11
|
+
|
|
12
|
+
export const RAIL_WIDTH = { min: 168, max: 460, default: 232 } as const;
|
|
13
|
+
export const DOCK_HEIGHT = { min: 120, max: 640, default: 300 } as const;
|
|
14
|
+
|
|
15
|
+
/** Clamp to whole pixels. Whole, because a fractional panel width puts every
|
|
16
|
+
* box positioned off it on a half pixel and the seams stop being crisp. */
|
|
17
|
+
export function clampSize(value: number, min: number, max: number): number {
|
|
18
|
+
if (!Number.isFinite(value)) return min;
|
|
19
|
+
return Math.round(Math.min(max, Math.max(min, value)));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The dock's ceiling for a given window: its own max, or most of the window,
|
|
23
|
+
* whichever is smaller. */
|
|
24
|
+
export function dockCeiling(viewportHeight: number): number {
|
|
25
|
+
return Math.max(DOCK_HEIGHT.min, Math.min(DOCK_HEIGHT.max, Math.round(viewportHeight * 0.7)));
|
|
26
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// The studio's SSE bus, as this chrome consumes it: render progress, and
|
|
2
|
+
// document changes made somewhere else.
|
|
3
|
+
//
|
|
4
|
+
// `/api/events` is the documented, versioned channel the whole studio already
|
|
5
|
+
// publishes on, so neither of these needs a socket of its own — they need event
|
|
6
|
+
// names. The alternative, polling, would be a second definition of "is it
|
|
7
|
+
// running" / "has it moved" that can disagree with the first.
|
|
8
|
+
//
|
|
9
|
+
// ── WHY THE AGENT APPLY ARRIVES HERE AND NOT IN agentActions ───────────
|
|
10
|
+
//
|
|
11
|
+
// Every write emits `doc-changed`, including an agent apply. So this chrome
|
|
12
|
+
// adopts an agent's change through EXACTLY the path it adopts a second tab's
|
|
13
|
+
// save, which is also the only path that carries the "you have unsaved edits"
|
|
14
|
+
// refusal. A bespoke re-read in the apply handler would be a second answer to
|
|
15
|
+
// "the document moved underneath me", and only one of them would have that
|
|
16
|
+
// refusal in it.
|
|
17
|
+
//
|
|
18
|
+
// EventSource reconnects on its own, and a missed frame costs a stale progress
|
|
19
|
+
// number until the next one. For the document that is safe too: `doc-changed`
|
|
20
|
+
// is a NOTIFICATION and `adoptExternalChange` re-reads from the server, so a
|
|
21
|
+
// dropped frame costs a stale view until the next write, which the baseRev 409
|
|
22
|
+
// protocol already makes safe.
|
|
23
|
+
import type { ShotEditorStore } from "./store.js";
|
|
24
|
+
import type { RenderJob } from "../api.js";
|
|
25
|
+
|
|
26
|
+
/** The SSE event names; `docEvents.ts` is the other half of this contract. */
|
|
27
|
+
const EVENT = "shot-render";
|
|
28
|
+
const DOC_EVENT = "doc-changed";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Subscribe until the returned function is called.
|
|
32
|
+
*
|
|
33
|
+
* `EventSource` absent (a test environment, an ancient browser) is a no-op
|
|
34
|
+
* rather than a throw: progress is a readout, and losing it must never stop the
|
|
35
|
+
* chrome from booting.
|
|
36
|
+
*/
|
|
37
|
+
export function subscribeStudioEvents(store: ShotEditorStore): () => void {
|
|
38
|
+
const Source = (globalThis as { EventSource?: typeof EventSource }).EventSource;
|
|
39
|
+
if (Source === undefined) return () => {};
|
|
40
|
+
const source = new Source("/api/events");
|
|
41
|
+
const onMessage = (e: MessageEvent<string>): void => {
|
|
42
|
+
const job = parseJob(e.data);
|
|
43
|
+
// Only THIS document's renders. The bus is project-wide and a second chrome
|
|
44
|
+
// may be exporting something else; showing its progress here would be a
|
|
45
|
+
// progress bar for a file this window is not making.
|
|
46
|
+
if (job !== null && job.name === store.getState().name) store.setRenderJob(job);
|
|
47
|
+
};
|
|
48
|
+
const onDoc = (e: MessageEvent<string>): void => {
|
|
49
|
+
const payload = parseDocChange(e.data);
|
|
50
|
+
// The SHOT lane only. The bus carries motion, canvas and scene writes too,
|
|
51
|
+
// and handing one of those to a shot store would ask it to reload a
|
|
52
|
+
// document that is not in its store at all.
|
|
53
|
+
if (payload !== null && payload.lane === "shot") store.adoptExternalChange(payload);
|
|
54
|
+
};
|
|
55
|
+
source.addEventListener(EVENT, onMessage as EventListener);
|
|
56
|
+
source.addEventListener(DOC_EVENT, onDoc as EventListener);
|
|
57
|
+
return () => {
|
|
58
|
+
source.removeEventListener(EVENT, onMessage as EventListener);
|
|
59
|
+
source.removeEventListener(DOC_EVENT, onDoc as EventListener);
|
|
60
|
+
source.close();
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The subset of `DocChangedPayload` this chrome acts on. A malformed frame is
|
|
65
|
+
* dropped for the same reason a malformed job frame is. */
|
|
66
|
+
function parseDocChange(
|
|
67
|
+
raw: string,
|
|
68
|
+
): { lane: string; name?: string; origin?: string; clientId?: string } | null {
|
|
69
|
+
try {
|
|
70
|
+
const data: unknown = JSON.parse(raw);
|
|
71
|
+
if (data === null || typeof data !== "object") return null;
|
|
72
|
+
const p = data as { lane?: unknown; name?: unknown; origin?: unknown; clientId?: unknown };
|
|
73
|
+
if (typeof p.lane !== "string") return null;
|
|
74
|
+
return {
|
|
75
|
+
lane: p.lane,
|
|
76
|
+
...(typeof p.name === "string" ? { name: p.name } : {}),
|
|
77
|
+
...(typeof p.origin === "string" ? { origin: p.origin } : {}),
|
|
78
|
+
...(typeof p.clientId === "string" ? { clientId: p.clientId } : {}),
|
|
79
|
+
};
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Narrow the wire payload. A malformed frame is dropped, not thrown: the bus
|
|
86
|
+
* is at-most-once by design and one bad frame must not kill the stream. */
|
|
87
|
+
function parseJob(raw: string): (RenderJob & { name: string }) | null {
|
|
88
|
+
try {
|
|
89
|
+
const data: unknown = JSON.parse(raw);
|
|
90
|
+
if (data === null || typeof data !== "object") return null;
|
|
91
|
+
const job = data as RenderJob;
|
|
92
|
+
if (typeof job.id !== "string" || typeof job.name !== "string") return null;
|
|
93
|
+
if (job.phase !== "running" && job.phase !== "done" && job.phase !== "failed" && job.phase !== "cancelled") {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
return job;
|
|
97
|
+
} catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|