@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,6 +1,6 @@
|
|
|
1
1
|
import type { Clip, StudioFailure, StudioInspectorDomain, StudioSnapshot } from "../shared.js";
|
|
2
2
|
import type { CanonicalValue, ValueSchema } from "@hypit/protocol";
|
|
3
|
-
import { validateParameterValue } from "../parameter-values.js";
|
|
3
|
+
import { parameterAuthorValue, parameterNumber, parameterOption, validateParameterValue } from "../parameter-values.js";
|
|
4
4
|
import { createCodePane } from "./code.js";
|
|
5
5
|
import { icon } from "./icons.js";
|
|
6
6
|
import { createLibraryPane } from "./library.js";
|
|
@@ -9,6 +9,8 @@ import type { Highlight } from "./code.js";
|
|
|
9
9
|
import { intentAtOffset, spanAtOffset } from "./markers.js";
|
|
10
10
|
import { clipAtOffset, createStore } from "./selection.js";
|
|
11
11
|
import { createStage } from "./stage.js";
|
|
12
|
+
import { semanticGestureSpan } from "../temporal-edit.js";
|
|
13
|
+
import type { SemanticTarget } from "../temporal-edit.js";
|
|
12
14
|
import { applyStudioMutation } from "./writeback.js";
|
|
13
15
|
import { createTimeline } from "./timeline.js";
|
|
14
16
|
import "../style.css";
|
|
@@ -60,9 +62,9 @@ app.innerHTML = `
|
|
|
60
62
|
|
|
61
63
|
const store = createStore();
|
|
62
64
|
const code = createCodePane();
|
|
63
|
-
const
|
|
65
|
+
const stage = createStage(store, (id) => library.selectArtifact(id));
|
|
66
|
+
const library = createLibraryPane(code, (artifact) => stage.openArtifact(artifact), (artifact) => stage.renameArtifact(artifact));
|
|
64
67
|
const timeline = createTimeline(store);
|
|
65
|
-
const stage = createStage(store);
|
|
66
68
|
app.querySelector<HTMLElement>("[data-library]")!.append(library.element);
|
|
67
69
|
app.querySelector<HTMLElement>("[data-timeline]")!.append(timeline.element);
|
|
68
70
|
app.querySelector<HTMLElement>("[data-stage]")!.append(stage.element);
|
|
@@ -72,8 +74,9 @@ app.querySelector<HTMLElement>("[data-stage]")!.append(stage.element);
|
|
|
72
74
|
const shell = app.querySelector<HTMLElement>(".studio-shell")!;
|
|
73
75
|
const upperShell = app.querySelector<HTMLElement>(".upper-shell")!;
|
|
74
76
|
const workspacePanel = app.querySelector<HTMLElement>(".workspace-panel")!;
|
|
77
|
+
const sidebarMinimum = 320;
|
|
75
78
|
const sourceHandle = createHandle({
|
|
76
|
-
axis: "column", initial:
|
|
79
|
+
axis: "column", initial: 416, minimum: sidebarMinimum,
|
|
77
80
|
maximum: () => Math.max(360, upperShell.clientWidth - 720),
|
|
78
81
|
apply: (size) => { upperShell.style.setProperty("--source-width", `${size}px`); },
|
|
79
82
|
remember: "hypit-studio.v3.source-width",
|
|
@@ -81,7 +84,7 @@ const sourceHandle = createHandle({
|
|
|
81
84
|
sourceHandle.classList.add("source-handle");
|
|
82
85
|
upperShell.insertBefore(sourceHandle, app.querySelector<HTMLElement>("[data-stage]")!);
|
|
83
86
|
const workspaceHandle = createHandle({
|
|
84
|
-
axis: "column", initial: Math.round(window.innerWidth * 0.22), minimum:
|
|
87
|
+
axis: "column", initial: Math.round(window.innerWidth * 0.22), minimum: sidebarMinimum, invert: true,
|
|
85
88
|
maximum: () => Math.max(320, upperShell.clientWidth - 680),
|
|
86
89
|
apply: (size) => { upperShell.style.setProperty("--workspace-width", `${size}px`); },
|
|
87
90
|
remember: "hypit-studio.v3.workspace-width",
|
|
@@ -191,9 +194,22 @@ function textValue(value: CanonicalValue): string {
|
|
|
191
194
|
return typeof value === "string" ? value : value === null ? "" : String(value);
|
|
192
195
|
}
|
|
193
196
|
|
|
197
|
+
function restoreParameterControls(entityId: string): void {
|
|
198
|
+
const current = store.current();
|
|
199
|
+
if (current?.selection.kind === "clip" && current.selection.clipId === entityId) {
|
|
200
|
+
renderInspector(current.snapshot, entityId);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
194
204
|
function commitControl(entityId: string, parameter: Clip["inspector"][number], replacement: CanonicalValue): void {
|
|
195
|
-
|
|
196
|
-
|
|
205
|
+
try {
|
|
206
|
+
if (sameValue(parameterAuthorValue(parameter, replacement), parameter.value)) return;
|
|
207
|
+
void writeParameter(entityId, parameter, replacement);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
restoreParameterControls(entityId);
|
|
210
|
+
status.textContent = "Invalid value"; status.className = "status error";
|
|
211
|
+
status.title = error instanceof Error ? error.message : String(error);
|
|
212
|
+
}
|
|
197
213
|
}
|
|
198
214
|
|
|
199
215
|
let openColorPicker: {
|
|
@@ -227,8 +243,11 @@ function selectControl(
|
|
|
227
243
|
trigger.setAttribute("aria-expanded", "false");
|
|
228
244
|
const selected = document.createElement("span");
|
|
229
245
|
selected.className = "parameter-select-value";
|
|
230
|
-
const
|
|
231
|
-
|
|
246
|
+
const choices = (parameter.options ?? []).map(parameterOption);
|
|
247
|
+
if (choices.some(option => option.description || option.preview)) control.classList.add("rich-options");
|
|
248
|
+
const current = choices.find(option => option.value === parameter.value)
|
|
249
|
+
?? choices.find(option => String(option.value) === String(parameter.value));
|
|
250
|
+
selected.textContent = current?.label ?? textValue(parameter.value);
|
|
232
251
|
const chevron = document.createElement("span");
|
|
233
252
|
chevron.className = "parameter-select-chevron";
|
|
234
253
|
chevron.innerHTML = icon("chevron");
|
|
@@ -242,22 +261,32 @@ function selectControl(
|
|
|
242
261
|
menu.hidden = true;
|
|
243
262
|
trigger.setAttribute("aria-controls", menu.id);
|
|
244
263
|
|
|
245
|
-
const options =
|
|
264
|
+
const options = choices.map((option) => {
|
|
246
265
|
const item = document.createElement("button");
|
|
247
266
|
item.type = "button";
|
|
248
267
|
item.className = `parameter-select-option${option === current ? " active" : ""}`;
|
|
249
|
-
|
|
268
|
+
const label = document.createElement("span"); label.textContent = option.label;
|
|
269
|
+
item.append(label);
|
|
270
|
+
if (option.description) { const description = document.createElement("small"); description.textContent = option.description; item.append(description); }
|
|
271
|
+
if (option.preview?.kind === "color") {
|
|
272
|
+
const swatch = document.createElement("span"); swatch.className = "parameter-option-swatch";
|
|
273
|
+
swatch.style.backgroundColor = option.preview.color; item.prepend(swatch);
|
|
274
|
+
} else if (option.preview?.kind === "font") {
|
|
275
|
+
const sample = document.createElement("span"); sample.className = "parameter-option-font";
|
|
276
|
+
sample.style.fontFamily = option.preview.family; sample.textContent = option.preview.sample ?? option.label;
|
|
277
|
+
sample.setAttribute("aria-hidden", "true"); item.append(sample);
|
|
278
|
+
}
|
|
250
279
|
item.setAttribute("role", "option");
|
|
251
280
|
item.setAttribute("aria-selected", String(option === current));
|
|
252
281
|
item.addEventListener("click", () => {
|
|
253
|
-
selected.textContent = option;
|
|
282
|
+
selected.textContent = option.label;
|
|
254
283
|
for (const sibling of options) {
|
|
255
284
|
sibling.classList.toggle("active", sibling === item);
|
|
256
285
|
sibling.setAttribute("aria-selected", String(sibling === item));
|
|
257
286
|
}
|
|
258
287
|
close(false);
|
|
259
288
|
trigger.focus();
|
|
260
|
-
commitControl(entityId, parameter, option);
|
|
289
|
+
commitControl(entityId, parameter, option.value);
|
|
261
290
|
});
|
|
262
291
|
return item;
|
|
263
292
|
});
|
|
@@ -358,7 +387,7 @@ function colorValueControl(
|
|
|
358
387
|
value.value = initial;
|
|
359
388
|
value.setAttribute("aria-label", label);
|
|
360
389
|
value.spellcheck = false;
|
|
361
|
-
|
|
390
|
+
let alpha = exact && initial.length === 9 ? initial.slice(7) : "";
|
|
362
391
|
const replacement = (): string => `${picker.value.toUpperCase()}${alpha}`;
|
|
363
392
|
const close = (): void => {
|
|
364
393
|
if (openColorPicker?.root === colorControl) openColorPicker = undefined;
|
|
@@ -389,7 +418,12 @@ function colorValueControl(
|
|
|
389
418
|
});
|
|
390
419
|
value.addEventListener(draft ? "input" : "change", () => {
|
|
391
420
|
const next = value.value.trim();
|
|
392
|
-
|
|
421
|
+
const valid = /^#[0-9a-f]{6}(?:[0-9a-f]{2})?$/iu.test(next);
|
|
422
|
+
swatch.style.background = valid ? next : "transparent";
|
|
423
|
+
if (valid) {
|
|
424
|
+
picker.value = next.slice(0, 7);
|
|
425
|
+
alpha = next.length === 9 ? next.slice(7) : "";
|
|
426
|
+
}
|
|
393
427
|
change(next);
|
|
394
428
|
});
|
|
395
429
|
colorControl.append(swatch, picker);
|
|
@@ -611,40 +645,71 @@ function parameterControl(entityId: string, parameter: Clip["inspector"][number]
|
|
|
611
645
|
name.title = parameter.summary ?? parameter.label;
|
|
612
646
|
const right = document.createElement("span");
|
|
613
647
|
right.className = "parameter-right parameter-control";
|
|
648
|
+
let unitLabel = parameter.unit;
|
|
614
649
|
if (parameter.control === "select") {
|
|
615
650
|
right.append(selectControl(entityId, parameter));
|
|
616
651
|
} else if (parameter.control === "color") {
|
|
617
652
|
right.append(colorValueControl(parameter.label, textValue(parameter.value), (next) => {
|
|
618
653
|
commitControl(entityId, parameter, next);
|
|
619
654
|
}));
|
|
655
|
+
if (parameter.swatches?.length) {
|
|
656
|
+
const palette = document.createElement("span"); palette.className = "parameter-swatches";
|
|
657
|
+
for (const color of parameter.swatches) {
|
|
658
|
+
const swatch = document.createElement("button"); swatch.type = "button";
|
|
659
|
+
swatch.style.backgroundColor = color; swatch.title = color; swatch.setAttribute("aria-label", `Use ${color}`);
|
|
660
|
+
swatch.addEventListener("click", () => commitControl(entityId, parameter, color)); palette.append(swatch);
|
|
661
|
+
}
|
|
662
|
+
right.append(palette);
|
|
663
|
+
}
|
|
620
664
|
} else if (parameter.control === "list" || parameter.control === "record") {
|
|
621
665
|
right.append(structuredControl(entityId, parameter));
|
|
622
666
|
} else {
|
|
667
|
+
if (parameter.multiline && parameter.control === "text") {
|
|
668
|
+
const value = document.createElement("textarea"); value.className = "parameter-value parameter-multiline";
|
|
669
|
+
value.value = textValue(parameter.value); value.setAttribute("aria-label", parameter.label);
|
|
670
|
+
value.addEventListener("change", () => commitControl(entityId, parameter, value.value)); right.append(value);
|
|
671
|
+
row.append(name, right); return row;
|
|
672
|
+
}
|
|
623
673
|
const value = document.createElement("input");
|
|
624
674
|
value.type = parameter.control === "boolean" ? "checkbox" : "text";
|
|
625
|
-
if (parameter.control === "number") value.inputMode = "decimal";
|
|
626
675
|
value.className = "parameter-value";
|
|
627
676
|
value.setAttribute("aria-label", parameter.label);
|
|
628
677
|
value.spellcheck = false;
|
|
629
678
|
if (value.type === "checkbox") value.checked = parameter.value === true || parameter.value === "true";
|
|
630
|
-
else
|
|
679
|
+
else if (parameter.control === "number") {
|
|
680
|
+
try {
|
|
681
|
+
const numeric = parameterNumber(parameter.value, parameter.number);
|
|
682
|
+
value.type = "number"; value.inputMode = "decimal"; value.value = String(numeric.value);
|
|
683
|
+
const schema = parameter.schema?.kind === "number" ? parameter.schema : undefined;
|
|
684
|
+
const scale = parameter.number?.scale ?? 1;
|
|
685
|
+
const minimum = parameter.number?.minimum ?? (schema?.minimum === undefined ? undefined : schema.minimum * scale);
|
|
686
|
+
const maximum = parameter.number?.maximum ?? (schema?.maximum === undefined ? undefined : schema.maximum * scale);
|
|
687
|
+
value.step = String(parameter.number?.step ?? (schema?.integer ? scale : "any"));
|
|
688
|
+
if (minimum !== undefined) value.min = String(minimum);
|
|
689
|
+
if (maximum !== undefined) value.max = String(maximum);
|
|
690
|
+
value.required = true;
|
|
691
|
+
unitLabel = numeric.suffix || parameter.unit;
|
|
692
|
+
} catch (error) {
|
|
693
|
+
value.value = textValue(parameter.value); value.readOnly = true;
|
|
694
|
+
value.title = error instanceof Error ? error.message : String(error);
|
|
695
|
+
}
|
|
696
|
+
} else value.value = textValue(parameter.value);
|
|
631
697
|
value.dataset.parameterId = parameter.id;
|
|
632
|
-
value.title = parameter.summary ?? parameter.label;
|
|
698
|
+
if (!value.title) value.title = parameter.summary ?? parameter.label;
|
|
633
699
|
value.addEventListener("change", () => {
|
|
634
700
|
if (value.type === "checkbox") {
|
|
635
701
|
commitControl(entityId, parameter, value.checked);
|
|
636
702
|
} else if (parameter.control === "number") {
|
|
637
|
-
|
|
638
|
-
if (Number.isFinite(number)) commitControl(entityId, parameter, number);
|
|
703
|
+
if (value.reportValidity()) commitControl(entityId, parameter, value.valueAsNumber);
|
|
639
704
|
} else {
|
|
640
705
|
commitControl(entityId, parameter, value.value);
|
|
641
706
|
}
|
|
642
707
|
});
|
|
643
708
|
right.append(value);
|
|
644
709
|
}
|
|
645
|
-
if (
|
|
710
|
+
if (unitLabel !== undefined) {
|
|
646
711
|
const unit = document.createElement("small");
|
|
647
|
-
unit.textContent =
|
|
712
|
+
unit.textContent = unitLabel;
|
|
648
713
|
right.append(unit);
|
|
649
714
|
}
|
|
650
715
|
row.append(name, right);
|
|
@@ -671,6 +736,7 @@ async function writeParameter(entityId: string, parameter: Clip["inspector"][num
|
|
|
671
736
|
const state = store.current();
|
|
672
737
|
if (state === undefined) return;
|
|
673
738
|
parameterWriteState = "Saving";
|
|
739
|
+
status.title = "";
|
|
674
740
|
status.textContent = parameterWriteState;
|
|
675
741
|
status.className = "status saving";
|
|
676
742
|
try {
|
|
@@ -685,9 +751,13 @@ async function writeParameter(entityId: string, parameter: Clip["inspector"][num
|
|
|
685
751
|
status.textContent = parameterWriteState;
|
|
686
752
|
status.className = "status saved";
|
|
687
753
|
} catch (error) {
|
|
754
|
+
// Validation and stale-revision rejections do not publish a new snapshot.
|
|
755
|
+
// Restore the accepted value just as a rejected compilation does.
|
|
756
|
+
restoreParameterControls(entityId);
|
|
688
757
|
parameterWriteState = "Failed";
|
|
689
758
|
status.textContent = error instanceof Error ? "Save failed" : parameterWriteState;
|
|
690
759
|
status.className = "status error";
|
|
760
|
+
status.title = error instanceof Error ? error.message : String(error);
|
|
691
761
|
}
|
|
692
762
|
}
|
|
693
763
|
|
|
@@ -784,24 +854,76 @@ function renderSemanticInspector(snapshot: StudioSnapshot, segmentId: string): v
|
|
|
784
854
|
inspector.replaceChildren(empty);
|
|
785
855
|
}
|
|
786
856
|
|
|
857
|
+
/** Exact identity choice for coincident anchors, through the same Companion-declared handles. */
|
|
858
|
+
function semanticAnchorInspector(snapshot: StudioSnapshot, kind: "selection" | "moment", id: string): HTMLElement {
|
|
859
|
+
const semantic = snapshot.semantic;
|
|
860
|
+
const consumers = snapshot.tracks.flatMap((track) => track.clips).flatMap((clip) => clip.editHandles
|
|
861
|
+
.filter((handle) => handle.enabled && handle.semantic?.kind === kind && handle.semantic.id === id)
|
|
862
|
+
.map((handle) => ({ clip, handle })));
|
|
863
|
+
const current = kind === "selection" ? semantic?.selections.find((item) => item.id === id)
|
|
864
|
+
: semantic?.moments.find((item) => item.id === id);
|
|
865
|
+
if (!semantic || !current) return group("Timing", []);
|
|
866
|
+
const endpoints = "anchorId" in current ? [["Moment", current.anchorId]]
|
|
867
|
+
: [["Start", current.startAnchorId], ["End", current.endAnchorId]];
|
|
868
|
+
return group("Semantic anchors", endpoints.map(([label, anchorId]) => {
|
|
869
|
+
const anchor = semantic.anchors.find((item) => item.id === anchorId)!;
|
|
870
|
+
const describe = (item: typeof anchor) => {
|
|
871
|
+
const word = semantic.tokens.find((token) => token.id === item.tokenId)?.text;
|
|
872
|
+
return `${item.kind.replaceAll("-", " ")}${word ? ` · ${word}` : ""}${item.segmentId ? ` · ${item.segmentId}` : ""}`;
|
|
873
|
+
};
|
|
874
|
+
const candidates = semantic.anchors.filter((item) => item.frame === anchor.frame).flatMap((item) => {
|
|
875
|
+
const target: SemanticTarget = "anchorId" in current ? { kind: "moment", anchorId: item.id }
|
|
876
|
+
: { kind: "selection", startAnchorId: label === "Start" ? item.id : current.startAnchorId,
|
|
877
|
+
endAnchorId: label === "End" ? item.id : current.endAnchorId };
|
|
878
|
+
const owner = consumers.find(({ handle }) => semanticGestureSpan(semantic.anchors, handle, target) !== undefined);
|
|
879
|
+
return owner ? [{ item, target, ...owner }] : [];
|
|
880
|
+
});
|
|
881
|
+
if (candidates.length < 2) return property(label!, describe(anchor));
|
|
882
|
+
const row = document.createElement("label");
|
|
883
|
+
row.className = "property";
|
|
884
|
+
const name = document.createElement("span");
|
|
885
|
+
name.textContent = label!;
|
|
886
|
+
const control = document.createElement("select");
|
|
887
|
+
control.className = "parameter-value";
|
|
888
|
+
control.setAttribute("aria-label", `${label} semantic anchor`);
|
|
889
|
+
for (const { item } of candidates) {
|
|
890
|
+
const option = document.createElement("option"); option.value = item.id; option.textContent = describe(item);
|
|
891
|
+
control.append(option);
|
|
892
|
+
}
|
|
893
|
+
control.value = anchorId!;
|
|
894
|
+
control.addEventListener("change", () => {
|
|
895
|
+
const choice = candidates.find(({ item }) => item.id === control.value)!;
|
|
896
|
+
const span = semanticGestureSpan(semantic.anchors, choice.handle, choice.target)!;
|
|
897
|
+
const temporal = choice.handle.temporal!;
|
|
898
|
+
control.disabled = true;
|
|
899
|
+
status.textContent = "Saving"; status.className = "status saving";
|
|
900
|
+
void applyStudioMutation({ type: "timeline.adjust", revision: snapshot.revision,
|
|
901
|
+
entityId: choice.clip.id, gesture: choice.handle.gesture,
|
|
902
|
+
target: temporal.kind === "instant" ? { kind: "instant", frame: span.startFrame, semantic: choice.target }
|
|
903
|
+
: { kind: "window", ...span, semantic: choice.target },
|
|
904
|
+
}).then(() => { status.textContent = "Saved"; status.className = "status saved"; })
|
|
905
|
+
.catch((error: unknown) => {
|
|
906
|
+
control.value = anchorId!; status.textContent = "Save failed"; status.className = "status error";
|
|
907
|
+
status.title = error instanceof Error ? error.message : String(error);
|
|
908
|
+
}).finally(() => { control.disabled = false; });
|
|
909
|
+
});
|
|
910
|
+
row.append(name, control);
|
|
911
|
+
return row;
|
|
912
|
+
}));
|
|
913
|
+
}
|
|
914
|
+
|
|
787
915
|
function renderSemanticSelectionInspector(snapshot: StudioSnapshot, selectionId: string): void {
|
|
788
916
|
const selection = snapshot.semantic?.selections.find((item) => item.id === selectionId);
|
|
789
917
|
if (selection === undefined) { inspector.replaceChildren(); return; }
|
|
790
918
|
defaultWorkspaceHeading();
|
|
791
|
-
|
|
792
|
-
empty.className = "inspector-empty";
|
|
793
|
-
empty.textContent = "Adjust on the timeline";
|
|
794
|
-
inspector.replaceChildren(empty);
|
|
919
|
+
inspector.replaceChildren(semanticAnchorInspector(snapshot, "selection", selectionId));
|
|
795
920
|
}
|
|
796
921
|
|
|
797
922
|
function renderSemanticMomentInspector(snapshot: StudioSnapshot, momentId: string): void {
|
|
798
923
|
const moment = snapshot.semantic?.moments.find((item) => item.id === momentId);
|
|
799
924
|
if (moment === undefined) { inspector.replaceChildren(); return; }
|
|
800
925
|
defaultWorkspaceHeading();
|
|
801
|
-
|
|
802
|
-
empty.className = "inspector-empty";
|
|
803
|
-
empty.textContent = "Adjust on the timeline";
|
|
804
|
-
inspector.replaceChildren(empty);
|
|
926
|
+
inspector.replaceChildren(semanticAnchorInspector(snapshot, "moment", momentId));
|
|
805
927
|
}
|
|
806
928
|
|
|
807
929
|
// The word being spoken at the playhead, which is the point of carrying token
|
|
@@ -966,6 +1088,7 @@ window.addEventListener("keydown", (event) => {
|
|
|
966
1088
|
function applySnapshot(snapshot: StudioSnapshot): void {
|
|
967
1089
|
failureView.textContent = "";
|
|
968
1090
|
status.className = "status";
|
|
1091
|
+
status.title = "";
|
|
969
1092
|
status.textContent = "";
|
|
970
1093
|
renderMeta(snapshot);
|
|
971
1094
|
library.show(snapshot);
|
|
@@ -993,7 +1116,6 @@ const response = await fetch("/__studio/session");
|
|
|
993
1116
|
const initial = await response.json() as StudioSnapshot | StudioFailure;
|
|
994
1117
|
if (response.ok && "tracks" in initial) applySnapshot(initial);
|
|
995
1118
|
else applyFailure(initial as StudioFailure);
|
|
996
|
-
void library.refresh();
|
|
997
1119
|
|
|
998
1120
|
type Hot = { on(event: string, listener: (value: unknown) => void): void };
|
|
999
1121
|
const hot = (import.meta as ImportMeta & { hot?: Hot }).hot;
|
|
@@ -129,7 +129,7 @@ async function audioWaveform(url: string): Promise<string | undefined> {
|
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
function audioPreview(url: string): Promise<string | undefined> {
|
|
132
|
+
export function audioPreview(url: string): Promise<string | undefined> {
|
|
133
133
|
const held = generatedAudio.get(url);
|
|
134
134
|
if (held !== undefined) return held;
|
|
135
135
|
const pending = audioWaveform(url).catch(() => undefined).then((waveform) => {
|
|
@@ -14,7 +14,7 @@ export type Box = {
|
|
|
14
14
|
readonly widthPx: number; readonly heightPx: number;
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
export type Measure = (clipId: string) => Box | undefined;
|
|
17
|
+
export type Measure = (clipId: string) => (Box & { readonly stackOrder: number }) | undefined;
|
|
18
18
|
|
|
19
19
|
export type Overlay = {
|
|
20
20
|
readonly element: SVGSVGElement;
|
|
@@ -62,21 +62,24 @@ export function createOverlay(store: Store, measure: Measure): Overlay {
|
|
|
62
62
|
element.setAttribute("class", "stage-overlay");
|
|
63
63
|
element.setAttribute("preserveAspectRatio", "none");
|
|
64
64
|
|
|
65
|
-
let
|
|
65
|
+
let entities: readonly Clip[] = [];
|
|
66
66
|
let canvas = { width: 1, height: 1 };
|
|
67
|
-
let last: { snapshot: StudioSnapshot; selected: Clip | undefined
|
|
67
|
+
let last: { snapshot: StudioSnapshot; selected: Clip | undefined } | undefined;
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
70
|
* The box is measured off the picture rather than restated from the Source:
|
|
71
71
|
* motion moves an element across its span, so only what was drawn knows where
|
|
72
72
|
* it ended up. A Present that has not mounted has no box to draw.
|
|
73
73
|
*/
|
|
74
|
-
const
|
|
75
|
-
const ids = clip.
|
|
76
|
-
|
|
74
|
+
const drawnParts = (clip: Clip) => {
|
|
75
|
+
const ids = clip.renderIds.length > 0 ? clip.renderIds : clip.presentId === undefined ? [] : [clip.presentId];
|
|
76
|
+
return ids.flatMap((id) => {
|
|
77
77
|
const found = measure(id);
|
|
78
78
|
return found === undefined ? [] : [found];
|
|
79
79
|
});
|
|
80
|
+
};
|
|
81
|
+
const drawnBox = (clip: Clip): Box | undefined => {
|
|
82
|
+
const boxes = drawnParts(clip);
|
|
80
83
|
if (boxes.length === 0) return undefined;
|
|
81
84
|
const left = Math.min(...boxes.map((box) => box.xPx));
|
|
82
85
|
const top = Math.min(...boxes.map((box) => box.yPx));
|
|
@@ -87,23 +90,23 @@ export function createOverlay(store: Store, measure: Measure): Overlay {
|
|
|
87
90
|
|
|
88
91
|
const draw = (): void => {
|
|
89
92
|
if (last === undefined) return;
|
|
90
|
-
const { snapshot, selected
|
|
93
|
+
const { snapshot, selected } = last;
|
|
91
94
|
canvas = { width: snapshot.space.canvasWidth, height: snapshot.space.canvasHeight };
|
|
92
95
|
element.setAttribute("viewBox", `0 0 ${canvas.width} ${canvas.height}`);
|
|
93
96
|
element.replaceChildren();
|
|
94
|
-
//
|
|
95
|
-
|
|
97
|
+
// An editing interval can end before its visual representation disappears.
|
|
98
|
+
// The renderer decides which associated parts are visible at this frame.
|
|
99
|
+
entities = snapshot.tracks.flatMap((track) => track.clips);
|
|
96
100
|
|
|
97
101
|
if (selected === undefined) return;
|
|
98
102
|
const tones = intentTones(snapshot);
|
|
99
103
|
const tone = tones.get(selected.authoredId);
|
|
100
|
-
const onScreen = frame >= selected.startFrame && frame < selected.endFrameExclusive;
|
|
101
104
|
const box = drawnBox(selected);
|
|
102
105
|
// A Present that is not on screen at this frame was never drawn, so there
|
|
103
106
|
// is nothing to put a box around.
|
|
104
107
|
if (box === undefined) return;
|
|
105
108
|
const group = document.createElementNS(SVG_NS, "g");
|
|
106
|
-
group.setAttribute("class", `box${
|
|
109
|
+
group.setAttribute("class", `box${tone === undefined ? "" : ` tone-${tone}`}`);
|
|
107
110
|
|
|
108
111
|
group.append(rect(box, "box-content"));
|
|
109
112
|
|
|
@@ -111,16 +114,15 @@ export function createOverlay(store: Store, measure: Measure): Overlay {
|
|
|
111
114
|
label.setAttribute("x", String(box.xPx + 10));
|
|
112
115
|
label.setAttribute("y", String(Math.max(28, box.yPx - 12)));
|
|
113
116
|
label.setAttribute("class", "box-label");
|
|
114
|
-
label.textContent =
|
|
117
|
+
label.textContent = selected.display.title;
|
|
115
118
|
group.append(label);
|
|
116
119
|
element.append(group);
|
|
117
120
|
};
|
|
118
121
|
|
|
119
|
-
store.subscribe(({ snapshot, selection
|
|
122
|
+
store.subscribe(({ snapshot, selection }) => {
|
|
120
123
|
last = {
|
|
121
124
|
snapshot,
|
|
122
125
|
selected: selection.kind === "clip" ? store.clip(selection.clipId) : undefined,
|
|
123
|
-
frame: playhead.frame,
|
|
124
126
|
};
|
|
125
127
|
draw();
|
|
126
128
|
});
|
|
@@ -133,12 +135,11 @@ export function createOverlay(store: Store, measure: Measure): Overlay {
|
|
|
133
135
|
if (box.width === 0 || box.height === 0) return undefined;
|
|
134
136
|
const x = (clientX - box.left) / box.width * canvas.width;
|
|
135
137
|
const y = (clientY - box.top) / box.height * canvas.height;
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
});
|
|
138
|
+
// Hit individual parts, not the empty space inside a group's union box.
|
|
139
|
+
return entities.flatMap((clip) => drawnParts(clip)
|
|
140
|
+
.filter((part) => inside(part, x, y))
|
|
141
|
+
.map((part) => ({ clip, order: part.stackOrder })))
|
|
142
|
+
.sort((left, right) => right.order - left.order)[0]?.clip;
|
|
142
143
|
},
|
|
143
144
|
};
|
|
144
145
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { icon } from "./icons.js";
|
|
2
|
+
|
|
3
|
+
/** Shared chrome; each view owns its navigation choices, toolbar and content. */
|
|
4
|
+
export function createSidebarPanel(mark: string, navigationLabel: string) {
|
|
5
|
+
const element = document.createElement("div");
|
|
6
|
+
element.className = "sidebar-panel";
|
|
7
|
+
element.innerHTML = `
|
|
8
|
+
<div class="sidebar-panel-heading"><span class="sidebar-panel-mark" aria-hidden="true">${icon(mark)}</span></div>
|
|
9
|
+
<div class="sidebar-panel-toolbar"></div>
|
|
10
|
+
<nav class="sidebar-panel-nav"></nav>
|
|
11
|
+
<div class="sidebar-panel-content"></div>`;
|
|
12
|
+
const navigation = element.querySelector<HTMLElement>("nav")!;
|
|
13
|
+
navigation.setAttribute("aria-label", navigationLabel);
|
|
14
|
+
return {
|
|
15
|
+
element, navigation,
|
|
16
|
+
toolbar: element.querySelector<HTMLElement>(".sidebar-panel-toolbar")!,
|
|
17
|
+
content: element.querySelector<HTMLElement>(".sidebar-panel-content")!,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function sidebarItem(input: { label: string; icon: string; selected: boolean; select(): void }) {
|
|
22
|
+
const button = document.createElement("button");
|
|
23
|
+
button.type = "button";
|
|
24
|
+
button.className = `sidebar-panel-item${input.selected ? " active" : ""}`;
|
|
25
|
+
button.setAttribute("aria-label", input.label);
|
|
26
|
+
button.setAttribute("aria-pressed", String(input.selected));
|
|
27
|
+
button.title = input.label;
|
|
28
|
+
button.innerHTML = icon(input.icon);
|
|
29
|
+
button.addEventListener("click", input.select);
|
|
30
|
+
return button;
|
|
31
|
+
}
|