codeboard-studio 0.2.1

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 (208) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +23 -0
  3. package/README.md +54 -0
  4. package/dist/src/animation/camera-plane.d.ts +8 -0
  5. package/dist/src/animation/camera-plane.d.ts.map +1 -0
  6. package/dist/src/animation/camera-plane.js +16 -0
  7. package/dist/src/animation/camera-plane.js.map +1 -0
  8. package/dist/src/animation/drawing-neighbors.d.ts +4 -0
  9. package/dist/src/animation/drawing-neighbors.d.ts.map +1 -0
  10. package/dist/src/animation/drawing-neighbors.js +38 -0
  11. package/dist/src/animation/drawing-neighbors.js.map +1 -0
  12. package/dist/src/animation/evaluate.d.ts +17 -0
  13. package/dist/src/animation/evaluate.d.ts.map +1 -0
  14. package/dist/src/animation/evaluate.js +70 -0
  15. package/dist/src/animation/evaluate.js.map +1 -0
  16. package/dist/src/animation/frame.d.ts +8 -0
  17. package/dist/src/animation/frame.d.ts.map +1 -0
  18. package/dist/src/animation/frame.js +16 -0
  19. package/dist/src/animation/frame.js.map +1 -0
  20. package/dist/src/animation/ik.d.ts +26 -0
  21. package/dist/src/animation/ik.d.ts.map +1 -0
  22. package/dist/src/animation/ik.js +47 -0
  23. package/dist/src/animation/ik.js.map +1 -0
  24. package/dist/src/animation/retime.d.ts +4 -0
  25. package/dist/src/animation/retime.d.ts.map +1 -0
  26. package/dist/src/animation/retime.js +116 -0
  27. package/dist/src/animation/retime.js.map +1 -0
  28. package/dist/src/audio/wav.d.ts +11 -0
  29. package/dist/src/audio/wav.d.ts.map +1 -0
  30. package/dist/src/audio/wav.js +58 -0
  31. package/dist/src/audio/wav.js.map +1 -0
  32. package/dist/src/authoring-runner.d.ts +2 -0
  33. package/dist/src/authoring-runner.d.ts.map +1 -0
  34. package/dist/src/authoring-runner.js +24 -0
  35. package/dist/src/authoring-runner.js.map +1 -0
  36. package/dist/src/cli.d.ts +3 -0
  37. package/dist/src/cli.d.ts.map +1 -0
  38. package/dist/src/cli.js +127 -0
  39. package/dist/src/cli.js.map +1 -0
  40. package/dist/src/core/coordinates.d.ts +16 -0
  41. package/dist/src/core/coordinates.d.ts.map +1 -0
  42. package/dist/src/core/coordinates.js +48 -0
  43. package/dist/src/core/coordinates.js.map +1 -0
  44. package/dist/src/core/handles.d.ts +93 -0
  45. package/dist/src/core/handles.d.ts.map +1 -0
  46. package/dist/src/core/handles.js +190 -0
  47. package/dist/src/core/handles.js.map +1 -0
  48. package/dist/src/core/history.d.ts +14 -0
  49. package/dist/src/core/history.d.ts.map +1 -0
  50. package/dist/src/core/history.js +80 -0
  51. package/dist/src/core/history.js.map +1 -0
  52. package/dist/src/core/inspection.d.ts +51 -0
  53. package/dist/src/core/inspection.d.ts.map +1 -0
  54. package/dist/src/core/inspection.js +72 -0
  55. package/dist/src/core/inspection.js.map +1 -0
  56. package/dist/src/core/production.d.ts +208 -0
  57. package/dist/src/core/production.d.ts.map +1 -0
  58. package/dist/src/core/production.js +894 -0
  59. package/dist/src/core/production.js.map +1 -0
  60. package/dist/src/core/project.d.ts +146 -0
  61. package/dist/src/core/project.d.ts.map +1 -0
  62. package/dist/src/core/project.js +700 -0
  63. package/dist/src/core/project.js.map +1 -0
  64. package/dist/src/drawing/brush-authoring.d.ts +6 -0
  65. package/dist/src/drawing/brush-authoring.d.ts.map +1 -0
  66. package/dist/src/drawing/brush-authoring.js +12 -0
  67. package/dist/src/drawing/brush-authoring.js.map +1 -0
  68. package/dist/src/drawing/brushes.d.ts +157 -0
  69. package/dist/src/drawing/brushes.d.ts.map +1 -0
  70. package/dist/src/drawing/brushes.js +50 -0
  71. package/dist/src/drawing/brushes.js.map +1 -0
  72. package/dist/src/drawing/color.d.ts +5 -0
  73. package/dist/src/drawing/color.d.ts.map +1 -0
  74. package/dist/src/drawing/color.js +63 -0
  75. package/dist/src/drawing/color.js.map +1 -0
  76. package/dist/src/drawing/curves.d.ts +27 -0
  77. package/dist/src/drawing/curves.d.ts.map +1 -0
  78. package/dist/src/drawing/curves.js +170 -0
  79. package/dist/src/drawing/curves.js.map +1 -0
  80. package/dist/src/drawing/math.d.ts +10 -0
  81. package/dist/src/drawing/math.d.ts.map +1 -0
  82. package/dist/src/drawing/math.js +40 -0
  83. package/dist/src/drawing/math.js.map +1 -0
  84. package/dist/src/drawing/path-geometry.d.ts +18 -0
  85. package/dist/src/drawing/path-geometry.d.ts.map +1 -0
  86. package/dist/src/drawing/path-geometry.js +115 -0
  87. package/dist/src/drawing/path-geometry.js.map +1 -0
  88. package/dist/src/drawing/path-sampling.d.ts +10 -0
  89. package/dist/src/drawing/path-sampling.d.ts.map +1 -0
  90. package/dist/src/drawing/path-sampling.js +64 -0
  91. package/dist/src/drawing/path-sampling.js.map +1 -0
  92. package/dist/src/drawing/path.d.ts +4 -0
  93. package/dist/src/drawing/path.d.ts.map +1 -0
  94. package/dist/src/drawing/path.js +27 -0
  95. package/dist/src/drawing/path.js.map +1 -0
  96. package/dist/src/drawing/pen-input.d.ts +5 -0
  97. package/dist/src/drawing/pen-input.d.ts.map +1 -0
  98. package/dist/src/drawing/pen-input.js +30 -0
  99. package/dist/src/drawing/pen-input.js.map +1 -0
  100. package/dist/src/drawing/pixel-selection.d.ts +23 -0
  101. package/dist/src/drawing/pixel-selection.d.ts.map +1 -0
  102. package/dist/src/drawing/pixel-selection.js +142 -0
  103. package/dist/src/drawing/pixel-selection.js.map +1 -0
  104. package/dist/src/drawing/pixels.d.ts +22 -0
  105. package/dist/src/drawing/pixels.d.ts.map +1 -0
  106. package/dist/src/drawing/pixels.js +79 -0
  107. package/dist/src/drawing/pixels.js.map +1 -0
  108. package/dist/src/drawing/resource-binary.d.ts +23 -0
  109. package/dist/src/drawing/resource-binary.d.ts.map +1 -0
  110. package/dist/src/drawing/resource-binary.js +135 -0
  111. package/dist/src/drawing/resource-binary.js.map +1 -0
  112. package/dist/src/drawing/resources.d.ts +51 -0
  113. package/dist/src/drawing/resources.d.ts.map +1 -0
  114. package/dist/src/drawing/resources.js +239 -0
  115. package/dist/src/drawing/resources.js.map +1 -0
  116. package/dist/src/drawing/stroke-outline.d.ts +5 -0
  117. package/dist/src/drawing/stroke-outline.d.ts.map +1 -0
  118. package/dist/src/drawing/stroke-outline.js +53 -0
  119. package/dist/src/drawing/stroke-outline.js.map +1 -0
  120. package/dist/src/drawing/swatch.d.ts +6 -0
  121. package/dist/src/drawing/swatch.d.ts.map +1 -0
  122. package/dist/src/drawing/swatch.js +15 -0
  123. package/dist/src/drawing/swatch.js.map +1 -0
  124. package/dist/src/export/animatic-export.d.ts +11 -0
  125. package/dist/src/export/animatic-export.d.ts.map +1 -0
  126. package/dist/src/export/animatic-export.js +52 -0
  127. package/dist/src/export/animatic-export.js.map +1 -0
  128. package/dist/src/export/movie-export.d.ts +16 -0
  129. package/dist/src/export/movie-export.d.ts.map +1 -0
  130. package/dist/src/export/movie-export.js +108 -0
  131. package/dist/src/export/movie-export.js.map +1 -0
  132. package/dist/src/export/sheet-layout.d.ts +27 -0
  133. package/dist/src/export/sheet-layout.d.ts.map +1 -0
  134. package/dist/src/export/sheet-layout.js +72 -0
  135. package/dist/src/export/sheet-layout.js.map +1 -0
  136. package/dist/src/export/storyboard-export.d.ts +8 -0
  137. package/dist/src/export/storyboard-export.d.ts.map +1 -0
  138. package/dist/src/export/storyboard-export.js +76 -0
  139. package/dist/src/export/storyboard-export.js.map +1 -0
  140. package/dist/src/index.d.ts +36 -0
  141. package/dist/src/index.d.ts.map +1 -0
  142. package/dist/src/index.js +27 -0
  143. package/dist/src/index.js.map +1 -0
  144. package/dist/src/model/query.d.ts +6 -0
  145. package/dist/src/model/query.d.ts.map +1 -0
  146. package/dist/src/model/query.js +7 -0
  147. package/dist/src/model/query.js.map +1 -0
  148. package/dist/src/model/schema.d.ts +754 -0
  149. package/dist/src/model/schema.d.ts.map +1 -0
  150. package/dist/src/model/schema.js +205 -0
  151. package/dist/src/model/schema.js.map +1 -0
  152. package/dist/src/model/types.d.ts +537 -0
  153. package/dist/src/model/types.d.ts.map +1 -0
  154. package/dist/src/model/types.js +8 -0
  155. package/dist/src/model/types.js.map +1 -0
  156. package/dist/src/model/validate.d.ts +12 -0
  157. package/dist/src/model/validate.d.ts.map +1 -0
  158. package/dist/src/model/validate.js +183 -0
  159. package/dist/src/model/validate.js.map +1 -0
  160. package/dist/src/preview/server.d.ts +6 -0
  161. package/dist/src/preview/server.d.ts.map +1 -0
  162. package/dist/src/preview/server.js +71 -0
  163. package/dist/src/preview/server.js.map +1 -0
  164. package/dist/src/render/annotations.d.ts +4 -0
  165. package/dist/src/render/annotations.d.ts.map +1 -0
  166. package/dist/src/render/annotations.js +27 -0
  167. package/dist/src/render/annotations.js.map +1 -0
  168. package/dist/src/render/brush-engine.d.ts +14 -0
  169. package/dist/src/render/brush-engine.d.ts.map +1 -0
  170. package/dist/src/render/brush-engine.js +230 -0
  171. package/dist/src/render/brush-engine.js.map +1 -0
  172. package/dist/src/render/layer-compositor.d.ts +33 -0
  173. package/dist/src/render/layer-compositor.d.ts.map +1 -0
  174. package/dist/src/render/layer-compositor.js +296 -0
  175. package/dist/src/render/layer-compositor.js.map +1 -0
  176. package/dist/src/render/panel-renderer.d.ts +33 -0
  177. package/dist/src/render/panel-renderer.d.ts.map +1 -0
  178. package/dist/src/render/panel-renderer.js +118 -0
  179. package/dist/src/render/panel-renderer.js.map +1 -0
  180. package/dist/src/render/png.d.ts +4 -0
  181. package/dist/src/render/png.d.ts.map +1 -0
  182. package/dist/src/render/png.js +10 -0
  183. package/dist/src/render/png.js.map +1 -0
  184. package/dist/src/render/review.d.ts +42 -0
  185. package/dist/src/render/review.d.ts.map +1 -0
  186. package/dist/src/render/review.js +192 -0
  187. package/dist/src/render/review.js.map +1 -0
  188. package/dist/src/render/vector-renderer.d.ts +5 -0
  189. package/dist/src/render/vector-renderer.d.ts.map +1 -0
  190. package/dist/src/render/vector-renderer.js +98 -0
  191. package/dist/src/render/vector-renderer.js.map +1 -0
  192. package/dist/src/starter.d.ts +2 -0
  193. package/dist/src/starter.d.ts.map +1 -0
  194. package/dist/src/starter.js +23 -0
  195. package/dist/src/starter.js.map +1 -0
  196. package/dist/src/storage/codec.d.ts +20 -0
  197. package/dist/src/storage/codec.d.ts.map +1 -0
  198. package/dist/src/storage/codec.js +203 -0
  199. package/dist/src/storage/codec.js.map +1 -0
  200. package/dist/src/storage/store.d.ts +89 -0
  201. package/dist/src/storage/store.d.ts.map +1 -0
  202. package/dist/src/storage/store.js +392 -0
  203. package/dist/src/storage/store.js.map +1 -0
  204. package/dist/src/updates.d.ts +10 -0
  205. package/dist/src/updates.d.ts.map +1 -0
  206. package/dist/src/updates.js +134 -0
  207. package/dist/src/updates.js.map +1 -0
  208. package/package.json +84 -0
