@alpha2-sh/editor 0.0.0 → 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.
Files changed (98) hide show
  1. package/README.md +107 -1
  2. package/dist/browser/bridge.d.ts +44 -0
  3. package/dist/browser/bridge.js +2176 -0
  4. package/dist/browser/extensions-worker-entry.js +3 -0
  5. package/dist/browser/extensions-worker.d.ts +46 -0
  6. package/dist/browser/extensions-worker.js +149 -0
  7. package/dist/browser/hash-worker-entry.d.ts +1 -0
  8. package/dist/browser/hash-worker-entry.js +3 -0
  9. package/dist/browser/hash-worker.d.ts +24 -0
  10. package/dist/browser/hash-worker.js +87 -0
  11. package/dist/browser/index.d.ts +11 -0
  12. package/dist/browser/index.js +9 -0
  13. package/dist/browser/jobs.d.ts +82 -0
  14. package/dist/browser/jobs.js +291 -0
  15. package/dist/browser/resources.d.ts +40 -0
  16. package/dist/browser/resources.js +83 -0
  17. package/dist/browser/runtime/assets.d.ts +233 -0
  18. package/dist/browser/runtime/assets.js +633 -0
  19. package/dist/browser/runtime/audio.d.ts +60 -0
  20. package/dist/browser/runtime/audio.js +549 -0
  21. package/dist/browser/runtime/budget.d.ts +11 -0
  22. package/dist/browser/runtime/budget.js +26 -0
  23. package/dist/browser/runtime/caption-fonts.d.ts +34 -0
  24. package/dist/browser/runtime/caption-fonts.js +69 -0
  25. package/dist/browser/runtime/color.d.ts +7 -0
  26. package/dist/browser/runtime/color.js +12 -0
  27. package/dist/browser/runtime/engine.d.ts +91 -0
  28. package/dist/browser/runtime/engine.js +352 -0
  29. package/dist/browser/runtime/export-audio.d.ts +23 -0
  30. package/dist/browser/runtime/export-audio.js +75 -0
  31. package/dist/browser/runtime/exporter.d.ts +140 -0
  32. package/dist/browser/runtime/exporter.js +751 -0
  33. package/dist/browser/runtime/extension-import.d.ts +196 -0
  34. package/dist/browser/runtime/extension-import.js +521 -0
  35. package/dist/browser/runtime/fingerprint.d.ts +57 -0
  36. package/dist/browser/runtime/fingerprint.js +157 -0
  37. package/dist/browser/runtime/generators.d.ts +188 -0
  38. package/dist/browser/runtime/generators.js +173 -0
  39. package/dist/browser/runtime/locks.d.ts +32 -0
  40. package/dist/browser/runtime/locks.js +86 -0
  41. package/dist/browser/runtime/output.d.ts +87 -0
  42. package/dist/browser/runtime/output.js +230 -0
  43. package/dist/browser/runtime/preview-utils.d.ts +3 -0
  44. package/dist/browser/runtime/preview-utils.js +49 -0
  45. package/dist/browser/runtime/proxies.d.ts +72 -0
  46. package/dist/browser/runtime/proxies.js +369 -0
  47. package/dist/browser/runtime/proxy-storage.d.ts +59 -0
  48. package/dist/browser/runtime/proxy-storage.js +119 -0
  49. package/dist/browser/runtime/sources.d.ts +82 -0
  50. package/dist/browser/runtime/sources.js +819 -0
  51. package/dist/browser/runtime/staging.d.ts +10 -0
  52. package/dist/browser/runtime/staging.js +32 -0
  53. package/dist/browser/runtime/still.d.ts +21 -0
  54. package/dist/browser/runtime/still.js +52 -0
  55. package/dist/browser/runtime/wasm.d.ts +9 -0
  56. package/dist/browser/runtime/wasm.js +20 -0
  57. package/dist/browser/types.d.ts +549 -0
  58. package/dist/browser/types.js +1 -0
  59. package/dist/browser/worker-entry.d.ts +1 -0
  60. package/dist/browser/worker-entry.js +2 -0
  61. package/dist/browser/worker.d.ts +24 -0
  62. package/dist/browser/worker.js +239 -0
  63. package/dist/core/captions.d.ts +34 -0
  64. package/dist/core/captions.js +108 -0
  65. package/dist/core/clip-time.d.ts +31 -0
  66. package/dist/core/clip-time.js +125 -0
  67. package/dist/core/commands.d.ts +413 -0
  68. package/dist/core/commands.js +1118 -0
  69. package/dist/core/compositions.d.ts +234 -0
  70. package/dist/core/compositions.js +851 -0
  71. package/dist/core/document.d.ts +245 -0
  72. package/dist/core/document.js +683 -0
  73. package/dist/core/keyframes.d.ts +82 -0
  74. package/dist/core/keyframes.js +153 -0
  75. package/dist/core/presets.d.ts +64 -0
  76. package/dist/core/presets.js +241 -0
  77. package/dist/core/session.d.ts +150 -0
  78. package/dist/core/session.js +541 -0
  79. package/dist/core/text.d.ts +17 -0
  80. package/dist/core/text.js +17 -0
  81. package/dist/core/time.d.ts +19 -0
  82. package/dist/core/time.js +39 -0
  83. package/dist/core/timeline-index.d.ts +44 -0
  84. package/dist/core/timeline-index.js +59 -0
  85. package/dist/core/transitions.d.ts +20 -0
  86. package/dist/core/transitions.js +118 -0
  87. package/dist/core/validated.d.ts +23 -0
  88. package/dist/core/validated.js +52 -0
  89. package/dist/index.d.ts +12 -0
  90. package/dist/index.js +11 -0
  91. package/dist/native-client/index.d.ts +131 -0
  92. package/dist/native-client/index.js +354 -0
  93. package/dist/ports/index.d.ts +146 -0
  94. package/dist/ports/index.js +1 -0
  95. package/dist/react/index.d.ts +37 -0
  96. package/dist/react/index.js +94 -0
  97. package/package.json +69 -8
  98. /package/{index.js → dist/browser/extensions-worker-entry.d.ts} +0 -0
