@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,172 @@
|
|
|
1
|
+
// Only for MESH layers, and absent otherwise — the same rule TypographySection
|
|
2
|
+
// follows, for the same reason: a model section over a rectangle is six
|
|
3
|
+
// controls that write nothing.
|
|
4
|
+
//
|
|
5
|
+
// Deliberately the SHORT list: the model, how it is shaded, and when its clip
|
|
6
|
+
// runs. `fit`, the toon steps, the key direction and the rim all exist in the
|
|
7
|
+
// schema and are reachable from the agent lane; they are not here because a
|
|
8
|
+
// first section that shows every field is a section nobody can find the model
|
|
9
|
+
// in.
|
|
10
|
+
//
|
|
11
|
+
// ponytail: add the toon detail rows when someone is actually art-directing
|
|
12
|
+
// one.
|
|
13
|
+
import {
|
|
14
|
+
Combobox,
|
|
15
|
+
InspectorRow,
|
|
16
|
+
InspectorSection,
|
|
17
|
+
NumberField,
|
|
18
|
+
SegmentedControl,
|
|
19
|
+
TextField,
|
|
20
|
+
} from "@dailies-studio/ui";
|
|
21
|
+
import { MESH_LOOPS, MESH_SHADING_MODES } from "@dailies-studio/shot/doc";
|
|
22
|
+
import type { Layer3D, MeshLayer3D, MeshLoop, MeshShading } from "@dailies-studio/shot/doc";
|
|
23
|
+
import { useShotEditor, useStore } from "../../../state/context.js";
|
|
24
|
+
import { useSelectedLayers, useSelectionValue, useSelectionWrite } from "../useDocWrite.js";
|
|
25
|
+
|
|
26
|
+
type MeshBlock = MeshLayer3D["mesh"];
|
|
27
|
+
|
|
28
|
+
export function MeshSection() {
|
|
29
|
+
const write = useSelectionWrite();
|
|
30
|
+
const layers = useSelectedLayers();
|
|
31
|
+
const store = useStore();
|
|
32
|
+
const doc = useShotEditor((s) => s.doc);
|
|
33
|
+
const library = useShotEditor((s) => s.library);
|
|
34
|
+
const mesh = useSelectionValue((layer) => (layer.kind === "mesh" ? layer.mesh : undefined));
|
|
35
|
+
if (!layers.some((l) => l.kind === "mesh") || mesh.value === undefined) return null;
|
|
36
|
+
|
|
37
|
+
const m = mesh.value;
|
|
38
|
+
// The catalog's display name, or the raw id when the library has not loaded
|
|
39
|
+
// (or when the document names a generator this build does not ship). Showing
|
|
40
|
+
// the id rather than "Choose…" is what makes the second case visible at all —
|
|
41
|
+
// a material that silently reads as unset is one an author would set twice.
|
|
42
|
+
const materialName =
|
|
43
|
+
m.material === undefined
|
|
44
|
+
? null
|
|
45
|
+
: (library.find((row) => row.id === m.material?.generator)?.name ?? m.material.generator);
|
|
46
|
+
// Model assets only. Offering an image would let an author point a mesh layer
|
|
47
|
+
// at a PNG, which `validateShotDoc` refuses on save — a menu that can only
|
|
48
|
+
// produce an invalid document is worse than a short menu.
|
|
49
|
+
const models = Object.entries(doc?.assets ?? {})
|
|
50
|
+
.filter(([, asset]) => asset.kind === "model")
|
|
51
|
+
.map(([id, asset]) => ({ value: id, label: asset.path.split("/").pop() ?? id }));
|
|
52
|
+
|
|
53
|
+
/** Every write goes through one mapper so a field that CLEARS itself deletes
|
|
54
|
+
* its key rather than setting it to `undefined`: codegen prints the block by
|
|
55
|
+
* key order, and a present-but-undefined key is a key. */
|
|
56
|
+
const patch = (next: (block: MeshBlock) => MeshBlock): void =>
|
|
57
|
+
write((layer) => (layer.kind === "mesh" ? ({ ...layer, mesh: next(layer.mesh) } as Layer3D) : layer));
|
|
58
|
+
|
|
59
|
+
const set = <K extends keyof MeshBlock>(key: K, value: MeshBlock[K]): void =>
|
|
60
|
+
patch((block) => ({ ...block, [key]: value }));
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<InspectorSection title="Model" persistKey="shot:mesh">
|
|
64
|
+
<InspectorRow label="Asset">
|
|
65
|
+
<Combobox
|
|
66
|
+
options={models}
|
|
67
|
+
value={m.asset}
|
|
68
|
+
ariaLabel="Model asset"
|
|
69
|
+
placeholder="Search models"
|
|
70
|
+
onChange={(next) => set("asset", next)}
|
|
71
|
+
/>
|
|
72
|
+
</InspectorRow>
|
|
73
|
+
|
|
74
|
+
<InspectorRow label="Shading">
|
|
75
|
+
<SegmentedControl
|
|
76
|
+
ariaLabel="Mesh shading"
|
|
77
|
+
value={m.shading?.mode ?? "unlit"}
|
|
78
|
+
options={MESH_SHADING_MODES.map((mode) => ({ value: mode, label: mode }))}
|
|
79
|
+
onChange={(next) => set("shading", { mode: next as MeshShading["mode"] } as MeshShading)}
|
|
80
|
+
/>
|
|
81
|
+
</InspectorRow>
|
|
82
|
+
|
|
83
|
+
{/* THE MATERIAL IS A NAME AND TWO BUTTONS, not a list. The catalog is 48
|
|
84
|
+
entries with thumbnails, which is a browser and not an inspector row —
|
|
85
|
+
so this says what is on and opens the picker, exactly as the Asset row
|
|
86
|
+
would if a model were 48 things you had to look at to choose between.
|
|
87
|
+
The generator's own params are edited in the picker's own lane; this
|
|
88
|
+
row's job is "what is it, and take it off". */}
|
|
89
|
+
<InspectorRow label="Material">
|
|
90
|
+
<div className="mesh-material-row">
|
|
91
|
+
<button
|
|
92
|
+
type="button"
|
|
93
|
+
className="mesh-material-name"
|
|
94
|
+
onClick={() => store.openMaterialPicker(layers.filter((l) => l.kind === "mesh").map((l) => l.id))}
|
|
95
|
+
>
|
|
96
|
+
{materialName ?? "Choose…"}
|
|
97
|
+
</button>
|
|
98
|
+
{m.material === undefined ? null : (
|
|
99
|
+
<button
|
|
100
|
+
type="button"
|
|
101
|
+
className="mesh-material-clear"
|
|
102
|
+
aria-label="Remove material"
|
|
103
|
+
title="Back to the model's own materials"
|
|
104
|
+
onClick={() =>
|
|
105
|
+
store.removeMeshMaterial(layers.filter((l) => l.kind === "mesh").map((l) => l.id))
|
|
106
|
+
}
|
|
107
|
+
>
|
|
108
|
+
×
|
|
109
|
+
</button>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
</InspectorRow>
|
|
113
|
+
|
|
114
|
+
{/* Free TEXT, not a dropdown. Clip names live inside the GLB and the
|
|
115
|
+
chrome never parses one — it holds a document, not a scene graph — so
|
|
116
|
+
a picker here could only ever offer the name that is already set.
|
|
117
|
+
Ceiling: no discovery; reporting the model's clips back over the
|
|
118
|
+
bridge is a feed field, not an inspector change. */}
|
|
119
|
+
<InspectorRow label="Clip">
|
|
120
|
+
<TextField
|
|
121
|
+
value={m.clip ?? ""}
|
|
122
|
+
ariaLabel="Animation clip"
|
|
123
|
+
onCommit={(next) =>
|
|
124
|
+
patch((block) => {
|
|
125
|
+
const trimmed = next.trim();
|
|
126
|
+
if (trimmed !== "") return { ...block, clip: trimmed };
|
|
127
|
+
// `clip: ""` would name a clip no glTF has: the layer would show
|
|
128
|
+
// its rest pose while the document claimed an animation. Absent
|
|
129
|
+
// is what "rest pose" actually spells.
|
|
130
|
+
const { clip: _drop, ...rest } = block;
|
|
131
|
+
return rest;
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
/>
|
|
135
|
+
</InspectorRow>
|
|
136
|
+
|
|
137
|
+
<InspectorRow label="Start">
|
|
138
|
+
<NumberField
|
|
139
|
+
value={m.clipStartFrame ?? 0}
|
|
140
|
+
mixed={mesh.mixed}
|
|
141
|
+
ariaLabel="Clip start frame"
|
|
142
|
+
unit="f"
|
|
143
|
+
step={1}
|
|
144
|
+
onCommit={(next) => set("clipStartFrame", Math.round(next))}
|
|
145
|
+
/>
|
|
146
|
+
</InspectorRow>
|
|
147
|
+
|
|
148
|
+
<InspectorRow label="Loop">
|
|
149
|
+
<SegmentedControl
|
|
150
|
+
ariaLabel="Clip loop"
|
|
151
|
+
value={m.loop ?? "hold"}
|
|
152
|
+
options={MESH_LOOPS.map((loop) => ({ value: loop, label: loop }))}
|
|
153
|
+
onChange={(next) => set("loop", next as MeshLoop)}
|
|
154
|
+
/>
|
|
155
|
+
</InspectorRow>
|
|
156
|
+
|
|
157
|
+
<InspectorRow label="Rate">
|
|
158
|
+
<NumberField
|
|
159
|
+
value={m.timeScale ?? 1}
|
|
160
|
+
mixed={mesh.mixed}
|
|
161
|
+
ariaLabel="Clip rate"
|
|
162
|
+
step={0.1}
|
|
163
|
+
// Zero is REFUSED by validation — it freezes the clip forever, which
|
|
164
|
+
// is a pose you can already spell by clearing the clip, and it is
|
|
165
|
+
// indistinguishable from a slider that failed to write. Negative
|
|
166
|
+
// plays the clip backwards and is allowed.
|
|
167
|
+
onCommit={(next) => set("timeScale", next === 0 ? 1 : next)}
|
|
168
|
+
/>
|
|
169
|
+
</InspectorRow>
|
|
170
|
+
</InspectorSection>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Drop shadow AND glow — ONE field with two affordances rather than two fields
|
|
2
|
+
// that can disagree. A glow IS a shadow at zero offset with a bright colour,
|
|
3
|
+
// which is exactly how the rasterizer draws it (raster.ts), so modelling them
|
|
4
|
+
// separately would be two schema fields for one Canvas2D call.
|
|
5
|
+
import { ColorPicker, InspectorRow, InspectorSection, NumberField } from "@dailies-studio/ui";
|
|
6
|
+
import type { Shadow } from "@dailies-studio/shot/doc";
|
|
7
|
+
import { NO_SCRUB, useSelectionValue, useSelectionWrite } from "../useDocWrite.js";
|
|
8
|
+
|
|
9
|
+
const DEFAULT_SHADOW: Shadow = { xPx: 0, yPx: 12, blurPx: 32, spreadPx: 0, color: "#00000080" };
|
|
10
|
+
|
|
11
|
+
export function ShadowSection() {
|
|
12
|
+
const write = useSelectionWrite();
|
|
13
|
+
const present = useSelectionValue((layer) => (layer.paint.shadows ?? []).length > 0);
|
|
14
|
+
const first = useSelectionValue((layer) => layer.paint.shadows?.[0]);
|
|
15
|
+
|
|
16
|
+
/** The FIRST shadow only. A layer may hold several (the schema is a list, and
|
|
17
|
+
* a glow plus a drop shadow is the common pair), but editing the second one
|
|
18
|
+
* needs a per-shadow list UI — which is a section of its own. */
|
|
19
|
+
const patch = (next: Partial<Shadow>): void =>
|
|
20
|
+
write((layer) => {
|
|
21
|
+
const shadows = [...(layer.paint.shadows ?? [DEFAULT_SHADOW])];
|
|
22
|
+
shadows[0] = { ...DEFAULT_SHADOW, ...shadows[0], ...next };
|
|
23
|
+
return { ...layer, paint: { ...layer.paint, shadows } };
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const shadow = first.value ?? DEFAULT_SHADOW;
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<InspectorSection
|
|
30
|
+
title="Shadow"
|
|
31
|
+
persistKey="shot:shadow"
|
|
32
|
+
action={
|
|
33
|
+
<button
|
|
34
|
+
type="button"
|
|
35
|
+
className="section-action"
|
|
36
|
+
title={present.value === true ? "Remove shadow" : "Add shadow"}
|
|
37
|
+
onClick={() =>
|
|
38
|
+
present.value === true
|
|
39
|
+
? write((layer) => ({ ...layer, paint: { ...layer.paint, shadows: [] } }))
|
|
40
|
+
: patch({})
|
|
41
|
+
}
|
|
42
|
+
>
|
|
43
|
+
{present.value === true ? "−" : "+"}
|
|
44
|
+
</button>
|
|
45
|
+
}
|
|
46
|
+
>
|
|
47
|
+
{present.value === true ? (
|
|
48
|
+
<>
|
|
49
|
+
<InspectorRow label="Color">
|
|
50
|
+
<ColorPicker
|
|
51
|
+
value={shadow.color}
|
|
52
|
+
ariaLabel="Shadow colour"
|
|
53
|
+
scrub={NO_SCRUB}
|
|
54
|
+
commit={(hex) => patch({ color: hex })}
|
|
55
|
+
/>
|
|
56
|
+
</InspectorRow>
|
|
57
|
+
<InspectorRow>
|
|
58
|
+
<NumberField value={shadow.xPx} label="X" ariaLabel="Shadow X" unit="px" step={1} onCommit={(v) => patch({ xPx: v })} />
|
|
59
|
+
</InspectorRow>
|
|
60
|
+
<InspectorRow>
|
|
61
|
+
<NumberField value={shadow.yPx} label="Y" ariaLabel="Shadow Y" unit="px" step={1} onCommit={(v) => patch({ yPx: v })} />
|
|
62
|
+
</InspectorRow>
|
|
63
|
+
<InspectorRow>
|
|
64
|
+
<NumberField value={shadow.blurPx} label="Blur" ariaLabel="Shadow blur" unit="px" min={0} step={1} onCommit={(v) => patch({ blurPx: v })} />
|
|
65
|
+
</InspectorRow>
|
|
66
|
+
<InspectorRow>
|
|
67
|
+
<NumberField value={shadow.spreadPx} label="Spread" ariaLabel="Shadow spread" unit="px" step={1} onCommit={(v) => patch({ spreadPx: v })} />
|
|
68
|
+
</InspectorRow>
|
|
69
|
+
</>
|
|
70
|
+
) : null}
|
|
71
|
+
</InspectorSection>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { ColorPicker, InspectorRow, InspectorSection, NumberField, SegmentedControl } from "@dailies-studio/ui";
|
|
2
|
+
import type { Stroke } from "@dailies-studio/shot/doc";
|
|
3
|
+
import { NO_SCRUB, useSelectionValue, useSelectionWrite } from "../useDocWrite.js";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_STROKE: Stroke = { color: "#ffffff", widthPx: 1, align: "center" };
|
|
6
|
+
|
|
7
|
+
export function StrokeSection() {
|
|
8
|
+
const write = useSelectionWrite();
|
|
9
|
+
const present = useSelectionValue((layer) => layer.paint.stroke !== undefined);
|
|
10
|
+
const color = useSelectionValue((layer) => layer.paint.stroke?.color ?? DEFAULT_STROKE.color);
|
|
11
|
+
const width = useSelectionValue((layer) => layer.paint.stroke?.widthPx ?? DEFAULT_STROKE.widthPx);
|
|
12
|
+
const align = useSelectionValue((layer) => layer.paint.stroke?.align ?? DEFAULT_STROKE.align);
|
|
13
|
+
|
|
14
|
+
const patch = (next: Partial<Stroke>): void =>
|
|
15
|
+
write((layer) => ({
|
|
16
|
+
...layer,
|
|
17
|
+
paint: { ...layer.paint, stroke: { ...DEFAULT_STROKE, ...layer.paint.stroke, ...next } },
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<InspectorSection
|
|
22
|
+
title="Stroke"
|
|
23
|
+
persistKey="shot:stroke"
|
|
24
|
+
action={
|
|
25
|
+
<button
|
|
26
|
+
type="button"
|
|
27
|
+
className="section-action"
|
|
28
|
+
title={present.value === true ? "Remove stroke" : "Add stroke"}
|
|
29
|
+
onClick={() =>
|
|
30
|
+
present.value === true
|
|
31
|
+
? write((layer) => ({ ...layer, paint: withoutStroke(layer.paint) }))
|
|
32
|
+
: patch({})
|
|
33
|
+
}
|
|
34
|
+
>
|
|
35
|
+
{present.value === true ? "−" : "+"}
|
|
36
|
+
</button>
|
|
37
|
+
}
|
|
38
|
+
>
|
|
39
|
+
{present.value === true ? (
|
|
40
|
+
<>
|
|
41
|
+
<InspectorRow label="Color">
|
|
42
|
+
<ColorPicker
|
|
43
|
+
value={color.value ?? DEFAULT_STROKE.color}
|
|
44
|
+
ariaLabel="Stroke colour"
|
|
45
|
+
scrub={NO_SCRUB}
|
|
46
|
+
commit={(hex) => patch({ color: hex })}
|
|
47
|
+
/>
|
|
48
|
+
</InspectorRow>
|
|
49
|
+
<InspectorRow>
|
|
50
|
+
<NumberField
|
|
51
|
+
value={width.value ?? 1}
|
|
52
|
+
mixed={width.mixed}
|
|
53
|
+
label="Width"
|
|
54
|
+
ariaLabel="Stroke width"
|
|
55
|
+
unit="px"
|
|
56
|
+
min={0}
|
|
57
|
+
step={0.5}
|
|
58
|
+
onCommit={(next) => patch({ widthPx: next })}
|
|
59
|
+
/>
|
|
60
|
+
</InspectorRow>
|
|
61
|
+
<InspectorRow label="Align">
|
|
62
|
+
{/* Canvas2D only CENTRES a stroke, so inside and outside are a
|
|
63
|
+
half-width inset either way — raster.ts does that, and the three
|
|
64
|
+
options are honest because the raster honours all three. */}
|
|
65
|
+
<SegmentedControl
|
|
66
|
+
ariaLabel="Stroke align"
|
|
67
|
+
value={align.value ?? "center"}
|
|
68
|
+
options={[
|
|
69
|
+
{ value: "inside", label: "In" },
|
|
70
|
+
{ value: "center", label: "Mid" },
|
|
71
|
+
{ value: "outside", label: "Out" },
|
|
72
|
+
]}
|
|
73
|
+
onChange={(next) => patch({ align: next as Stroke["align"] })}
|
|
74
|
+
/>
|
|
75
|
+
</InspectorRow>
|
|
76
|
+
</>
|
|
77
|
+
) : null}
|
|
78
|
+
</InspectorSection>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function withoutStroke(paint: { stroke?: Stroke }): typeof paint {
|
|
83
|
+
const { stroke: _drop, ...rest } = paint;
|
|
84
|
+
return rest;
|
|
85
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Position, size, depth, tilt, roll — every field the schema's `Transform3D`
|
|
2
|
+
// carries, and the section where the read/write asymmetry actually bites.
|
|
3
|
+
//
|
|
4
|
+
// Every readout is the layer AS PAINTED at the playhead and every commit goes
|
|
5
|
+
// through `setEffectiveField`, so the number typed is the number drawn. See
|
|
6
|
+
// ../useDocWrite.ts for why both halves are required.
|
|
7
|
+
import { InspectorRow, InspectorSection, NumberField, PropertyGrid } from "@dailies-studio/ui";
|
|
8
|
+
import type { TransformField } from "@dailies-studio/shot/doc";
|
|
9
|
+
import { useSelectionValue, useTransformWrite } from "../useDocWrite.js";
|
|
10
|
+
import { AlignRow } from "./AlignRow.js";
|
|
11
|
+
|
|
12
|
+
export function TransformSection() {
|
|
13
|
+
return (
|
|
14
|
+
<InspectorSection title="Transform" persistKey="shot:transform">
|
|
15
|
+
{/* Above the fields, the way Raylight leads its Position section: the
|
|
16
|
+
alignment buttons are how a layout actually gets made, and the
|
|
17
|
+
numbers are how it gets adjusted afterwards. */}
|
|
18
|
+
<AlignRow />
|
|
19
|
+
<PropertyGrid>
|
|
20
|
+
<Field field="x" label="X" unit="px" />
|
|
21
|
+
<Field field="y" label="Y" unit="px" />
|
|
22
|
+
<Field field="w" label="W" unit="px" min={1} />
|
|
23
|
+
<Field field="h" label="H" unit="px" min={1} />
|
|
24
|
+
{/* Depth is the field that makes this lane 3D, so it sits with the
|
|
25
|
+
position rather than in a section of its own. */}
|
|
26
|
+
<Field field="z" label="Z" unit="px" />
|
|
27
|
+
<Field field="roll" label="Roll" unit="°" step={1} />
|
|
28
|
+
<Field field="tiltX" label="Tilt X" unit="°" step={1} min={-180} max={180} />
|
|
29
|
+
<Field field="tiltY" label="Tilt Y" unit="°" step={1} min={-180} max={180} />
|
|
30
|
+
</PropertyGrid>
|
|
31
|
+
</InspectorSection>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function Field({
|
|
36
|
+
field,
|
|
37
|
+
label,
|
|
38
|
+
unit,
|
|
39
|
+
step = 1,
|
|
40
|
+
min,
|
|
41
|
+
max,
|
|
42
|
+
}: {
|
|
43
|
+
field: TransformField;
|
|
44
|
+
label: string;
|
|
45
|
+
unit?: string;
|
|
46
|
+
step?: number;
|
|
47
|
+
min?: number;
|
|
48
|
+
max?: number;
|
|
49
|
+
}) {
|
|
50
|
+
const write = useTransformWrite();
|
|
51
|
+
const { value, mixed } = useSelectionValue((_layer, effective) => readField(effective, field));
|
|
52
|
+
if (value === undefined) return null;
|
|
53
|
+
return (
|
|
54
|
+
<InspectorRow>
|
|
55
|
+
<NumberField
|
|
56
|
+
value={value}
|
|
57
|
+
mixed={mixed}
|
|
58
|
+
label={label}
|
|
59
|
+
ariaLabel={label}
|
|
60
|
+
{...(unit === undefined ? {} : { unit })}
|
|
61
|
+
step={step}
|
|
62
|
+
{...(min === undefined ? {} : { min })}
|
|
63
|
+
{...(max === undefined ? {} : { max })}
|
|
64
|
+
onCommit={(next) => write(field, next)}
|
|
65
|
+
/>
|
|
66
|
+
</InspectorRow>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function readField(t: { position: readonly number[]; size: readonly number[]; tilt: readonly number[]; rollDeg: number; opacity: number }, field: TransformField): number {
|
|
71
|
+
switch (field) {
|
|
72
|
+
case "x":
|
|
73
|
+
return t.position[0]!;
|
|
74
|
+
case "y":
|
|
75
|
+
return t.position[1]!;
|
|
76
|
+
case "z":
|
|
77
|
+
return t.position[2]!;
|
|
78
|
+
case "w":
|
|
79
|
+
return t.size[0]!;
|
|
80
|
+
case "h":
|
|
81
|
+
return t.size[1]!;
|
|
82
|
+
case "tiltX":
|
|
83
|
+
return t.tilt[0]!;
|
|
84
|
+
case "tiltY":
|
|
85
|
+
return t.tilt[1]!;
|
|
86
|
+
case "roll":
|
|
87
|
+
return t.rollDeg;
|
|
88
|
+
case "opacity":
|
|
89
|
+
return t.opacity;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// Only for TEXT layers, and absent otherwise — a typography section over a
|
|
2
|
+
// rectangle is four controls that write nothing.
|
|
3
|
+
//
|
|
4
|
+
// `verticalAlign` is offered here and is HONOURED, unlike in the 2D lane where
|
|
5
|
+
// it is declared-and-inert: this lane owns the text box, so top/middle/bottom
|
|
6
|
+
// is one offset in the rasterizer rather than a change to a layout model.
|
|
7
|
+
import { ColorPicker, InspectorRow, InspectorSection, NumberField, SegmentedControl, TextField } from "@dailies-studio/ui";
|
|
8
|
+
import { DEFAULT_TEXT_STAGGER } from "@dailies-studio/shot/doc";
|
|
9
|
+
import type { TextStagger, TextStyle } from "@dailies-studio/shot/doc";
|
|
10
|
+
import type { Layer3D, TextLayer3D } from "@dailies-studio/shot/doc";
|
|
11
|
+
import { NO_SCRUB, useSelectedLayers, useSelectionValue, useSelectionWrite } from "../useDocWrite.js";
|
|
12
|
+
|
|
13
|
+
export function TypographySection() {
|
|
14
|
+
const write = useSelectionWrite();
|
|
15
|
+
const layers = useSelectedLayers();
|
|
16
|
+
const anyText = layers.some((l) => l.kind === "text");
|
|
17
|
+
const style = useSelectionValue((layer) => (layer.kind === "text" ? layer.text.style : undefined));
|
|
18
|
+
const content = useSelectionValue((layer) => (layer.kind === "text" ? layer.text.content : ""));
|
|
19
|
+
if (!anyText || style.value === undefined) return null;
|
|
20
|
+
|
|
21
|
+
const patchStyle = (next: Partial<TextStyle>): void =>
|
|
22
|
+
write((layer) =>
|
|
23
|
+
layer.kind === "text"
|
|
24
|
+
? ({ ...layer, text: { ...layer.text, style: { ...layer.text.style, ...next } } } as Layer3D)
|
|
25
|
+
: layer,
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const s = style.value;
|
|
29
|
+
return (
|
|
30
|
+
<InspectorSection title="Text" persistKey="shot:text">
|
|
31
|
+
<InspectorRow label="Content">
|
|
32
|
+
<TextField
|
|
33
|
+
value={content.value ?? ""}
|
|
34
|
+
ariaLabel="Text content"
|
|
35
|
+
onCommit={(next) =>
|
|
36
|
+
write((layer) =>
|
|
37
|
+
layer.kind === "text" ? ({ ...layer, text: { ...layer.text, content: next } } as Layer3D) : layer,
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
/>
|
|
41
|
+
</InspectorRow>
|
|
42
|
+
<InspectorRow label="Font">
|
|
43
|
+
<TextField value={s.fontFamily} ariaLabel="Font family" onCommit={(next) => patchStyle({ fontFamily: next })} />
|
|
44
|
+
</InspectorRow>
|
|
45
|
+
<InspectorRow>
|
|
46
|
+
<NumberField value={s.sizePx} label="Size" ariaLabel="Font size" unit="px" min={1} step={1} onCommit={(v) => patchStyle({ sizePx: v })} />
|
|
47
|
+
</InspectorRow>
|
|
48
|
+
<InspectorRow>
|
|
49
|
+
<NumberField value={s.weight} label="Weight" ariaLabel="Font weight" min={100} max={900} step={100} onCommit={(v) => patchStyle({ weight: v })} />
|
|
50
|
+
</InspectorRow>
|
|
51
|
+
<InspectorRow>
|
|
52
|
+
<NumberField value={s.lineHeight} label="Line" ariaLabel="Line height" step={0.05} min={0.5} onCommit={(v) => patchStyle({ lineHeight: v })} />
|
|
53
|
+
</InspectorRow>
|
|
54
|
+
<InspectorRow>
|
|
55
|
+
<NumberField value={s.letterSpacingPx} label="Letter" ariaLabel="Letter spacing" unit="px" step={0.1} onCommit={(v) => patchStyle({ letterSpacingPx: v })} />
|
|
56
|
+
</InspectorRow>
|
|
57
|
+
<InspectorRow label="Color">
|
|
58
|
+
<ColorPicker value={s.color} ariaLabel="Text colour" scrub={NO_SCRUB} commit={(hex) => patchStyle({ color: hex })} />
|
|
59
|
+
</InspectorRow>
|
|
60
|
+
<InspectorRow label="Align">
|
|
61
|
+
<SegmentedControl
|
|
62
|
+
ariaLabel="Text align"
|
|
63
|
+
value={s.align}
|
|
64
|
+
options={[
|
|
65
|
+
{ value: "left", label: "L" },
|
|
66
|
+
{ value: "center", label: "C" },
|
|
67
|
+
{ value: "right", label: "R" },
|
|
68
|
+
]}
|
|
69
|
+
onChange={(next) => patchStyle({ align: next as TextStyle["align"] })}
|
|
70
|
+
/>
|
|
71
|
+
</InspectorRow>
|
|
72
|
+
<InspectorRow label="Vertical">
|
|
73
|
+
<SegmentedControl
|
|
74
|
+
ariaLabel="Vertical align"
|
|
75
|
+
value={s.verticalAlign ?? "top"}
|
|
76
|
+
options={[
|
|
77
|
+
{ value: "top", label: "T" },
|
|
78
|
+
{ value: "middle", label: "M" },
|
|
79
|
+
{ value: "bottom", label: "B" },
|
|
80
|
+
]}
|
|
81
|
+
onChange={(next) => patchStyle({ verticalAlign: next as NonNullable<TextStyle["verticalAlign"]> })}
|
|
82
|
+
/>
|
|
83
|
+
</InspectorRow>
|
|
84
|
+
</InspectorSection>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The SHAPE of a per-unit stagger, which is a property of the text rather than
|
|
90
|
+
* of any one block — see `TextStagger` for why it cannot live on the block.
|
|
91
|
+
*
|
|
92
|
+
* Its own section rather than four more rows under Text: it is inert until a
|
|
93
|
+
* `stagger` block exists on the layer, so it deserves to be collapsed away for
|
|
94
|
+
* the layers that never animate.
|
|
95
|
+
*
|
|
96
|
+
* Every field writes a WHOLE record spread from the default, so the first edit
|
|
97
|
+
* materialises what "absent means the default" already meant. Reading does the
|
|
98
|
+
* same, so the fields show the real starting values instead of blanks.
|
|
99
|
+
*/
|
|
100
|
+
export function TextStaggerSection() {
|
|
101
|
+
const write = useSelectionWrite();
|
|
102
|
+
const layers = useSelectedLayers();
|
|
103
|
+
const anyText = layers.some((l) => l.kind === "text");
|
|
104
|
+
const stagger = useSelectionValue((layer) =>
|
|
105
|
+
layer.kind === "text" ? (layer.text.stagger ?? DEFAULT_TEXT_STAGGER) : undefined,
|
|
106
|
+
);
|
|
107
|
+
if (!anyText || stagger.value === undefined) return null;
|
|
108
|
+
|
|
109
|
+
const patch = (next: Partial<TextStagger>): void =>
|
|
110
|
+
write((layer) =>
|
|
111
|
+
layer.kind === "text"
|
|
112
|
+
? ({
|
|
113
|
+
...layer,
|
|
114
|
+
text: { ...layer.text, stagger: { ...DEFAULT_TEXT_STAGGER, ...layer.text.stagger, ...next } },
|
|
115
|
+
} as Layer3D)
|
|
116
|
+
: layer,
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const g = stagger.value;
|
|
120
|
+
return (
|
|
121
|
+
<InspectorSection title="Stagger" persistKey="shot:textStagger">
|
|
122
|
+
<InspectorRow label="Unit">
|
|
123
|
+
<SegmentedControl
|
|
124
|
+
ariaLabel="Stagger unit"
|
|
125
|
+
value={g.unit}
|
|
126
|
+
options={[
|
|
127
|
+
{ value: "char", label: "Char" },
|
|
128
|
+
{ value: "word", label: "Word" },
|
|
129
|
+
{ value: "line", label: "Line" },
|
|
130
|
+
]}
|
|
131
|
+
onChange={(next) => patch({ unit: next as TextStagger["unit"] })}
|
|
132
|
+
/>
|
|
133
|
+
</InspectorRow>
|
|
134
|
+
<InspectorRow>
|
|
135
|
+
<NumberField
|
|
136
|
+
value={g.spread}
|
|
137
|
+
label="Spread"
|
|
138
|
+
ariaLabel="Stagger spread"
|
|
139
|
+
min={0}
|
|
140
|
+
max={1}
|
|
141
|
+
step={0.05}
|
|
142
|
+
onCommit={(v) => patch({ spread: v })}
|
|
143
|
+
/>
|
|
144
|
+
</InspectorRow>
|
|
145
|
+
<InspectorRow>
|
|
146
|
+
<NumberField
|
|
147
|
+
value={g.risePx}
|
|
148
|
+
label="Rise"
|
|
149
|
+
ariaLabel="Stagger rise"
|
|
150
|
+
unit="px"
|
|
151
|
+
step={1}
|
|
152
|
+
onCommit={(v) => patch({ risePx: v })}
|
|
153
|
+
/>
|
|
154
|
+
</InspectorRow>
|
|
155
|
+
<InspectorRow>
|
|
156
|
+
<NumberField
|
|
157
|
+
value={g.fadeFrom}
|
|
158
|
+
label="Fade from"
|
|
159
|
+
ariaLabel="Stagger fade from"
|
|
160
|
+
min={0}
|
|
161
|
+
max={1}
|
|
162
|
+
step={0.05}
|
|
163
|
+
onCommit={(v) => patch({ fadeFrom: v })}
|
|
164
|
+
/>
|
|
165
|
+
</InspectorRow>
|
|
166
|
+
</InspectorSection>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
void ({} as TextLayer3D);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// The scene's camera, as five fields. Shown under the scene, when nothing is
|
|
2
|
+
// selected.
|
|
3
|
+
//
|
|
4
|
+
// ── THE SAME TWO TRUTHS THE LAYER INSPECTOR FOLLOWS ─────────────────────
|
|
5
|
+
//
|
|
6
|
+
// READ EFFECTIVE: `cameraAt` at the playhead, which is what is on screen. A
|
|
7
|
+
// section that read `scene.camera` would say x=0 while a finished move held the
|
|
8
|
+
// camera at 400, and the author would type against a number nothing agrees
|
|
9
|
+
// with.
|
|
10
|
+
//
|
|
11
|
+
// WRITE BASE, AND REFUSE WHEN THAT IS INVISIBLE: from the first shot's start
|
|
12
|
+
// onwards `cameraAt` is painting a shot's framing, so writing `scene.camera`
|
|
13
|
+
// there would dirty the document and move nothing. That refusal is not written
|
|
14
|
+
// here. It is `writeFraming`'s, reached through the store's `writeCamera`, the
|
|
15
|
+
// same seam the frustum drag, the align and the fit all go through. This panel
|
|
16
|
+
// is a keyboard frustum and deliberately has no opinion of its own.
|
|
17
|
+
//
|
|
18
|
+
// The two halves are coherent by construction rather than by care:
|
|
19
|
+
// `shotOwningCamera` is null exactly when `cameraAt` returns `scene.camera`, so
|
|
20
|
+
// whenever a base write is allowed the effective value IS the base.
|
|
21
|
+
import { InspectorSection } from "@dailies-studio/ui";
|
|
22
|
+
import { cameraAt, shotOwningCamera } from "@dailies-studio/shot/doc";
|
|
23
|
+
import { useShotEditor, useStore } from "../../../state/context.js";
|
|
24
|
+
import { FramingRows } from "../FramingRows.js";
|
|
25
|
+
import { useInspectorFrame } from "../useDocWrite.js";
|
|
26
|
+
|
|
27
|
+
export function CameraSection() {
|
|
28
|
+
const store = useStore();
|
|
29
|
+
const doc = useShotEditor((s) => s.doc);
|
|
30
|
+
const animate = useShotEditor((s) => s.animateMode);
|
|
31
|
+
const { sceneId, sceneFrame } = useInspectorFrame();
|
|
32
|
+
if (doc === null || sceneId === null) return null;
|
|
33
|
+
const scene = doc.scenesById[sceneId];
|
|
34
|
+
if (scene === undefined) return null;
|
|
35
|
+
|
|
36
|
+
// Only worth saying with animate OFF: with it on the same write authors a
|
|
37
|
+
// move instead of being refused, so naming the owner would be a warning about
|
|
38
|
+
// something that is not going to happen.
|
|
39
|
+
const owner = animate ? null : shotOwningCamera(scene, sceneFrame);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<InspectorSection title="Camera" persistKey="shot:camera">
|
|
43
|
+
<FramingRows
|
|
44
|
+
framing={cameraAt(scene, sceneFrame, doc.fps)}
|
|
45
|
+
name="Camera"
|
|
46
|
+
onCommit={(patch) => store.setSceneCamera(patch)}
|
|
47
|
+
/>
|
|
48
|
+
{owner !== null && (
|
|
49
|
+
// The fields stay LIVE. Typing into one produces the refusal by name in
|
|
50
|
+
// the error strip, which is how the author learns the rule; a disabled
|
|
51
|
+
// field would only teach them that the panel is broken.
|
|
52
|
+
<span className="insp-dim" data-testid="camera-held-by">
|
|
53
|
+
Held by the camera move “{owner.label}” from frame {owner.frame}. Turn on Animate (⇧A) to author the
|
|
54
|
+
move, or put the playhead before it.
|
|
55
|
+
</span>
|
|
56
|
+
)}
|
|
57
|
+
</InspectorSection>
|
|
58
|
+
);
|
|
59
|
+
}
|