@@ -0,0 +1,700 @@
1
+ import { assertRenderFrame, selectFramePanels } from "../animation/frame.js";
2
+ import { drawingNeighbors } from "../animation/drawing-neighbors.js";
3
+ import { validateRigLayerChange } from "../animation/ik.js";
4
+ import { coordinateSpace } from "./coordinates.js";
5
+ import { pageBounds } from "../model/query.js";
6
+ import { resolve } from "node:path";
7
+ import { isDeepStrictEqual } from "node:util";
8
+ import { readPixelRegion } from "../drawing/pixels.js";
9
+ import { ProjectStore } from "../storage/store.js";
10
+ import { difference, applyRevision } from "./history.js";
11
+ import { identityTransform } from "../model/types.js";
12
+ import { storyboardSchema, layerChangesSchema } from "../model/schema.js";
13
+ import { LayerHandle, PanelHandle, SceneHandle, Selection, ShotHandle, SequenceHandle } from "./handles.js";
14
+ import { assertUniqueIds, validateRelationships, validateLayerDependencies, parseStoryboardDocument } from "../model/validate.js";
15
+ import { ProductionTools, editTimelineStructure, removeReviewAnchors } from "./production.js";
16
+ import { brushes as builtinBrushes } from "../drawing/brushes.js";
17
+ import { findObjects, inspectProject } from "./inspection.js";
18
+ import { retimePanel } from "../animation/retime.js";
19
+ import { assertDrawingColors } from "../drawing/color.js";
20
+ const clone = (value) => structuredClone(value);
21
+ const now = () => new Date().toISOString();
22
+ function layerBase(id, name, options) {
23
+ const transform = { ...identityTransform(), ...options.transform };
24
+ return {
25
+ id,
26
+ name,
27
+ visible: options.visible ?? true,
28
+ opacity: options.opacity ?? 1,
29
+ blendMode: options.blendMode ?? "source-over",
30
+ transform,
31
+ ...(options.pivot ? { pivot: clone(options.pivot) } : {}),
32
+ ...(options.maskLayerId ? { maskLayerId: options.maskLayerId } : {}),
33
+ clipToBelow: options.clipToBelow ?? false,
34
+ keyframes: [],
35
+ depth: options.depth ?? 1,
36
+ exposure: clone(options.exposure ?? null),
37
+ };
38
+ }
39
+ function allLayers(layers) {
40
+ return layers.flatMap((layer) => layer.kind === "group" ? [layer, ...allLayers(layer.children)] : [layer]);
41
+ }
42
+ function findDocumentLayer(document, id) {
43
+ for (const owner of [...document.panels, ...document.components]) {
44
+ const layer = allLayers(owner.layers).find(layer => layer.id === id);
45
+ if (layer)
46
+ return layer;
47
+ }
48
+ throw new Error(`Layer not found: ${id}`);
49
+ }
50
+ function findLayer(panel, layerId) {
51
+ const layer = allLayers(panel.layers).find((entry) => entry.id === layerId);
52
+ if (!layer)
53
+ throw new Error(`Layer not found: ${layerId}`);
54
+ return layer;
55
+ }
56
+ function findDrawingLayer(panel, layerId) {
57
+ const layer = findLayer(panel, layerId);
58
+ if (layer.kind === "group")
59
+ throw new Error(`Layer ${layerId} is a group, not a drawing layer`);
60
+ return layer;
61
+ }
62
+ export class StoryboardProject {
63
+ actor;
64
+ #document;
65
+ #undo = [];
66
+ #redo = [];
67
+ #transactionDepth = 0;
68
+ #transactionTargets = new Set();
69
+ #writablePanels = new Set();
70
+ #writableMetadata = false;
71
+ #writableAudio = false;
72
+ #writableShots = new Set();
73
+ #writableDocument = false;
74
+ #savedVersions = new Map();
75
+ #assetPath;
76
+ production;
77
+ constructor(document, actor = "agent:local") {
78
+ this.actor = actor;
79
+ this.#document = document;
80
+ this.production = new ProductionTools(this);
81
+ }
82
+ static create(options) {
83
+ const timestamp = now();
84
+ return new StoryboardProject({
85
+ schemaVersion: 3,
86
+ version: 0,
87
+ id: options.id ?? "project:1",
88
+ title: options.title,
89
+ ...(options.author ? { author: options.author } : {}),
90
+ createdAt: timestamp,
91
+ updatedAt: timestamp,
92
+ canvas: {
93
+ width: options.width ?? 1280,
94
+ height: options.height ?? 720,
95
+ background: options.background ?? "#ffffff",
96
+ },
97
+ seed: options.seed ?? 1,
98
+ frameRate: options.frameRate ?? 24,
99
+ idCounter: 1,
100
+ scenes: [],
101
+ sequences: [{ id: "sequence:main", name: "Main sequence", sceneIds: [] }],
102
+ shots: [],
103
+ panels: [],
104
+ brushes: Object.values(builtinBrushes).map((brush) => clone(brush)),
105
+ assets: [],
106
+ audioTracks: [],
107
+ comments: [],
108
+ locks: [],
109
+ changes: [],
110
+ metadata: {},
111
+ components: [],
112
+ });
113
+ }
114
+ static fromJSON(input, options = {}) {
115
+ return new StoryboardProject(parseStoryboardDocument(input), options.actor ?? "agent:local");
116
+ }
117
+ static async open(path, options = {}) {
118
+ const store = ProjectStore.open(path);
119
+ try {
120
+ const project = new StoryboardProject(store.readDocument(), options.actor ?? "agent:local");
121
+ project.#savedVersions.set(resolve(path), project.version);
122
+ project.#assetPath = resolve(path);
123
+ return project;
124
+ }
125
+ finally {
126
+ store.close();
127
+ }
128
+ }
129
+ get id() { return this.#document.id; }
130
+ get title() { return this.#document.title; }
131
+ get canUndo() { return this.#undo.length > 0; }
132
+ get canRedo() { return this.#redo.length > 0; }
133
+ get version() { return this.#document.version; }
134
+ toJSON() {
135
+ return clone(this.#document);
136
+ }
137
+ _findObjects(query) { return findObjects(this.#document, query); }
138
+ _coordinates(targetId, options) { return coordinateSpace(this.#document, targetId, options); }
139
+ _inspectProject() { return inspectProject(this.#document); }
140
+ _readChanges(version, options) {
141
+ if (!Number.isSafeInteger(version) || version < 0)
142
+ throw new Error("Audit version must be a nonnegative safe integer");
143
+ const { limit, offset } = pageBounds(options), result = [];
144
+ let skipped = 0;
145
+ for (const change of this.#document.changes) {
146
+ if (change.version <= version)
147
+ continue;
148
+ if (skipped++ < offset)
149
+ continue;
150
+ result.push(change);
151
+ if (result.length === limit)
152
+ break;
153
+ }
154
+ return clone(result);
155
+ }
156
+ _readBrush(id) {
157
+ const brush = this.#document.brushes.find(entry => entry.id === id);
158
+ if (!brush)
159
+ throw new Error(`Brush not found: ${id}`);
160
+ return clone(brush);
161
+ }
162
+ _readLock(id) {
163
+ const lock = this.#document.locks.find(entry => entry.id === id);
164
+ if (!lock)
165
+ throw new Error(`Lock not found: ${id}`);
166
+ return clone(lock);
167
+ }
168
+ _readLayer(id) {
169
+ return clone(findDocumentLayer(this.#document, id));
170
+ }
171
+ _readLayerKeyframes(id, options) {
172
+ const { limit, offset } = pageBounds(options);
173
+ return clone([...findDocumentLayer(this.#document, id).keyframes].sort((a, b) => a.frame - b.frame).slice(offset, offset + limit));
174
+ }
175
+ _readCameraKeyframes(id, options) {
176
+ const { limit, offset } = pageBounds(options), shot = this.#document.shots.find(shot => shot.id === id);
177
+ if (!shot)
178
+ throw new Error(`Shot not found: ${id}`);
179
+ return clone([...shot.cameraKeyframes].sort((a, b) => a.frame - b.frame).slice(offset, offset + limit));
180
+ }
181
+ _readTwoBoneRig(id) {
182
+ const layer = findDocumentLayer(this.#document, id);
183
+ if (layer.kind !== "group")
184
+ throw new Error(`Rig requires a group: ${id}`);
185
+ return clone(layer.twoBoneRig ?? null);
186
+ }
187
+ _readDrawingSequence(id) {
188
+ const layer = findDocumentLayer(this.#document, id);
189
+ if (layer.kind !== "group")
190
+ throw new Error(`Drawing sequence requires a group: ${id}`);
191
+ return { keys: clone(layer.drawingSequence ?? null), drawings: layer.children.map(({ id, name, kind }) => ({ id, name, kind })) };
192
+ }
193
+ _readAudioTracks(options) {
194
+ const { offset, limit } = pageBounds(options);
195
+ return this.#document.audioTracks.slice(offset, offset + limit).map(({ clips, ...track }) => ({ ...track, clipCount: clips.length }));
196
+ }
197
+ _readAudioClips(trackId, options) {
198
+ const { offset, limit } = pageBounds(options);
199
+ if (options.frame !== undefined)
200
+ assertRenderFrame(options.frame);
201
+ const track = this.#document.audioTracks.find(track => track.id === trackId);
202
+ if (!track)
203
+ throw new Error(`Audio track not found: ${trackId}`);
204
+ let skipped = 0;
205
+ const result = [];
206
+ for (const clip of track.clips) {
207
+ if (options.assetId !== undefined && clip.assetId !== options.assetId)
208
+ continue;
209
+ if (options.frame !== undefined && (options.frame < clip.startFrame || options.frame >= clip.startFrame + clip.durationFrames))
210
+ continue;
211
+ if (skipped++ < offset)
212
+ continue;
213
+ result.push(clone(clip));
214
+ if (result.length === limit)
215
+ break;
216
+ }
217
+ return result;
218
+ }
219
+ _readAudioClip(id) {
220
+ for (const track of this.#document.audioTracks) {
221
+ const clip = track.clips.find(clip => clip.id === id);
222
+ if (clip)
223
+ return { ...clone(clip), trackId: track.id };
224
+ }
225
+ throw new Error(`Audio clip not found: ${id}`);
226
+ }
227
+ _readRenderPanels(ids) {
228
+ const panels = [...new Set(ids)].map(id => this.#getPanel(id));
229
+ const shots = new Set(panels.map(panel => panel.shotId));
230
+ return clone({ canvas: this.#document.canvas, panels, shots: this.#document.shots.filter(shot => shots.has(shot.id)) });
231
+ }
232
+ _readRenderFrame(frame) {
233
+ const { panel, incoming } = selectFramePanels(this.#document.panels, frame);
234
+ return this._readRenderPanels(incoming ? [panel.id, incoming.id] : [panel.id]);
235
+ }
236
+ _readDrawingNeighbors(id, frame, skipBlank) {
237
+ const panel = this.#document.panels.find(panel => allLayers(panel.layers).some(layer => layer.id === id));
238
+ if (!panel)
239
+ throw new Error(`Panel drawing track not found: ${id}`);
240
+ const layer = findLayer(panel, id);
241
+ if (layer.kind !== "group" || layer.drawingSequence === undefined)
242
+ throw new Error(`Layer is not a drawing sequence: ${id}`);
243
+ return drawingNeighbors(layer.drawingSequence, frame, panel.startFrame, panel.startFrame + panel.durationFrames, skipBlank);
244
+ }
245
+ _readElement(id) {
246
+ for (const owner of [...this.#document.panels, ...this.#document.components]) {
247
+ for (const layer of allLayers(owner.layers)) {
248
+ if (layer.kind === "group")
249
+ continue;
250
+ const element = layer.elements.find(entry => entry.id === id);
251
+ if (element)
252
+ return clone(element);
253
+ }
254
+ }
255
+ throw new Error(`Drawing element not found: ${id}`);
256
+ }
257
+ readAsset(id) {
258
+ if (!this.#assetPath)
259
+ throw new Error("Unsaved project assets require assetRoot; save the project to embed them first");
260
+ const store = ProjectStore.open(this.#assetPath);
261
+ try {
262
+ return store.readAsset(id, { expectedVersion: this.#savedVersions.get(this.#assetPath) });
263
+ }
264
+ finally {
265
+ store.close();
266
+ }
267
+ }
268
+ async save(path, options = {}) {
269
+ if (this.#transactionDepth > 0)
270
+ throw new Error("Cannot save during an authoring transaction; save after the transaction commits");
271
+ const store = ProjectStore.create(path);
272
+ try {
273
+ const expected = options.expectedVersion ?? this.#savedVersions.get(resolve(path));
274
+ const committedVersion = store.save(this.#document, { ...options, ...(expected === undefined ? {} : { expectedVersion: expected }), ...(this.#assetPath && this.#assetPath !== resolve(path) ? { readAsset: (id) => {
275
+ const original = ProjectStore.open(this.#assetPath);
276
+ try {
277
+ if (original.version !== this.#savedVersions.get(this.#assetPath))
278
+ throw new Error("Embedded asset source changed since open; reopen before saving a copy");
279
+ return original.readHeader().assets.some(a => a.id === id) ? original.readAsset(id, { expectedVersion: this.#savedVersions.get(this.#assetPath) }) : undefined;
280
+ }
281
+ finally {
282
+ original.close();
283
+ }
284
+ } } : {}) });
285
+ this.#document.version = committedVersion;
286
+ this.#savedVersions.set(resolve(path), this.version);
287
+ this.#assetPath = resolve(path);
288
+ }
289
+ finally {
290
+ store.close();
291
+ }
292
+ }
293
+ transaction(label, work) {
294
+ if (work.constructor.name === "AsyncFunction")
295
+ throw new Error("Transactions must be synchronous; load assets before authoring");
296
+ if (this.#transactionDepth > 0)
297
+ return work();
298
+ const before = this.#document;
299
+ this.#document = { ...before, panels: before.panels.slice(), changes: before.changes.slice() };
300
+ this.#writablePanels.clear();
301
+ this.#writableShots.clear();
302
+ this.#writableMetadata = false;
303
+ this.#writableAudio = false;
304
+ this.#writableDocument = false;
305
+ this.#transactionTargets.clear();
306
+ this.#transactionDepth += 1;
307
+ try {
308
+ const result = work();
309
+ if (result && typeof result.then === "function")
310
+ throw new Error("Transactions must not return a Promise");
311
+ this.#normalizePanelRevisions(before);
312
+ this.#validate();
313
+ this.#assertLocksUnchanged(before);
314
+ this.#undo.push(difference(before, this.#document));
315
+ if (this.#undo.length > 32)
316
+ this.#undo.shift();
317
+ this.#redo = [];
318
+ this.#document.updatedAt = now();
319
+ this.#recordChange(label, [...this.#transactionTargets, ...this.#document.panels.filter(p => !isDeepStrictEqual(p, before.panels.find(old => old.id === p.id))).map(p => p.id)]);
320
+ return result;
321
+ }
322
+ catch (error) {
323
+ this.#document = before;
324
+ throw new Error(`Transaction "${label}" failed: ${error instanceof Error ? error.message : String(error)}`);
325
+ }
326
+ finally {
327
+ this.#transactionDepth -= 1;
328
+ }
329
+ }
330
+ undo() {
331
+ if (this.#transactionDepth > 0)
332
+ throw new Error("Cannot undo during an authoring transaction; finish or cancel the transaction first");
333
+ const previous = this.#undo.at(-1);
334
+ if (!previous)
335
+ return false;
336
+ const current = this.#document;
337
+ this.#document = applyRevision(current, previous, "before");
338
+ this.#undo.pop();
339
+ this.#redo.push(previous);
340
+ this.#document.version = current.version;
341
+ this.#document.changes = current.changes.slice();
342
+ this.#document.idCounter = Math.max(current.idCounter, this.#document.idCounter);
343
+ this.#document.updatedAt = now();
344
+ this.#recordChange("undo", []);
345
+ return true;
346
+ }
347
+ redo() {
348
+ if (this.#transactionDepth > 0)
349
+ throw new Error("Cannot redo during an authoring transaction; finish or cancel the transaction first");
350
+ const next = this.#redo.at(-1);
351
+ if (!next)
352
+ return false;
353
+ const current = this.#document;
354
+ this.#document = applyRevision(current, next, "after");
355
+ this.#redo.pop();
356
+ this.#undo.push(next);
357
+ this.#document.version = current.version;
358
+ this.#document.changes = current.changes.slice();
359
+ this.#document.idCounter = Math.max(current.idCounter, this.#document.idCounter);
360
+ this.#document.updatedAt = now();
361
+ this.#recordChange("redo", []);
362
+ return true;
363
+ }
364
+ setMetadata(key, value) {
365
+ this.#mutate("set metadata", [this.id], () => { this.#document.metadata[key] = value; }, "metadata");
366
+ return this;
367
+ }
368
+ addScene(name, id) {
369
+ return this._addScene("sequence:main", name, id);
370
+ }
371
+ addSequence(name, id) {
372
+ let sequenceId = "";
373
+ this.#mutate("add sequence", [this.id], () => { sequenceId = id ?? this.#nextId("sequence"); this.#document.sequences.push({ id: sequenceId, name, sceneIds: [] }); });
374
+ return new SequenceHandle(this, sequenceId);
375
+ }
376
+ _addScene(sequenceId, name, id) {
377
+ let sceneId = "";
378
+ this.#mutate("add scene", [this.id], () => {
379
+ const sequence = this.#document.sequences.find(s => s.id === sequenceId);
380
+ if (!sequence)
381
+ throw new Error(`Sequence not found: ${sequenceId}`);
382
+ sceneId = id ?? this.#nextId("scene");
383
+ this.#document.scenes.push({ id: sceneId, name, sequenceId, shotIds: [] });
384
+ sequence.sceneIds.push(sceneId);
385
+ this.#document.scenes = this.#document.sequences.flatMap(s => s.sceneIds.map(id => this.#document.scenes.find(s => s.id === id)));
386
+ });
387
+ return new SceneHandle(this, sceneId);
388
+ }
389
+ scene(id) {
390
+ this.#getScene(id);
391
+ return new SceneHandle(this, id);
392
+ }
393
+ panel(id) {
394
+ this.#getPanel(id);
395
+ return new PanelHandle(this, id);
396
+ }
397
+ select(query) {
398
+ return new Selection(this, query.panelId, query.layerId, query.elementIds ?? []);
399
+ }
400
+ #prepareMutation(scope) {
401
+ if (this.#writableDocument)
402
+ return;
403
+ if (scope === "audio") {
404
+ if (!this.#writableAudio)
405
+ this.#document.audioTracks = clone(this.#document.audioTracks);
406
+ this.#writableAudio = true;
407
+ }
408
+ else if (scope === "metadata") {
409
+ if (!this.#writableMetadata)
410
+ this.#document.metadata = clone(this.#document.metadata);
411
+ this.#writableMetadata = true;
412
+ }
413
+ else if (scope === "document") {
414
+ const { panels, metadata, ...rest } = this.#document;
415
+ Object.assign(this.#document, clone(rest));
416
+ this.#document.panels = panels.map(panel => this.#writablePanels.has(panel.id) ? panel : clone(panel));
417
+ if (!this.#writableMetadata)
418
+ this.#document.metadata = clone(metadata);
419
+ this.#writableDocument = true;
420
+ }
421
+ else if ("shotId" in scope) {
422
+ if (!this.#writableShots.has(scope.shotId)) {
423
+ const index = this.#document.shots.findIndex(shot => shot.id === scope.shotId);
424
+ if (index < 0)
425
+ throw new Error(`Shot not found: ${scope.shotId}`);
426
+ if (!this.#writableShots.size)
427
+ this.#document.shots = this.#document.shots.slice();
428
+ this.#document.shots[index] = clone(this.#document.shots[index]);
429
+ this.#writableShots.add(scope.shotId);
430
+ }
431
+ }
432
+ else if (!this.#writablePanels.has(scope.panelId)) {
433
+ const index = this.#document.panels.findIndex(p => p.id === scope.panelId);
434
+ if (index < 0)
435
+ throw new Error(`Panel not found: ${scope.panelId}`);
436
+ this.#document.panels[index] = clone(this.#document.panels[index]);
437
+ this.#writablePanels.add(scope.panelId);
438
+ }
439
+ }
440
+ #mutate(operation, targetIds, work, scope = "document") {
441
+ this.#assertEditableTargets(targetIds);
442
+ if (this.#transactionDepth > 0) {
443
+ this.#prepareMutation(scope);
444
+ targetIds.forEach(id => this.#transactionTargets.add(id));
445
+ work();
446
+ return;
447
+ }
448
+ this.transaction(operation, () => this.#mutate(operation, targetIds, work, scope));
449
+ }
450
+ #nextId(prefix) {
451
+ const id = `${prefix}:${this.#document.idCounter}`;
452
+ this.#document.idCounter += 1;
453
+ return id;
454
+ }
455
+ #validate() {
456
+ storyboardSchema.parse(this.#document);
457
+ assertUniqueIds(this.#document);
458
+ validateRelationships(this.#document);
459
+ }
460
+ #normalizePanelRevisions(before) {
461
+ for (const panel of this.#document.panels) {
462
+ const previous = before.panels.find(p => p.id === panel.id);
463
+ if (!isDeepStrictEqual(panel, previous))
464
+ panel.revision = (previous?.revision ?? 0) + 1;
465
+ }
466
+ }
467
+ #assertLocksUnchanged(before) {
468
+ for (const lock of before.locks.filter(l => l.owner !== this.actor)) {
469
+ const target = (d) => lock.targetType === "project" ? d : lock.targetType === "panel" ? d.panels.find(p => p.id === lock.targetId) : d.panels.flatMap(p => allLayers(p.layers)).find(l => l.id === lock.targetId);
470
+ if (!isDeepStrictEqual(target(before), target(this.#document)))
471
+ throw new Error(`Locked by ${lock.owner}: ${lock.reason}`);
472
+ }
473
+ }
474
+ #recordChange(operation, targetIds) {
475
+ this.#document.version += 1;
476
+ this.#document.changes.push({
477
+ id: this.#nextId("change"), version: this.#document.version, actor: this.actor,
478
+ operation, targetIds: [...targetIds], timestamp: now(),
479
+ });
480
+ }
481
+ #getScene(id) {
482
+ const scene = this.#document.scenes.find((entry) => entry.id === id);
483
+ if (!scene)
484
+ throw new Error(`Scene not found: ${id}`);
485
+ return scene;
486
+ }
487
+ #getShot(id) {
488
+ const shot = this.#document.shots.find((entry) => entry.id === id);
489
+ if (!shot)
490
+ throw new Error(`Shot not found: ${id}`);
491
+ return shot;
492
+ }
493
+ #getPanel(id) {
494
+ const panel = this.#document.panels.find((entry) => entry.id === id);
495
+ if (!panel)
496
+ throw new Error(`Panel not found: ${id}`);
497
+ return panel;
498
+ }
499
+ _addShot(sceneId, name, id) {
500
+ let shotId = "";
501
+ this.#mutate("add shot", [sceneId], () => {
502
+ const scene = this.#getScene(sceneId);
503
+ shotId = id ?? this.#nextId("shot");
504
+ this.#document.shots.push({ id: shotId, sceneId, name, panelIds: [], cameraKeyframes: [] });
505
+ scene.shotIds.push(shotId);
506
+ });
507
+ return new ShotHandle(this, shotId);
508
+ }
509
+ _addPanel(shotId, options) {
510
+ let panelId = "";
511
+ this.#mutate("add panel", [shotId], () => editTimelineStructure(this.#document, () => {
512
+ const shot = this.#getShot(shotId);
513
+ panelId = options.id ?? this.#nextId("panel");
514
+ const startFrame = this.#document.panels.reduce((maximum, entry) => Math.max(maximum, entry.startFrame + entry.durationFrames), 0);
515
+ const panel = {
516
+ id: panelId,
517
+ shotId,
518
+ number: options.number ?? String(this.#document.panels.length + 1),
519
+ title: options.title ?? "Untitled panel",
520
+ width: options.width ?? this.#document.canvas.width,
521
+ height: options.height ?? this.#document.canvas.height,
522
+ durationFrames: options.durationFrames ?? 48,
523
+ startFrame,
524
+ transition: { type: "cut", durationFrames: 0 },
525
+ status: "working",
526
+ action: options.action ?? "",
527
+ dialogue: options.dialogue ?? "",
528
+ camera: options.camera ?? "",
529
+ notes: options.notes ?? "",
530
+ layers: [],
531
+ motion: [],
532
+ revision: 0,
533
+ };
534
+ this.#document.panels.push(panel);
535
+ shot.panelIds.push(panelId);
536
+ }));
537
+ return new PanelHandle(this, panelId);
538
+ }
539
+ _addLayer(panelId, kind, name, options, parentGroupId) {
540
+ let id = "";
541
+ this.#assertEditable(panelId);
542
+ this.#mutate("add layer", [panelId], () => {
543
+ const panel = this.#getPanel(panelId);
544
+ id = options.id ?? this.#nextId("layer");
545
+ const base = layerBase(id, name, options);
546
+ const layer = kind === "group" ? { ...base, kind, children: [] } : { ...base, kind, elements: [] };
547
+ if (parentGroupId) {
548
+ const group = findLayer(panel, parentGroupId);
549
+ if (group.kind !== "group")
550
+ throw new Error(`Parent ${parentGroupId} is not a group`);
551
+ group.children.push(layer);
552
+ }
553
+ else {
554
+ panel.layers.push(layer);
555
+ }
556
+ panel.revision += 1;
557
+ }, { panelId });
558
+ return new LayerHandle(this, panelId, id);
559
+ }
560
+ _addElement(panelId, layerId, element) {
561
+ let id = "";
562
+ this.#assertEditable(panelId, layerId);
563
+ assertDrawingColors(element);
564
+ this.#mutate("add drawing element", [panelId, layerId], () => {
565
+ const panel = this.#getPanel(panelId);
566
+ const layer = findDrawingLayer(panel, layerId);
567
+ const raster = element.kind === "raster-stroke" || element.kind === "raster-surface";
568
+ if (raster !== (layer.kind === "raster"))
569
+ throw new Error(`${element.kind} requires a ${raster ? "raster" : "vector"} layer`);
570
+ id = element.id ?? this.#nextId("element");
571
+ layer.elements.push({ ...element, id });
572
+ panel.revision += 1;
573
+ }, { panelId });
574
+ return id;
575
+ }
576
+ _addMotion(panelId, annotation) {
577
+ let id = "";
578
+ this.#assertEditable(panelId);
579
+ assertDrawingColors(annotation);
580
+ this.#mutate("add motion annotation", [panelId], () => {
581
+ const panel = this.#getPanel(panelId);
582
+ id = annotation.id ?? this.#nextId("motion");
583
+ panel.motion.push({ ...clone(annotation), id });
584
+ panel.revision += 1;
585
+ }, { panelId });
586
+ return id;
587
+ }
588
+ _updatePanel(panelId, changes) {
589
+ this.#assertEditable(panelId);
590
+ this.#mutate("update panel", [panelId], () => {
591
+ const panel = this.#getPanel(panelId);
592
+ const { durationFrames, ...metadata } = changes;
593
+ if (durationFrames !== undefined)
594
+ retimePanel(this.#document, panelId, durationFrames);
595
+ Object.assign(panel, metadata);
596
+ panel.revision += 1;
597
+ }, changes.durationFrames === undefined ? { panelId } : "document");
598
+ }
599
+ _updateLayer(panelId, layerId, changes) {
600
+ this.#assertEditable(panelId, layerId);
601
+ this.#mutate("update layer", [panelId, layerId], () => {
602
+ const panel = this.#getPanel(panelId);
603
+ const layer = findLayer(panel, layerId);
604
+ const { maskLayerId, ...parsed } = layerChangesSchema.parse(changes);
605
+ const properties = Object.fromEntries(Object.entries(parsed).filter(([, value]) => value !== undefined));
606
+ if (maskLayerId !== undefined && maskLayerId !== null) {
607
+ const dependencies = new Map(allLayers(panel.layers).map(entry => [entry.id, entry]));
608
+ dependencies.set(layerId, { ...layer, maskLayerId });
609
+ validateLayerDependencies(dependencies);
610
+ }
611
+ validateRigLayerChange(panel.layers, { ...layer, ...properties });
612
+ Object.assign(layer, properties);
613
+ if (maskLayerId === null)
614
+ delete layer.maskLayerId;
615
+ else if (maskLayerId !== undefined)
616
+ layer.maskLayerId = maskLayerId;
617
+ panel.revision += 1;
618
+ }, { panelId });
619
+ }
620
+ _updateElement(panelId, layerId, elementId, updater) {
621
+ this._updateElements(panelId, layerId, [elementId], updater);
622
+ }
623
+ _updateElements(panelId, layerId, elementIds, updater) {
624
+ this.#assertEditable(panelId, layerId);
625
+ const selected = new Set(elementIds);
626
+ if (!selected.size)
627
+ throw new Error("Element update requires at least one target");
628
+ this.#mutate("update drawing elements", [panelId, layerId, ...selected], () => {
629
+ const panel = this.#getPanel(panelId);
630
+ const layer = findDrawingLayer(panel, layerId);
631
+ const existing = new Set(layer.elements.map(e => e.id));
632
+ for (const id of selected)
633
+ if (!existing.has(id))
634
+ throw new Error(`Element not found: ${id}`);
635
+ const updated = layer.elements.map(original => {
636
+ if (!selected.has(original.id))
637
+ return original;
638
+ const result = updater(clone(original));
639
+ if (result.id !== original.id)
640
+ throw new Error("Editing an element may not change its stable id");
641
+ assertDrawingColors(result);
642
+ return clone(result);
643
+ });
644
+ layer.elements = updated;
645
+ panel.revision += 1;
646
+ }, { panelId });
647
+ }
648
+ _readPixels(panelId, layerId, elementId, region) {
649
+ const element = findDrawingLayer(this.#getPanel(panelId), layerId).elements.find(e => e.id === elementId);
650
+ if (element?.kind !== "raster-surface")
651
+ throw new Error(`Element ${elementId} is not a pixel surface`);
652
+ return readPixelRegion(element, region ?? { x: 0, y: 0, width: element.width, height: element.height });
653
+ }
654
+ _remove(panelId, layerId, elementIds = []) {
655
+ this.#assertEditable(panelId, layerId);
656
+ this.#mutate("remove drawing elements", [panelId, ...(layerId ? [layerId] : []), ...elementIds], () => {
657
+ const panel = this.#getPanel(panelId);
658
+ if (!layerId)
659
+ throw new Error("Selection.remove() requires a layerId");
660
+ const layer = findDrawingLayer(panel, layerId);
661
+ const existing = new Set(layer.elements.map(e => e.id));
662
+ for (const id of elementIds)
663
+ if (!existing.has(id))
664
+ throw new Error(`Element not found: ${id}`);
665
+ const before = layer.elements.length;
666
+ layer.elements = layer.elements.filter((element) => !elementIds.includes(element.id));
667
+ if (layer.elements.length === before)
668
+ throw new Error("Selection did not match any elements");
669
+ removeReviewAnchors(this.#document, new Set(elementIds));
670
+ panel.revision += 1;
671
+ }, { panelId });
672
+ }
673
+ _applyProduction(operation, targetIds, expectedVersion, work, scope) {
674
+ if (expectedVersion !== undefined && expectedVersion !== this.version) {
675
+ throw new Error(`Version conflict: expected ${expectedVersion}, current ${this.version}`);
676
+ }
677
+ this.#assertEditableTargets(targetIds);
678
+ let panelId;
679
+ if (scope && !("shotId" in scope) && !("audio" in scope)) {
680
+ if ("panelId" in scope)
681
+ panelId = scope.panelId;
682
+ else {
683
+ panelId = this.#document.panels.find(panel => allLayers(panel.layers).some(layer => layer.id === scope.layerId))?.id;
684
+ if (!panelId)
685
+ throw new Error(`Layer not found: ${scope.layerId}`);
686
+ }
687
+ this.#assertEditableTargets([panelId]);
688
+ }
689
+ this.#mutate(operation, targetIds, () => work(this.#document, (prefix) => this.#nextId(prefix)), panelId ? { panelId } : scope && "shotId" in scope ? scope : scope && "audio" in scope ? "audio" : "document");
690
+ }
691
+ #assertEditable(panelId, layerId) {
692
+ this.#assertEditableTargets([...(panelId ? [panelId] : []), ...(layerId ? [layerId] : [])]);
693
+ }
694
+ #assertEditableTargets(targetIds) {
695
+ const blocking = this.#document.locks.find((lock) => lock.owner !== this.actor && (lock.targetType === "project" || targetIds.includes(lock.targetId)));
696
+ if (blocking)
697
+ throw new Error(`Locked by ${blocking.owner}: ${blocking.reason}`);
698
+ }
699
+ }
700
+ //# sourceMappingURL=project.js.map