@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.
Files changed (112) hide show
  1. package/dist/assets/index-B7notge7.css +1 -0
  2. package/dist/assets/index-BBE6Ta9L.js +52 -0
  3. package/dist/index.html +13 -0
  4. package/package.json +45 -0
  5. package/src/api.ts +289 -0
  6. package/src/app/App.tsx +394 -0
  7. package/src/audio/index.tsx +74 -0
  8. package/src/audio/preview.ts +364 -0
  9. package/src/bridge.ts +45 -0
  10. package/src/bridgeTypes.ts +582 -0
  11. package/src/channels.ts +40 -0
  12. package/src/main.tsx +15 -0
  13. package/src/panels/ResizeHandle.tsx +113 -0
  14. package/src/panels/agent/AgentPanel.tsx +478 -0
  15. package/src/panels/agent/agent.css +322 -0
  16. package/src/panels/export/ExportDialog.tsx +284 -0
  17. package/src/panels/files/FilePanel.tsx +564 -0
  18. package/src/panels/files/McpSection.tsx +66 -0
  19. package/src/panels/files/icons.tsx +118 -0
  20. package/src/panels/header/DocSwitcher.tsx +85 -0
  21. package/src/panels/header/Header.tsx +132 -0
  22. package/src/panels/header/HistoryControls.tsx +20 -0
  23. package/src/panels/header/StatusBadges.tsx +49 -0
  24. package/src/panels/header/TransportBar.tsx +86 -0
  25. package/src/panels/header/ZoomControls.tsx +38 -0
  26. package/src/panels/help/HelpSection.tsx +190 -0
  27. package/src/panels/inspector/FramingRows.tsx +69 -0
  28. package/src/panels/inspector/InspectorPanel.tsx +178 -0
  29. package/src/panels/inspector/MarkerSection.tsx +63 -0
  30. package/src/panels/inspector/ParamRow.tsx +71 -0
  31. package/src/panels/inspector/animate/BlockTiming.tsx +341 -0
  32. package/src/panels/inspector/animate/DuringBlocks.tsx +85 -0
  33. package/src/panels/inspector/animate/InOutPresets.tsx +57 -0
  34. package/src/panels/inspector/animate/ResolvedChannels.tsx +150 -0
  35. package/src/panels/inspector/animate/ShotSection.tsx +126 -0
  36. package/src/panels/inspector/animate/useBlocks.ts +43 -0
  37. package/src/panels/inspector/design/AlignRow.tsx +63 -0
  38. package/src/panels/inspector/design/AppearanceSection.tsx +49 -0
  39. package/src/panels/inspector/design/ClonerSection.tsx +142 -0
  40. package/src/panels/inspector/design/ComponentSection.tsx +94 -0
  41. package/src/panels/inspector/design/CornerSection.tsx +52 -0
  42. package/src/panels/inspector/design/FillSection.tsx +89 -0
  43. package/src/panels/inspector/design/HtmlSection.tsx +50 -0
  44. package/src/panels/inspector/design/InstanceSection.tsx +302 -0
  45. package/src/panels/inspector/design/MediaSection.tsx +251 -0
  46. package/src/panels/inspector/design/MeshSection.tsx +172 -0
  47. package/src/panels/inspector/design/ShadowSection.tsx +73 -0
  48. package/src/panels/inspector/design/StrokeSection.tsx +85 -0
  49. package/src/panels/inspector/design/TransformSection.tsx +91 -0
  50. package/src/panels/inspector/design/TypographySection.tsx +170 -0
  51. package/src/panels/inspector/scene/CameraSection.tsx +59 -0
  52. package/src/panels/inspector/scene/EffectsSection.tsx +123 -0
  53. package/src/panels/inspector/scene/SceneSection.tsx +71 -0
  54. package/src/panels/inspector/scene/TransitionSection.tsx +136 -0
  55. package/src/panels/inspector/useDocWrite.ts +268 -0
  56. package/src/panels/material/MaterialPicker.tsx +172 -0
  57. package/src/panels/stage/StageMenu.tsx +38 -0
  58. package/src/panels/stage/StagePanel.tsx +131 -0
  59. package/src/panels/timeline/AudioLane.tsx +340 -0
  60. package/src/panels/timeline/BlockRow.tsx +242 -0
  61. package/src/panels/timeline/ChannelRow.tsx +266 -0
  62. package/src/panels/timeline/LifespanRow.tsx +165 -0
  63. package/src/panels/timeline/MarkerLane.tsx +146 -0
  64. package/src/panels/timeline/SceneStrip.tsx +145 -0
  65. package/src/panels/timeline/TimelineDock.tsx +488 -0
  66. package/src/panels/timeline/model.ts +641 -0
  67. package/src/panels/timeline/snap.ts +191 -0
  68. package/src/panels/toolbar/AddMenu.tsx +230 -0
  69. package/src/panels/toolbar/CameraViewBar.tsx +70 -0
  70. package/src/panels/toolbar/ShadingBar.tsx +42 -0
  71. package/src/panels/toolbar/ToolDockBar.tsx +110 -0
  72. package/src/panels/toolbar/ViewModeBar.tsx +49 -0
  73. package/src/shotTokens.css +131 -0
  74. package/src/stage/FrustumGizmo.tsx +184 -0
  75. package/src/stage/GridPlane.tsx +274 -0
  76. package/src/stage/MoveGizmo.tsx +411 -0
  77. package/src/stage/PerfHud.tsx +206 -0
  78. package/src/stage/SplitRenderer.tsx +511 -0
  79. package/src/stage/StageApp.tsx +3423 -0
  80. package/src/stage/StageTools.tsx +581 -0
  81. package/src/stage/bridgeClient.ts +278 -0
  82. package/src/stage/feed.ts +243 -0
  83. package/src/stage/frustumGeometry.ts +172 -0
  84. package/src/stage/gizmoMath.ts +494 -0
  85. package/src/stage/handles.ts +151 -0
  86. package/src/stage/instanceModules.ts +152 -0
  87. package/src/stage/interactions/move.ts +172 -0
  88. package/src/stage/interactions/panZoom.ts +60 -0
  89. package/src/stage/isolation.ts +155 -0
  90. package/src/stage/main.tsx +110 -0
  91. package/src/stage/modalState.ts +427 -0
  92. package/src/stage/overlays/Overlays.tsx +1046 -0
  93. package/src/stage/panes.ts +121 -0
  94. package/src/stage/penState.ts +112 -0
  95. package/src/stage/project.ts +402 -0
  96. package/src/stage/shading.ts +55 -0
  97. package/src/stage/tokenColor.ts +26 -0
  98. package/src/stage/view.ts +182 -0
  99. package/src/stage/workingView.ts +636 -0
  100. package/src/state/agentActions.ts +203 -0
  101. package/src/state/clientId.ts +25 -0
  102. package/src/state/context.tsx +33 -0
  103. package/src/state/insert.ts +507 -0
  104. package/src/state/keymap.ts +560 -0
  105. package/src/state/menu.ts +141 -0
  106. package/src/state/panelSize.ts +26 -0
  107. package/src/state/renderEvents.ts +100 -0
  108. package/src/state/store.ts +4094 -0
  109. package/src/state/triangles.ts +70 -0
  110. package/src/state/types.ts +384 -0
  111. package/src/styles.css +2948 -0
  112. 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
+ }