@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,70 @@
1
+ // What a scene's geometry costs, and when to say something about it.
2
+ //
3
+ // ── WHY A BUDGET AT ALL ─────────────────────────────────────────────────
4
+ //
5
+ // A model's triangle count is the one authoring decision in this lane that can
6
+ // make the editor unusable, and it is the one an author has no way to see. A
7
+ // GLB is a file with a name; nothing on screen distinguishes a 4k-triangle prop
8
+ // from a 900k-triangle scan until the pane stops keeping up, and by then the
9
+ // document is built around it.
10
+ //
11
+ // ── AND WHY THE TWO NUMBERS ARE WHAT THEY ARE ───────────────────────────
12
+ //
13
+ // The split viewport draws the scene ONCE PER PANE, so an authored count is
14
+ // doubled before it reaches the GPU, and every one of those frames is a
15
+ // re-draw the author is watching happen under their hands.
16
+ //
17
+ // SOFT 500k (1M drawn) is where a per-commit redraw stops being instant on
18
+ // integrated graphics with this lane's unlit single-pipeline materials. It
19
+ // WARNS: the outliner tints the badges and the title says why. A hard block
20
+ // here would refuse work that renders perfectly well on a workstation.
21
+ //
22
+ // HARD 2M (4M drawn) is where even discrete hardware drops below a usable
23
+ // frame rate for a drag. It refuses the INSERT — the moment a model is added
24
+ // is the only moment refusing costs nothing, because nothing has been built
25
+ // on it yet. An already-heavy document still opens and still edits: a cap
26
+ // that locked an author out of their own file would be the worse failure.
27
+ //
28
+ // Both are reasoned from how the lane draws rather than measured on target
29
+ // hardware, and the HUD's `tris` readout is what will eventually replace the
30
+ // reasoning with numbers.
31
+ export const TRIANGLE_SOFT_BUDGET = 500_000;
32
+ export const TRIANGLE_HARD_CAP = 2_000_000;
33
+
34
+ /**
35
+ * The scene's total.
36
+ *
37
+ * A SUM OVER LOADED MODELS, and it under-counts on purpose in one place: a
38
+ * cloner draws its subtree N times and this counts it once. Multiplying would
39
+ * need the clone count at this level, which is a document walk for a number
40
+ * whose job is to be a rough warning.
41
+ *
42
+ * ponytail: multiply by clone count when someone ships a cloned model heavy
43
+ * enough that the badge disagrees with the frame rate.
44
+ */
45
+ export function sumTriangles(byLayer: Readonly<Record<string, number>>): number {
46
+ let total = 0;
47
+ for (const n of Object.values(byLayer)) total += n;
48
+ return total;
49
+ }
50
+
51
+ /** `12.4k`, `1.2M`. Short enough for a row that already carries an icon, a
52
+ * name and two toggles. */
53
+ export function formatTriangles(count: number): string {
54
+ if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`;
55
+ if (count >= 1_000) return `${(count / 1_000).toFixed(count >= 10_000 ? 0 : 1)}k`;
56
+ return String(count);
57
+ }
58
+
59
+ /** The soft-budget message, on the badge's own title. */
60
+ export function sceneBudgetWarning(total: number): string {
61
+ return (
62
+ `THIS SCENE CARRIES ${total.toLocaleString()} TRIANGLES. ` +
63
+ `ABOVE ${TRIANGLE_SOFT_BUDGET.toLocaleString()} THE WORKING PANE STOPS FEELING INSTANT.`
64
+ );
65
+ }
66
+
67
+ /** The insert refusal, verbatim. */
68
+ export const TRIANGLE_CAP_REFUSAL =
69
+ `A SCENE HOLDS AT MOST ${TRIANGLE_HARD_CAP.toLocaleString()} TRIANGLES. ` +
70
+ `REMOVE OR SIMPLIFY A MODEL BEFORE ADDING ANOTHER.`;
@@ -0,0 +1,384 @@
1
+ // The shot chrome's state shape. ONE document, flat fields.
2
+ //
3
+ // ── WHY THERE ARE NO BOARDS ───────────────────────────────────────────────
4
+ //
5
+ // The canvas lane keeps N documents open on an infinite pasteboard because a
6
+ // Doc2D is a COMPONENT and a project holds dozens. A shot document is a
7
+ // DELIVERABLE VIDEO; Raylight's "many things at once" is scenes INSIDE one
8
+ // document, which is the timeline's macro lane, not a pasteboard.
9
+ //
10
+ // The non-aesthetic reason is `GL_CONTEXT_BUDGET = 12` (cli/src/pageCount.ts):
11
+ // a measured per-origin browser ceiling on live WebGL contexts whose failure
12
+ // mode is a SILENT WEDGE, not an error. Every mounted shot owns a `<Canvas>`.
13
+ // The canvas lane never had that ceiling because its boards are DOM.
14
+ //
15
+ // So: one document, flat fields, and roughly 1700 lines of board machinery
16
+ // simply not written. Autosave debounce, the sha256 rev and the generation
17
+ // fence all still apply — they were never board-shaped, only board-keyed.
18
+ import { DOCK_HEIGHT, RAIL_WIDTH } from "./panelSize.js";
19
+ import type { Doc3D } from "@dailies-studio/shot/doc";
20
+ import type { AgentCredentialsInfo, AgentTranscriptItem } from "@dailies-studio/agent-client";
21
+ import type { Contract } from "@dailies-studio/contract";
22
+ import type { ComponentRow, RenderJob, ShotListing } from "../api.js";
23
+ import type { ShadingMode, ShotStageInfo, ShotView, StageLayout, StagePane, WorkingView } from "../bridgeTypes.js";
24
+ import { INITIAL_WORKING_VIEW } from "../stage/workingView.js";
25
+ import type { DrawPoint } from "./insert.js";
26
+ import type { ShotTool } from "../tools.js";
27
+
28
+ export type SaveState = "idle" | "saving" | "saved" | "error";
29
+
30
+ export interface ShotEditorState {
31
+ /** Everything in `.studio/shots/`, for the switcher. */
32
+ shots: readonly ShotListing[];
33
+ /** The open document's store name, or null at the empty state. */
34
+ name: string | null;
35
+ doc: Doc3D | null;
36
+ /** The rev the open document was READ at, and the baseline every save
37
+ * claims. null = nothing open, or a create that has not landed yet. */
38
+ rev: string | null;
39
+
40
+ /** MASTER frame. Integer, always. */
41
+ frame: number;
42
+ playing: boolean;
43
+
44
+ /** Which scene the micro timeline and the inspector are looking at. This is
45
+ * the SINGLE bit that decides macro vs micro, and it is the same bit the URL
46
+ * mirrors — one two-level model, not two. */
47
+ sceneId: string | null;
48
+ /** Layer ids, in document order, deduped. */
49
+ selection: readonly string[];
50
+
51
+ /** Which tool is armed. Decides what a canvas press does, and which plane a
52
+ * move unprojects onto. */
53
+ tool: ShotTool;
54
+
55
+ /**
56
+ * TWO MODE BITS, and collapsing them would be a data-corrupting surprise.
57
+ *
58
+ * `inspectorMode` is a VIEW choice, set by clicking the inspector's tabs.
59
+ * `animateMode` is a BEHAVIOUR choice, set ONLY by ⇧A: with it on, a canvas
60
+ * drag authors a BLOCK ending at the playhead instead of writing the base.
61
+ *
62
+ * One bit would mean that clicking "Animate" to LOOK at the blocks silently
63
+ * arms block authoring on the next drag.
64
+ */
65
+ inspectorMode: "design" | "animate";
66
+ animateMode: boolean;
67
+
68
+ /**
69
+ * The block the dock has focused, or null.
70
+ *
71
+ * Addressed by (scene, layer, block) even though a block id is unique across
72
+ * the document. The layer is what tells a row which lane it is in without a
73
+ * lookup; the SCENE is what stopped being derivable when the dock became one
74
+ * master strip. `state.sceneId` follows the playhead, so a block clicked in
75
+ * scene 3 while the playhead sits in scene 1 would have sent Delete and Split
76
+ * at scene 1 — where the id does not exist, so both would have silently done
77
+ * nothing.
78
+ */
79
+ selectedBlock: { sceneId: string; layerId: string; blockId: string } | null;
80
+ /**
81
+ * The marker the strip has selected, or null.
82
+ *
83
+ * A BARE ID, unlike `selectedBlock`: a marker belongs to the document rather
84
+ * than to a scene (`MarkerRef.frame` is a master frame), so there is nothing
85
+ * to address it relative to and nothing that can go stale under a retime.
86
+ *
87
+ * It is the FIRST subject Delete and the inspector consult — see
88
+ * `deleteSelection`. Any layer selection clears it, so "click a layer, press
89
+ * Delete" can never remove a marker the author stopped looking at.
90
+ */
91
+ selectedMarker: string | null;
92
+ /**
93
+ * The box a drawn IMAGE layer is waiting to land in, or null.
94
+ *
95
+ * The image tool draws its box first and asks for a file second, and a file
96
+ * dialog can only be opened from a real click on a real `<input>` — which the
97
+ * store is not. So the geometry parks here and the toolbar's hidden input
98
+ * picks it up; `completeImageDraw` is the other half. Never persisted, never
99
+ * a document edit: nothing exists yet.
100
+ */
101
+ imagePick: readonly DrawPoint[] | null;
102
+ /**
103
+ * The stage's last right press, or null. A MAILBOX, not a menu.
104
+ *
105
+ * Fresh identity per event, so `StageMenu`'s effect fires again when the
106
+ * author right-clicks a second time at the same point on the same layer —
107
+ * value equality here would silently swallow the second menu.
108
+ *
109
+ * Cleared by `onActivePane`, which is the seam EVERY stage press and wheel
110
+ * already crosses. It has to be cleared from the stage side: a pointerdown
111
+ * inside the iframe is a different document's event and never reaches the
112
+ * chrome window's outside-click listener.
113
+ */
114
+ stageMenu: { at: { x: number; y: number }; hits: readonly string[]; pane: StagePane } | null;
115
+ /**
116
+ * The mesh layers a material pick would be applied to, or null when the
117
+ * picker is closed. Editor state: nothing exists in the document until a
118
+ * card is chosen.
119
+ */
120
+ materialPicker: { layerIds: readonly string[] } | null;
121
+ /**
122
+ * Triangle counts by mesh layer id, pushed up from the stage as models load.
123
+ *
124
+ * The chrome is three-free by test, so it cannot count them itself. Held here
125
+ * rather than derived because the count is a property of the loaded GLB, not
126
+ * of the document: two layers on one asset report the same number, and a
127
+ * layer whose model has not arrived yet reports nothing at all.
128
+ */
129
+ meshTriangles: Readonly<Record<string, number>>;
130
+ /**
131
+ * The subtree being edited in isolation, or null for the whole scene.
132
+ *
133
+ * EDITOR STATE, never a document field. It scopes three things at once — the
134
+ * working pane's camera, what is pickable, and what is dimmed — and all three
135
+ * derive from this ONE id rather than from copies, so they cannot disagree
136
+ * about what the author is inside.
137
+ *
138
+ * Validated on every document write (`onHistoryChange`): a delete, an undo or
139
+ * an agent edit can take the layer away, and an isolation pointing at nothing
140
+ * is a pane framing nothing with everything else unpickable.
141
+ */
142
+ isolation: string | null;
143
+ /**
144
+ * Inside isolation, the AUTHOR owns the working camera.
145
+ *
146
+ * False is the entry state: the derived orthographic head-on rig draws the
147
+ * pane, which is the only camera that can frame a rolled or face-down card.
148
+ * The first orbit, pan or dolly flips this and the ordinary perspective orbit
149
+ * rig takes over — Blender's local view scopes what you SEE and leaves the
150
+ * camera free, and this is that.
151
+ *
152
+ * Home and numpad-dot clear it, so head-on is the RESET rather than a cage.
153
+ * Entering another subtree clears it too (`setIsolation` on the stage does
154
+ * the same on its own side), so a second card always opens head-on.
155
+ *
156
+ * WORKSPACE STATE. It is meaningless while `isolation` is null, and
157
+ * `writeFramingFromView` reads the pair to decide whether the pose it is
158
+ * about to project onto the shot camera is one anybody can see.
159
+ */
160
+ isolationOrbit: boolean;
161
+ /** The shading the working pane was in before the current one, so ⇧Z can
162
+ * toggle wireframe and come BACK rather than stranding the author in it. */
163
+ previousShading: ShadingMode;
164
+ /**
165
+ * The component definition the OUTLINE is scoped to, or null for the scene.
166
+ *
167
+ * Separate from `isolation` on purpose. Isolation is a SPATIAL move — the
168
+ * working pane frames a subtree that is on screen — and a definition's layers
169
+ * are in no scene and have no camera at all. Conflating them would make
170
+ * "what am I looking at" and "what am I editing" one field that has to answer
171
+ * both, and the answer for a definition would be a camera pointing at
172
+ * nothing.
173
+ *
174
+ * Cleared with the other selection bits on document close, and validated on
175
+ * every write beside `isolation` — a definition can be deleted too.
176
+ */
177
+ editingDef: string | null;
178
+ /** The export dialog is open. A store bit rather than local state, because
179
+ * the keymap opens it (⌘E) and the header's button toggles it, and two
180
+ * places holding "is it open" is one place for them to disagree. */
181
+ exportOpen: boolean;
182
+ /**
183
+ * The live (or most recent) render, mirrored off the SSE bus.
184
+ *
185
+ * Held in the STORE and not in the dialog, so closing the dialog does not
186
+ * lose the render: the header keeps showing progress and the author can walk
187
+ * away from the modal without cancelling anything.
188
+ */
189
+ renderJob: RenderJob | null;
190
+ // ── the agent drawer ─────────────────────────────────────────────────────
191
+ //
192
+ // Held in the STORE and not in the panel, for the reason the render job is:
193
+ // closing the drawer must not cancel a running turn or lose its transcript.
194
+ agentOpen: boolean;
195
+ agentItems: readonly AgentTranscriptItem[];
196
+ agentRunning: boolean;
197
+ agentError: string | null;
198
+ /** The session the CURRENT turn is streaming. Apply targets the session
199
+ * that MINTED a card, which is carried on the card itself — this is only
200
+ * for cancel. */
201
+ agentSessionId: string | null;
202
+ /** A per-chrome model override, or null for the server's default. */
203
+ agentModel: string | null;
204
+ agentCostUsd: number | null;
205
+ agentCredentials: AgentCredentialsInfo | null;
206
+ agentCredentialsError: string | null;
207
+ /** Hidden, not unmounted — see TimelineDock. */
208
+ dockCollapsed: boolean;
209
+ /** The two side rails, hidden so the stage can be seen. Unlike the dock
210
+ * these ARE unmounted (`display: none`): they hold no live geometry that a
211
+ * remount would lose, and the widths they occupy are variables the control
212
+ * strip and the dock position themselves against. */
213
+ railCollapsed: boolean;
214
+ inspectorCollapsed: boolean;
215
+ /**
216
+ * What entering the working view auto-collapsed, per panel: the collapse
217
+ * value to restore on leaving, or null when there is nothing pending —
218
+ * either the mode is not on, or the author toggled that panel themselves
219
+ * inside it and took ownership (the collapse verbs null the slot).
220
+ */
221
+ focusRestoreInspector: boolean | null;
222
+ focusRestoreDock: boolean | null;
223
+ /** Dragged sizes, in PIXELS — see state/panelSize.ts for why not `vh`. */
224
+ railWidth: number;
225
+ dockHeight: number;
226
+ /** Pixels per frame in the dock. The unit @dailies-studio/timeline's geometry
227
+ * and `useAnchoredZoom` both speak, so there is no second zoom scale. */
228
+ dockPpf: number;
229
+
230
+ /**
231
+ * PREVIEW audio, and only preview: the monitoring level in this browser tab.
232
+ *
233
+ * Never the document, and the distinction is load-bearing. `clip.gain` and
234
+ * `clip.muted` are the mix and they are exported; these two are the author
235
+ * turning their speakers down, and an export that got quieter because
236
+ * somebody muted their monitor would be the worst kind of silent corruption.
237
+ * They ride `workspacePrefs` with the rest of "where the author is standing".
238
+ */
239
+ previewMuted: boolean;
240
+ /** Linear, 0..1. */
241
+ previewVolume: number;
242
+
243
+ /** The stage's working-area view. Owned by the stage, mirrored here so the
244
+ * zoom readout has something to read and "fit" has something to write. */
245
+ view: ShotView;
246
+ /**
247
+ * How many views the stage is showing, and how the width is divided.
248
+ *
249
+ * A WORKSPACE PREF, listed in `workspacePrefs`: it survives a document switch
250
+ * and never dirties the document. The split is where the author is standing,
251
+ * not something about the shot — and a layout that reset on every open would
252
+ * be a setting nobody could keep.
253
+ */
254
+ stageLayout: StageLayout;
255
+ /** The working pane's orbit camera. A workspace pref for the same reason, and
256
+ * additionally: it is EDITOR STATE that must never reach an export. */
257
+ workingView: WorkingView;
258
+ /** Which pane the author last acted in. The zoom controls and the fit
259
+ * shortcut point at it, so it follows the POINTER and not the selection. */
260
+ activePane: StagePane;
261
+ /**
262
+ * "Camera follows this view": orbiting the working pane writes the shot
263
+ * camera's framing continuously.
264
+ *
265
+ * A WORKSPACE PREF like the other two, and deliberately NOT called a lock.
266
+ * The word says "nothing can move" when what is true is the opposite — the
267
+ * camera is being moved by everything you do. It is armed only by its own
268
+ * control, never as a side effect of entering a mode, for the reason
269
+ * `animateMode` is: a bit that silently makes drags write the document must
270
+ * be turned on on purpose.
271
+ */
272
+ cameraFollowsView: boolean;
273
+ /**
274
+ * The WORKING pane's shading (solid / wireframe / material / rendered).
275
+ *
276
+ * A workspace pref like the layout: how the author looks at the scene, never
277
+ * something about the shot. The camera pane is not addressable — it is
278
+ * always the graded render, which is what makes "preview === export" a
279
+ * sentence with no footnotes.
280
+ */
281
+ shadingMode: ShadingMode;
282
+ /** What the stage reported on ready, or null before it did. */
283
+ stageInfo: ShotStageInfo | null;
284
+ /** A stage connect or runtime failure, for the overlay strip. */
285
+ stageError: string | null;
286
+
287
+ // ── the component registry, for `instance` layers ────────────────────────
288
+ /** Every registry row, whatever its host: the ADD menu filters to "three",
289
+ * and the inspector needs the others in order to SAY why a layer's target
290
+ * no longer qualifies rather than reporting it as missing. */
291
+ library: readonly ComponentRow[];
292
+ /** Has `GET /api/components` ever ANSWERED? "loading finished" is not the
293
+ * same thing — a FAILED fetch also ends with an empty list, and judging an
294
+ * instance against that would report every one of them as an unknown
295
+ * component, which is a claim nobody established. */
296
+ libraryLoaded: boolean;
297
+ libraryError: string | null;
298
+ /** Lazily fetched contracts by component id — the inspector's row source. */
299
+ contracts: Readonly<Record<string, Contract>>;
300
+
301
+ saveState: SaveState;
302
+ /** Unsaved edits exist. Separate from `saveState` because a save that is
303
+ * in flight while a new edit lands is BOTH "saving" and dirty. */
304
+ dirty: boolean;
305
+ /** The last API failure, in words the author can act on. */
306
+ error: string | null;
307
+ /** Codegen refused to overwrite a hand-edited generated file. Sticky until
308
+ * the next clean save, because it means the .tsx on disk no longer matches
309
+ * the document — which is exactly the thing an author must not miss. */
310
+ emitWarning: string | null;
311
+
312
+ canUndo: boolean;
313
+ canRedo: boolean;
314
+ }
315
+
316
+ export const INITIAL_VIEW: ShotView = { zoom: 1, panX: 0, panY: 0 };
317
+
318
+ /** Camera only, because that is the pane that IS the export: an editor that
319
+ * opened onto a working pane would open onto a view of the shot that is not
320
+ * the shot. */
321
+ export const INITIAL_STAGE_LAYOUT: StageLayout = { mode: "camera", splitRatio: 0.5 };
322
+
323
+ export const INITIAL_STATE: ShotEditorState = {
324
+ shots: [],
325
+ name: null,
326
+ doc: null,
327
+ rev: null,
328
+ frame: 0,
329
+ playing: false,
330
+ sceneId: null,
331
+ selection: [],
332
+ isolation: null,
333
+ isolationOrbit: false,
334
+ previousShading: "solid",
335
+ editingDef: null,
336
+ tool: "select",
337
+ inspectorMode: "design",
338
+ animateMode: false,
339
+ selectedBlock: null,
340
+ selectedMarker: null,
341
+ imagePick: null,
342
+ stageMenu: null,
343
+ materialPicker: null,
344
+ meshTriangles: {},
345
+ exportOpen: false,
346
+ renderJob: null,
347
+ agentOpen: false,
348
+ agentItems: [],
349
+ agentRunning: false,
350
+ agentError: null,
351
+ agentSessionId: null,
352
+ agentModel: null,
353
+ agentCostUsd: null,
354
+ agentCredentials: null,
355
+ agentCredentialsError: null,
356
+ dockCollapsed: false,
357
+ railCollapsed: false,
358
+ inspectorCollapsed: false,
359
+ focusRestoreInspector: null,
360
+ focusRestoreDock: null,
361
+ railWidth: RAIL_WIDTH.default,
362
+ dockHeight: DOCK_HEIGHT.default,
363
+ dockPpf: 6,
364
+ previewMuted: false,
365
+ previewVolume: 1,
366
+ view: INITIAL_VIEW,
367
+ stageLayout: INITIAL_STAGE_LAYOUT,
368
+ workingView: INITIAL_WORKING_VIEW,
369
+ activePane: "camera",
370
+ cameraFollowsView: false,
371
+ shadingMode: "solid",
372
+ stageInfo: null,
373
+ stageError: null,
374
+ library: [],
375
+ libraryLoaded: false,
376
+ libraryError: null,
377
+ contracts: {},
378
+ saveState: "idle",
379
+ dirty: false,
380
+ error: null,
381
+ emitWarning: null,
382
+ canUndo: false,
383
+ canRedo: false,
384
+ };