@@ -0,0 +1,82 @@
1
+ import type { Animated } from "alpha2/project";
2
+ type Keyframes<T> = Extract<Animated<T>, {
3
+ mode: "keyframes";
4
+ }>;
5
+ type Keyframe<T> = Keyframes<T>["keyframes"][number];
6
+ /** How a keyframe eases to the next one. */
7
+ export type Interpolation = Keyframe<unknown>["interpolation"];
8
+ /** Common easing curves, as CSS names them. */
9
+ export declare const EASINGS: {
10
+ readonly ease_in: {
11
+ readonly type: "cubic_bezier";
12
+ readonly x1: 0.42;
13
+ readonly y1: 0;
14
+ readonly x2: 1;
15
+ readonly y2: 1;
16
+ };
17
+ readonly ease_out: {
18
+ readonly type: "cubic_bezier";
19
+ readonly x1: 0;
20
+ readonly y1: 0;
21
+ readonly x2: 0.58;
22
+ readonly y2: 1;
23
+ };
24
+ readonly ease_in_out: {
25
+ readonly type: "cubic_bezier";
26
+ readonly x1: 0.42;
27
+ readonly y1: 0;
28
+ readonly x2: 0.58;
29
+ readonly y2: 1;
30
+ };
31
+ };
32
+ /**
33
+ * The keyframe at `from` moved to `to`, kept strictly between its neighbours (a tick from each), so
34
+ * the order of keyframes never changes. Returns the value and where the keyframe landed.
35
+ *
36
+ * Throws when `value` is not keyframed or has no keyframe at `from`. The keyframe-editing
37
+ * functions below throw in the same cases.
38
+ */
39
+ export declare function moveKeyframe<T>(value: Animated<T>, from: number, to: number): {
40
+ readonly value: Animated<T>;
41
+ readonly time: number;
42
+ };
43
+ /** The keyframe at `time` removed; the last one removed leaves its value, constant. */
44
+ export declare function removeKeyframe<T>(value: Animated<T>, time: number): Animated<T>;
45
+ /** How the keyframe at `time` eases to the next. */
46
+ export declare function setInterpolation<T>(value: Animated<T>, time: number, interpolation: Interpolation): Animated<T>;
47
+ /**
48
+ * The timing curve from the keyframe at `time` to the next, as its two Bezier handles: `x` in
49
+ * `0..=1` of the way in time, `y` any fraction of the way in value (beyond `0..=1` overshoots).
50
+ */
51
+ export declare function setHandles<T>(value: Animated<T>, time: number, handles: {
52
+ x1: number;
53
+ y1: number;
54
+ x2: number;
55
+ y2: number;
56
+ }): Animated<T>;
57
+ /** The keyframe at `time` given `next` as its value. */
58
+ export declare function setKeyframeValue<T>(value: Animated<T>, time: number, next: T): Animated<T>;
59
+ /**
60
+ * The handles a timing curve has in a curve editor: a Bezier's own, and for a linear segment the
61
+ * straight line's (a third and two thirds of the way). A held segment has none.
62
+ */
63
+ export declare function handlesOf(interpolation: Interpolation): {
64
+ x1: number;
65
+ y1: number;
66
+ x2: number;
67
+ y2: number;
68
+ } | null;
69
+ /**
70
+ * A CSS `cubic-bezier(x1, y1, x2, y2)` curve's output at linear `progress` (clamped to `0..=1`),
71
+ * bit for bit as the engine evaluates it (`CubicBezier::apply` in alpha2-model): the handles as the
72
+ * 32-bit floats a document holds, Newton from `t = progress` while it stays on the curve, then
73
+ * bisection, which converges where the curve is flat (both `x` handles near one end). A display
74
+ * that shows a keyframed value takes it from here, so it shows what the render draws.
75
+ */
76
+ export declare function cubicBezierAt(curve: {
77
+ x1: number;
78
+ y1: number;
79
+ x2: number;
80
+ y2: number;
81
+ }, progress: number): number;
82
+ export {};
@@ -0,0 +1,153 @@
1
+ /** Common easing curves, as CSS names them. */
2
+ export const EASINGS = {
3
+ ease_in: { type: "cubic_bezier", x1: 0.42, y1: 0, x2: 1, y2: 1 },
4
+ ease_out: { type: "cubic_bezier", x1: 0, y1: 0, x2: 0.58, y2: 1 },
5
+ ease_in_out: { type: "cubic_bezier", x1: 0.42, y1: 0, x2: 0.58, y2: 1 },
6
+ };
7
+ function keyframesOf(value) {
8
+ if (value.mode !== "keyframes")
9
+ throw new Error("the value has no keyframes");
10
+ return [...value.keyframes];
11
+ }
12
+ function indexAt(keys, time) {
13
+ const index = keys.findIndex((key) => key.time === time);
14
+ if (index < 0)
15
+ throw new Error(`no keyframe at ${time}`);
16
+ return index;
17
+ }
18
+ /**
19
+ * The keyframe at `from` moved to `to`, kept strictly between its neighbours (a tick from each), so
20
+ * the order of keyframes never changes. Returns the value and where the keyframe landed.
21
+ *
22
+ * Throws when `value` is not keyframed or has no keyframe at `from`. The keyframe-editing
23
+ * functions below throw in the same cases.
24
+ */
25
+ export function moveKeyframe(value, from, to) {
26
+ const keys = keyframesOf(value);
27
+ const index = indexAt(keys, from);
28
+ const low = index > 0 ? keys[index - 1].time + 1 : -Infinity;
29
+ const high = index + 1 < keys.length ? keys[index + 1].time - 1 : Infinity;
30
+ const time = Math.round(Math.min(high, Math.max(low, to)));
31
+ keys[index] = { ...keys[index], time };
32
+ return { value: { mode: "keyframes", keyframes: keys }, time };
33
+ }
34
+ /** The keyframe at `time` removed; the last one removed leaves its value, constant. */
35
+ export function removeKeyframe(value, time) {
36
+ const keys = keyframesOf(value);
37
+ const [removed] = keys.splice(indexAt(keys, time), 1);
38
+ return keys.length
39
+ ? { mode: "keyframes", keyframes: keys }
40
+ : { mode: "constant", value: removed.value };
41
+ }
42
+ /** How the keyframe at `time` eases to the next. */
43
+ export function setInterpolation(value, time, interpolation) {
44
+ const keys = keyframesOf(value);
45
+ const index = indexAt(keys, time);
46
+ keys[index] = { ...keys[index], interpolation };
47
+ return { mode: "keyframes", keyframes: keys };
48
+ }
49
+ /**
50
+ * The timing curve from the keyframe at `time` to the next, as its two Bezier handles: `x` in
51
+ * `0..=1` of the way in time, `y` any fraction of the way in value (beyond `0..=1` overshoots).
52
+ */
53
+ export function setHandles(value, time, handles) {
54
+ const clampX = (x) => Math.min(1, Math.max(0, x));
55
+ return setInterpolation(value, time, {
56
+ type: "cubic_bezier",
57
+ x1: clampX(handles.x1),
58
+ y1: handles.y1,
59
+ x2: clampX(handles.x2),
60
+ y2: handles.y2,
61
+ });
62
+ }
63
+ /** The keyframe at `time` given `next` as its value. */
64
+ export function setKeyframeValue(value, time, next) {
65
+ const keys = keyframesOf(value);
66
+ const index = indexAt(keys, time);
67
+ keys[index] = { ...keys[index], value: next };
68
+ return { mode: "keyframes", keyframes: keys };
69
+ }
70
+ /**
71
+ * The handles a timing curve has in a curve editor: a Bezier's own, and for a linear segment the
72
+ * straight line's (a third and two thirds of the way). A held segment has none.
73
+ */
74
+ export function handlesOf(interpolation) {
75
+ if (interpolation.type === "cubic_bezier")
76
+ return {
77
+ x1: interpolation.x1,
78
+ y1: interpolation.y1,
79
+ x2: interpolation.x2,
80
+ y2: interpolation.y2,
81
+ };
82
+ if (interpolation.type === "linear")
83
+ return { x1: 1 / 3, y1: 1 / 3, x2: 2 / 3, y2: 2 / 3 };
84
+ return null;
85
+ }
86
+ /** Newton steps tried before bisection takes over. */
87
+ const NEWTON_STEPS = 8;
88
+ /** Bisection halvings; 2^-64 is far below the tolerance. */
89
+ const BISECTION_STEPS = 64;
90
+ /** Accepted distance between the curve's `x` and the requested progress. */
91
+ const TOLERANCE = 1e-12;
92
+ /** A slope this flat would send a Newton step far outside the curve. */
93
+ const MIN_SLOPE = 1e-9;
94
+ /**
95
+ * A CSS `cubic-bezier(x1, y1, x2, y2)` curve's output at linear `progress` (clamped to `0..=1`),
96
+ * bit for bit as the engine evaluates it (`CubicBezier::apply` in alpha2-model): the handles as the
97
+ * 32-bit floats a document holds, Newton from `t = progress` while it stays on the curve, then
98
+ * bisection, which converges where the curve is flat (both `x` handles near one end). A display
99
+ * that shows a keyframed value takes it from here, so it shows what the render draws.
100
+ */
101
+ export function cubicBezierAt(curve, progress) {
102
+ const x = Math.min(1, Math.max(0, progress));
103
+ if (x <= 0)
104
+ return 0;
105
+ if (x >= 1)
106
+ return 1;
107
+ const x1 = Math.fround(curve.x1);
108
+ const x2 = Math.fround(curve.x2);
109
+ const parameter = newton(x, x1, x2) ?? bisection(x, x1, x2);
110
+ return component(parameter, Math.fround(curve.y1), Math.fround(curve.y2));
111
+ }
112
+ function newton(x, x1, x2) {
113
+ let parameter = x;
114
+ for (let step = 0; step < NEWTON_STEPS; step += 1) {
115
+ const error = component(parameter, x1, x2) - x;
116
+ if (Math.abs(error) < TOLERANCE)
117
+ return parameter >= 0 && parameter <= 1 ? parameter : undefined;
118
+ const slope = slopeAt(parameter, x1, x2);
119
+ if (Math.abs(slope) < MIN_SLOPE)
120
+ return undefined;
121
+ parameter -= error / slope;
122
+ }
123
+ return undefined;
124
+ }
125
+ function bisection(x, x1, x2) {
126
+ let low = 0;
127
+ let high = 1;
128
+ let parameter = x;
129
+ for (let step = 0; step < BISECTION_STEPS; step += 1) {
130
+ const value = component(parameter, x1, x2);
131
+ if (Math.abs(value - x) < TOLERANCE)
132
+ break;
133
+ if (value < x)
134
+ low = parameter;
135
+ else
136
+ high = parameter;
137
+ parameter = low + (high - low) * 0.5;
138
+ }
139
+ return parameter;
140
+ }
141
+ /** One coordinate of the curve at parameter `t`, in the engine's Horner form. */
142
+ function component(t, p1, p2) {
143
+ const c = 3 * p1;
144
+ const b = 3 * (p2 - p1) - c;
145
+ const a = 1 - c - b;
146
+ return ((a * t + b) * t + c) * t;
147
+ }
148
+ function slopeAt(t, p1, p2) {
149
+ const c = 3 * p1;
150
+ const b = 3 * (p2 - p1) - c;
151
+ const a = 1 - c - b;
152
+ return (3 * a * t + 2 * b) * t + c;
153
+ }
@@ -0,0 +1,64 @@
1
+ import type { Composition, EffectResource, MediaSource, Project } from "alpha2/project";
2
+ import type { Command } from "./commands.js";
3
+ import type { EditorDocument } from "./document.js";
4
+ /**
5
+ * A reusable group: the composition a group node draws, the compositions it uses in turn, and what
6
+ * it needs from the project it is applied to. Saved outside any project, as JSON.
7
+ */
8
+ export interface GraphPreset {
9
+ readonly format: "alpha2-graph-preset";
10
+ readonly version: 1;
11
+ readonly name: string;
12
+ /** The group's composition first, then those its group nodes and composition sources use. */
13
+ readonly compositions: readonly Composition[];
14
+ /** Media, effect resources, and effects its nodes read. */
15
+ readonly requires: {
16
+ readonly sources: readonly MediaSource[];
17
+ readonly resources: readonly EffectResource[];
18
+ readonly effects: readonly {
19
+ readonly id: string;
20
+ readonly version: number;
21
+ }[];
22
+ };
23
+ }
24
+ /** What a preset needs that a project lacks. */
25
+ export interface PresetShortfall {
26
+ /** Sources its media nodes read that the project has not got, by id. */
27
+ readonly sources: readonly string[];
28
+ /** Effect resources (LUTs) its effects use that the project has not got, by id. */
29
+ readonly resources: readonly string[];
30
+ /** Effects the engine does not run, as `id@version`. */
31
+ readonly effects: readonly string[];
32
+ }
33
+ /**
34
+ * The preset of composition `id`, as a group node would draw it: it, every composition it uses, and
35
+ * the sources, resources, and effects they read.
36
+ */
37
+ export declare function graphPreset(project: Project, id: string, name: string): GraphPreset;
38
+ /**
39
+ * What `preset` needs that `document`'s project lacks: file-backed sources and effect resources by
40
+ * id (a composition source it brings along is not missing), and effects `runs` says the engine does
41
+ * not have.
42
+ */
43
+ export declare function presetShortfall(document: EditorDocument, preset: GraphPreset, runs: (id: string, version: number) => boolean): PresetShortfall;
44
+ /**
45
+ * The command that places `preset` in composition `compositionId` as one undoable edit: its
46
+ * compositions added under free ids, and an unconnected group node `groupId` drawing the first, at
47
+ * `position` in the node editor.
48
+ * Returns `null` when the preset has no composition or `compositionId` does not exist. The command
49
+ * may still be refused when executed.
50
+ */
51
+ export declare function placeGraphPreset(document: EditorDocument, preset: GraphPreset, compositionId: string, groupId: string, position?: {
52
+ readonly x: number;
53
+ readonly y: number;
54
+ }): Command | null;
55
+ /**
56
+ * The command that applies `preset` to clip `clipId` as one undoable edit: its compositions added
57
+ * under free ids, and a group node drawing the first placed after the clip composition's output
58
+ * (reading it, when the group has an input) and made the output. A clip without a composition gets
59
+ * one. Returns `null` for a preset with more than one input, which has no single place to go, and
60
+ * for an unknown clip.
61
+ */
62
+ export declare function applyGraphPreset(document: EditorDocument, preset: GraphPreset, clipId: string, ids: {
63
+ readonly groupId: string;
64
+ }): Command | null;
@@ -0,0 +1,241 @@
1
+ import { findComposition, freeCompositionId } from "./compositions.js";
2
+ /**
3
+ * The preset of composition `id`, as a group node would draw it: it, every composition it uses, and
4
+ * the sources, resources, and effects they read.
5
+ */
6
+ export function graphPreset(project, id, name) {
7
+ const compositions = [];
8
+ const sources = new Map();
9
+ const resources = new Map();
10
+ const effects = new Map();
11
+ const pending = [id];
12
+ while (pending.length) {
13
+ const current = findComposition(project, pending.shift());
14
+ if (!current || compositions.some((c) => c.id === current.id))
15
+ continue;
16
+ compositions.push(current);
17
+ for (const node of current.nodes ?? []) {
18
+ const op = node.op;
19
+ if (op.type === "group")
20
+ pending.push(op.composition);
21
+ if (op.type === "media") {
22
+ const source = project.sources.find((s) => s.id === op.source_id);
23
+ if (source?.composition)
24
+ pending.push(source.composition);
25
+ else if (source)
26
+ sources.set(source.id, source);
27
+ }
28
+ if (op.type === "effect" || op.type === "transition") {
29
+ effects.set(`${op.effect.id}@${op.effect.version}`, {
30
+ id: op.effect.id,
31
+ version: op.effect.version,
32
+ });
33
+ for (const resource of Object.values(op.effect.resources ?? {})) {
34
+ const found = (project.effect_resources ?? []).find((r) => r.id === resource);
35
+ if (found)
36
+ resources.set(found.id, found);
37
+ }
38
+ }
39
+ }
40
+ }
41
+ // Composition sources the preset's compositions read travel with it.
42
+ for (const composition of compositions)
43
+ for (const node of composition.nodes ?? [])
44
+ if (node.op.type === "media") {
45
+ const source = project.sources.find((s) => s.id === node.op.source_id);
46
+ if (source?.composition)
47
+ sources.set(source.id, source);
48
+ }
49
+ return {
50
+ format: "alpha2-graph-preset",
51
+ version: 1,
52
+ name,
53
+ compositions,
54
+ requires: {
55
+ sources: [...sources.values()],
56
+ resources: [...resources.values()],
57
+ effects: [...effects.values()],
58
+ },
59
+ };
60
+ }
61
+ /**
62
+ * What `preset` needs that `document`'s project lacks: file-backed sources and effect resources by
63
+ * id (a composition source it brings along is not missing), and effects `runs` says the engine does
64
+ * not have.
65
+ */
66
+ export function presetShortfall(document, preset, runs) {
67
+ const project = document.project;
68
+ return {
69
+ sources: preset.requires.sources
70
+ .filter((source) => source.kind !== "composition" &&
71
+ !project.sources.some((s) => s.id === source.id))
72
+ .map((source) => source.id),
73
+ resources: preset.requires.resources
74
+ .filter((resource) => !(project.effect_resources ?? []).some((r) => r.id === resource.id))
75
+ .map((resource) => resource.id),
76
+ effects: preset.requires.effects
77
+ .filter((effect) => !runs(effect.id, effect.version))
78
+ .map((effect) => `${effect.id}@${effect.version}`),
79
+ };
80
+ }
81
+ /**
82
+ * Commands that add `preset`'s compositions (and the composition sources they read) to `document`
83
+ * under free ids, and the id its group composition got.
84
+ */
85
+ function presetAdditions(document, preset) {
86
+ const project = document.project;
87
+ let taken = project;
88
+ const renamed = new Map();
89
+ for (const composition of preset.compositions) {
90
+ const id = freeCompositionId(taken, composition.id);
91
+ renamed.set(composition.id, id);
92
+ taken = {
93
+ ...taken,
94
+ compositions: [...(taken.compositions ?? []), { id, nodes: [] }],
95
+ };
96
+ }
97
+ // An id no existing source, bin asset or earlier-added source has: two of the preset's may share
98
+ // a base id (`card`, and `card~2` from an earlier rename).
99
+ const sourceIds = new Map();
100
+ const takenSources = new Set([
101
+ ...project.sources.map((s) => s.id),
102
+ ...Object.keys(document.assets),
103
+ ]);
104
+ for (const source of preset.requires.sources) {
105
+ if (source.kind !== "composition" || !source.composition)
106
+ continue;
107
+ let id = source.id;
108
+ for (let n = 2; takenSources.has(id); n += 1)
109
+ id = `${source.id}~${n}`;
110
+ takenSources.add(id);
111
+ sourceIds.set(source.id, id);
112
+ }
113
+ const rename = (node) => {
114
+ const op = node.op;
115
+ if (op.type === "group")
116
+ return {
117
+ ...node,
118
+ op: {
119
+ ...op,
120
+ composition: renamed.get(op.composition) ?? op.composition,
121
+ },
122
+ };
123
+ if (op.type === "media" && sourceIds.has(op.source_id))
124
+ return {
125
+ ...node,
126
+ op: { ...op, source_id: sourceIds.get(op.source_id) },
127
+ };
128
+ return node;
129
+ };
130
+ const commands = preset.compositions.map((composition) => ({
131
+ type: "composition.add",
132
+ composition: {
133
+ ...structuredClone(composition),
134
+ id: renamed.get(composition.id),
135
+ nodes: (composition.nodes ?? []).map(rename),
136
+ },
137
+ }));
138
+ for (const source of preset.requires.sources) {
139
+ const id = sourceIds.get(source.id);
140
+ if (!id)
141
+ continue;
142
+ commands.push({
143
+ type: "asset.put",
144
+ asset: { id, kind: "composition", name: preset.name, contentRevision: 0 },
145
+ source: {
146
+ ...source,
147
+ id,
148
+ composition: renamed.get(source.composition) ?? source.composition,
149
+ },
150
+ });
151
+ }
152
+ const root = renamed.get(preset.compositions[0].id);
153
+ commands.push({
154
+ type: "metadata.update",
155
+ patch: {
156
+ graphNames: {
157
+ ...document.metadata.graphNames,
158
+ [root]: preset.name,
159
+ },
160
+ },
161
+ });
162
+ return { commands, root, taken };
163
+ }
164
+ /**
165
+ * The command that places `preset` in composition `compositionId` as one undoable edit: its
166
+ * compositions added under free ids, and an unconnected group node `groupId` drawing the first, at
167
+ * `position` in the node editor.
168
+ * Returns `null` when the preset has no composition or `compositionId` does not exist. The command
169
+ * may still be refused when executed.
170
+ */
171
+ export function placeGraphPreset(document, preset, compositionId, groupId, position) {
172
+ const root = preset.compositions[0];
173
+ if (!root || !findComposition(document.project, compositionId))
174
+ return null;
175
+ const added = presetAdditions(document, preset);
176
+ return {
177
+ type: "transaction",
178
+ commands: [
179
+ ...added.commands,
180
+ {
181
+ type: "graph.node.add",
182
+ compositionId,
183
+ node: {
184
+ id: groupId,
185
+ op: {
186
+ type: "group",
187
+ composition: added.root,
188
+ inputs: (root.inputs ?? []).map(() => null),
189
+ },
190
+ },
191
+ ...(position ? { position } : {}),
192
+ },
193
+ ],
194
+ };
195
+ }
196
+ /**
197
+ * The command that applies `preset` to clip `clipId` as one undoable edit: its compositions added
198
+ * under free ids, and a group node drawing the first placed after the clip composition's output
199
+ * (reading it, when the group has an input) and made the output. A clip without a composition gets
200
+ * one. Returns `null` for a preset with more than one input, which has no single place to go, and
201
+ * for an unknown clip.
202
+ */
203
+ export function applyGraphPreset(document, preset, clipId, ids) {
204
+ const root = preset.compositions[0];
205
+ if (!root || (root.inputs ?? []).length > 1)
206
+ return null;
207
+ const project = document.project;
208
+ const clip = project.timeline.tracks
209
+ .flatMap((track) => track.clips ?? [])
210
+ .find((c) => c.id === clipId);
211
+ if (!clip)
212
+ return null;
213
+ const added = presetAdditions(document, preset);
214
+ const commands = [...added.commands];
215
+ const target = clip.composition?.id ?? freeCompositionId(added.taken, `${clipId}-fx`);
216
+ if (!clip.composition)
217
+ commands.push({ type: "graph.create", clipId, compositionId: target });
218
+ const existing = clip.composition
219
+ ? findComposition(project, clip.composition.id)
220
+ : undefined;
221
+ // A new composition's output is its clip node.
222
+ const output = existing ? (existing.output ?? null) : "clip";
223
+ commands.push({
224
+ type: "graph.node.add",
225
+ compositionId: target,
226
+ node: {
227
+ id: ids.groupId,
228
+ op: {
229
+ type: "group",
230
+ composition: added.root,
231
+ inputs: (root.inputs ?? []).length === 1 ? [output] : [],
232
+ },
233
+ },
234
+ });
235
+ commands.push({
236
+ type: "graph.output",
237
+ compositionId: target,
238
+ nodeId: ids.groupId,
239
+ });
240
+ return { type: "transaction", commands };
241
+ }
@@ -0,0 +1,150 @@
1
+ import type { EditorDocument, Result } from "./document.js";
2
+ import type { Command } from "./commands.js";
3
+ /** An immutable view of an {@link EditorSession}; a new object after every change. */
4
+ export interface EditorSnapshot {
5
+ /** The committed document. */
6
+ readonly document: EditorDocument;
7
+ /**
8
+ * Views and preview use the active gesture's valid candidate, or `document`. History, export and
9
+ * `canExecute` use only the committed `document`.
10
+ */
11
+ readonly draft: EditorDocument;
12
+ /** Selected clip ids; ids of clips the document no longer has are dropped. */
13
+ readonly selection: readonly string[];
14
+ /** Increases with every change of the committed document (commit, undo, redo, open). */
15
+ readonly revision: number;
16
+ /** Increases when the document is replaced wholesale ({@link EditorSession.open}) or disposed. */
17
+ readonly generation: number;
18
+ readonly canUndo: boolean;
19
+ readonly canRedo: boolean;
20
+ }
21
+ /**
22
+ * Holds an editor document with undo history, selection, clipboard and pointer gestures. Every
23
+ * document it holds is frozen; edits produce new documents through {@link Command}s.
24
+ *
25
+ * Methods other than `cancelGesture` and `dispose` throw an `EditorError` (`disposed`) after
26
+ * {@link EditorSession.dispose}.
27
+ */
28
+ export declare class EditorSession {
29
+ #private;
30
+ readonly historyLimit: number;
31
+ readonly historyByteLimit: number;
32
+ /**
33
+ * @param document The initial document, copied; an empty document by default. Throws an
34
+ * `EditorError` when it does not validate.
35
+ * @param historyLimit The most undo steps kept. Defaults to 120.
36
+ * @param historyByteLimit The most bytes the undo history keeps beyond the document: each step
37
+ * counts the serialized size of what it does not share with the document after it, by
38
+ * object identity, and the oldest steps are dropped first. Defaults to 64 MiB.
39
+ * @throws RangeError when a limit is not a non-negative safe integer.
40
+ */
41
+ constructor(document?: EditorDocument, historyLimit?: number, historyByteLimit?: number);
42
+ get document(): EditorDocument;
43
+ get draft(): EditorDocument;
44
+ get revision(): number;
45
+ get generation(): number;
46
+ get disposed(): boolean;
47
+ /**
48
+ * Every document the session can still show or return to: the undo history, the document, the
49
+ * active gesture's draft and the redo history. A host keeps what these refer to and may release
50
+ * the rest.
51
+ */
52
+ reachableDocuments(): readonly EditorDocument[];
53
+ /**
54
+ * The current snapshot; the same object until the next change, as `useSyncExternalStore` needs.
55
+ */
56
+ getSnapshot: () => EditorSnapshot;
57
+ /** Calls `listener` after every change. Returns the unsubscribe function. */
58
+ subscribe: (listener: () => void) => (() => void);
59
+ /**
60
+ * Calls `listener` with `selector`'s result whenever it changes by `equal` (`Object.is` by
61
+ * default). Returns the unsubscribe function.
62
+ */
63
+ subscribeSelector<T>(selector: (snapshot: EditorSnapshot) => T, listener: (value: T) => void, equal?: (a: T, b: T) => boolean): () => void;
64
+ /**
65
+ * The document `command` would produce from the committed one, or why it is refused. Changes
66
+ * nothing.
67
+ */
68
+ canExecute(command: Command): Result<EditorDocument>;
69
+ /**
70
+ * Runs `command` on the committed document and commits the result as one undo step, cancelling an
71
+ * active gesture first. A refusal is returned, not thrown, and leaves the session unchanged; so
72
+ * does a command that changes nothing (its result is the committed document itself), which keeps
73
+ * the redo history.
74
+ */
75
+ dispatch(command: Command): Result<EditorDocument>;
76
+ /**
77
+ * Replaces the document, clearing history, selection and clipboard and increasing `generation`.
78
+ * Throws an `EditorError` when `document` does not validate, leaving the session unchanged.
79
+ */
80
+ open(document: EditorDocument): void;
81
+ /** Steps back one committed change. Returns `false` when there is none. */
82
+ undo(): boolean;
83
+ /** Reapplies the last undone change. Returns `false` when there is none. */
84
+ redo(): boolean;
85
+ /**
86
+ * Forgets the undo and redo history and keeps everything else: the document (so `revision` and
87
+ * `generation` stay), the selection, the clipboard and an active gesture. For a host that made
88
+ * changes the user should not step back through, such as relinking media it restored, without
89
+ * {@link open}'s new generation, which tells views and the bridge the document was replaced.
90
+ */
91
+ clearHistory(): void;
92
+ /**
93
+ * Selects the clips `ids` names; unknown ids and duplicates are dropped. The selection it would
94
+ * set already holds (the same ids in the same order), it changes nothing and tells no one: a
95
+ * marquee calls it on every pointer move.
96
+ */
97
+ select(ids: readonly string[]): void;
98
+ /** The committed document's tracks and clips by id. */
99
+ query(): import("./document.js").EntityIndex;
100
+ /**
101
+ * Copies the clips `ids` names (the selection by default) to the session's clipboard, with their
102
+ * names and the compositions they draw. Unknown ids are skipped.
103
+ */
104
+ copy(ids?: readonly string[]): void;
105
+ /**
106
+ * Inserts the clipboard's clips on their original tracks, under their names, as one undo step,
107
+ * the earliest starting at tick `at` and the others keeping their offsets from it. New clip ids
108
+ * come from `idFactory`. With nothing copied it changes nothing and returns the document.
109
+ */
110
+ paste(at: number, idFactory?: () => string): Result<EditorDocument>;
111
+ /**
112
+ * Cancels the active gesture, if any, discarding its draft: nothing it previewed is committed.
113
+ * Returns whether a gesture was active. A host calls it before it tears down the view a gesture
114
+ * belongs to.
115
+ */
116
+ cancelGesture(): boolean;
117
+ /**
118
+ * Starts a pointer gesture, cancelling an active one. Each `update` runs a command on the
119
+ * document the gesture started from and, when valid, shows the result as the snapshot's `draft`
120
+ * without touching history. `commit` validates the last candidate fully and commits it as one
121
+ * undo step, returning whether anything was committed; `cancel` discards it. A gesture ends on
122
+ * commit, cancel, a dispatch, undo, redo, open or dispose, or the next `beginGesture`; `update`
123
+ * then returns a `stale_gesture` error.
124
+ */
125
+ beginGesture(): {
126
+ readonly candidate: EditorDocument;
127
+ update: (command: Command) => Result<EditorDocument>;
128
+ cancel: () => void;
129
+ commit: () => boolean;
130
+ };
131
+ /**
132
+ * Disposes `resource` when the session is disposed. Returns a function that releases it from the
133
+ * session without disposing it.
134
+ */
135
+ own(resource: {
136
+ dispose(): void | Promise<void>;
137
+ }): () => void;
138
+ /**
139
+ * Ends the session: cancels a gesture, drops listeners, history and clipboard, and disposes every
140
+ * owned resource. Idempotent; every call returns the same promise, which rejects with an
141
+ * `AggregateError` when any resource's disposal failed.
142
+ */
143
+ dispose(): Promise<void>;
144
+ }
145
+ /** Creates an {@link EditorSession}; see its constructor for the options and their defaults. */
146
+ export declare function createEditor(options?: {
147
+ document?: EditorDocument;
148
+ historyLimit?: number;
149
+ historyByteLimit?: number;
150
+ }): EditorSession;