@hypit/hypit 0.1.3 → 0.1.4
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/README.md +1 -1
- package/dist/public/studio-adapter.d.ts +31 -2
- package/package.json +4 -1
- package/packages/audio-track-studio/src/index.ts +7 -4
- package/packages/build-result/README.md +58 -0
- package/packages/build-result/package.json +2 -1
- package/packages/build-result/src/decode.ts +10 -8
- package/packages/build-result/src/file-reference.ts +42 -0
- package/packages/build-result/src/index.ts +3 -0
- package/packages/build-result/src/replace-file-windows.ts +58 -0
- package/packages/build-result/src/replace-file.ts +11 -0
- package/packages/build-result/src/store.ts +44 -13
- package/packages/build-result/src/types.ts +38 -8
- package/packages/build-result/src/writer.ts +12 -1
- package/packages/build-result-fs/README.md +23 -1
- package/packages/build-result-s3/README.md +5 -0
- package/packages/build-result-s3/src/repository.ts +19 -5
- package/packages/caption-fine/README.md +17 -2
- package/packages/caption-fine/src/manifest.ts +1 -1
- package/packages/caption-fine/src/render.ts +48 -36
- package/packages/caption-fine-studio/package.json +8 -3
- package/packages/caption-fine-studio/src/index.ts +10 -1
- package/packages/cli/README.md +25 -0
- package/packages/cli/src/arguments.ts +16 -11
- package/packages/cli/src/build-planning.ts +6 -1
- package/packages/cli/src/command.ts +1 -1
- package/packages/cli/src/commands/environment.ts +42 -13
- package/packages/cli/src/machine-view.ts +12 -3
- package/packages/cli/src/main.ts +4 -0
- package/packages/cli/src/output.ts +28 -12
- package/packages/cli/src/result-export.ts +22 -13
- package/packages/cli/src/run-file.ts +19 -8
- package/packages/comment-sticker-studio/src/index.ts +2 -1
- package/packages/compiler-node/src/compiler.ts +1 -0
- package/packages/estimate/README.md +56 -3
- package/packages/estimate/src/program.ts +7 -8
- package/packages/fonts-open/README.md +5 -0
- package/packages/fonts-open/package.json +5 -1
- package/packages/fonts-open/src/studio.ts +21 -0
- package/packages/media-track-studio/src/index.ts +7 -2
- package/packages/provider-whisperx-local/README.md +57 -0
- package/packages/ranking/src/render.ts +11 -9
- package/packages/ranking-studio/README.md +11 -0
- package/packages/ranking-studio/package.json +8 -3
- package/packages/ranking-studio/src/index.ts +11 -4
- package/packages/runtime/src/catalog.ts +2 -0
- package/packages/runtime-host-node/src/index.ts +1 -0
- package/packages/runtime-local/README.md +19 -0
- package/packages/runtime-local/src/programs.ts +39 -24
- package/packages/runtime-local/src/runtime.ts +2 -0
- package/packages/script/README.md +22 -0
- package/packages/script/src/edit.ts +123 -73
- package/packages/script/src/lexical.ts +40 -6
- package/packages/script/src/parser.ts +36 -8
- package/packages/script/src/types.ts +4 -0
- package/packages/seedance-kits/README.md +3 -1
- package/packages/seedance-kits/kits/speaker-v1.svs +5 -5
- package/packages/speech-alignment/src/align.ts +45 -21
- package/packages/studio/INSPECTOR.md +104 -0
- package/packages/studio/README.md +65 -4
- package/packages/studio/src/build-library.ts +99 -84
- package/packages/studio/src/library-media.ts +19 -0
- package/packages/studio/src/parameter-values.ts +41 -1
- package/packages/studio/src/parameters.ts +20 -21
- package/packages/studio/src/server.ts +58 -30
- package/packages/studio/src/shared.ts +20 -6
- package/packages/studio/src/studio-registry.ts +22 -0
- package/packages/studio/src/style.css +135 -144
- package/packages/studio/src/temporal-edit.ts +131 -0
- package/packages/studio/src/ui/artifact-name.ts +80 -0
- package/packages/studio/src/ui/artifact-preview.ts +99 -0
- package/packages/studio/src/ui/icons.ts +5 -1
- package/packages/studio/src/ui/library.ts +373 -192
- package/packages/studio/src/ui/main.ts +153 -31
- package/packages/studio/src/ui/material-preview.ts +1 -1
- package/packages/studio/src/ui/overlay.ts +21 -20
- package/packages/studio/src/ui/sidebar-panel.ts +31 -0
- package/packages/studio/src/ui/stage.ts +124 -8
- package/packages/studio/src/ui/timeline.ts +16 -179
- package/packages/studio/start.ts +15 -3
- package/packages/studio-adapter/README.md +21 -0
- package/packages/studio-adapter/src/index.ts +26 -1
- package/packages/temporal-markup/EDITING.md +183 -0
- package/packages/temporal-markup/README.md +4 -0
- package/packages/typography-track-studio/package.json +8 -3
- package/packages/typography-track-studio/src/index.ts +5 -1
- package/packages/video-cli/README.md +12 -3
- package/packages/video-cli/src/creation.ts +11 -5
- package/packages/whisperx/README.md +7 -0
- package/packages/workspace/src/index.ts +2 -0
- package/packages/workspace-fs-node/README.md +4 -1
- package/packages/workspace-fs-node/src/workspace.ts +3 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { createArtifactPreview } from "./artifact-preview.js";
|
|
2
|
+
import type { StudioArtifactView, StudioSnapshot } from "../shared.js";
|
|
2
3
|
import { createOverlay } from "./overlay.js";
|
|
3
4
|
import { icon, setIcon } from "./icons.js";
|
|
4
5
|
import type { State, Store } from "./selection.js";
|
|
@@ -7,6 +8,8 @@ export type Stage = {
|
|
|
7
8
|
readonly element: HTMLElement;
|
|
8
9
|
/** Start or stop playback, however the request arrived. */
|
|
9
10
|
toggle(): void;
|
|
11
|
+
openArtifact(artifact: StudioArtifactView): void;
|
|
12
|
+
renameArtifact(artifact: StudioArtifactView): void;
|
|
10
13
|
};
|
|
11
14
|
|
|
12
15
|
/**
|
|
@@ -17,19 +20,23 @@ export type Stage = {
|
|
|
17
20
|
* drives frames, not seconds, and lands on exactly the frame the renderer would
|
|
18
21
|
* photograph.
|
|
19
22
|
*/
|
|
20
|
-
export function createStage(store: Store): Stage {
|
|
23
|
+
export function createStage(store: Store, selectedArtifact: (id: string | undefined) => void): Stage {
|
|
21
24
|
const element = document.createElement("section");
|
|
22
25
|
element.className = "stage";
|
|
23
26
|
element.innerHTML = `
|
|
24
27
|
<div class="stage-heading">
|
|
25
28
|
<div class="stage-title">${icon("preview")}<strong>Preview</strong></div>
|
|
29
|
+
<button type="button" class="stage-return" hidden>Back to composition</button>
|
|
26
30
|
</div>
|
|
27
31
|
<div class="stage-viewport">
|
|
28
32
|
<div class="stage-scaler">
|
|
29
33
|
<iframe title="Preview" sandbox="allow-scripts allow-same-origin" allow="autoplay"></iframe>
|
|
30
34
|
</div>
|
|
31
35
|
</div>
|
|
36
|
+
<input class="stage-scrubber" type="range" min="0" max="0" value="0" step="0.01" aria-label="Media time" hidden>
|
|
32
37
|
<div class="stage-bar">
|
|
38
|
+
<span class="stage-time" hidden></span>
|
|
39
|
+
<div class="stage-transport">
|
|
33
40
|
<button type="button" data-previous aria-label="Previous frame" title="Previous frame">
|
|
34
41
|
${icon("previous")}
|
|
35
42
|
</button>
|
|
@@ -39,6 +46,7 @@ export function createStage(store: Store): Stage {
|
|
|
39
46
|
<button type="button" data-next aria-label="Next frame" title="Next frame">
|
|
40
47
|
${icon("next")}
|
|
41
48
|
</button>
|
|
49
|
+
</div>
|
|
42
50
|
<button type="button" data-mute aria-label="Mute" class="stage-mute">
|
|
43
51
|
<span data-mute-icon>${icon("volume")}</span>
|
|
44
52
|
</button>
|
|
@@ -57,17 +65,30 @@ export function createStage(store: Store): Stage {
|
|
|
57
65
|
* of its enter and exit, and selecting a clip lands the playhead on exactly
|
|
58
66
|
* the frame where that displacement is largest.
|
|
59
67
|
*/
|
|
60
|
-
const measure = (clipId: string): { xPx: number; yPx: number; widthPx: number; heightPx: number } | undefined => {
|
|
68
|
+
const measure = (clipId: string): { xPx: number; yPx: number; widthPx: number; heightPx: number; stackOrder: number } | undefined => {
|
|
61
69
|
const document_ = iframe.contentDocument;
|
|
62
70
|
if (document_ === null) return undefined;
|
|
63
71
|
const present = document_.querySelector(`[data-hypit-present-id="${CSS.escape(clipId)}"]`);
|
|
72
|
+
if (present === null) return undefined;
|
|
73
|
+
const current = store.current();
|
|
74
|
+
if (current === undefined) return undefined;
|
|
75
|
+
const frameRate = fps(current.snapshot);
|
|
76
|
+
const start = Number(present.getAttribute("data-start"));
|
|
77
|
+
const end = start + Number(present.getAttribute("data-duration"));
|
|
78
|
+
if (current.playhead.frame < Math.round(start * frameRate)
|
|
79
|
+
|| current.playhead.frame >= Math.round(end * frameRate)) return undefined;
|
|
64
80
|
// The present spans the whole canvas; the drawn box is its frame element.
|
|
65
81
|
const drawn = present?.querySelector(`[data-hypit-element-id="${CSS.escape(`${clipId}:frame`)}"]`)
|
|
66
82
|
?? present?.querySelector("[data-hypit-element-id]");
|
|
67
83
|
if (drawn === null || drawn === undefined) return undefined;
|
|
84
|
+
for (let ancestor: Element | null = drawn; ancestor !== null; ancestor = ancestor.parentElement) {
|
|
85
|
+
const style = document_.defaultView!.getComputedStyle(ancestor);
|
|
86
|
+
if (style.display === "none" || style.visibility === "hidden" || Number(style.opacity) === 0) return undefined;
|
|
87
|
+
}
|
|
68
88
|
const box = drawn.getBoundingClientRect();
|
|
69
89
|
if (box.width === 0 && box.height === 0) return undefined;
|
|
70
|
-
return { xPx: box.left, yPx: box.top, widthPx: box.width, heightPx: box.height
|
|
90
|
+
return { xPx: box.left, yPx: box.top, widthPx: box.width, heightPx: box.height,
|
|
91
|
+
stackOrder: Number(document_.defaultView!.getComputedStyle(present).zIndex) || 0 };
|
|
71
92
|
};
|
|
72
93
|
|
|
73
94
|
// The overlay is a sibling of the iframe inside the scaler, so it inherits the
|
|
@@ -152,6 +173,7 @@ export function createStage(store: Store): Stage {
|
|
|
152
173
|
};
|
|
153
174
|
|
|
154
175
|
const toggle = (): void => {
|
|
176
|
+
if (artifactPreview.selected !== undefined) { artifactPreview.toggle(); return; }
|
|
155
177
|
if (state === undefined) return;
|
|
156
178
|
if (playing) { stop(); return; }
|
|
157
179
|
playing = true;
|
|
@@ -172,21 +194,107 @@ export function createStage(store: Store): Stage {
|
|
|
172
194
|
};
|
|
173
195
|
play.addEventListener("click", toggle);
|
|
174
196
|
previous.addEventListener("click", () => {
|
|
175
|
-
if (
|
|
197
|
+
if (artifactPreview.selected !== undefined) artifactPreview.seek((artifactPreview.media?.currentTime ?? 0) - 5);
|
|
198
|
+
else if (state !== undefined) store.seek(state.playhead.frame - 1, "video");
|
|
176
199
|
});
|
|
177
200
|
next.addEventListener("click", () => {
|
|
178
|
-
if (
|
|
201
|
+
if (artifactPreview.selected !== undefined) artifactPreview.seek((artifactPreview.media?.currentTime ?? 0) + 5);
|
|
202
|
+
else if (state !== undefined) store.seek(state.playhead.frame + 1, "video");
|
|
179
203
|
});
|
|
180
204
|
|
|
181
205
|
const applyMuted = (): void => {
|
|
182
206
|
setIcon(muteIcon, muted ? "muted" : "volume");
|
|
183
207
|
mute.setAttribute("aria-label", muted ? "Unmute" : "Mute");
|
|
208
|
+
if (artifactPreview.media !== undefined) artifactPreview.media.muted = muted;
|
|
184
209
|
(iframe.contentWindow as SeekWindow | null)?.__hypitSetMuted?.(muted);
|
|
185
210
|
};
|
|
186
211
|
mute.addEventListener("click", () => { muted = !muted; applyMuted(); });
|
|
187
212
|
|
|
213
|
+
const title = element.querySelector<HTMLElement>(".stage-title strong")!;
|
|
214
|
+
const back = element.querySelector<HTMLButtonElement>(".stage-return")!;
|
|
215
|
+
const scrubber = element.querySelector<HTMLInputElement>(".stage-scrubber")!;
|
|
216
|
+
const time = element.querySelector<HTMLElement>(".stage-time")!;
|
|
217
|
+
const transport = element.querySelector<HTMLElement>(".stage-transport")!;
|
|
218
|
+
const clock = (seconds: number): string => {
|
|
219
|
+
const ticks = Math.max(0, Math.round(seconds * 100));
|
|
220
|
+
const whole = Math.floor(ticks / 100);
|
|
221
|
+
return `${Math.floor(whole / 60)}:${String(whole % 60).padStart(2, "0")}.${String(ticks % 100).padStart(2, "0")}`;
|
|
222
|
+
};
|
|
223
|
+
const artifactPreview = createArtifactPreview(() => {
|
|
224
|
+
if (artifactPreview.selected === undefined) return;
|
|
225
|
+
const media = artifactPreview.media;
|
|
226
|
+
const timed = media !== undefined;
|
|
227
|
+
const duration = media !== undefined && Number.isFinite(media.duration) ? media.duration : 0;
|
|
228
|
+
transport.hidden = !timed;
|
|
229
|
+
mute.hidden = !timed;
|
|
230
|
+
scrubber.hidden = !timed;
|
|
231
|
+
time.hidden = !timed;
|
|
232
|
+
play.disabled = duration <= 0 || media?.error != null;
|
|
233
|
+
previous.disabled = next.disabled = play.disabled;
|
|
234
|
+
scrubber.disabled = duration <= 0;
|
|
235
|
+
scrubber.step = "0.01";
|
|
236
|
+
scrubber.setAttribute("aria-label", "Media time");
|
|
237
|
+
scrubber.max = String(duration);
|
|
238
|
+
scrubber.value = String(media?.currentTime ?? 0);
|
|
239
|
+
time.textContent = `${clock(media?.currentTime ?? 0)} / ${clock(duration)}`;
|
|
240
|
+
setIcon(playIcon, media !== undefined && !media.paused ? "pause" : "play");
|
|
241
|
+
play.setAttribute("aria-label", media !== undefined && !media.paused ? "Pause" : "Play");
|
|
242
|
+
});
|
|
243
|
+
viewport.append(artifactPreview.element);
|
|
244
|
+
const compositionTime = (): void => {
|
|
245
|
+
if (state === undefined || artifactPreview.selected !== undefined) return;
|
|
246
|
+
const rate = fps(state.snapshot);
|
|
247
|
+
scrubber.hidden = time.hidden = false;
|
|
248
|
+
scrubber.disabled = !ready;
|
|
249
|
+
scrubber.step = String(1 / rate);
|
|
250
|
+
scrubber.max = String(Math.max(0, state.snapshot.space.frameCount - 1) / rate);
|
|
251
|
+
scrubber.value = String(state.playhead.frame / rate);
|
|
252
|
+
scrubber.setAttribute("aria-label", "Composition time");
|
|
253
|
+
time.textContent = `${clock(state.playhead.frame / rate)} / ${clock(state.snapshot.space.frameCount / rate)}`;
|
|
254
|
+
};
|
|
255
|
+
scrubber.addEventListener("input", () => {
|
|
256
|
+
if (artifactPreview.selected !== undefined) artifactPreview.seek(Number(scrubber.value));
|
|
257
|
+
else if (state !== undefined) store.seek(Math.round(Number(scrubber.value) * fps(state.snapshot)), "video");
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const returnToComposition = (): void => {
|
|
261
|
+
artifactPreview.close();
|
|
262
|
+
selectedArtifact(undefined);
|
|
263
|
+
scaler.hidden = false;
|
|
264
|
+
back.hidden = true;
|
|
265
|
+
compositionTime();
|
|
266
|
+
transport.hidden = mute.hidden = false;
|
|
267
|
+
play.disabled = previous.disabled = next.disabled = false;
|
|
268
|
+
previous.title = "Previous frame";
|
|
269
|
+
previous.setAttribute("aria-label", previous.title);
|
|
270
|
+
next.title = "Next frame";
|
|
271
|
+
next.setAttribute("aria-label", next.title);
|
|
272
|
+
title.textContent = "Preview";
|
|
273
|
+
title.removeAttribute("title");
|
|
274
|
+
setIcon(playIcon, "play");
|
|
275
|
+
play.setAttribute("aria-label", "Play");
|
|
276
|
+
fit();
|
|
277
|
+
if (state !== undefined && ready) seekPicture(state.playhead.frame);
|
|
278
|
+
};
|
|
279
|
+
back.addEventListener("click", returnToComposition);
|
|
280
|
+
|
|
281
|
+
const openArtifact = (artifact: StudioArtifactView): void => {
|
|
282
|
+
stop();
|
|
283
|
+
scaler.hidden = true;
|
|
284
|
+
back.hidden = false;
|
|
285
|
+
title.textContent = artifact.displayName ?? artifact.output;
|
|
286
|
+
title.title = artifact.output;
|
|
287
|
+
previous.title = "Back 5 seconds";
|
|
288
|
+
next.title = "Forward 5 seconds";
|
|
289
|
+
previous.setAttribute("aria-label", previous.title);
|
|
290
|
+
next.setAttribute("aria-label", next.title);
|
|
291
|
+
artifactPreview.open(artifact, muted);
|
|
292
|
+
selectedArtifact(artifact.id);
|
|
293
|
+
};
|
|
294
|
+
|
|
188
295
|
iframe.addEventListener("load", () => {
|
|
189
296
|
ready = true;
|
|
297
|
+
compositionTime();
|
|
190
298
|
if (state !== undefined) seekPicture(state.playhead.frame);
|
|
191
299
|
applyMuted();
|
|
192
300
|
// The box could not be measured until now.
|
|
@@ -197,16 +305,19 @@ export function createStage(store: Store): Stage {
|
|
|
197
305
|
|
|
198
306
|
store.subscribe((value) => {
|
|
199
307
|
const first = state === undefined;
|
|
308
|
+
const moved = state !== undefined && (state.playhead.frame !== value.playhead.frame || state.selection !== value.selection);
|
|
200
309
|
state = value;
|
|
310
|
+
if (moved && value.playhead.origin !== "play" && artifactPreview.selected !== undefined) returnToComposition();
|
|
201
311
|
if (value.snapshot.revision !== mounted) {
|
|
202
312
|
mounted = value.snapshot.revision;
|
|
203
|
-
stop();
|
|
313
|
+
if (artifactPreview.selected === undefined) stop();
|
|
204
314
|
ready = false;
|
|
205
315
|
const preview = value.snapshot.preview;
|
|
206
316
|
iframe.srcdoc = preview.srcdoc;
|
|
207
317
|
fit();
|
|
208
318
|
}
|
|
209
319
|
if (first) fit();
|
|
320
|
+
compositionTime();
|
|
210
321
|
|
|
211
322
|
// Playing and scrubbing are different requests: one lets material run on
|
|
212
323
|
// its own clock, the other moves it. Asking a decoder for a fresh seek
|
|
@@ -227,5 +338,10 @@ export function createStage(store: Store): Stage {
|
|
|
227
338
|
if (playing) overlay.refresh();
|
|
228
339
|
});
|
|
229
340
|
|
|
230
|
-
return { element, toggle
|
|
341
|
+
return { element, toggle, openArtifact,
|
|
342
|
+
renameArtifact(artifact) {
|
|
343
|
+
const selected = artifactPreview.selected;
|
|
344
|
+
if (selected?.build === artifact.build && selected.output === artifact.output) title.textContent = artifact.displayName ?? artifact.output;
|
|
345
|
+
},
|
|
346
|
+
};
|
|
231
347
|
}
|
|
@@ -8,6 +8,7 @@ import { mountMaterialPreview } from "./material-preview.js";
|
|
|
8
8
|
import type { State, Store } from "./selection.js";
|
|
9
9
|
import { createHandle } from "./resize.js";
|
|
10
10
|
import { createZoom } from "./zoom.js";
|
|
11
|
+
import { chooseSemanticGesture, semanticGestureSpan } from "../temporal-edit.js";
|
|
11
12
|
import { applyStudioMutation } from "./writeback.js";
|
|
12
13
|
|
|
13
14
|
export type Timeline = {
|
|
@@ -17,7 +18,6 @@ export type Timeline = {
|
|
|
17
18
|
readonly fit: () => void;
|
|
18
19
|
};
|
|
19
20
|
|
|
20
|
-
const opened = new Set<string>();
|
|
21
21
|
const itemMetrics = {
|
|
22
22
|
// Ordinary items fill their rows. Only the semantic lane owns compact word cells.
|
|
23
23
|
insetYPx: 1,
|
|
@@ -221,107 +221,17 @@ export function createTimeline(store: Store): Timeline {
|
|
|
221
221
|
readonly clip: StudioSnapshot["tracks"][number]["clips"][number];
|
|
222
222
|
readonly handle: StudioEditHandle;
|
|
223
223
|
readonly startFrame: number;
|
|
224
|
-
readonly pointerAnchorIndex?: number;
|
|
225
224
|
readonly pointerId: number;
|
|
226
225
|
readonly node: HTMLElement;
|
|
227
226
|
readonly originalLeft: string;
|
|
228
227
|
readonly originalWidth: string;
|
|
229
228
|
} | undefined;
|
|
230
229
|
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
if (state === undefined) return undefined;
|
|
237
|
-
const anchors = state.snapshot.semantic?.anchors ?? [];
|
|
238
|
-
const preferredKind = preferredIndex === undefined ? undefined : anchors[preferredIndex]?.kind;
|
|
239
|
-
return [...indices].sort((left, right) => {
|
|
240
|
-
const leftAnchor = anchors[left]!;
|
|
241
|
-
const rightAnchor = anchors[right]!;
|
|
242
|
-
return Math.abs(leftAnchor.frame - frame) - Math.abs(rightAnchor.frame - frame)
|
|
243
|
-
|| Number(leftAnchor.kind !== preferredKind) - Number(rightAnchor.kind !== preferredKind)
|
|
244
|
-
|| Math.abs(left - (preferredIndex ?? left)) - Math.abs(right - (preferredIndex ?? right))
|
|
245
|
-
|| left - right;
|
|
246
|
-
})[0];
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
const semanticTarget = (
|
|
250
|
-
edit: NonNullable<typeof activeEdit>,
|
|
251
|
-
nextFrame: number,
|
|
252
|
-
): { readonly kind: "selection"; readonly startAnchorId: string; readonly endAnchorId: string }
|
|
253
|
-
| { readonly kind: "moment"; readonly anchorId: string }
|
|
254
|
-
| undefined => {
|
|
255
|
-
if (state === undefined || edit.handle.semantic === undefined) return undefined;
|
|
256
|
-
const semantic = edit.handle.semantic;
|
|
257
|
-
const anchors = state.snapshot.semantic?.anchors ?? [];
|
|
258
|
-
if (semantic.kind === "moment") {
|
|
259
|
-
const currentIndex = anchors.findIndex((anchor) => anchor.id === semantic.anchorId);
|
|
260
|
-
const anchorIndex = nearestAnchorIndex(
|
|
261
|
-
nextFrame,
|
|
262
|
-
anchors.map((_, index) => index),
|
|
263
|
-
currentIndex < 0 ? undefined : currentIndex,
|
|
264
|
-
);
|
|
265
|
-
return anchorIndex === undefined ? undefined : { kind: "moment", anchorId: anchors[anchorIndex]!.id };
|
|
266
|
-
}
|
|
267
|
-
const startIndex = anchors.findIndex((anchor) => anchor.id === semantic.startAnchorId);
|
|
268
|
-
const endIndex = anchors.findIndex((anchor) => anchor.id === semantic.endAnchorId);
|
|
269
|
-
if (startIndex < 0 || endIndex < 0 || startIndex >= endIndex) return undefined;
|
|
270
|
-
let nextStart = startIndex;
|
|
271
|
-
let nextEnd = endIndex;
|
|
272
|
-
if (edit.handle.gesture === "trim-start") {
|
|
273
|
-
const allowed = anchors.flatMap((anchor, index) =>
|
|
274
|
-
index < endIndex && anchor.frame < anchors[endIndex]!.frame ? [index] : []);
|
|
275
|
-
nextStart = nearestAnchorIndex(nextFrame, allowed, startIndex) ?? startIndex;
|
|
276
|
-
} else if (edit.handle.gesture === "trim-end") {
|
|
277
|
-
const allowed = anchors.flatMap((anchor, index) =>
|
|
278
|
-
index > startIndex && anchor.frame > anchors[startIndex]!.frame ? [index] : []);
|
|
279
|
-
nextEnd = nearestAnchorIndex(nextFrame, allowed, endIndex) ?? endIndex;
|
|
280
|
-
} else if (edit.handle.gesture === "move") {
|
|
281
|
-
const instantBoundary = edit.handle.temporal?.kind === "instant"
|
|
282
|
-
&& edit.handle.temporal.authority.kind === "semantic"
|
|
283
|
-
&& edit.handle.temporal.authority.source.kind === "selection"
|
|
284
|
-
? edit.handle.temporal.authority.boundary
|
|
285
|
-
: undefined;
|
|
286
|
-
if (instantBoundary === "start") {
|
|
287
|
-
const allowed = anchors.flatMap((anchor, index) =>
|
|
288
|
-
index < endIndex && anchor.frame < anchors[endIndex]!.frame ? [index] : []);
|
|
289
|
-
nextStart = nearestAnchorIndex(nextFrame, allowed, startIndex) ?? startIndex;
|
|
290
|
-
return { kind: "selection", startAnchorId: anchors[nextStart]!.id, endAnchorId: anchors[nextEnd]!.id };
|
|
291
|
-
}
|
|
292
|
-
if (instantBoundary === "end") {
|
|
293
|
-
const allowed = anchors.flatMap((anchor, index) =>
|
|
294
|
-
index > startIndex && anchor.frame > anchors[startIndex]!.frame ? [index] : []);
|
|
295
|
-
nextEnd = nearestAnchorIndex(nextFrame, allowed, endIndex) ?? endIndex;
|
|
296
|
-
return { kind: "selection", startAnchorId: anchors[nextStart]!.id, endAnchorId: anchors[nextEnd]!.id };
|
|
297
|
-
}
|
|
298
|
-
const pointerIndex = edit.pointerAnchorIndex ?? startIndex;
|
|
299
|
-
const deltas = anchors.flatMap((_, index) => {
|
|
300
|
-
const delta = index - pointerIndex;
|
|
301
|
-
const movedStart = startIndex + delta;
|
|
302
|
-
const movedEnd = endIndex + delta;
|
|
303
|
-
return movedStart >= 0 && movedEnd < anchors.length
|
|
304
|
-
&& anchors[movedStart]!.frame < anchors[movedEnd]!.frame
|
|
305
|
-
? [delta]
|
|
306
|
-
: [];
|
|
307
|
-
});
|
|
308
|
-
const targetPointer = nearestAnchorIndex(
|
|
309
|
-
nextFrame,
|
|
310
|
-
deltas.map((delta) => pointerIndex + delta),
|
|
311
|
-
pointerIndex,
|
|
312
|
-
);
|
|
313
|
-
const delta = targetPointer === undefined ? 0 : targetPointer - pointerIndex;
|
|
314
|
-
nextStart += delta;
|
|
315
|
-
nextEnd += delta;
|
|
316
|
-
} else {
|
|
317
|
-
return undefined;
|
|
318
|
-
}
|
|
319
|
-
return {
|
|
320
|
-
kind: "selection",
|
|
321
|
-
startAnchorId: anchors[nextStart]!.id,
|
|
322
|
-
endAnchorId: anchors[nextEnd]!.id,
|
|
323
|
-
};
|
|
324
|
-
};
|
|
230
|
+
const semanticTarget = (edit: NonNullable<typeof activeEdit>, nextFrame: number) =>
|
|
231
|
+
state === undefined ? undefined : chooseSemanticGesture({
|
|
232
|
+
anchors: state.snapshot.semantic?.anchors ?? [], handle: edit.handle,
|
|
233
|
+
pointerStart: edit.startFrame, pointerNow: nextFrame, frameCount: state.snapshot.space.frameCount,
|
|
234
|
+
});
|
|
325
235
|
|
|
326
236
|
const absoluteWindowTarget = (
|
|
327
237
|
edit: NonNullable<typeof activeEdit>,
|
|
@@ -354,40 +264,8 @@ export function createTimeline(store: Store): Timeline {
|
|
|
354
264
|
): { readonly startFrame: number; readonly endFrameExclusive: number } | undefined => {
|
|
355
265
|
if (state === undefined) return undefined;
|
|
356
266
|
const target = semanticTarget(edit, nextFrame);
|
|
357
|
-
if (target
|
|
358
|
-
|
|
359
|
-
const nextStart = state.snapshot.semantic?.anchors.find((anchor) => anchor.id === target.startAnchorId)?.frame;
|
|
360
|
-
const nextEnd = state.snapshot.semantic?.anchors.find((anchor) => anchor.id === target.endAnchorId)?.frame;
|
|
361
|
-
if (current === undefined || nextStart === undefined || nextEnd === undefined) return undefined;
|
|
362
|
-
const instantBoundary = edit.handle.temporal?.kind === "instant"
|
|
363
|
-
&& edit.handle.temporal.authority.kind === "semantic"
|
|
364
|
-
&& edit.handle.temporal.authority.source.kind === "selection"
|
|
365
|
-
? edit.handle.temporal.authority.boundary
|
|
366
|
-
: undefined;
|
|
367
|
-
if (instantBoundary === "start" || instantBoundary === "end") {
|
|
368
|
-
const frame = instantBoundary === "start" ? nextStart : nextEnd;
|
|
369
|
-
return { startFrame: frame, endFrameExclusive: frame + 1 };
|
|
370
|
-
}
|
|
371
|
-
return {
|
|
372
|
-
startFrame: nextStart + edit.clip.startFrame - current.startFrame,
|
|
373
|
-
endFrameExclusive: nextEnd + edit.clip.endFrameExclusive - current.endFrameExclusive,
|
|
374
|
-
};
|
|
375
|
-
}
|
|
376
|
-
if (target?.kind === "moment" && edit.handle.semantic?.kind === "moment") {
|
|
377
|
-
const current = state.snapshot.semantic?.moments.find((moment) => moment.id === edit.handle.semantic!.id);
|
|
378
|
-
const next = state.snapshot.semantic?.anchors.find((anchor) => anchor.id === target.anchorId)?.frame;
|
|
379
|
-
if (current === undefined || next === undefined) return undefined;
|
|
380
|
-
const delta = next - current.frame;
|
|
381
|
-
if (edit.handle.gesture === "trim-start") {
|
|
382
|
-
return { startFrame: next, endFrameExclusive: edit.clip.endFrameExclusive };
|
|
383
|
-
}
|
|
384
|
-
if (edit.handle.gesture === "trim-end") {
|
|
385
|
-
return { startFrame: edit.clip.startFrame, endFrameExclusive: next };
|
|
386
|
-
}
|
|
387
|
-
return edit.handle.moveEffect === "move-start"
|
|
388
|
-
? { startFrame: edit.clip.startFrame + delta, endFrameExclusive: edit.clip.endFrameExclusive }
|
|
389
|
-
: { startFrame: edit.clip.startFrame + delta, endFrameExclusive: edit.clip.endFrameExclusive + delta };
|
|
390
|
-
}
|
|
267
|
+
if (target !== undefined) return semanticGestureSpan(state.snapshot.semantic?.anchors ?? [], edit.handle, target);
|
|
268
|
+
if (edit.handle.semantic !== undefined) return undefined;
|
|
391
269
|
return edit.handle.temporal === undefined ? undefined : absoluteWindowTarget(edit, nextFrame);
|
|
392
270
|
};
|
|
393
271
|
lanes.addEventListener("pointerdown", (event) => {
|
|
@@ -577,18 +455,15 @@ export function createTimeline(store: Store): Timeline {
|
|
|
577
455
|
detail: string,
|
|
578
456
|
height: number,
|
|
579
457
|
attached = false,
|
|
580
|
-
hasState = false,
|
|
581
458
|
): HTMLElement => {
|
|
582
459
|
const label = document.createElement("div");
|
|
583
|
-
label.className = `track-label track-${kind}
|
|
460
|
+
label.className = `track-label track-${kind}`;
|
|
584
461
|
if (attached) label.classList.add("track-label-attached");
|
|
585
462
|
label.style.height = `${height}px`;
|
|
586
463
|
label.title = `${name} · ${detail}`;
|
|
587
464
|
label.innerHTML = `${attached
|
|
588
465
|
? '<span class="track-attachment-mark" aria-hidden="true"></span>'
|
|
589
|
-
: '<span class="track-icon"></span>'}<span class="track-copy"><strong></strong></span
|
|
590
|
-
? '<span class="track-state"></span>'
|
|
591
|
-
: ""}`;
|
|
466
|
+
: '<span class="track-icon"></span>'}<span class="track-copy"><strong></strong></span>`;
|
|
592
467
|
if (!attached) setIcon(label.querySelector(".track-icon")!, iconName);
|
|
593
468
|
label.querySelector("strong")!.textContent = name;
|
|
594
469
|
return label;
|
|
@@ -604,7 +479,7 @@ export function createTimeline(store: Store): Timeline {
|
|
|
604
479
|
.sort((left, right) => (left.binding.lane.order ?? 0) - (right.binding.lane.order ?? 0));
|
|
605
480
|
|
|
606
481
|
const buildSemanticLane = (snapshot: StudioSnapshot): void => {
|
|
607
|
-
if (snapshot.semantic === undefined
|
|
482
|
+
if (snapshot.semantic === undefined) {
|
|
608
483
|
return;
|
|
609
484
|
}
|
|
610
485
|
const presentation = snapshot.semantic.presentation;
|
|
@@ -781,17 +656,6 @@ export function createTimeline(store: Store): Timeline {
|
|
|
781
656
|
attached = false,
|
|
782
657
|
): void => {
|
|
783
658
|
const tone = track.binding.tone;
|
|
784
|
-
const freeFrom: number[] = [];
|
|
785
|
-
const rowOf = new Map<string, number>();
|
|
786
|
-
for (const clip of [...track.clips].sort((a, b) => b.stackOrder - a.stackOrder || a.startFrame - b.startFrame)) {
|
|
787
|
-
let row = freeFrom.findIndex((free) => free <= clip.startFrame);
|
|
788
|
-
if (row < 0) { row = freeFrom.length; freeFrom.push(0); }
|
|
789
|
-
freeFrom[row] = clip.endFrameExclusive;
|
|
790
|
-
rowOf.set(clip.id, row);
|
|
791
|
-
}
|
|
792
|
-
const depth = Math.max(1, freeFrom.length);
|
|
793
|
-
const folded = depth > 1 && !opened.has(track.id);
|
|
794
|
-
const shownRows = folded ? 1 : depth;
|
|
795
659
|
const laneHeight = track.binding.lane.heightPx;
|
|
796
660
|
const displayedItems = track.clips.length;
|
|
797
661
|
const label = createTrackLabel(
|
|
@@ -799,44 +663,26 @@ export function createTimeline(store: Store): Timeline {
|
|
|
799
663
|
tone,
|
|
800
664
|
track.binding.icon,
|
|
801
665
|
`${displayedItems} item${displayedItems === 1 ? "" : "s"}`,
|
|
802
|
-
|
|
666
|
+
laneHeight,
|
|
803
667
|
attached,
|
|
804
|
-
depth > 1,
|
|
805
668
|
);
|
|
806
669
|
label.classList.add(`track-facet-${track.binding.facet}`);
|
|
807
|
-
if (depth > 1) {
|
|
808
|
-
const fold = document.createElement("button");
|
|
809
|
-
fold.type = "button";
|
|
810
|
-
fold.className = "track-fold";
|
|
811
|
-
fold.textContent = folded ? `+${depth - 1}` : "−";
|
|
812
|
-
fold.title = folded ? `Show ${depth} rows` : "Collapse rows";
|
|
813
|
-
fold.setAttribute("aria-expanded", String(!folded));
|
|
814
|
-
fold.addEventListener("click", (event) => {
|
|
815
|
-
event.stopPropagation();
|
|
816
|
-
if (opened.has(track.id)) opened.delete(track.id);
|
|
817
|
-
else opened.add(track.id);
|
|
818
|
-
build(snapshot);
|
|
819
|
-
paint();
|
|
820
|
-
});
|
|
821
|
-
label.querySelector(".track-state")!.append(fold);
|
|
822
|
-
}
|
|
823
670
|
labels.append(label);
|
|
824
671
|
|
|
825
672
|
const lane = document.createElement("div");
|
|
826
673
|
lane.className = `lane track-tone-${tone} track-facet-${track.binding.facet}${attached ? " lane-attached" : ""}`;
|
|
827
|
-
lane.style.height = `${
|
|
674
|
+
lane.style.height = `${laneHeight}px`;
|
|
828
675
|
lane.style.setProperty("--lane-height", `${laneHeight}px`);
|
|
829
676
|
const laneWidth = lanes.clientWidth;
|
|
830
677
|
const materialMounts: {
|
|
831
678
|
readonly target: HTMLElement;
|
|
832
679
|
readonly preview: Extract<StudioSnapshot["tracks"][number]["clips"][number]["display"]["layers"][number], { readonly kind: "preview" }>["preview"];
|
|
833
680
|
}[] = [];
|
|
834
|
-
|
|
681
|
+
// Stable order preserves Companion projection order when stack levels tie.
|
|
682
|
+
for (const clip of [...track.clips].sort((a, b) => a.stackOrder - b.stackOrder)) {
|
|
835
683
|
const from = place(clip.startFrame, snapshot.space.frameCount, zoom.window());
|
|
836
684
|
const to = place(clip.endFrameExclusive, snapshot.space.frameCount, zoom.window());
|
|
837
685
|
if (to <= 0 || from >= 1) continue;
|
|
838
|
-
const row = rowOf.get(clip.id) ?? 0;
|
|
839
|
-
if (folded && row > 0) continue;
|
|
840
686
|
const node = document.createElement("button");
|
|
841
687
|
node.type = "button";
|
|
842
688
|
node.className = `clip clip-tone-${tone} clip-facet-${track.binding.facet} clip-chrome-${clip.presentation.chrome}`;
|
|
@@ -847,7 +693,6 @@ export function createTimeline(store: Store): Timeline {
|
|
|
847
693
|
node.style.width = `max(2px, calc(${Math.max(0, to - from) * 100}% - ${itemMetrics.gapPx}px))`;
|
|
848
694
|
const visibleWidthPx = visibleItemWidth(from, to, laneWidth);
|
|
849
695
|
node.classList.toggle("clip-preview-wide", visibleWidthPx >= 92);
|
|
850
|
-
node.style.top = `${row * laneHeight}px`;
|
|
851
696
|
node.title = `${clip.display.title} · ${clip.startFrame}-${clip.endFrameExclusive}f`;
|
|
852
697
|
node.innerHTML = `<span class="clip-head"><span class="clip-name"></span><span class="clip-meta"></span></span><span class="clip-body"><span class="clip-layers" aria-hidden="true"></span><span class="clip-phases"></span></span><span class="clip-selection" aria-hidden="true"></span>`;
|
|
853
698
|
const layers = node.querySelector<HTMLElement>(".clip-layers")!;
|
|
@@ -894,17 +739,10 @@ export function createTimeline(store: Store): Timeline {
|
|
|
894
739
|
const pointerFrame = handle.coordinate === "semantic-anchor"
|
|
895
740
|
? rawFrameAt(event.clientX)
|
|
896
741
|
: frameAt(event.clientX);
|
|
897
|
-
const pointerAnchorIndex = handle.semantic !== undefined
|
|
898
|
-
? nearestAnchorIndex(
|
|
899
|
-
pointerFrame,
|
|
900
|
-
state?.snapshot.semantic?.anchors.map((_, index) => index) ?? [],
|
|
901
|
-
)
|
|
902
|
-
: undefined;
|
|
903
742
|
activeEdit = {
|
|
904
743
|
clip,
|
|
905
744
|
handle,
|
|
906
745
|
startFrame: pointerFrame,
|
|
907
|
-
...(pointerAnchorIndex === undefined ? {} : { pointerAnchorIndex }),
|
|
908
746
|
pointerId: event.pointerId,
|
|
909
747
|
node,
|
|
910
748
|
originalLeft: node.style.left,
|
|
@@ -940,8 +778,7 @@ export function createTimeline(store: Store): Timeline {
|
|
|
940
778
|
const attachedTo = track.binding.lane.attachedTo;
|
|
941
779
|
if (attachedTo !== undefined) continue;
|
|
942
780
|
buildTrack(snapshot, track, nextClipNodes);
|
|
943
|
-
const slot = track.binding.lane.groupId;
|
|
944
|
-
if (slot === undefined) continue;
|
|
781
|
+
const slot = track.binding.lane.groupId ?? track.binding.groupId;
|
|
945
782
|
const attachments = attachedTracks(snapshot, slot, track.binding.groupId);
|
|
946
783
|
for (const attachment of attachments) {
|
|
947
784
|
buildTrack(snapshot, attachment, nextClipNodes, true);
|
package/packages/studio/start.ts
CHANGED
|
@@ -11,9 +11,12 @@ Open a Run in the browser to inspect its composition, Sources and Results.
|
|
|
11
11
|
hypit studio --run <build.svrun> [--runtime <hypit.runtime.json>]
|
|
12
12
|
[--port <number>] [--workspace <directory>] [--package-root <directory>]
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
Relative command-line paths start at the current directory; --workspace selects
|
|
15
|
+
the project, without rebasing those paths. Otherwise the nearest package.json
|
|
16
|
+
above the current directory defines the project (or the current directory if none).
|
|
17
|
+
Without --runtime, Studio uses that project's .hypit/runtime selection.
|
|
18
|
+
The server prints its project, Run, Runtime selection and browser URL.
|
|
19
|
+
Press Ctrl+C to stop it.
|
|
17
20
|
`);
|
|
18
21
|
}
|
|
19
22
|
|
|
@@ -76,6 +79,15 @@ export async function runStudio(argv: readonly string[], io: Pick<CliIo, "write"
|
|
|
76
79
|
const runtimePath = runtimeArgument === undefined
|
|
77
80
|
? selectedRuntime?.profile
|
|
78
81
|
: resolve(invokedFrom, runtimeArgument);
|
|
82
|
+
console.info([
|
|
83
|
+
` Project ${workspaceRoot}`,
|
|
84
|
+
` Run ${runPath}`,
|
|
85
|
+
` Runtime Profile ${runtimePath ?? "not selected"}`,
|
|
86
|
+
` Runtime selection ${runtimeArgument !== undefined
|
|
87
|
+
? "command argument (this session only)" : selectedRuntime?.selectionFile ?? "none"}`,
|
|
88
|
+
...(packageRoot === workspaceRoot ? [] : [` Package root ${packageRoot}`]),
|
|
89
|
+
"",
|
|
90
|
+
].join("\n"));
|
|
79
91
|
const port = Number(values.get("port") ?? "5179");
|
|
80
92
|
if (!Number.isSafeInteger(port) || port <= 0) invalidArguments("--port must be a positive integer");
|
|
81
93
|
|
|
@@ -48,6 +48,22 @@ nominal output Type and authored input names of the component. A Companion does
|
|
|
48
48
|
author attribute. Omitting `project` uses generic terminal entities; `poster` adds the declared
|
|
49
49
|
Surface preview when one exists, without changing the component's rendered video.
|
|
50
50
|
|
|
51
|
+
Each lane occupies one timeline row. Entities overlap in ascending `stackOrder`,
|
|
52
|
+
with later projected entities above earlier ones when orders tie. Studio keeps
|
|
53
|
+
overlapping entities mounted and raises the selected entity within its lane;
|
|
54
|
+
selection does not change the rendered composition or write stacking back to Source.
|
|
55
|
+
Expose `presentId` or `renderIds` for visual entities so picture selection can
|
|
56
|
+
address the same entity shown in the timeline. Attached lanes represent distinct
|
|
57
|
+
Companion projections, not extra rows allocated to avoid temporal overlap.
|
|
58
|
+
|
|
59
|
+
An entity's timeline interval describes the operation being edited; its rendered
|
|
60
|
+
parts may remain visible afterward. Associate every relevant phase through
|
|
61
|
+
`renderIds` (for example, an entrance and the settled object). Studio picks the
|
|
62
|
+
currently visible part in rendered stacking order and selects the same entity,
|
|
63
|
+
without extending its editable interval. Give the parent the board or background
|
|
64
|
+
parts and its children their own parts when they should be independently selectable.
|
|
65
|
+
The component decides this granularity. A composite can remain one selectable entity.
|
|
66
|
+
|
|
51
67
|
Merge the facet into the package's existing activation. In this example `authorContribution`
|
|
52
68
|
exports its existing modules, deterministic component and Markup facets:
|
|
53
69
|
|
|
@@ -168,6 +184,11 @@ silently becoming one policy. Studio resolves the declarations against the
|
|
|
168
184
|
current Source closure, publishes only real writable fields, renders all DOM
|
|
169
185
|
and CSS itself, and commits changes through `parameter.adjust`.
|
|
170
186
|
|
|
187
|
+
Fields can declare `number` display scaling, supported suffixes, limits and step; `unit` alone is
|
|
188
|
+
only a label. Select options can carry separate scalar values, labels, descriptions and color/font
|
|
189
|
+
preview hints. Text may be multiline; colors may offer package-chosen swatches. See
|
|
190
|
+
[Inspector presentation and conversion](../studio/INSPECTOR.md) for examples and ownership.
|
|
191
|
+
|
|
171
192
|
Structured controls keep a local draft and commit one complete canonical value.
|
|
172
193
|
Their codec is the author language (`@hypit/svs` for Recipe values), not a
|
|
173
194
|
Companion callback. Companions cannot inject DOM, CSS, parsing code or filesystem
|
|
@@ -122,6 +122,24 @@ export const studioParameterControls = [
|
|
|
122
122
|
export type StudioParameterControl = typeof studioParameterControls[number];
|
|
123
123
|
export type StudioParameterLanguage = "svml" | "svs" | "svrun";
|
|
124
124
|
|
|
125
|
+
/** Data-only choices. Packages supply meaning; Studio owns all option rendering. */
|
|
126
|
+
export type StudioParameterOption = string | {
|
|
127
|
+
readonly value: string | number | boolean;
|
|
128
|
+
readonly label: string;
|
|
129
|
+
readonly description?: string;
|
|
130
|
+
readonly preview?: { readonly kind: "color"; readonly color: string }
|
|
131
|
+
| { readonly kind: "font"; readonly family: string; readonly sample?: string };
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/** Displayed number = authored number × scale. Suffixes are retained, never converted. */
|
|
135
|
+
export type StudioNumberPresentation = {
|
|
136
|
+
readonly scale?: number;
|
|
137
|
+
readonly suffixes?: readonly string[];
|
|
138
|
+
readonly minimum?: number;
|
|
139
|
+
readonly maximum?: number;
|
|
140
|
+
readonly step?: number;
|
|
141
|
+
};
|
|
142
|
+
|
|
125
143
|
/** A real author endpoint. Its existence never implies Inspector visibility. */
|
|
126
144
|
export type StudioSourceBinding = {
|
|
127
145
|
readonly id: string;
|
|
@@ -170,8 +188,12 @@ export type StudioInspectorFieldDeclaration = {
|
|
|
170
188
|
readonly summary?: string;
|
|
171
189
|
/** Omit when the binding's public schema selects the finite Studio control. */
|
|
172
190
|
readonly control?: StudioParameterControl;
|
|
173
|
-
readonly options?: readonly
|
|
191
|
+
readonly options?: readonly StudioParameterOption[];
|
|
174
192
|
readonly unit?: string;
|
|
193
|
+
readonly number?: StudioNumberPresentation;
|
|
194
|
+
readonly multiline?: boolean;
|
|
195
|
+
/** Optional suggested colors, independent of the accepted color value. */
|
|
196
|
+
readonly swatches?: readonly string[];
|
|
175
197
|
};
|
|
176
198
|
|
|
177
199
|
/** Resolved Inspector DTO. Studio renders it and executes its exact source write. */
|
|
@@ -308,6 +330,7 @@ export type StudioEntityDisplay = {
|
|
|
308
330
|
};
|
|
309
331
|
|
|
310
332
|
export type StudioLaneDescription = {
|
|
333
|
+
/** Height of this single timeline lane; overlapping entities share it. */
|
|
311
334
|
readonly heightPx: number;
|
|
312
335
|
readonly groupId?: string;
|
|
313
336
|
readonly attachedTo?: string;
|
|
@@ -482,10 +505,12 @@ export type StudioEntityDraft = {
|
|
|
482
505
|
readonly display: StudioEntityDisplay;
|
|
483
506
|
readonly startFrame: number;
|
|
484
507
|
readonly endFrameExclusive: number;
|
|
508
|
+
/** Back-to-front timeline order. Ties preserve projection order. Selection raises only the timeline item. */
|
|
485
509
|
readonly stackOrder: number;
|
|
486
510
|
readonly elementRange?: Range;
|
|
487
511
|
readonly markerId?: string;
|
|
488
512
|
readonly presentId?: string;
|
|
513
|
+
/** Rendered parts belonging to this entity, including phases beyond its editable timeline interval. */
|
|
489
514
|
readonly renderIds?: readonly string[];
|
|
490
515
|
/** Resolved author references that differ per derived entity, such as one Cue's actual Style. */
|
|
491
516
|
readonly parameterReferences?: Readonly<Record<string, string>>;
|