@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,190 @@
|
|
|
1
|
+
// The left rail's Manual: how the editor works, the tool table and the keymap,
|
|
2
|
+
// rendered where a first-time author actually looks.
|
|
3
|
+
//
|
|
4
|
+
// ── ONE LIST, PINNED BY A TEST ──────────────────────────────────────────
|
|
5
|
+
//
|
|
6
|
+
// The tool rows render straight from TOOL_SPECS, so that half cannot drift.
|
|
7
|
+
// The shortcut rows cannot be derived — `resolveKey` is a function, not a
|
|
8
|
+
// table — so each row carries the EXACT EVENT it claims to describe plus the
|
|
9
|
+
// action kind the keymap must answer for it, and test/helpSection.test.tsx
|
|
10
|
+
// walks every row through `resolveKey`. A binding change without a manual
|
|
11
|
+
// edit is a red test, not a stale manual.
|
|
12
|
+
//
|
|
13
|
+
// No store, no context: this section is static prose over static tables,
|
|
14
|
+
// which is what lets the test mount it bare. The agent connect steps moved to
|
|
15
|
+
// the File tab's MCP section (McpSection.tsx), where an author actually looks.
|
|
16
|
+
import { SHOT_TOOLS, specOf } from "../../tools.js";
|
|
17
|
+
import type { KeyAction, KeyEventLike } from "../../state/keymap.js";
|
|
18
|
+
|
|
19
|
+
export interface ShortcutRow {
|
|
20
|
+
/** What the manual shows, house glyph style (⌘ ⇧ ⌥ — ⌘ is Ctrl on Windows). */
|
|
21
|
+
combo: string;
|
|
22
|
+
does: string;
|
|
23
|
+
group: "View" | "Working view" | "Edit" | "Animate & camera";
|
|
24
|
+
/** The event this combo produces, so the test can ask the keymap itself. */
|
|
25
|
+
probe: KeyEventLike;
|
|
26
|
+
/** Arrows answer differently with a selection; everything else omits it. */
|
|
27
|
+
hasSelection?: boolean;
|
|
28
|
+
/** The pen's two rows answer differently with a path open, and ONLY with one
|
|
29
|
+
* open — Escape means deselect the rest of the time, which is why the
|
|
30
|
+
* context carries "holds points" and not "the pen tool is armed". */
|
|
31
|
+
penActive?: boolean;
|
|
32
|
+
/** Escape's third meaning: leave one level of isolation. It only answers
|
|
33
|
+
* that with NOTHING selected, so the row carries the bit and the test
|
|
34
|
+
* probes with `hasSelection` false. */
|
|
35
|
+
isolation?: boolean;
|
|
36
|
+
/** The modal starters (G/R/S) answer only with the cursor over the working
|
|
37
|
+
* pane — the same letters arm tools everywhere else, and both readings
|
|
38
|
+
* have rows. */
|
|
39
|
+
workingPane?: boolean;
|
|
40
|
+
/** The axis and terminator rows answer only while a transform is LIVE. */
|
|
41
|
+
transformModal?: boolean;
|
|
42
|
+
/** The action `resolveKey(probe)` must return. */
|
|
43
|
+
action: KeyAction["kind"];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const probe = (
|
|
47
|
+
key: string,
|
|
48
|
+
m: { shift?: boolean; ctrl?: boolean; alt?: boolean; code?: string } = {},
|
|
49
|
+
): KeyEventLike => ({
|
|
50
|
+
key,
|
|
51
|
+
shiftKey: m.shift === true,
|
|
52
|
+
altKey: m.alt === true,
|
|
53
|
+
ctrlKey: m.ctrl === true,
|
|
54
|
+
metaKey: false,
|
|
55
|
+
...(m.code === undefined ? {} : { code: m.code }),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
export const SHORTCUTS: readonly ShortcutRow[] = [
|
|
59
|
+
{ group: "View", combo: "Space", does: "Play / pause", probe: probe(" "), action: "playToggle" },
|
|
60
|
+
{ group: "View", combo: "← →", does: "Step one frame (nothing selected)", probe: probe("ArrowRight"), action: "step" },
|
|
61
|
+
{ group: "View", combo: "1 / 2 / 3", does: "Working / split / camera view", probe: probe("1"), action: "stageLayout" },
|
|
62
|
+
{ group: "View", combo: "[", does: "Hide / show the left panel", probe: probe("["), action: "togglePanel" },
|
|
63
|
+
{ group: "View", combo: "]", does: "Hide / show the right panel", probe: probe("]"), action: "togglePanel" },
|
|
64
|
+
{ group: "View", combo: ".", does: "Hide every panel", probe: probe("."), action: "togglePanel" },
|
|
65
|
+
{ group: "View", combo: "\\", does: "Fit the timeline", probe: probe("\\"), action: "fitTimeline" },
|
|
66
|
+
{ group: "View", combo: "⌘+ / ⌘−", does: "Zoom in / out", probe: probe("=", { ctrl: true }), action: "zoom" },
|
|
67
|
+
{ group: "View", combo: "⌘0", does: "Zoom to 100%", probe: probe("0", { ctrl: true }), action: "zoomReset" },
|
|
68
|
+
{ group: "View", combo: "Home", does: "Fit the active pane", probe: probe("Home"), action: "fit" },
|
|
69
|
+
{ group: "View", combo: "⇧C", does: "Fit the active pane (Blender's frame all)", probe: probe("c", { shift: true }), action: "fit" },
|
|
70
|
+
{ group: "View", combo: "Z", does: "Cycle working-pane shading (solid / wire / material / rendered)", probe: probe("z"), action: "shadingCycle" },
|
|
71
|
+
{ group: "View", combo: "⇧Z", does: "Wireframe on / off", probe: probe("z", { shift: true }), action: "wireframeToggle" },
|
|
72
|
+
|
|
73
|
+
{ group: "Working view", combo: "G", does: "Grab the selection — move it with the mouse, click to place", probe: probe("g"), workingPane: true, hasSelection: true, action: "modalStart" },
|
|
74
|
+
{ group: "Working view", combo: "R", does: "Rotate the selection (roll)", probe: probe("r"), workingPane: true, hasSelection: true, action: "modalStart" },
|
|
75
|
+
{ group: "Working view", combo: "S", does: "Scale the selection", probe: probe("s"), workingPane: true, hasSelection: true, action: "modalStart" },
|
|
76
|
+
{ group: "Working view", combo: "G / S", does: "Nothing selected: move / zoom the camera", probe: probe("g"), workingPane: true, action: "modalStart" },
|
|
77
|
+
{ group: "Working view", combo: "X / Y / Z", does: "Constrain the transform to an axis (again clears; G+Z = depth)", probe: probe("x"), transformModal: true, action: "modalAxis" },
|
|
78
|
+
{ group: "Working view", combo: "0-9", does: "Type an exact value: G X 200 Enter", probe: probe("2"), transformModal: true, action: "modalNumber" },
|
|
79
|
+
// `hasSelection` because R is the one that needs it: the CAMERA has no modal
|
|
80
|
+
// roll, so switching to rotate on a camera transform would arm one that could
|
|
81
|
+
// never emit.
|
|
82
|
+
{ group: "Working view", combo: "G / R / S", does: "Switch transform without leaving the gesture", probe: probe("r"), transformModal: true, hasSelection: true, action: "modalStart" },
|
|
83
|
+
{ group: "Working view", combo: "Enter", does: "Confirm the transform (or click)", probe: probe("Enter"), transformModal: true, action: "modalConfirm" },
|
|
84
|
+
{ group: "Working view", combo: "Esc", does: "Cancel the transform (or right-click)", probe: probe("Escape"), transformModal: true, action: "modalCancel" },
|
|
85
|
+
{ group: "Working view", combo: "Num1 / 3 / 7", does: "Front / right / top view (⌘ = the opposite)", probe: probe("1", { code: "Numpad1" }), action: "workingViewPreset" },
|
|
86
|
+
{ group: "Working view", combo: "Num0", does: "Camera view, and back again", probe: probe("0", { code: "Numpad0" }), action: "stageLayout" },
|
|
87
|
+
{ group: "Working view", combo: "Num.", does: "Frame the selection", probe: probe(".", { code: "NumpadDecimal" }), action: "fitSelection" },
|
|
88
|
+
{ group: "Working view", combo: "Num2 / 4 / 6 / 8", does: "Orbit 15°; Num9 looks from the opposite side", probe: probe("4", { code: "Numpad4" }), action: "orbitStep" },
|
|
89
|
+
{ group: "Working view", combo: "Num5", does: "Orthographic / perspective", probe: probe("5", { code: "Numpad5" }), action: "toggleWorkingOrtho" },
|
|
90
|
+
{ group: "Working view", combo: "Num/", does: "Go inside the selection, and back out", probe: probe("/", { code: "NumpadDivide" }), action: "toggleIsolation" },
|
|
91
|
+
{ group: "Working view", combo: "Num+ / Num−", does: "Zoom the active pane", probe: probe("+", { code: "NumpadAdd" }), action: "zoom" },
|
|
92
|
+
|
|
93
|
+
{ group: "Edit", combo: "Arrows", does: "Nudge selection 1 px (⇧ = 10)", probe: probe("ArrowRight"), hasSelection: true, action: "nudge" },
|
|
94
|
+
{ group: "Edit", combo: "⌘Z", does: "Undo", probe: probe("z", { ctrl: true }), action: "undo" },
|
|
95
|
+
{ group: "Edit", combo: "⌘⇧Z", does: "Redo", probe: probe("z", { ctrl: true, shift: true }), action: "redo" },
|
|
96
|
+
{ group: "Edit", combo: "⌘S", does: "Save", probe: probe("s", { ctrl: true }), action: "save" },
|
|
97
|
+
{ group: "Edit", combo: "⌘D", does: "Duplicate", probe: probe("d", { ctrl: true }), action: "duplicate" },
|
|
98
|
+
{ group: "Edit", combo: "⌘A / A", does: "Select all, in the scope you are in", probe: probe("a", { ctrl: true }), action: "selectAll" },
|
|
99
|
+
{ group: "Edit", combo: "⌥A", does: "Deselect all", probe: probe("a", { alt: true }), action: "deselect" },
|
|
100
|
+
{ group: "Edit", combo: "B", does: "Box select with the next drag", probe: probe("b"), action: "boxSelect" },
|
|
101
|
+
{ group: "Edit", combo: "⇧D", does: "Duplicate", probe: probe("d", { shift: true }), action: "duplicate" },
|
|
102
|
+
{ group: "Edit", combo: "Esc", does: "Deselect", probe: probe("Escape"), action: "deselect" },
|
|
103
|
+
{ group: "Edit", combo: "Del", does: "Delete selection", probe: probe("Delete"), action: "deleteSelection" },
|
|
104
|
+
{ group: "Edit", combo: "⌘G", does: "Group", probe: probe("g", { ctrl: true }), action: "group" },
|
|
105
|
+
{ group: "Edit", combo: "⌘⇧G", does: "Ungroup", probe: probe("g", { ctrl: true, shift: true }), action: "ungroup" },
|
|
106
|
+
{ group: "Edit", combo: "⌥L", does: "Lock / unlock", probe: probe("l", { alt: true }), action: "toggleLock" },
|
|
107
|
+
{ group: "Edit", combo: "H", does: "Hide the selection", probe: probe("h"), action: "setHidden" },
|
|
108
|
+
{ group: "Edit", combo: "⇧H", does: "Hide everything else", probe: probe("h", { shift: true }), action: "setHidden" },
|
|
109
|
+
{ group: "Edit", combo: "⌥H", does: "Show everything again", probe: probe("h", { alt: true }), action: "setHidden" },
|
|
110
|
+
{ group: "Edit", combo: "⌥G / ⌥R", does: "Clear the position / the rotation", probe: probe("g", { alt: true }), action: "clearTransform" },
|
|
111
|
+
{ group: "Edit", combo: "Enter", does: "Edit inside the selection", probe: probe("Enter"), hasSelection: true, action: "enterIsolation" },
|
|
112
|
+
{ group: "Edit", combo: "Esc", does: "Come back out one level", probe: probe("Escape"), isolation: true, action: "exitIsolation" },
|
|
113
|
+
{ group: "Edit", combo: "Enter", does: "Finish the path you are drawing", probe: probe("Enter"), penActive: true, action: "penCommit" },
|
|
114
|
+
{ group: "Edit", combo: "Esc", does: "Throw away the path you are drawing", probe: probe("Escape"), penActive: true, action: "penAbandon" },
|
|
115
|
+
|
|
116
|
+
{ group: "Animate & camera", combo: "⇧A", does: "Animate mode — edits become blocks", probe: probe("a", { shift: true }), action: "animateToggle" },
|
|
117
|
+
{ group: "Animate & camera", combo: "M", does: "Drop a marker at the playhead", probe: probe("m"), action: "addMarker" },
|
|
118
|
+
{ group: "Animate & camera", combo: "⌥C", does: "Point the camera at the working view", probe: probe("c", { alt: true }), action: "alignCameraToView" },
|
|
119
|
+
{ group: "Animate & camera", combo: "⌘B", does: "Split layer at the playhead", probe: probe("b", { ctrl: true }), action: "splitAtPlayhead" },
|
|
120
|
+
{ group: "Animate & camera", combo: "⌘E", does: "Export video", probe: probe("e", { ctrl: true }), action: "openExport" },
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
const GROUPS = ["View", "Working view", "Edit", "Animate & camera"] as const;
|
|
124
|
+
|
|
125
|
+
/** The sentences a first session needs, in the order a session meets them.
|
|
126
|
+
* Mouse gestures live HERE and not in the shortcut table below, because every
|
|
127
|
+
* row of that table is walked through the real keymap by its test — a row for
|
|
128
|
+
* a gesture no key produces would have to carry a fake event to pass. */
|
|
129
|
+
const BASICS: readonly string[] = [
|
|
130
|
+
"Camera view: scroll to pan, ctrl+scroll to zoom at the pointer, space-drag to pan.",
|
|
131
|
+
"Working view (1 / 2): drag empty space or middle-drag to orbit, scroll to dolly, space-drag or ⇧middle-drag to pan. Click a layer to select it, then G / R / S transform it Blender-style; the panels tuck away while you work.",
|
|
132
|
+
"Working view aids: a ground grid under the frame that coarsens as you pull back, three coloured world axes (X red, Y green, Z blue) through the canvas centre, and an axis ball top-right — click an end to look down that axis.",
|
|
133
|
+
"Right-click a layer, on the stage or in the outline, for duplicate, hide, lock, isolate, delete — and on a model, a material.",
|
|
134
|
+
"Models: the outline shows each one's triangle count, and tints it once the scene gets heavy enough to feel it. Give a model a surface with right-click → Apply material; a toon model also grows a sun handle you can drag to aim its key.",
|
|
135
|
+
"Double-click a layer to go INSIDE it: the working pane frames that subtree flat and head-on and everything else dims. Orbit, pan or dolly and the camera is yours; Home puts it back head-on. A blue bar over the pane says you are inside; Esc backs out one level, or click a crumb in the left panel.",
|
|
136
|
+
"Drag arms (red/green/blue) sit on the selection, or on the CAMERA when nothing is selected — the same subject G and S pick. Z moves a layer in depth; on the camera it dollies.",
|
|
137
|
+
"Header zoom, ⌘+ / ⌘− and ⇧C all act on the pane you last clicked. ⇧Z is the wireframe toggle.",
|
|
138
|
+
"Draw: arm a tool in the top bar, then drag on the stage. Text takes a click.",
|
|
139
|
+
"Animate: toggle ⇧A, then edit — the change becomes a block ending at the playhead.",
|
|
140
|
+
"Camera: use the timeline's + shot button for a move, or in the working view (1) drag the yellow frustum's edge to pan, its apex to zoom, or its arms for one axis at a time.",
|
|
141
|
+
"Audio: drop a file on the timeline's audio lane. Markers ride their clip.",
|
|
142
|
+
"Everything exports exactly as previewed — same engine, same frames.",
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
export function HelpSection() {
|
|
146
|
+
return (
|
|
147
|
+
<details className="help-section" data-testid="help-section">
|
|
148
|
+
<summary className="file-section-label help-summary">Manual</summary>
|
|
149
|
+
|
|
150
|
+
<ul className="help-basics">
|
|
151
|
+
{BASICS.map((line) => (
|
|
152
|
+
<li key={line}>{line}</li>
|
|
153
|
+
))}
|
|
154
|
+
</ul>
|
|
155
|
+
|
|
156
|
+
<div className="file-section-label">Tools</div>
|
|
157
|
+
<ul className="help-rows">
|
|
158
|
+
{SHOT_TOOLS.map((tool) => {
|
|
159
|
+
const spec = specOf(tool);
|
|
160
|
+
return (
|
|
161
|
+
<li key={tool} className="help-row">
|
|
162
|
+
<span>{spec.label}</span>
|
|
163
|
+
{spec.key !== null && (
|
|
164
|
+
<kbd>{`${spec.shift === true ? "⇧" : ""}${spec.key.toUpperCase()}`}</kbd>
|
|
165
|
+
)}
|
|
166
|
+
</li>
|
|
167
|
+
);
|
|
168
|
+
})}
|
|
169
|
+
</ul>
|
|
170
|
+
|
|
171
|
+
{GROUPS.map((group) => (
|
|
172
|
+
<div key={group}>
|
|
173
|
+
<div className="file-section-label">{group}</div>
|
|
174
|
+
<ul className="help-rows">
|
|
175
|
+
{SHORTCUTS.filter((row) => row.group === group).map((row) => (
|
|
176
|
+
// Keyed on the combo AND what it does: Esc and Enter each mean
|
|
177
|
+
// three things now, depending on what is open, and every one of
|
|
178
|
+
// them is a row.
|
|
179
|
+
<li key={`${row.combo}:${row.does}`} className="help-row">
|
|
180
|
+
<span>{row.does}</span>
|
|
181
|
+
<kbd>{row.combo}</kbd>
|
|
182
|
+
</li>
|
|
183
|
+
))}
|
|
184
|
+
</ul>
|
|
185
|
+
</div>
|
|
186
|
+
))}
|
|
187
|
+
<p className="help-note">⌘ is Ctrl on Windows and Linux.</p>
|
|
188
|
+
</details>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// A `Framing` as two rows of fields. ONE definition, three subjects: the
|
|
2
|
+
// scene's own camera, and a move's `from` and `to`.
|
|
3
|
+
//
|
|
4
|
+
// It is shared rather than copied because the list IS the schema. A sixth
|
|
5
|
+
// framing field would otherwise have to be remembered in two files, and the one
|
|
6
|
+
// that got forgotten would silently keep an authored value the author could
|
|
7
|
+
// never see or reach.
|
|
8
|
+
//
|
|
9
|
+
// The write is a PATCH and not a whole framing, so a caller can route it
|
|
10
|
+
// straight at whichever seam owns its subject: `setSceneCamera` goes through
|
|
11
|
+
// the refusal fork, `setShotFraming` does not, and this component knows about
|
|
12
|
+
// neither.
|
|
13
|
+
import { InspectorRow, NumberField } from "@dailies-studio/ui";
|
|
14
|
+
import { MAX_FRAMING_ZOOM, MIN_FRAMING_ZOOM } from "@dailies-studio/shot/doc";
|
|
15
|
+
import type { Framing } from "@dailies-studio/shot/doc";
|
|
16
|
+
|
|
17
|
+
/** The five fields, in the order an author thinks about them: where it looks,
|
|
18
|
+
* how much it sees, how it is held, what is sharp. */
|
|
19
|
+
const FIELDS: ReadonlyArray<{
|
|
20
|
+
key: keyof Framing;
|
|
21
|
+
label: string;
|
|
22
|
+
unit?: string;
|
|
23
|
+
step: number;
|
|
24
|
+
min?: number;
|
|
25
|
+
max?: number;
|
|
26
|
+
}> = [
|
|
27
|
+
{ key: "x", label: "X", step: 1 },
|
|
28
|
+
{ key: "y", label: "Y", step: 1 },
|
|
29
|
+
// Clamped at the widget as well as in the store: the store's clamp is the
|
|
30
|
+
// boundary that must hold, and this one is what stops the author typing a
|
|
31
|
+
// value that silently becomes a different one.
|
|
32
|
+
{ key: "zoom", label: "Zoom", step: 0.01, min: MIN_FRAMING_ZOOM, max: MAX_FRAMING_ZOOM },
|
|
33
|
+
{ key: "rollDeg", label: "Roll", unit: "deg", step: 0.5 },
|
|
34
|
+
{ key: "focusZ", label: "Focus", step: 1 },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
export function FramingRows(props: {
|
|
38
|
+
framing: Framing;
|
|
39
|
+
/** Prefixes every field's accessible name ("Camera x", "From x"). Three
|
|
40
|
+
* framings can be on screen at once, so the label alone is not a name. */
|
|
41
|
+
name: string;
|
|
42
|
+
onCommit(patch: Partial<Framing>): void;
|
|
43
|
+
}) {
|
|
44
|
+
return (
|
|
45
|
+
<>
|
|
46
|
+
{[FIELDS.slice(0, 2), FIELDS.slice(2)].map((group, i) => (
|
|
47
|
+
<InspectorRow key={i}>
|
|
48
|
+
{group.map((f) => {
|
|
49
|
+
const limits: { min?: number; max?: number } = {};
|
|
50
|
+
if (f.min !== undefined) limits.min = f.min;
|
|
51
|
+
if (f.max !== undefined) limits.max = f.max;
|
|
52
|
+
return (
|
|
53
|
+
<NumberField
|
|
54
|
+
key={f.key}
|
|
55
|
+
value={props.framing[f.key]}
|
|
56
|
+
label={f.label}
|
|
57
|
+
ariaLabel={`${props.name} ${f.label.toLowerCase()}`}
|
|
58
|
+
{...(f.unit === undefined ? {} : { unit: f.unit })}
|
|
59
|
+
{...limits}
|
|
60
|
+
step={f.step}
|
|
61
|
+
onCommit={(next) => props.onCommit({ [f.key]: next })}
|
|
62
|
+
/>
|
|
63
|
+
);
|
|
64
|
+
})}
|
|
65
|
+
</InspectorRow>
|
|
66
|
+
))}
|
|
67
|
+
</>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// The right rail: Design when a layer is selected, Scene when nothing is.
|
|
2
|
+
//
|
|
3
|
+
// ── TWO MODE BITS, AND THE TAB ONLY MOVES ONE ───────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// Clicking these tabs sets `inspectorMode`, which is a VIEW choice. It does NOT
|
|
6
|
+
// set `animateMode`, which is a BEHAVIOUR choice and belongs to ⇧A alone.
|
|
7
|
+
// Collapsing them would mean that looking at the blocks silently armed block
|
|
8
|
+
// authoring on the next canvas drag — a data-corrupting surprise dressed as a
|
|
9
|
+
// convenience.
|
|
10
|
+
//
|
|
11
|
+
// The reverse dependency DOES exist and is deliberate: turning animate ON moves
|
|
12
|
+
// the tab to Animate, because the author is about to author blocks and wants to
|
|
13
|
+
// see them. One way only.
|
|
14
|
+
import { SegmentedControl } from "@dailies-studio/ui";
|
|
15
|
+
import { useShotEditor, useStore } from "../../state/context.js";
|
|
16
|
+
import { useSelectedLayers } from "./useDocWrite.js";
|
|
17
|
+
import { TransformSection } from "./design/TransformSection.js";
|
|
18
|
+
import { AppearanceSection } from "./design/AppearanceSection.js";
|
|
19
|
+
import { FillSection } from "./design/FillSection.js";
|
|
20
|
+
import { StrokeSection } from "./design/StrokeSection.js";
|
|
21
|
+
import { ShadowSection } from "./design/ShadowSection.js";
|
|
22
|
+
import { CornerSection } from "./design/CornerSection.js";
|
|
23
|
+
import { HtmlSection } from "./design/HtmlSection.js";
|
|
24
|
+
import { TextStaggerSection, TypographySection } from "./design/TypographySection.js";
|
|
25
|
+
import { MeshSection } from "./design/MeshSection.js";
|
|
26
|
+
import { ClonerSection } from "./design/ClonerSection.js";
|
|
27
|
+
import { MediaSection } from "./design/MediaSection.js";
|
|
28
|
+
import { InstanceSection } from "./design/InstanceSection.js";
|
|
29
|
+
import { ComponentSection } from "./design/ComponentSection.js";
|
|
30
|
+
import { SceneSection } from "./scene/SceneSection.js";
|
|
31
|
+
import { CameraSection } from "./scene/CameraSection.js";
|
|
32
|
+
import { EffectsSection } from "./scene/EffectsSection.js";
|
|
33
|
+
import { TransitionSection } from "./scene/TransitionSection.js";
|
|
34
|
+
import { InOutPresets } from "./animate/InOutPresets.js";
|
|
35
|
+
import { DuringBlocks } from "./animate/DuringBlocks.js";
|
|
36
|
+
import { BlockTiming } from "./animate/BlockTiming.js";
|
|
37
|
+
import { ResolvedChannels } from "./animate/ResolvedChannels.js";
|
|
38
|
+
import { ShotSection } from "./animate/ShotSection.js";
|
|
39
|
+
import { MarkerSection } from "./MarkerSection.js";
|
|
40
|
+
import { CAMERA_ROW_ID } from "../timeline/model.js";
|
|
41
|
+
|
|
42
|
+
export function InspectorPanel() {
|
|
43
|
+
const store = useStore();
|
|
44
|
+
const doc = useShotEditor((s) => s.doc);
|
|
45
|
+
const mode = useShotEditor((s) => s.inspectorMode);
|
|
46
|
+
// The camera's row holds SHOTS, not blocks, and a shot belongs to the scene
|
|
47
|
+
// rather than to any layer. So it is the one Animate subject that does not
|
|
48
|
+
// come from the selection, and it wins: a bar the author just clicked is what
|
|
49
|
+
// they are looking at.
|
|
50
|
+
//
|
|
51
|
+
// It RESOLVES THE SHOT rather than testing the row id, because this branch
|
|
52
|
+
// replaces the whole tab: a focus that ShotSection cannot resolve would render
|
|
53
|
+
// the Animate tab as literally nothing, no placeholder and no layer sections.
|
|
54
|
+
// Two ways to get there, both reachable — focusing a bar in one scene and
|
|
55
|
+
// scrubbing into another (the panel reads the PLAYHEAD's scene), and undoing
|
|
56
|
+
// the ⇧C that created the shot the focus points at.
|
|
57
|
+
const cameraShot = useShotEditor((s) => {
|
|
58
|
+
const focused = s.selectedBlock;
|
|
59
|
+
if (focused === null || focused.layerId !== CAMERA_ROW_ID || focused.sceneId !== s.sceneId) return false;
|
|
60
|
+
return s.doc?.scenesById[focused.sceneId]?.shots.some((shot) => shot.id === focused.blockId) ?? false;
|
|
61
|
+
});
|
|
62
|
+
const layers = useSelectedLayers();
|
|
63
|
+
// THE FIRST SUBJECT, above the mode fork and above the selection. It matches
|
|
64
|
+
// `deleteSelection`'s order — marker, then block, then layers — so the panel
|
|
65
|
+
// and the Delete key can never be looking at two different things. A marker
|
|
66
|
+
// is neither a design property nor an animation, so it replaces the body
|
|
67
|
+
// rather than living in one of the tabs.
|
|
68
|
+
//
|
|
69
|
+
// It RESOLVES THE MARKER rather than testing the id for null, for the reason
|
|
70
|
+
// the camera branch below does: this replaces the whole panel, so a selection
|
|
71
|
+
// pointing at a marker that is gone would render the rail as literally
|
|
72
|
+
// nothing. Undoing the `M` that created it is the one-keystroke way there.
|
|
73
|
+
const markerSelected = useShotEditor(
|
|
74
|
+
(s) => s.selectedMarker !== null && (s.doc?.markers ?? []).some((m) => m.id === s.selectedMarker),
|
|
75
|
+
);
|
|
76
|
+
if (doc === null) return null;
|
|
77
|
+
if (markerSelected) {
|
|
78
|
+
return (
|
|
79
|
+
<aside className="inspector" data-testid="inspector">
|
|
80
|
+
<MarkerSection />
|
|
81
|
+
</aside>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<aside className="inspector" data-testid="inspector">
|
|
87
|
+
<SegmentedControl
|
|
88
|
+
ariaLabel="Inspector mode"
|
|
89
|
+
value={mode}
|
|
90
|
+
options={[
|
|
91
|
+
{ value: "design", label: "Design" },
|
|
92
|
+
{ value: "animate", label: "Animate" },
|
|
93
|
+
]}
|
|
94
|
+
onChange={(next) => store.setInspectorMode(next as "design" | "animate")}
|
|
95
|
+
/>
|
|
96
|
+
|
|
97
|
+
{mode === "animate" ? (
|
|
98
|
+
cameraShot ? (
|
|
99
|
+
<ShotSection />
|
|
100
|
+
) : layers.length === 0 ? (
|
|
101
|
+
// Nothing selected: the tab has no subject. Saying what to do beats an
|
|
102
|
+
// empty panel that reads as broken.
|
|
103
|
+
<p className="inspector-empty" data-testid="animate-placeholder">
|
|
104
|
+
Select a layer to add an entrance, an exit, or a During block.
|
|
105
|
+
</p>
|
|
106
|
+
) : (
|
|
107
|
+
<>
|
|
108
|
+
{/* First, because it is the only thing here that describes the
|
|
109
|
+
layer rather than proposing an edit to it: what the stack adds
|
|
110
|
+
up to at this frame is the context every choice below is made
|
|
111
|
+
against. */}
|
|
112
|
+
<ResolvedChannels />
|
|
113
|
+
<InOutPresets slot="in" title="In" />
|
|
114
|
+
<DuringBlocks />
|
|
115
|
+
<InOutPresets slot="out" title="Out" />
|
|
116
|
+
{/* Last, because it is about ONE block and everything above chooses
|
|
117
|
+
which. A timing editor above the list it belongs to would read as
|
|
118
|
+
the panel's subject rather than the focused row's. */}
|
|
119
|
+
<BlockTiming />
|
|
120
|
+
</>
|
|
121
|
+
)
|
|
122
|
+
) : layers.length === 0 ? (
|
|
123
|
+
<>
|
|
124
|
+
<SceneSection />
|
|
125
|
+
{/* Between the scene and its post stack, which is the order the
|
|
126
|
+
picture is made in: the scene, the camera looking at it, then what
|
|
127
|
+
happens to the frame after. */}
|
|
128
|
+
<CameraSection />
|
|
129
|
+
<EffectsSection />
|
|
130
|
+
{/* …and last, what happens when the frame stops being this shot's. */}
|
|
131
|
+
<TransitionSection />
|
|
132
|
+
</>
|
|
133
|
+
) : (
|
|
134
|
+
<>
|
|
135
|
+
<TransformSection />
|
|
136
|
+
<AppearanceSection />
|
|
137
|
+
{/* Above the paint sections: a mesh's look comes from its material,
|
|
138
|
+
and `paint` is inert on it (types.ts), so the model IS the
|
|
139
|
+
subject the fills below have nothing to say about. */}
|
|
140
|
+
<MeshSection />
|
|
141
|
+
{/* Beside the model section for the same reason it sits above the
|
|
142
|
+
paint ones: an instance paints itself, so its contract IS the
|
|
143
|
+
subject and the fills below have nothing to say about it. */}
|
|
144
|
+
<InstanceSection />
|
|
145
|
+
{/* …and a component placement, for the same reason again: what it
|
|
146
|
+
draws comes from its definition, so the fills below are inert. */}
|
|
147
|
+
<ComponentSection />
|
|
148
|
+
{/* Beside the model section and above the paint ones for the same
|
|
149
|
+
reason: a cloner paints nothing of its own, so what it DOES is the
|
|
150
|
+
subject the fills below have nothing to say about. */}
|
|
151
|
+
<ClonerSection />
|
|
152
|
+
{/* Above the paint sections for the same reason the three geometry
|
|
153
|
+
sections are: a media layer PAINTS ITSELF from its asset, and
|
|
154
|
+
`paint.fill` never reaches it (the runtime asks the rasterizer
|
|
155
|
+
only for a "mask" and a "plate", and the fill branch lives in
|
|
156
|
+
neither) — so the source IS the subject the fill below has
|
|
157
|
+
nothing to say about. Corner, stroke and shadow still apply,
|
|
158
|
+
through exactly those two rasters. */}
|
|
159
|
+
<MediaSection />
|
|
160
|
+
<FillSection />
|
|
161
|
+
<StrokeSection />
|
|
162
|
+
<CornerSection />
|
|
163
|
+
<ShadowSection />
|
|
164
|
+
<TypographySection />
|
|
165
|
+
{/* Beside Typography, because it is the other kind whose content is
|
|
166
|
+
authored here rather than dragged on the stage. Below it because a
|
|
167
|
+
snippet's two fields are taller than everything above them. */}
|
|
168
|
+
<HtmlSection />
|
|
169
|
+
{/* Directly under Typography, because it says how that type moves —
|
|
170
|
+
and it is the one Design-tab section that only matters once a
|
|
171
|
+
`stagger` block exists, so it sits last and collapsed. */}
|
|
172
|
+
<TextStaggerSection />
|
|
173
|
+
</>
|
|
174
|
+
)}
|
|
175
|
+
</aside>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// The selected marker: where it is, what it says, what colour it is drawn in.
|
|
2
|
+
//
|
|
3
|
+
// It sits OUTSIDE the Design/Animate fork rather than inside one of the tabs. A
|
|
4
|
+
// marker is neither a design property nor an animation: putting it in a tab
|
|
5
|
+
// would mean the panel could be showing the wrong tab for its own subject, and
|
|
6
|
+
// the author would click a diamond and see nothing.
|
|
7
|
+
//
|
|
8
|
+
// The FRAME field is a master frame, like every other number about a marker, and
|
|
9
|
+
// it is the only place in this chrome where one is typed rather than dragged.
|
|
10
|
+
import { ColorPicker, InspectorRow, InspectorSection, NumberField, TextField } from "@dailies-studio/ui";
|
|
11
|
+
import { useShotEditor, useStore } from "../../state/context.js";
|
|
12
|
+
import { timecode } from "../timeline/model.js";
|
|
13
|
+
import { NO_SCRUB } from "./useDocWrite.js";
|
|
14
|
+
|
|
15
|
+
/** What a marker with no `color` is drawn in. The same value shotTokens.css
|
|
16
|
+
* gives `--marker-pin`, restated here because the swatch needs a concrete
|
|
17
|
+
* colour to show and a CSS variable is not one. */
|
|
18
|
+
const DEFAULT_MARKER_COLOR = "#ffffff";
|
|
19
|
+
|
|
20
|
+
export function MarkerSection() {
|
|
21
|
+
const store = useStore();
|
|
22
|
+
const doc = useShotEditor((s) => s.doc);
|
|
23
|
+
const selected = useShotEditor((s) => s.selectedMarker);
|
|
24
|
+
if (doc === null || selected === null) return null;
|
|
25
|
+
const marker = (doc.markers ?? []).find((m) => m.id === selected);
|
|
26
|
+
if (marker === undefined) return null;
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<InspectorSection title="Marker" persistKey="shot:marker" collapsible={false}>
|
|
30
|
+
<InspectorRow label="Label">
|
|
31
|
+
<TextField
|
|
32
|
+
value={marker.label ?? ""}
|
|
33
|
+
ariaLabel="Marker label"
|
|
34
|
+
onCommit={(label) => store.setMarker(marker.id, { label })}
|
|
35
|
+
/>
|
|
36
|
+
</InspectorRow>
|
|
37
|
+
<InspectorRow>
|
|
38
|
+
<NumberField
|
|
39
|
+
value={marker.frame}
|
|
40
|
+
label="Frame"
|
|
41
|
+
ariaLabel="Marker frame"
|
|
42
|
+
min={0}
|
|
43
|
+
step={1}
|
|
44
|
+
onCommit={(frame) => store.setMarker(marker.id, { frame: Math.max(0, Math.round(frame)) })}
|
|
45
|
+
/>
|
|
46
|
+
<span className="insp-dim">{timecode(marker.frame, doc.fps)}</span>
|
|
47
|
+
</InspectorRow>
|
|
48
|
+
<InspectorRow label="Colour">
|
|
49
|
+
<ColorPicker
|
|
50
|
+
value={marker.color ?? DEFAULT_MARKER_COLOR}
|
|
51
|
+
ariaLabel="Marker colour"
|
|
52
|
+
scrub={NO_SCRUB}
|
|
53
|
+
commit={(color) => store.setMarker(marker.id, { color })}
|
|
54
|
+
/>
|
|
55
|
+
</InspectorRow>
|
|
56
|
+
<InspectorRow>
|
|
57
|
+
<button type="button" className="btn" onClick={() => store.removeMarker(marker.id)}>
|
|
58
|
+
Delete marker
|
|
59
|
+
</button>
|
|
60
|
+
</InspectorRow>
|
|
61
|
+
</InspectorSection>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// One preset param, spec-driven. Shared by the motion-block panel and the
|
|
2
|
+
// transition panel — both drive a `TransitionParamSpec`, and two copies of a
|
|
3
|
+
// widget that turns a spec into a control is two places for "vec2 means two
|
|
4
|
+
// fields" to be decided differently.
|
|
5
|
+
//
|
|
6
|
+
// Written here rather than through `PropControl`, because that widget
|
|
7
|
+
// dispatches a `ControlSpec` from @dailies-studio/contract and these are
|
|
8
|
+
// `TransitionParamSpec`s — a deliberately smaller vocabulary of exactly three
|
|
9
|
+
// kinds. Adapting the one into the other would be more code than the three
|
|
10
|
+
// arms, and would put a lossy translation between the author and the value.
|
|
11
|
+
import { InspectorRow, NumberField, Toggle } from "@dailies-studio/ui";
|
|
12
|
+
import type { TransitionParamSpec } from "@dailies-studio/transitions";
|
|
13
|
+
import type { Value, Vec2 } from "@dailies-studio/engine";
|
|
14
|
+
|
|
15
|
+
export function ParamRow(props: {
|
|
16
|
+
spec: TransitionParamSpec;
|
|
17
|
+
value: Value;
|
|
18
|
+
onCommit(next: Value): void;
|
|
19
|
+
}) {
|
|
20
|
+
const { spec } = props;
|
|
21
|
+
const label = spec.label ?? spec.name;
|
|
22
|
+
const limits: { min?: number; max?: number } = {};
|
|
23
|
+
if (spec.min !== undefined) limits.min = spec.min;
|
|
24
|
+
if (spec.max !== undefined) limits.max = spec.max;
|
|
25
|
+
|
|
26
|
+
if (spec.kind === "boolean") {
|
|
27
|
+
return (
|
|
28
|
+
<InspectorRow label={label}>
|
|
29
|
+
<Toggle
|
|
30
|
+
checked={props.value === true}
|
|
31
|
+
ariaLabel={label}
|
|
32
|
+
onChange={(next) => props.onCommit(next)}
|
|
33
|
+
/>
|
|
34
|
+
</InspectorRow>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
if (spec.kind === "vec2") {
|
|
38
|
+
const v = asVec2(props.value);
|
|
39
|
+
return (
|
|
40
|
+
<InspectorRow label={label}>
|
|
41
|
+
{([0, 1] as const).map((i) => (
|
|
42
|
+
<NumberField
|
|
43
|
+
key={i}
|
|
44
|
+
value={v[i]}
|
|
45
|
+
ariaLabel={`${label} ${i === 0 ? "X" : "Y"}`}
|
|
46
|
+
{...limits}
|
|
47
|
+
step={spec.step ?? 0.01}
|
|
48
|
+
onCommit={(next) => props.onCommit(i === 0 ? [next, v[1]] : [v[0], next])}
|
|
49
|
+
/>
|
|
50
|
+
))}
|
|
51
|
+
</InspectorRow>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return (
|
|
55
|
+
<InspectorRow label={label}>
|
|
56
|
+
<NumberField
|
|
57
|
+
value={typeof props.value === "number" ? props.value : 0}
|
|
58
|
+
ariaLabel={label}
|
|
59
|
+
{...limits}
|
|
60
|
+
step={spec.step ?? 0.01}
|
|
61
|
+
onCommit={(next) => props.onCommit(next)}
|
|
62
|
+
/>
|
|
63
|
+
</InspectorRow>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function asVec2(v: Value): Vec2 {
|
|
68
|
+
return Array.isArray(v) && typeof v[0] === "number" && typeof v[1] === "number"
|
|
69
|
+
? [v[0], v[1]]
|
|
70
|
+
: [0, 0];
|
|
71
|
+
}
|