@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,278 @@
|
|
|
1
|
+
// The stage's half of the bridge: a tiny store the chrome writes INTO through
|
|
2
|
+
// `window.__shotStudio`, and that `StageApp` subscribes to.
|
|
3
|
+
//
|
|
4
|
+
// Splitting it out of the React tree is what lets `boot()` install the handle
|
|
5
|
+
// SYNCHRONOUSLY, before anything renders. The chrome polls
|
|
6
|
+
// `iframe.contentWindow.__shotStudio` on load and must never observe a
|
|
7
|
+
// rendered-but-bridgeless stage; if the handle lived in a component it could
|
|
8
|
+
// only appear after the first commit, and the poll would race it.
|
|
9
|
+
import { clearRasterCache } from "@dailies-studio/shot";
|
|
10
|
+
import { markPerf } from "./PerfHud.js";
|
|
11
|
+
import type { Doc3D, Scene3D } from "@dailies-studio/shot/doc";
|
|
12
|
+
import type {
|
|
13
|
+
ShadingMode,
|
|
14
|
+
ShotBridgeHandlers,
|
|
15
|
+
ShotScreenQuad,
|
|
16
|
+
ShotStageBridge,
|
|
17
|
+
ShotTool,
|
|
18
|
+
ShotView,
|
|
19
|
+
SnapGuide,
|
|
20
|
+
StageInset,
|
|
21
|
+
StageLayout,
|
|
22
|
+
StagePane,
|
|
23
|
+
WorkingView,
|
|
24
|
+
} from "../bridgeTypes.js";
|
|
25
|
+
import { SHOT_BRIDGE_VERSION } from "../bridgeTypes.js";
|
|
26
|
+
import { clampSplitRatio } from "./panes.js";
|
|
27
|
+
import { clampShading } from "./shading.js";
|
|
28
|
+
import { INITIAL_WORKING_VIEW } from "./workingView.js";
|
|
29
|
+
|
|
30
|
+
export interface StageClientState {
|
|
31
|
+
/** A chrome has registered its handlers. Nothing renders before this. */
|
|
32
|
+
connected: boolean;
|
|
33
|
+
doc: Doc3D | null;
|
|
34
|
+
/** MASTER frame. */
|
|
35
|
+
frame: number;
|
|
36
|
+
selection: readonly string[];
|
|
37
|
+
view: ShotView;
|
|
38
|
+
/** Live only while a gesture is snapping. */
|
|
39
|
+
guides: readonly SnapGuide[];
|
|
40
|
+
tool: ShotTool;
|
|
41
|
+
/** How many panes are on screen. Pushed by the chrome; never derived here,
|
|
42
|
+
* so there is one owner of "what is the author looking at". */
|
|
43
|
+
layout: StageLayout;
|
|
44
|
+
workingView: WorkingView;
|
|
45
|
+
/** "Camera follows this view" is armed. The stage brackets its orbit with the
|
|
46
|
+
* framing gesture lane while it is, and draws the badge that says so. */
|
|
47
|
+
cameraFollowsView: boolean;
|
|
48
|
+
/** The isolated subtree's root layer id, or null for the whole scene. The
|
|
49
|
+
* stage derives the members, the camera and the dimming from the document
|
|
50
|
+
* it already has — see `setIsolation`. */
|
|
51
|
+
isolation: string | null;
|
|
52
|
+
/** Inside isolation, has the author taken the working camera? False means the
|
|
53
|
+
* derived orthographic head-on rig draws the pane; true means the ordinary
|
|
54
|
+
* orbit rig does. Meaningless while `isolation` is null. */
|
|
55
|
+
isolationOrbit: boolean;
|
|
56
|
+
/** The working pane's shading. Workspace state, mirrored from the chrome. */
|
|
57
|
+
shading: ShadingMode;
|
|
58
|
+
/** How far the chrome's floating panels reach into the stage, per side. Paint
|
|
59
|
+
* only: the overlay lays its furniture inside it and `paneRects` ignores it
|
|
60
|
+
* entirely. Zero until the chrome says otherwise. */
|
|
61
|
+
inset: StageInset;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const INITIAL: StageClientState = {
|
|
65
|
+
connected: false,
|
|
66
|
+
doc: null,
|
|
67
|
+
frame: 0,
|
|
68
|
+
selection: [],
|
|
69
|
+
view: { zoom: 1, panX: 0, panY: 0 },
|
|
70
|
+
guides: [],
|
|
71
|
+
tool: "select",
|
|
72
|
+
layout: { mode: "camera", splitRatio: 0.5 },
|
|
73
|
+
workingView: INITIAL_WORKING_VIEW,
|
|
74
|
+
cameraFollowsView: false,
|
|
75
|
+
isolation: null,
|
|
76
|
+
isolationOrbit: false,
|
|
77
|
+
shading: "solid",
|
|
78
|
+
inset: { top: 0, right: 0, bottom: 0, left: 0 },
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** Finite and non-negative on every side. A boundary, like `clampSplitRatio`:
|
|
82
|
+
* a NaN here becomes a NaN `y` on a `<text>` and the badge vanishes with no
|
|
83
|
+
* error anywhere. */
|
|
84
|
+
function clampInset(inset: StageInset): StageInset {
|
|
85
|
+
const one = (n: number): number => (Number.isFinite(n) && n > 0 ? n : 0);
|
|
86
|
+
return { top: one(inset.top), right: one(inset.right), bottom: one(inset.bottom), left: one(inset.left) };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface StageClient {
|
|
90
|
+
readonly bridge: ShotStageBridge;
|
|
91
|
+
getState(): StageClientState;
|
|
92
|
+
subscribe(listener: () => void): () => void;
|
|
93
|
+
/** The chrome's handlers, or null before connect. The stage calls up through
|
|
94
|
+
* these; every one must be treated as possibly-absent. */
|
|
95
|
+
handlers(): ShotBridgeHandlers | null;
|
|
96
|
+
/** How the stage answers `selectionQuads()`. Registered by the overlay layer
|
|
97
|
+
* once it exists; until then the bridge answers null. */
|
|
98
|
+
setQuadSource(source: ((pane: StagePane) => readonly ShotScreenQuad[] | null) | null): void;
|
|
99
|
+
/** How the stage answers `fitView()`. Registered by the viewport. */
|
|
100
|
+
setFitter(fit: (() => ShotView) | null): void;
|
|
101
|
+
/** …and `fitWorkingView()`. A separate registration rather than one fitter
|
|
102
|
+
* returning both, because the two are computed from different things: the
|
|
103
|
+
* camera fit needs the PANE SIZE and the working fit needs the DOCUMENT. */
|
|
104
|
+
setWorkingFitter(fit: (() => WorkingView) | null): void;
|
|
105
|
+
setDisposer(dispose: () => void): void;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function createStageClient(): StageClient {
|
|
109
|
+
let state: StageClientState = INITIAL;
|
|
110
|
+
const listeners = new Set<() => void>();
|
|
111
|
+
let handlers: ShotBridgeHandlers | null = null;
|
|
112
|
+
let quadSource: ((pane: StagePane) => readonly ShotScreenQuad[] | null) | null = null;
|
|
113
|
+
let fitter: (() => ShotView) | null = null;
|
|
114
|
+
let workingFitter: (() => WorkingView) | null = null;
|
|
115
|
+
let dispose: (() => void) | null = null;
|
|
116
|
+
|
|
117
|
+
function set(patch: Partial<StageClientState>): void {
|
|
118
|
+
let changed = false;
|
|
119
|
+
for (const key of Object.keys(patch) as (keyof StageClientState)[]) {
|
|
120
|
+
if (!Object.is(state[key], patch[key])) {
|
|
121
|
+
changed = true;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (!changed) return;
|
|
126
|
+
state = { ...state, ...patch };
|
|
127
|
+
for (const l of listeners) l();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const bridge: ShotStageBridge = {
|
|
131
|
+
version: SHOT_BRIDGE_VERSION,
|
|
132
|
+
connect(next) {
|
|
133
|
+
handlers = next;
|
|
134
|
+
set({ connected: true });
|
|
135
|
+
},
|
|
136
|
+
loadDoc(doc) {
|
|
137
|
+
// A DIFFERENT document opening, not an edit echo: the span from here to
|
|
138
|
+
// the next presented frame is the author's "how long until I see it".
|
|
139
|
+
// markPerf no-ops without ?perf=1. Set BEFORE the state write, because
|
|
140
|
+
// the commit this write triggers is the frame that settles it.
|
|
141
|
+
if (state.doc?.id !== doc.id) markPerf("open→frame");
|
|
142
|
+
set({ doc });
|
|
143
|
+
},
|
|
144
|
+
seek(frame) {
|
|
145
|
+
// Integer frames over a rational fps is the engine's invariant, and this
|
|
146
|
+
// is the boundary a float could sneak through: a chrome that computed a
|
|
147
|
+
// frame from a pixel position would otherwise hand the studio clock a
|
|
148
|
+
// fractional one and break byte-identity against the export.
|
|
149
|
+
set({ frame: Math.round(frame) });
|
|
150
|
+
},
|
|
151
|
+
setSelection(ids) {
|
|
152
|
+
set({ selection: [...ids] });
|
|
153
|
+
},
|
|
154
|
+
selectionQuads(pane = "camera") {
|
|
155
|
+
return quadSource === null ? null : quadSource(pane);
|
|
156
|
+
},
|
|
157
|
+
setTool(tool) {
|
|
158
|
+
set({ tool });
|
|
159
|
+
},
|
|
160
|
+
setSnapGuides(guides) {
|
|
161
|
+
// Identity-stable when both are empty, so the common case (no gesture
|
|
162
|
+
// running) never notifies and never re-renders the stage.
|
|
163
|
+
if (guides.length === 0 && state.guides.length === 0) return;
|
|
164
|
+
set({ guides: [...guides] });
|
|
165
|
+
},
|
|
166
|
+
setView(view) {
|
|
167
|
+
set({ view });
|
|
168
|
+
},
|
|
169
|
+
fitView() {
|
|
170
|
+
if (fitter === null) return state.view;
|
|
171
|
+
const view = fitter();
|
|
172
|
+
set({ view });
|
|
173
|
+
return view;
|
|
174
|
+
},
|
|
175
|
+
setStageLayout(layout) {
|
|
176
|
+
// Gaining a working pane is the compile-storm moment (SplitRenderer +
|
|
177
|
+
// clay mount); the span to the next presented frame measures it.
|
|
178
|
+
if (layout.mode !== "camera" && state.layout.mode === "camera") markPerf("split→frame");
|
|
179
|
+
// Clamped HERE and not only in the chrome. This is a boundary: the ratio
|
|
180
|
+
// becomes a scissor rect, and a NaN scissor draws nothing with no error
|
|
181
|
+
// anywhere — the stage does not get to assume its caller was careful.
|
|
182
|
+
set({ layout: { mode: layout.mode, splitRatio: clampSplitRatio(layout.splitRatio) } });
|
|
183
|
+
},
|
|
184
|
+
setIsolation(isolation) {
|
|
185
|
+
// Validated against the DOCUMENT here, not trusted: the chrome's id can
|
|
186
|
+
// be one edit behind the stage's doc, and an isolation naming a layer
|
|
187
|
+
// that no longer exists would leave the pane pointed at nothing with no
|
|
188
|
+
// way for the author to get out.
|
|
189
|
+
const scene = state.doc === null ? null : sceneWithLayer(state.doc, isolation);
|
|
190
|
+
// THE ORBIT MODE RESETS WITH THE SUBTREE. Entering a second card while
|
|
191
|
+
// the first one's camera was taken would otherwise start it in a
|
|
192
|
+
// perspective pose derived from somewhere else entirely, and the head-on
|
|
193
|
+
// entry view — the whole point of isolation — would only appear the first
|
|
194
|
+
// time.
|
|
195
|
+
set({ isolation: isolation !== null && scene !== null ? isolation : null, isolationOrbit: false });
|
|
196
|
+
},
|
|
197
|
+
setIsolationOrbit(taken) {
|
|
198
|
+
set({ isolationOrbit: taken === true });
|
|
199
|
+
},
|
|
200
|
+
setStageInset(inset) {
|
|
201
|
+
const next = clampInset(inset);
|
|
202
|
+
// Identity-stable when nothing moved: the chrome writes this from a raw
|
|
203
|
+
// store subscription, so it fires on every pointermove of a panel resize
|
|
204
|
+
// and `set`'s `Object.is` check would see a fresh object every time.
|
|
205
|
+
const at = state.inset;
|
|
206
|
+
if (at.top === next.top && at.right === next.right && at.bottom === next.bottom && at.left === next.left) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
set({ inset: next });
|
|
210
|
+
},
|
|
211
|
+
setWorkingView(workingView) {
|
|
212
|
+
set({ workingView });
|
|
213
|
+
},
|
|
214
|
+
fitWorkingView() {
|
|
215
|
+
if (workingFitter === null) return state.workingView;
|
|
216
|
+
const workingView = workingFitter();
|
|
217
|
+
set({ workingView });
|
|
218
|
+
return workingView;
|
|
219
|
+
},
|
|
220
|
+
setShadingMode(mode) {
|
|
221
|
+
// Clamped at the boundary like the split ratio: an unknown mode from a
|
|
222
|
+
// careless caller means the default clay, not a pane that draws nothing.
|
|
223
|
+
set({ shading: clampShading(mode) });
|
|
224
|
+
},
|
|
225
|
+
setCameraFollowsView(on) {
|
|
226
|
+
set({ cameraFollowsView: on === true });
|
|
227
|
+
},
|
|
228
|
+
closeDoc() {
|
|
229
|
+
set({ doc: null, selection: [] });
|
|
230
|
+
},
|
|
231
|
+
clearCaches() {
|
|
232
|
+
// Called right after closeDoc. The chrome's two calls run synchronously
|
|
233
|
+
// before the stage's doc-null commit can land, so the clear disposes
|
|
234
|
+
// textures some planes still bind — safe, because no GL draw happens
|
|
235
|
+
// between synchronous JS and that commit (draws ride commits), and the
|
|
236
|
+
// unmount commit runs cleanups, never a draw of the old doc.
|
|
237
|
+
clearRasterCache();
|
|
238
|
+
},
|
|
239
|
+
dispose() {
|
|
240
|
+
dispose?.();
|
|
241
|
+
dispose = null;
|
|
242
|
+
handlers = null;
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
bridge,
|
|
248
|
+
getState: () => state,
|
|
249
|
+
subscribe(listener) {
|
|
250
|
+
listeners.add(listener);
|
|
251
|
+
return () => listeners.delete(listener);
|
|
252
|
+
},
|
|
253
|
+
handlers: () => handlers,
|
|
254
|
+
setQuadSource(source) {
|
|
255
|
+
quadSource = source;
|
|
256
|
+
},
|
|
257
|
+
setFitter(fit) {
|
|
258
|
+
fitter = fit;
|
|
259
|
+
},
|
|
260
|
+
setWorkingFitter(fit) {
|
|
261
|
+
workingFitter = fit;
|
|
262
|
+
},
|
|
263
|
+
setDisposer(fn) {
|
|
264
|
+
dispose = fn;
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** The scene holding a layer, or null. Exported for the isolation guard and for
|
|
270
|
+
* the stage's own scoping — "is this id still real" is the question both ask,
|
|
271
|
+
* and a second implementation of it is how the two drift. */
|
|
272
|
+
export function sceneWithLayer(doc: Doc3D, layerId: string | null): Scene3D | null {
|
|
273
|
+
if (layerId === null) return null;
|
|
274
|
+
for (const scene of Object.values(doc.scenesById)) {
|
|
275
|
+
if (scene.layersById[layerId] !== undefined) return scene;
|
|
276
|
+
}
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
// The mutable handoff between the half of the stage that lives INSIDE the
|
|
2
|
+
// canvas and the half that lives outside it.
|
|
3
|
+
//
|
|
4
|
+
// WHY A MUTABLE OBJECT AND NOT STATE. The pointer lane sits on the container
|
|
5
|
+
// div (pointer events land on the DOM, not on the scene graph) while picking
|
|
6
|
+
// and projection need the live camera, which only exists inside `<Canvas>`.
|
|
7
|
+
// Passing the camera out through React state would make every pointermove a
|
|
8
|
+
// render; passing a mutable box in makes it a property read. The same pattern
|
|
9
|
+
// the 2D lane's `feed.ts` uses, for the same reason.
|
|
10
|
+
//
|
|
11
|
+
// `version` is the ONE thing React is told about, and only when a selected
|
|
12
|
+
// quad actually moved by more than half a pixel — see `quadMoved`. Everything
|
|
13
|
+
// else on here is read imperatively at the moment it is needed.
|
|
14
|
+
//
|
|
15
|
+
// ── EVERY QUERY NAMES A PANE ────────────────────────────────────────────
|
|
16
|
+
//
|
|
17
|
+
// The stage can show two views of one scene at once (panes.ts), and the two
|
|
18
|
+
// have DIFFERENT CAMERAS. So there is no such thing as "the ray at x, y": the
|
|
19
|
+
// same canvas pixel unprojects to two different rays, picks two different
|
|
20
|
+
// layers and, in the working pane, sorts its hits by a different rule
|
|
21
|
+
// (`pickLayersByDistance` vs `pickLayers`). Making the pane the first argument
|
|
22
|
+
// rather than a mode on the feed is deliberate — a mode would be one more piece
|
|
23
|
+
// of state to keep in step with the pointer, and getting it stale means the
|
|
24
|
+
// author's click is answered by the pane they are not pointing at.
|
|
25
|
+
//
|
|
26
|
+
// COORDINATES ARE PANE-LOCAL CSS px, from the pane's top-left. `paneAt` returns
|
|
27
|
+
// exactly that pairing, so the caller passes through what it was handed and
|
|
28
|
+
// there is no second place where a pane origin gets subtracted.
|
|
29
|
+
import type { Camera, Matrix4, Ray } from "three";
|
|
30
|
+
import type { GestureKind, ResizeHandle } from "@dailies-studio/shot/doc";
|
|
31
|
+
import type { StagePane } from "./panes.js";
|
|
32
|
+
import type { GizmoGrab } from "./gizmoMath.js";
|
|
33
|
+
import type { LayerFrame, ScreenQuad, WorldQuad } from "./project.js";
|
|
34
|
+
|
|
35
|
+
/** What a press on the current selection turned out to be. */
|
|
36
|
+
export interface Grab {
|
|
37
|
+
kind: GestureKind;
|
|
38
|
+
/** resize only. */
|
|
39
|
+
handle?: ResizeHandle;
|
|
40
|
+
/** The grabbed layer's group matrix, for the gesture's plane. */
|
|
41
|
+
matrixWorld: Matrix4;
|
|
42
|
+
/**
|
|
43
|
+
* resize only: which way the grabbed handle points ON SCREEN, in radians from
|
|
44
|
+
* the quad's centre, measured in the overlay's own y-DOWN space.
|
|
45
|
+
*
|
|
46
|
+
* COMPUTED HERE, WHERE THE QUAD IS. The cursor has to be the handle's real
|
|
47
|
+
* direction and not its NAME: these are projected trapezoids, so the "nw"
|
|
48
|
+
* corner of a layer rolled 45 degrees points north, and a name-based
|
|
49
|
+
* `nwse-resize` would sit at 45 degrees to the edge the author is about to
|
|
50
|
+
* drag. Under the working pane's free orbit a name means even less.
|
|
51
|
+
*
|
|
52
|
+
* The pointer lane buckets it to the four bidirectional builtins and never
|
|
53
|
+
* derives it a second time, so the drawn handle, the hit test and the cursor
|
|
54
|
+
* all come off one number.
|
|
55
|
+
*/
|
|
56
|
+
handleAngle?: number;
|
|
57
|
+
/**
|
|
58
|
+
* resize and roll: the projected CENTRE of the quad the handle belongs to, in
|
|
59
|
+
* the same pane-local px `handleAngle` was measured about.
|
|
60
|
+
*
|
|
61
|
+
* CARRIED, not re-derived. The rotate lane used to look the centre up again
|
|
62
|
+
* from `feed.quadsCamera[feed.ids.indexOf(selection[0])]`, and `selection[0]`
|
|
63
|
+
* is whichever id sorts LOWEST in document order — not the one the author
|
|
64
|
+
* grabbed. With a multi-selection the first pointermove jumped by the angle
|
|
65
|
+
* between the two centres, and when the lowest id had no frame at the
|
|
66
|
+
* playhead `indexOf` answered -1 and the press died silently.
|
|
67
|
+
*/
|
|
68
|
+
centre?: readonly [number, number];
|
|
69
|
+
/**
|
|
70
|
+
* scale only: the UNION box's four world corners, snapshotted at the press.
|
|
71
|
+
*
|
|
72
|
+
* The drag needs them to place the pivot at the corner opposite the grabbed
|
|
73
|
+
* handle, and it needs them FROZEN: the box is rebuilt from the members every
|
|
74
|
+
* frame, so a live reference would move the pivot with the very layers the
|
|
75
|
+
* scale is moving and the ratio would chase itself.
|
|
76
|
+
*/
|
|
77
|
+
union?: WorldQuad;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface StageFeed {
|
|
81
|
+
/** Bumped by the in-canvas half when the overlay needs repainting. */
|
|
82
|
+
version: number;
|
|
83
|
+
/**
|
|
84
|
+
* Bumped whenever a layer object MOUNTS OR UNMOUNTS in the three scene graph.
|
|
85
|
+
*
|
|
86
|
+
* `collectFrames` walks the whole graph, and its answer is a pure function of
|
|
87
|
+
* the graph's SHAPE — the matrices inside it are live references, so motion
|
|
88
|
+
* needs no invalidation and neither does a seek that only moves things. What
|
|
89
|
+
* it cannot see is membership, and membership changes for a reason no
|
|
90
|
+
* document value predicts: a GLB or an instance module resolves and suspense
|
|
91
|
+
* commits a subtree that was not there a frame ago. This counter is that
|
|
92
|
+
* event, and it is what lets the walk be memoised across an orbit.
|
|
93
|
+
*/
|
|
94
|
+
sceneEpoch: number;
|
|
95
|
+
/**
|
|
96
|
+
* The overlay's OWN repaint hook, registered by the component that draws
|
|
97
|
+
* the quads and called by `StageTools` when they move.
|
|
98
|
+
*
|
|
99
|
+
* It used to be a `bumpOverlay` reducer on `StageApp`, and that shape was a
|
|
100
|
+
* double render: a camera dolly with a selection committed the whole stage
|
|
101
|
+
* (ShotRuntime included, whose dependency-less `ClockDriver` effect draws a
|
|
102
|
+
* GL frame per commit), then the quads moved, then `bumpOverlay` committed
|
|
103
|
+
* it all AGAIN. Registering the listener at the overlay leaf means a moved
|
|
104
|
+
* quad re-renders one SVG and zero canvases.
|
|
105
|
+
*/
|
|
106
|
+
onOverlay: (() => void) | null;
|
|
107
|
+
/** The CURRENT selection, in the order both quad arrays are parallel to. One
|
|
108
|
+
* list for both panes: the selection is one selection, only its projection
|
|
109
|
+
* differs. */
|
|
110
|
+
ids: string[];
|
|
111
|
+
/** Screen quads for `ids` in each pane, in that pane's LOCAL CSS px. A pane
|
|
112
|
+
* that is not on screen has an empty array rather than stale quads — an
|
|
113
|
+
* overlay drawn from last layout's numbers is worse than no overlay. */
|
|
114
|
+
quadsCamera: ScreenQuad[];
|
|
115
|
+
quadsWorking: ScreenQuad[];
|
|
116
|
+
/** Parallel to `ids`: is each selected layer locked? Written here rather than
|
|
117
|
+
* looked up by the overlay because `collectFrames` is the one place that
|
|
118
|
+
* already knows, and a second lookup could disagree with the `grabAt` that
|
|
119
|
+
* refuses the handle. */
|
|
120
|
+
locked: boolean[];
|
|
121
|
+
/**
|
|
122
|
+
* The multi-select's ONE box, in world and projected into each pane, or null.
|
|
123
|
+
*
|
|
124
|
+
* Null covers three different situations on purpose, because the overlay
|
|
125
|
+
* treats them identically: fewer than two layers selected, a selection that
|
|
126
|
+
* is not coplanar, and one that is not axis-parallel (`unionQuad` says which).
|
|
127
|
+
* In all three the members keep their own outlines and their own handles.
|
|
128
|
+
*
|
|
129
|
+
* All three projections in ONE field so they cannot go out of step: the world
|
|
130
|
+
* corners are the pivot the drag measures against and the screen corners are
|
|
131
|
+
* what the author grabbed, and a frame where those disagreed would scale
|
|
132
|
+
* about a box nobody drew.
|
|
133
|
+
*/
|
|
134
|
+
union: { world: WorldQuad; camera: ScreenQuad | null; working: ScreenQuad | null } | null;
|
|
135
|
+
/** Every alive layer this frame, for picking and the marquee. Null before
|
|
136
|
+
* the canvas has committed once. */
|
|
137
|
+
frames: readonly LayerFrame[] | null;
|
|
138
|
+
/**
|
|
139
|
+
* The working pane's camera, or null when that pane is off screen.
|
|
140
|
+
*
|
|
141
|
+
* WRITTEN BY `StageTools` AT PRIORITY 0 AND READ BY `SplitRenderer` AT
|
|
142
|
+
* PRIORITY 1, which is the whole reason it lives on the feed rather than
|
|
143
|
+
* being owned by the renderer that draws with it. The camera that answers a
|
|
144
|
+
* pick and the camera that drew the pixel are then the same object in the
|
|
145
|
+
* same state, by construction — a second camera written a frame later would
|
|
146
|
+
* put every working-pane click an orbit-step away from what the author saw.
|
|
147
|
+
*
|
|
148
|
+
* A `Camera` and not a `PerspectiveCamera`: isolation puts an ORTHOGRAPHIC
|
|
149
|
+
* camera in this slot (see stage/isolation.ts), and every consumer here —
|
|
150
|
+
* raycasting, projection, `renderBins` — takes the base class already.
|
|
151
|
+
*/
|
|
152
|
+
workingCamera: Camera | null;
|
|
153
|
+
/** Ray-pick at a point in the pane's own CSS px. Front-most first, where
|
|
154
|
+
* "front-most" is paint order in the camera pane and nearest in the working
|
|
155
|
+
* one. Null until the canvas exists; empty for a pane that is not on
|
|
156
|
+
* screen. */
|
|
157
|
+
pick: ((pane: StagePane, x: number, y: number) => string[]) | null;
|
|
158
|
+
/**
|
|
159
|
+
* The outline a CLICK here would select, already projected — or null over
|
|
160
|
+
* nothing.
|
|
161
|
+
*
|
|
162
|
+
* Separate from `pick` because the answer is a QUAD and not an id list: the
|
|
163
|
+
* overlay draws it, and projecting one frame stage-side is cheaper and more
|
|
164
|
+
* honest than sending an id to the chrome and waiting a round trip for a quad
|
|
165
|
+
* that will be one pointermove stale by the time it arrives.
|
|
166
|
+
*
|
|
167
|
+
* It walks to the same container `onPick` selects (`layerOps.outermost`), so
|
|
168
|
+
* the outline the author sees before they click IS the thing the click
|
|
169
|
+
* selects. A second walk here would be that bug.
|
|
170
|
+
*/
|
|
171
|
+
hoverQuad: ((pane: StagePane, x: number, y: number) => ScreenQuad | null) | null;
|
|
172
|
+
/** Marquee, in the same coordinates. */
|
|
173
|
+
pickRect:
|
|
174
|
+
| ((pane: StagePane, rect: { x0: number; y0: number; x1: number; y1: number }) => string[])
|
|
175
|
+
| null;
|
|
176
|
+
/** Is this press a transform gesture on the current selection, and which?
|
|
177
|
+
* Null for a press that is a selection change instead. */
|
|
178
|
+
grabAt:
|
|
179
|
+
| ((pane: StagePane, x: number, y: number, selection: readonly string[]) => Grab | null)
|
|
180
|
+
| null;
|
|
181
|
+
/** The camera ray through a point, for the move gesture's unprojection.
|
|
182
|
+
* Null for a pane that has no camera on screen — a pane with no camera has
|
|
183
|
+
* no ray, and answering with the other pane's would silently drag the layer
|
|
184
|
+
* along a plane the author is not looking at. */
|
|
185
|
+
rayAt: ((pane: StagePane, x: number, y: number) => Ray | null) | null;
|
|
186
|
+
/**
|
|
187
|
+
* Did a press in the WORKING pane land on the shot camera's frustum gizmo,
|
|
188
|
+
* and on which part?
|
|
189
|
+
*
|
|
190
|
+
* No pane argument, because there is only one pane this can be true in: the
|
|
191
|
+
* frustum gizmo IS the camera pane's camera, drawn as an object. It cannot
|
|
192
|
+
* appear inside the view it represents.
|
|
193
|
+
*/
|
|
194
|
+
frustumHit: ((x: number, y: number) => "body" | "apex" | null) | null;
|
|
195
|
+
/**
|
|
196
|
+
* Did a press in the WORKING pane land on one of the selection's three drag
|
|
197
|
+
* arms, and on which?
|
|
198
|
+
*
|
|
199
|
+
* No pane argument, for `frustumHit`'s reason: the gizmo is drawn in the
|
|
200
|
+
* working pane alone (the camera pane has resize handles, which the working
|
|
201
|
+
* pane deliberately does not — see `grabAt`).
|
|
202
|
+
*
|
|
203
|
+
* The answer carries the arm's ORIGIN AND DIRECTION as a snapshot rather
|
|
204
|
+
* than a live reference: the gizmo re-centres on the selection every frame,
|
|
205
|
+
* and an axis line that moved with the layer being dragged along it would
|
|
206
|
+
* chase its own translation.
|
|
207
|
+
*/
|
|
208
|
+
gizmoHit: ((x: number, y: number) => GizmoGrab | null) | null;
|
|
209
|
+
/**
|
|
210
|
+
* The arm the pointer is over, or null.
|
|
211
|
+
*
|
|
212
|
+
* WRITTEN BY THE POINTER LANE, READ INSIDE THE FRAME LOOP, which is what
|
|
213
|
+
* keeps a hover off React entirely: `MoveGizmo` already runs at priority 0
|
|
214
|
+
* and re-colours a material there, so highlighting costs one uniform write
|
|
215
|
+
* per frame and zero renders. Blender's gizmos highlight under the cursor and
|
|
216
|
+
* ours did not, which is the difference between "is this grabbable" being
|
|
217
|
+
* obvious and being something the author finds out by trying.
|
|
218
|
+
*/
|
|
219
|
+
hoverArm: GizmoGrab["axis"] | null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function createStageFeed(): StageFeed {
|
|
223
|
+
return {
|
|
224
|
+
version: 0,
|
|
225
|
+
sceneEpoch: 0,
|
|
226
|
+
onOverlay: null,
|
|
227
|
+
ids: [],
|
|
228
|
+
quadsCamera: [],
|
|
229
|
+
quadsWorking: [],
|
|
230
|
+
locked: [],
|
|
231
|
+
union: null,
|
|
232
|
+
frames: null,
|
|
233
|
+
workingCamera: null,
|
|
234
|
+
pick: null,
|
|
235
|
+
hoverQuad: null,
|
|
236
|
+
pickRect: null,
|
|
237
|
+
grabAt: null,
|
|
238
|
+
rayAt: null,
|
|
239
|
+
frustumHit: null,
|
|
240
|
+
gizmoHit: null,
|
|
241
|
+
hoverArm: null,
|
|
242
|
+
};
|
|
243
|
+
}
|