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,894 @@
1
+ import { solveTwoBoneIK, validateTwoBoneRig, validateRigLayerChange } from "../animation/ik.js";
2
+ import { evaluateDrawing, evaluateLayer } from "../animation/evaluate.js";
3
+ import { assertRenderFrame } from "../animation/frame.js";
4
+ import { identityTransform } from "../model/types.js";
5
+ import { retimePanel } from "../animation/retime.js";
6
+ import { audioClipSchema, audioTrackChangesSchema, audioTrackSchema, cameraKeyframeSchema, exposureSchema, layerKeyframeSchema, transitionSchema, drawingSequenceSchema, twoBoneRigSchema } from "../model/schema.js";
7
+ import { parseBrushDefinition, validateAudioFades, validateKeyframePositions, validateLayerDependencies, validateDrawingSequence } from "../model/validate.js";
8
+ /** Replace the array without mutating comment records shared with an undo snapshot. */
9
+ export function removeReviewAnchors(document, removedIds) {
10
+ document.comments = document.comments.filter(({ anchor }) => ![anchor.panelId, anchor.layerId, anchor.elementId].some(id => id !== undefined && removedIds.has(id)));
11
+ }
12
+ function assertRemovalUnlocked(document, removedIds) {
13
+ const lock = document.locks.find(entry => removedIds.has(entry.targetId));
14
+ if (lock)
15
+ throw new Error(`Cannot remove locked artwork: unlock ${lock.targetType} ${lock.targetId} first (${lock.id})`);
16
+ }
17
+ function findPanel(document, id) {
18
+ const panel = document.panels.find((entry) => entry.id === id);
19
+ if (!panel)
20
+ throw new Error(`Panel not found: ${id}`);
21
+ return panel;
22
+ }
23
+ function visitLayers(layers, work) {
24
+ for (const layer of layers) {
25
+ work(layer);
26
+ if (layer.kind === "group")
27
+ visitLayers(layer.children, work);
28
+ }
29
+ }
30
+ function findLayer(document, id) {
31
+ for (const panel of document.panels) {
32
+ let found;
33
+ visitLayers(panel.layers, (layer) => { if (layer.id === id)
34
+ found = layer; });
35
+ if (found)
36
+ return { panel, layer: found };
37
+ }
38
+ throw new Error(`Layer not found: ${id}`);
39
+ }
40
+ function orderedPanels(document, shotId) {
41
+ const shot = document.shots.find((entry) => entry.id === shotId);
42
+ if (!shot)
43
+ throw new Error(`Shot not found: ${shotId}`);
44
+ return shot.panelIds.map((id) => findPanel(document, id));
45
+ }
46
+ function reflowTimeline(document) {
47
+ const offsets = new Map();
48
+ const updates = [];
49
+ const before = document.panels.map(p => ({ id: p.id, start: p.startFrame, end: p.startFrame + p.durationFrames }));
50
+ let cursor = 0;
51
+ for (const scene of document.scenes)
52
+ for (const shotId of scene.shotIds)
53
+ for (const panel of orderedPanels(document, shotId)) {
54
+ const start = cursor, delta = start - panel.startFrame;
55
+ offsets.set(panel.id, delta);
56
+ cursor += panel.durationFrames;
57
+ updates.push(() => {
58
+ panel.startFrame = start;
59
+ if (delta) {
60
+ panel.revision++;
61
+ visitLayers(panel.layers, layer => {
62
+ for (const key of layer.keyframes)
63
+ key.frame += delta;
64
+ if (layer.exposure) {
65
+ layer.exposure.startFrame += delta;
66
+ layer.exposure.endFrame += delta;
67
+ }
68
+ if (layer.kind === "group")
69
+ for (const key of layer.drawingSequence ?? [])
70
+ key.frame += delta;
71
+ if (layer.kind !== "group")
72
+ for (const element of layer.elements) {
73
+ if (element.kind === "raster-stroke" && element.reveal) {
74
+ element.reveal.startFrame += delta;
75
+ element.reveal.endFrame += delta;
76
+ }
77
+ }
78
+ });
79
+ }
80
+ });
81
+ }
82
+ const map = (frame) => { const p = before.find(p => frame >= p.start && frame < p.end); return frame + (p ? offsets.get(p.id) ?? 0 : 0); };
83
+ for (const shot of document.shots) {
84
+ for (const key of shot.cameraKeyframes) {
85
+ const frame = map(key.frame);
86
+ updates.push(() => { key.frame = frame; });
87
+ }
88
+ updates.push(() => shot.cameraKeyframes.sort((a, b) => a.frame - b.frame));
89
+ }
90
+ for (const track of document.audioTracks)
91
+ for (const clip of track.clips) {
92
+ const frame = map(clip.startFrame);
93
+ if (frame !== clip.startFrame && track.locked)
94
+ throw new Error(`Audio track is locked: ${track.id}`);
95
+ updates.push(() => { clip.startFrame = frame; });
96
+ }
97
+ for (const comment of document.comments)
98
+ if (comment.anchor.frame !== undefined) {
99
+ const frame = map(comment.anchor.frame);
100
+ updates.push(() => { comment.anchor.frame = frame; });
101
+ }
102
+ for (const update of updates)
103
+ update();
104
+ document.panels.sort((a, b) => a.startFrame - b.startFrame);
105
+ }
106
+ /** Structural changes stage only reference lists; reflow checks locks before touching artwork timing. */
107
+ export function editTimelineStructure(document, work) {
108
+ const original = { shots: document.shots, panels: document.panels, comments: document.comments, idCounter: document.idCounter };
109
+ document.shots = original.shots.map(shot => ({ ...shot, panelIds: shot.panelIds.slice(), cameraKeyframes: shot.cameraKeyframes.slice() }));
110
+ document.panels = original.panels.slice();
111
+ document.comments = original.comments.slice();
112
+ try {
113
+ const result = work();
114
+ reflowTimeline(document);
115
+ return result;
116
+ }
117
+ catch (error) {
118
+ Object.assign(document, original);
119
+ throw error;
120
+ }
121
+ }
122
+ function cloneLayers(layers, nextId) {
123
+ const originals = [];
124
+ visitLayers(layers, layer => originals.push(layer));
125
+ const owned = new Set(originals.map(layer => layer.id));
126
+ for (const layer of originals)
127
+ if (layer.maskLayerId && !owned.has(layer.maskLayerId))
128
+ throw new Error("Copy must include the mask dependency");
129
+ const copied = structuredClone(layers), ids = new Map();
130
+ visitLayers(copied, layer => {
131
+ const id = nextId("layer");
132
+ ids.set(layer.id, id);
133
+ layer.id = id;
134
+ for (const key of layer.keyframes)
135
+ key.id = nextId("layer-key");
136
+ if (layer.kind !== "group")
137
+ for (const element of layer.elements)
138
+ element.id = nextId("element");
139
+ });
140
+ visitLayers(copied, layer => {
141
+ if (layer.maskLayerId)
142
+ layer.maskLayerId = ids.get(layer.maskLayerId);
143
+ if (layer.kind === "group" && layer.twoBoneRig)
144
+ layer.twoBoneRig.elbowId = ids.get(layer.twoBoneRig.elbowId);
145
+ if (layer.kind === "group")
146
+ for (const key of layer.drawingSequence ?? [])
147
+ if (key.drawingId !== null)
148
+ key.drawingId = ids.get(key.drawingId);
149
+ });
150
+ return copied;
151
+ }
152
+ function assertStaticComponent(layers) {
153
+ visitLayers(layers, layer => { if (layer.kind === "group" && layer.drawingSequence !== undefined)
154
+ throw new Error("Components capture static artwork; capture an individual drawing instead of a drawing sequence"); });
155
+ }
156
+ export class ProductionTools {
157
+ host;
158
+ constructor(host) {
159
+ this.host = host;
160
+ }
161
+ captureComponent(layerId, name, options = {}) {
162
+ let id = "";
163
+ this.host._applyProduction("capture drawing component", [layerId], options.expectedVersion, (d, next) => {
164
+ const source = [findLayer(d, layerId).layer];
165
+ assertStaticComponent(source);
166
+ const layers = cloneLayers(source, next);
167
+ id = options.id ?? next("component");
168
+ visitLayers(layers, l => { l.keyframes = []; l.exposure = null; delete l.componentSource; if (l.kind !== "group")
169
+ for (const e of l.elements)
170
+ if (e.kind === "raster-stroke")
171
+ delete e.reveal; });
172
+ layers[0].transform = identityTransform();
173
+ d.components.push({ id, name, version: 1, layers });
174
+ });
175
+ return id;
176
+ }
177
+ reviseComponent(id, sourceLayerId, options = {}) {
178
+ this.host._applyProduction("revise component source", [id], options.expectedVersion, (d, next) => {
179
+ const c = d.components.find(c => c.id === id);
180
+ if (!c)
181
+ throw new Error(`Component not found: ${id}`);
182
+ const source = [findLayer(d, sourceLayerId).layer];
183
+ assertStaticComponent(source);
184
+ const layers = cloneLayers(source, next);
185
+ visitLayers(layers, l => { l.keyframes = []; l.exposure = null; delete l.componentSource; if (l.kind !== "group")
186
+ for (const e of l.elements)
187
+ if (e.kind === "raster-stroke")
188
+ delete e.reveal; });
189
+ layers[0].transform = identityTransform();
190
+ c.layers = layers;
191
+ c.version++;
192
+ });
193
+ }
194
+ instantiateComponent(componentId, panelId, transform = {}, options = {}) {
195
+ let id = "";
196
+ this.host._applyProduction("instantiate component", [panelId, componentId], options.expectedVersion, (d, next) => {
197
+ const c = d.components.find(c => c.id === componentId);
198
+ if (!c)
199
+ throw new Error(`Component not found: ${componentId}`);
200
+ const layers = cloneLayers(c.layers, next);
201
+ id = options.id ?? next("layer");
202
+ findPanel(d, panelId).layers.push({ id, name: c.name, kind: "group", children: layers, visible: true, opacity: 1, blendMode: "source-over", transform: { ...identityTransform(), ...transform }, clipToBelow: false, keyframes: [], depth: 1, exposure: null, componentSource: { id: c.id, version: c.version } });
203
+ }, { panelId });
204
+ return id;
205
+ }
206
+ refreshComponentInstance(layerId, options = {}) {
207
+ if (options.comments !== undefined && !['reject', 'anchor-to-instance'].includes(options.comments))
208
+ throw new Error("Invalid component refresh comment policy");
209
+ this.host._applyProduction("explicitly refresh component instance", [layerId], options.expectedVersion, (d, next) => {
210
+ const { panel, layer } = findLayer(d, layerId);
211
+ if (layer.kind !== "group" || !layer.componentSource)
212
+ throw new Error("Layer is not a component instance");
213
+ const c = d.components.find(c => c.id === layer.componentSource.id);
214
+ if (!c)
215
+ throw new Error("Missing component source");
216
+ const removedLayers = new Set(), removedElements = new Set();
217
+ visitLayers(layer.children, child => {
218
+ removedLayers.add(child.id);
219
+ if (child.kind !== "group")
220
+ for (const element of child.elements)
221
+ removedElements.add(element.id);
222
+ });
223
+ const lock = d.locks.find(lock => removedLayers.has(lock.targetId));
224
+ if (lock)
225
+ throw new Error(`Cannot refresh locked descendant: unlock ${lock.targetId} first (${lock.id})`);
226
+ visitLayers(panel.layers, entry => {
227
+ if (removedLayers.has(entry.id))
228
+ return;
229
+ if (entry.kind === "group" && entry.twoBoneRig && removedLayers.has(entry.twoBoneRig.elbowId))
230
+ throw new Error("Unbind the two-bone rig before refreshing its elbow artwork");
231
+ if (entry.maskLayerId && removedLayers.has(entry.maskLayerId))
232
+ throw new Error(`Cannot refresh ${layerId}: layer ${entry.id} uses descendant mask ${entry.maskLayerId}`);
233
+ if (entry.kind === "group" && entry.drawingSequence?.some(key => key.drawingId !== null && removedLayers.has(key.drawingId)))
234
+ throw new Error(`Cannot refresh ${layerId}: drawing sequence ${entry.id} uses its descendants`);
235
+ });
236
+ const affected = (comment) => removedLayers.has(comment.anchor.layerId ?? "") || removedElements.has(comment.anchor.elementId ?? "");
237
+ const anchored = d.comments.filter(affected);
238
+ if (anchored.length && options.comments !== "anchor-to-instance")
239
+ throw new Error(`Refresh would replace artwork referenced by comments: ${anchored.map(comment => comment.id).join(", ")}. Use comments: "anchor-to-instance" to retain those notes on the instance`);
240
+ const children = cloneLayers(c.layers, next);
241
+ if (anchored.length)
242
+ d.comments = d.comments.map(comment => {
243
+ if (!affected(comment))
244
+ return comment;
245
+ const { layerId: _layer, elementId: _element, panelId: _panel, ...position } = comment.anchor;
246
+ return { ...comment, anchor: { ...position, panelId: panel.id, layerId: layer.id } };
247
+ });
248
+ layer.children = children;
249
+ layer.componentSource.version = c.version;
250
+ }, { layerId });
251
+ }
252
+ moveLayer(layerId, beforeLayerId, options = {}) {
253
+ this.host._applyProduction("reorder layer", [layerId, ...(beforeLayerId ? [beforeLayerId] : [])], options.expectedVersion, d => {
254
+ const { panel } = findLayer(d, layerId);
255
+ const move = (layers) => {
256
+ const index = layers.findIndex(l => l.id === layerId);
257
+ if (index >= 0) {
258
+ if (beforeLayerId === layerId)
259
+ return true;
260
+ const destination = beforeLayerId ? layers.findIndex(l => l.id === beforeLayerId) : layers.length;
261
+ if (destination < 0)
262
+ throw new Error("Destination must be a sibling layer");
263
+ const [l] = layers.splice(index, 1);
264
+ layers.splice(destination > index ? destination - 1 : destination, 0, l);
265
+ return true;
266
+ }
267
+ return layers.some(l => l.kind === "group" && move(l.children));
268
+ };
269
+ move(panel.layers);
270
+ panel.revision++;
271
+ }, { layerId });
272
+ }
273
+ reparentLayer(layerId, parentId, options = {}) {
274
+ this.host._applyProduction("reparent layer", [layerId, ...(parentId ? [parentId] : [])], options.expectedVersion, document => {
275
+ const { panel, layer } = findLayer(document, layerId);
276
+ const locate = (layers, id, ancestors = []) => {
277
+ for (const entry of layers) {
278
+ if (entry.id === id)
279
+ return { layer: entry, ancestors };
280
+ if (entry.kind === "group") {
281
+ const found = locate(entry.children, id, [...ancestors, entry.id]);
282
+ if (found)
283
+ return found;
284
+ }
285
+ }
286
+ return undefined;
287
+ };
288
+ const source = locate(panel.layers, layerId), destination = parentId === null ? undefined : locate(panel.layers, parentId);
289
+ if (parentId !== null && destination?.layer.kind !== "group")
290
+ throw new Error("Destination must be a group in the same panel, or null for panel root");
291
+ if (parentId !== null && layer.keyframes.some(key => key.depth !== undefined))
292
+ throw new Error("Remove depth keyframes before nesting a plane beneath another layer");
293
+ const subtree = new Set();
294
+ visitLayers([layer], entry => subtree.add(entry.id));
295
+ if (parentId !== null && subtree.has(parentId))
296
+ throw new Error("Cannot parent a layer to itself or its descendants");
297
+ const affected = new Set([...subtree, ...source.ancestors, ...(destination?.ancestors ?? []), ...(parentId ? [parentId] : [])]);
298
+ const lock = document.locks.find(lock => lock.targetType === "layer" && affected.has(lock.targetId));
299
+ if (lock)
300
+ throw new Error(`Cannot reparent locked hierarchy: unlock ${lock.targetId} first (${lock.id})`);
301
+ const oldParent = source.ancestors.at(-1), owner = oldParent ? locate(panel.layers, oldParent).layer : undefined;
302
+ if (oldParent !== parentId && owner?.kind === "group" && owner.twoBoneRig?.elbowId === layerId)
303
+ throw new Error("Unbind the two-bone rig before moving its elbow");
304
+ if (oldParent !== parentId && owner?.kind === "group" && owner.drawingSequence?.some(key => key.drawingId === layerId))
305
+ throw new Error(`Drawing sequence ${owner.id} uses ${layerId}; revise its exposures before reparenting`);
306
+ const siblings = destination?.layer.kind === "group" ? destination.layer.children : panel.layers;
307
+ if (options.beforeLayerId !== undefined && (options.beforeLayerId === layerId || !siblings.some(entry => entry.id === options.beforeLayerId)))
308
+ throw new Error("Insertion target must be another child of the destination");
309
+ if (destination?.layer.kind === "group" && destination.layer.drawingSequence !== undefined && layer.clipToBelow)
310
+ throw new Error("A drawing-sequence child cannot clip to another drawing; move its complete clipping group");
311
+ const detach = (layers) => layers.filter(entry => entry.id !== layerId).map(entry => entry.kind === "group" ? { ...entry, children: detach(entry.children) } : entry);
312
+ const insert = (layers) => {
313
+ const index = options.beforeLayerId === undefined ? layers.length : layers.findIndex(entry => entry.id === options.beforeLayerId);
314
+ return [...layers.slice(0, index), layer, ...layers.slice(index)];
315
+ };
316
+ const attach = (layers) => layers.map(entry => entry.kind === "group" ? { ...entry, children: entry.id === parentId ? insert(entry.children) : attach(entry.children) } : entry);
317
+ const detached = detach(panel.layers), proposed = parentId === null ? insert(detached) : attach(detached), dependencies = new Map();
318
+ visitLayers(proposed, entry => dependencies.set(entry.id, entry));
319
+ validateLayerDependencies(dependencies);
320
+ panel.layers = proposed;
321
+ panel.revision++;
322
+ }, { layerId });
323
+ }
324
+ removeLayer(layerId, options = {}) {
325
+ this.host._applyProduction("remove layer", [layerId], options.expectedVersion, d => {
326
+ const { panel, layer } = findLayer(d, layerId), removedLayers = new Set(), removedElements = new Set();
327
+ visitLayers([layer], entry => {
328
+ removedLayers.add(entry.id);
329
+ if (entry.kind !== "group")
330
+ for (const element of entry.elements)
331
+ removedElements.add(element.id);
332
+ });
333
+ visitLayers(panel.layers, entry => {
334
+ if (!removedLayers.has(entry.id) && entry.kind === "group" && entry.twoBoneRig && removedLayers.has(entry.twoBoneRig.elbowId))
335
+ throw new Error("Unbind the two-bone rig before removing its elbow");
336
+ if (!removedLayers.has(entry.id) && entry.kind === "group" && entry.drawingSequence?.some(key => key.drawingId !== null && removedLayers.has(key.drawingId)))
337
+ throw new Error(`Cannot remove ${layerId}: drawing sequence ${entry.id} uses it; revise its exposures first`);
338
+ if (!removedLayers.has(entry.id) && entry.maskLayerId && removedLayers.has(entry.maskLayerId))
339
+ throw new Error(`Cannot remove ${layerId}: layer ${entry.id} uses mask ${entry.maskLayerId}; remove or replace its mask reference first`);
340
+ });
341
+ assertRemovalUnlocked(d, removedLayers);
342
+ const remove = (layers) => layers.filter(l => l.id !== layerId).map(l => l.kind === "group" ? { ...l, children: remove(l.children) } : l);
343
+ panel.layers = remove(panel.layers);
344
+ removeReviewAnchors(d, new Set([...removedLayers, ...removedElements]));
345
+ panel.revision++;
346
+ }, { layerId });
347
+ }
348
+ find(query = {}) { return this.host._findObjects(query); }
349
+ coordinates(targetId, options = {}) { return this.host._coordinates(targetId, options); }
350
+ layer(id) { return this.host._readLayer(id); }
351
+ layerKeyframes(id, options = {}) { return this.host._readLayerKeyframes(id, options); }
352
+ cameraKeyframes(shotId, options = {}) { return this.host._readCameraKeyframes(shotId, options); }
353
+ element(id) { return this.host._readElement(id); }
354
+ drawingSequence(groupId) { return this.host._readDrawingSequence(groupId); }
355
+ setPlaneDepth(layerId, depth, options = {}) {
356
+ if (!Number.isFinite(depth) || depth <= 0)
357
+ throw new Error("Plane depth must be positive and finite");
358
+ this.host._applyProduction("set multiplane depth", [layerId], options.expectedVersion, d => {
359
+ const { layer, panel } = findLayer(d, layerId);
360
+ if (!panel.layers.includes(layer))
361
+ throw new Error(`Multiplane depth belongs to a top-level layer or group; revise its root plane instead: ${layerId}`);
362
+ layer.depth = depth;
363
+ }, { layerId });
364
+ }
365
+ drawingNeighbors(groupId, frame, options = {}) { return this.host._readDrawingNeighbors(groupId, frame, options.skipBlank ?? true); }
366
+ twoBoneRig(rootId) { return this.host._readTwoBoneRig(rootId); }
367
+ setTwoBoneRig(rootId, definition, options = {}) {
368
+ const rig = definition === null ? null : twoBoneRigSchema.parse(definition);
369
+ this.host._applyProduction("configure two-bone rig", [rootId], options.expectedVersion, document => {
370
+ const { layer } = findLayer(document, rootId);
371
+ if (layer.kind !== "group")
372
+ throw new Error("Rig requires a group");
373
+ if (rig) {
374
+ validateTwoBoneRig(layer, rig);
375
+ layer.twoBoneRig = rig;
376
+ }
377
+ else
378
+ delete layer.twoBoneRig;
379
+ }, { layerId: rootId });
380
+ }
381
+ poseTwoBoneRig(rootId, frame, target, options = {}) {
382
+ assertRenderFrame(frame);
383
+ let solution;
384
+ this.host._applyProduction("pose two-bone rig", [rootId], options.expectedVersion, (document, next) => {
385
+ const { layer } = findLayer(document, rootId);
386
+ if (layer.kind !== "group" || !layer.twoBoneRig)
387
+ throw new Error(`No two-bone rig on ${rootId}`);
388
+ const elbow = validateTwoBoneRig(layer, layer.twoBoneRig);
389
+ const lock = document.locks.find(lock => lock.targetType === "layer" && lock.targetId === elbow.id && lock.owner !== this.host.actor);
390
+ if (lock)
391
+ throw new Error(`Locked by ${lock.owner}: ${lock.reason}`);
392
+ const rootState = evaluateLayer(layer, frame).transform, elbowState = evaluateLayer(elbow, frame).transform;
393
+ solution = solveTwoBoneIK(rootState, target, layer.twoBoneRig.upperLength, layer.twoBoneRig.lowerLength, options.bend ?? 1);
394
+ const nearest = (angle, reference) => reference + Math.atan2(Math.sin(angle - reference), Math.cos(angle - reference));
395
+ const prepare = (joint, angle, reference) => {
396
+ const existing = joint.keyframes.find(key => key.frame === frame);
397
+ return layerKeyframeSchema.parse({ ...existing, id: existing?.id ?? "", frame, transform: { ...existing?.transform, rotation: nearest(angle, reference) }, easing: existing?.easing ?? options.easing ?? "linear", channelEasing: { ...existing?.channelEasing, rotation: options.easing ?? "linear" } });
398
+ };
399
+ const rootKey = prepare(layer, solution.rootRotation, rootState.rotation), elbowKey = prepare(elbow, solution.elbowRotation, elbowState.rotation);
400
+ for (const [joint, key] of [[layer, rootKey], [elbow, elbowKey]]) {
401
+ if (!key.id)
402
+ key.id = next("layer-key");
403
+ joint.keyframes = [...joint.keyframes.filter(key => key.frame !== frame), key].sort((a, b) => a.frame - b.frame);
404
+ }
405
+ }, { layerId: rootId });
406
+ return solution;
407
+ }
408
+ setDrawingSequence(groupId, keys, options = {}) {
409
+ const proposed = keys === null ? null : drawingSequenceSchema.parse(keys);
410
+ this.host._applyProduction("set drawing sequence", [groupId], options.expectedVersion, d => {
411
+ const { layer, panel } = findLayer(d, groupId);
412
+ if (layer.kind !== "group")
413
+ throw new Error(`Drawing sequence requires a group: ${groupId}`);
414
+ if (proposed !== null)
415
+ validateDrawingSequence({ ...layer, drawingSequence: proposed });
416
+ if (proposed === null)
417
+ delete layer.drawingSequence;
418
+ else
419
+ layer.drawingSequence = proposed;
420
+ panel.revision++;
421
+ }, { layerId: groupId });
422
+ }
423
+ duplicateDrawing(groupId, drawingId, name, options = {}) {
424
+ let id = "";
425
+ this.host._applyProduction("duplicate drawing for local variation", [groupId, drawingId], options.expectedVersion, (d, next) => {
426
+ const { layer } = findLayer(d, groupId);
427
+ if (layer.kind !== "group" || layer.drawingSequence === undefined)
428
+ throw new Error(`Layer is not a drawing sequence: ${groupId}`);
429
+ const source = layer.children.find(child => child.id === drawingId);
430
+ if (!source)
431
+ throw new Error(`Drawing ${drawingId} is not a child of ${groupId}`);
432
+ const drawing = cloneLayers([source], next)[0];
433
+ drawing.name = name;
434
+ id = drawing.id;
435
+ layer.children.push(drawing);
436
+ }, { layerId: groupId });
437
+ return id;
438
+ }
439
+ setDrawingRange(groupId, startFrame, endFrame, drawingId, options = {}) {
440
+ assertRenderFrame(startFrame);
441
+ assertRenderFrame(endFrame);
442
+ if (endFrame <= startFrame)
443
+ throw new Error("Drawing range end must follow start (end is exclusive)");
444
+ this.host._applyProduction("replace drawing exposure range", [groupId], options.expectedVersion, d => {
445
+ const { layer, panel } = findLayer(d, groupId);
446
+ if (layer.kind !== "group" || layer.drawingSequence === undefined)
447
+ throw new Error(`Layer is not a drawing sequence: ${groupId}`);
448
+ const sequence = layer.drawingSequence;
449
+ const restore = evaluateDrawing(sequence, endFrame);
450
+ const keys = sequence.filter(key => key.frame < startFrame || key.frame >= endFrame);
451
+ keys.push({ frame: startFrame, drawingId });
452
+ if (!keys.some(key => key.frame === endFrame))
453
+ keys.push({ frame: endFrame, drawingId: restore });
454
+ const proposed = drawingSequenceSchema.parse(keys.sort((a, b) => a.frame - b.frame));
455
+ validateDrawingSequence({ ...layer, drawingSequence: proposed });
456
+ layer.drawingSequence = proposed;
457
+ panel.revision++;
458
+ }, { layerId: groupId });
459
+ }
460
+ setExposure(layerId, exposure, options = {}) {
461
+ this.host._applyProduction("set drawing exposure", [layerId], options.expectedVersion, d => {
462
+ const proposed = exposureSchema.parse(exposure);
463
+ findLayer(d, layerId).layer.exposure = proposed;
464
+ }, { layerId });
465
+ }
466
+ inspect() { return this.host._inspectProject(); }
467
+ audioTracks(options = {}) { return this.host._readAudioTracks(options); }
468
+ audioClips(trackId, options = {}) { return this.host._readAudioClips(trackId, options); }
469
+ audioClip(id) { return this.host._readAudioClip(id); }
470
+ changesSince(version, options = {}) {
471
+ return this.host._readChanges(version, options);
472
+ }
473
+ brush(id) {
474
+ return this.host._readBrush(id);
475
+ }
476
+ createBrush(definition, options = {}) {
477
+ let id = "";
478
+ this.host._applyProduction("create brush", [], options.expectedVersion, (document, nextId) => {
479
+ const proposed = parseBrushDefinition({ ...definition, id: definition.id ?? "brush:pending", version: 1 });
480
+ id = definition.id ?? nextId("brush");
481
+ document.brushes.push({ ...proposed, id });
482
+ });
483
+ return id;
484
+ }
485
+ reviseBrush(id, changes, options = {}) {
486
+ let version = 0;
487
+ this.host._applyProduction("revise brush", [id], options.expectedVersion, (document) => {
488
+ const index = document.brushes.findIndex((entry) => entry.id === id);
489
+ if (index < 0)
490
+ throw new Error(`Brush not found: ${id}`);
491
+ const current = document.brushes[index];
492
+ version = current.version + 1;
493
+ const proposed = parseBrushDefinition({ ...current, ...changes, id, version });
494
+ document.brushes[index] = proposed;
495
+ });
496
+ return version;
497
+ }
498
+ duplicateBrush(id, name, options = {}) {
499
+ const source = this.brush(id);
500
+ const { id: _id, version: _version, ...definition } = source;
501
+ return this.createBrush({ ...definition, name }, options);
502
+ }
503
+ setPanelDuration(panelId, durationFrames, mode = "ripple", options = {}) {
504
+ if (!Number.isSafeInteger(durationFrames) || durationFrames < 1)
505
+ throw new Error("Panel duration must be a positive safe integer frame count");
506
+ if (mode !== "ripple" && mode !== "preserve")
507
+ throw new Error(`Unknown retiming mode: ${mode}`);
508
+ this.host._applyProduction(`retime panel (${mode})`, [panelId], options.expectedVersion, (document) => {
509
+ const panel = findPanel(document, panelId);
510
+ if (mode === "preserve" && durationFrames !== panel.durationFrames)
511
+ throw new Error("Preserving subsequent timing would create a gap or overlap. Use ripple retiming.");
512
+ retimePanel(document, panelId, durationFrames);
513
+ });
514
+ }
515
+ setTransition(panelId, transition, options = {}) {
516
+ this.host._applyProduction("set transition", [panelId], options.expectedVersion, (document) => {
517
+ const panel = findPanel(document, panelId);
518
+ const proposed = transitionSchema.parse(transition);
519
+ if (proposed.durationFrames >= panel.durationFrames)
520
+ throw new Error("Transition duration must fit inside the panel");
521
+ panel.transition = proposed;
522
+ panel.revision += 1;
523
+ }, { panelId });
524
+ }
525
+ movePanel(panelId, beforePanelId, options = {}) {
526
+ this.host._applyProduction("move panel", [panelId, ...(beforePanelId ? [beforePanelId] : [])], options.expectedVersion, (document) => editTimelineStructure(document, () => {
527
+ const panel = findPanel(document, panelId);
528
+ const shot = document.shots.find((entry) => entry.id === panel.shotId);
529
+ shot.panelIds = shot.panelIds.filter((id) => id !== panelId);
530
+ const index = beforePanelId ? shot.panelIds.indexOf(beforePanelId) : shot.panelIds.length;
531
+ if (index < 0)
532
+ throw new Error(`Destination panel is not in shot ${shot.id}`);
533
+ shot.panelIds.splice(index, 0, panelId);
534
+ }));
535
+ }
536
+ duplicatePanel(panelId, options = {}) {
537
+ let created = "";
538
+ this.host._applyProduction("duplicate panel", [panelId], options.expectedVersion, (document, nextId) => editTimelineStructure(document, () => {
539
+ const source = findPanel(document, panelId);
540
+ const shot = document.shots.find((entry) => entry.id === source.shotId);
541
+ created = nextId("panel");
542
+ const copy = {
543
+ ...structuredClone(source), id: created, number: `${source.number}A`, title: `${source.title} copy`,
544
+ layers: cloneLayers(source.layers, nextId),
545
+ motion: source.motion.map((motion) => ({ ...motion, id: nextId("motion") })), revision: 0,
546
+ };
547
+ const index = shot.panelIds.indexOf(panelId) + 1;
548
+ shot.panelIds.splice(index, 0, created);
549
+ document.panels.push(copy);
550
+ }));
551
+ return created;
552
+ }
553
+ deletePanel(panelId, options = {}) {
554
+ this.host._applyProduction("delete panel", [panelId], options.expectedVersion, (document) => editTimelineStructure(document, () => {
555
+ const panel = findPanel(document, panelId);
556
+ const shot = document.shots.find((entry) => entry.id === panel.shotId);
557
+ if (shot.panelIds.length <= 1)
558
+ throw new Error("A shot must keep at least one panel");
559
+ const removedIds = new Set([panelId]);
560
+ visitLayers(panel.layers, layer => {
561
+ removedIds.add(layer.id);
562
+ if (layer.kind !== "group")
563
+ for (const element of layer.elements)
564
+ removedIds.add(element.id);
565
+ });
566
+ assertRemovalUnlocked(document, removedIds);
567
+ const end = panel.startFrame + panel.durationFrames;
568
+ if (document.audioTracks.some(t => t.clips.some(c => c.startFrame >= panel.startFrame && c.startFrame < end)))
569
+ throw new Error("Panel contains audio cues; remove or reposition those clips before deleting it");
570
+ shot.cameraKeyframes = shot.cameraKeyframes.filter(k => k.frame < panel.startFrame || k.frame >= end);
571
+ shot.panelIds = shot.panelIds.filter((id) => id !== panelId);
572
+ document.panels = document.panels.filter((entry) => entry.id !== panelId);
573
+ removeReviewAnchors(document, removedIds);
574
+ document.comments = document.comments.filter(({ anchor }) => !(anchor.panelId === undefined && anchor.layerId === undefined && anchor.elementId === undefined && anchor.frame !== undefined && anchor.frame >= panel.startFrame && anchor.frame < end));
575
+ }));
576
+ }
577
+ setPanelStatus(panelId, status, options = {}) {
578
+ this.host._applyProduction("set panel review status", [panelId], options.expectedVersion, (document) => { findPanel(document, panelId).status = status; });
579
+ }
580
+ setPanelNumber(panelId, number, options = {}) {
581
+ this.host._applyProduction("renumber panel", [panelId], options.expectedVersion, d => { findPanel(d, panelId).number = number; });
582
+ }
583
+ addCameraKeyframe(shotId, frame, value, options = {}) {
584
+ let id = "";
585
+ this.host._applyProduction("add camera keyframe", [shotId], options.expectedVersion, (document, nextId) => {
586
+ const shot = document.shots.find((entry) => entry.id === shotId);
587
+ if (!shot)
588
+ throw new Error(`Shot not found: ${shotId}`);
589
+ const existing = shot.cameraKeyframes.find(key => key.frame === frame);
590
+ const channels = ["x", "y", "zoom", "rotation"];
591
+ if (!channels.some(channel => value[channel] !== undefined))
592
+ throw new Error("Camera keyframe must author at least one property");
593
+ const supplied = Object.fromEntries(channels.filter(channel => value[channel] !== undefined).map(channel => [channel, value[channel]]));
594
+ const channelEasing = { ...existing?.channelEasing };
595
+ if (existing && value.easing !== undefined)
596
+ for (const channel of channels)
597
+ if (value[channel] !== undefined)
598
+ channelEasing[channel] = value.easing;
599
+ const keyframe = cameraKeyframeSchema.parse({ ...existing, ...supplied, id: "", frame, easing: existing?.easing ?? value.easing ?? "linear", ...(Object.keys(channelEasing).length ? { channelEasing } : {}) });
600
+ id = existing?.id ?? nextId("camera-key");
601
+ shot.cameraKeyframes = [...shot.cameraKeyframes.filter((keyframe) => keyframe.frame !== frame), { ...keyframe, id }].sort((a, b) => a.frame - b.frame);
602
+ }, { shotId });
603
+ return id;
604
+ }
605
+ updateCameraKeyframe(shotId, keyframeId, changes, options = {}) {
606
+ this.host._applyProduction("update camera keyframe", [shotId, keyframeId], options.expectedVersion, (document) => {
607
+ const shot = document.shots.find((entry) => entry.id === shotId);
608
+ const keyframe = shot?.cameraKeyframes.find((entry) => entry.id === keyframeId);
609
+ if (!keyframe)
610
+ throw new Error(`Camera keyframe not found: ${keyframeId}`);
611
+ const proposed = cameraKeyframeSchema.parse({ ...keyframe, ...changes, id: keyframe.id });
612
+ const keys = shot.cameraKeyframes.map(entry => entry.id === keyframeId ? proposed : entry);
613
+ validateKeyframePositions(keys, shotId);
614
+ shot.cameraKeyframes = keys.sort((a, b) => a.frame - b.frame);
615
+ }, { shotId });
616
+ }
617
+ removeCameraKeyframe(shotId, keyframeId, options = {}) {
618
+ this.host._applyProduction("remove camera keyframe", [shotId, keyframeId], options.expectedVersion, (document) => {
619
+ const shot = document.shots.find((entry) => entry.id === shotId);
620
+ if (!shot || !shot.cameraKeyframes.some((entry) => entry.id === keyframeId))
621
+ throw new Error(`Camera keyframe not found: ${keyframeId}`);
622
+ shot.cameraKeyframes = shot.cameraKeyframes.filter((entry) => entry.id !== keyframeId);
623
+ }, { shotId });
624
+ }
625
+ removeCameraKeyframeChannels(shotId, keyframeId, channels, options = {}) {
626
+ if (!channels.length || new Set(channels).size !== channels.length || channels.some(channel => !["x", "y", "zoom", "rotation"].includes(channel)))
627
+ throw new Error("Keyframe channels must be nonempty, unique supported property names");
628
+ this.host._applyProduction("remove camera keyframe channels", [shotId, keyframeId], options.expectedVersion, document => {
629
+ const shot = document.shots.find(shot => shot.id === shotId), key = shot?.cameraKeyframes.find(key => key.id === keyframeId);
630
+ if (!shot || !key)
631
+ throw new Error(`Camera keyframe not found: ${keyframeId}`);
632
+ const revised = structuredClone(key);
633
+ for (const channel of channels) {
634
+ if (revised[channel] === undefined)
635
+ throw new Error(`Property ${channel} is not keyed at ${keyframeId}`);
636
+ delete revised[channel];
637
+ if (revised.channelEasing)
638
+ delete revised.channelEasing[channel];
639
+ }
640
+ if (![revised.x, revised.y, revised.zoom, revised.rotation].some(value => value !== undefined))
641
+ shot.cameraKeyframes = shot.cameraKeyframes.filter(key => key.id !== keyframeId);
642
+ else {
643
+ const proposed = cameraKeyframeSchema.parse(revised);
644
+ shot.cameraKeyframes = shot.cameraKeyframes.map(key => key.id === keyframeId ? proposed : key);
645
+ }
646
+ }, { shotId });
647
+ }
648
+ addLayerKeyframe(layerId, frame, value, options = {}) {
649
+ let id = "";
650
+ this.host._applyProduction("add layer keyframe", [layerId], options.expectedVersion, (document, nextId) => {
651
+ const { panel, layer } = findLayer(document, layerId);
652
+ const existing = layer.keyframes.find(key => key.frame === frame);
653
+ const channelEasing = { ...existing?.channelEasing };
654
+ if (existing && value.easing !== undefined) {
655
+ for (const channel of Object.keys(value.transform ?? {}))
656
+ if (value.transform?.[channel] !== undefined)
657
+ channelEasing[channel] = value.easing;
658
+ if (value.opacity !== undefined)
659
+ channelEasing.opacity = value.easing;
660
+ if (value.depth !== undefined)
661
+ channelEasing.depth = value.easing;
662
+ }
663
+ const keyframe = layerKeyframeSchema.parse({ id: "", frame, transform: { ...existing?.transform, ...value.transform }, opacity: value.opacity ?? existing?.opacity, depth: value.depth ?? existing?.depth, easing: existing?.easing ?? value.easing ?? "linear", ...(Object.keys(channelEasing).length ? { channelEasing } : {}) });
664
+ if (keyframe.depth !== undefined && !panel.layers.includes(layer))
665
+ throw new Error("Depth keyframes require a top-level plane");
666
+ validateRigLayerChange(panel.layers, { ...layer, keyframes: [...layer.keyframes.filter(entry => entry.frame !== frame), keyframe] });
667
+ id = existing?.id ?? nextId("layer-key");
668
+ layer.keyframes = [...layer.keyframes.filter((entry) => entry.frame !== frame), { ...keyframe, id }].sort((a, b) => a.frame - b.frame);
669
+ }, { layerId });
670
+ return id;
671
+ }
672
+ updateLayerKeyframe(layerId, keyframeId, changes, options = {}) {
673
+ this.host._applyProduction("update layer keyframe", [layerId, keyframeId], options.expectedVersion, (document) => {
674
+ const { panel, layer } = findLayer(document, layerId);
675
+ const keyframe = layer.keyframes.find((entry) => entry.id === keyframeId);
676
+ if (!keyframe)
677
+ throw new Error(`Layer keyframe not found: ${keyframeId}`);
678
+ const proposed = layerKeyframeSchema.parse({ ...keyframe, ...changes, id: keyframe.id });
679
+ if (proposed.depth !== undefined && !panel.layers.includes(layer))
680
+ throw new Error("Depth keyframes require a top-level plane");
681
+ const keys = layer.keyframes.map(entry => entry.id === keyframeId ? proposed : entry);
682
+ validateKeyframePositions(keys, layerId);
683
+ validateRigLayerChange(panel.layers, { ...layer, keyframes: keys });
684
+ layer.keyframes = keys.sort((a, b) => a.frame - b.frame);
685
+ }, { layerId });
686
+ }
687
+ removeLayerKeyframe(layerId, keyframeId, options = {}) {
688
+ this.host._applyProduction("remove layer keyframe", [layerId, keyframeId], options.expectedVersion, (document) => {
689
+ const { layer } = findLayer(document, layerId);
690
+ if (!layer.keyframes.some((entry) => entry.id === keyframeId))
691
+ throw new Error(`Layer keyframe not found: ${keyframeId}`);
692
+ layer.keyframes = layer.keyframes.filter((entry) => entry.id !== keyframeId);
693
+ }, { layerId });
694
+ }
695
+ removeLayerKeyframeChannels(layerId, keyframeId, channels, options = {}) {
696
+ if (!channels.length || new Set(channels).size !== channels.length || channels.some(channel => !["x", "y", "scaleX", "scaleY", "rotation", "opacity", "depth"].includes(channel)))
697
+ throw new Error("Keyframe channels must be nonempty, unique supported property names");
698
+ this.host._applyProduction("remove keyframe channels", [layerId, keyframeId], options.expectedVersion, d => {
699
+ const { layer } = findLayer(d, layerId), key = layer.keyframes.find(k => k.id === keyframeId);
700
+ if (!key)
701
+ throw new Error(`Layer keyframe not found: ${keyframeId}`);
702
+ const revised = structuredClone(key);
703
+ for (const channel of channels) {
704
+ if ((channel === "opacity" || channel === "depth" ? revised[channel] : revised.transform[channel]) === undefined)
705
+ throw new Error(`Property ${channel} is not keyed at ${keyframeId}`);
706
+ if (channel === "opacity" || channel === "depth")
707
+ delete revised[channel];
708
+ else
709
+ delete revised.transform[channel];
710
+ if (revised.channelEasing)
711
+ delete revised.channelEasing[channel];
712
+ }
713
+ if (revised.opacity === undefined && revised.depth === undefined && !Object.keys(revised.transform).length)
714
+ layer.keyframes = layer.keyframes.filter(k => k.id !== keyframeId);
715
+ else {
716
+ const proposed = layerKeyframeSchema.parse(revised);
717
+ layer.keyframes = layer.keyframes.map(k => k.id === keyframeId ? proposed : k);
718
+ }
719
+ }, { layerId });
720
+ }
721
+ addAsset(asset, options = {}) {
722
+ let id = "";
723
+ this.host._applyProduction("add asset", [], options.expectedVersion, (document, nextId) => {
724
+ id = asset.id ?? nextId("asset");
725
+ document.assets.push({ ...asset, id });
726
+ });
727
+ return id;
728
+ }
729
+ updateAsset(id, changes, options = {}) {
730
+ this.host._applyProduction("update asset", [id], options.expectedVersion, document => {
731
+ const asset = document.assets.find(a => a.id === id);
732
+ if (!asset)
733
+ throw new Error(`Asset not found: ${id}`);
734
+ Object.assign(asset, structuredClone(changes));
735
+ });
736
+ }
737
+ addAudioTrack(name, options = {}) {
738
+ audioTrackSchema.parse({ id: options.id ?? "", name, muted: false, locked: false, clips: [] });
739
+ let id = "";
740
+ this.host._applyProduction("add audio track", [], options.expectedVersion, (document, nextId) => {
741
+ id = options.id ?? nextId("audio-track");
742
+ document.audioTracks.push({ id, name, muted: false, locked: false, clips: [] });
743
+ }, { audio: true });
744
+ return id;
745
+ }
746
+ updateAudioTrack(trackId, changes, options = {}) {
747
+ const proposed = audioTrackChangesSchema.parse(changes);
748
+ this.host._applyProduction("update audio track", [trackId], options.expectedVersion, d => {
749
+ const track = d.audioTracks.find(track => track.id === trackId);
750
+ if (!track)
751
+ throw new Error(`Audio track not found: ${trackId}`);
752
+ if (track.locked && (proposed.name !== undefined || proposed.muted !== undefined))
753
+ throw new Error(`Unlock audio track before editing: ${trackId}`);
754
+ for (const [key, value] of Object.entries(proposed))
755
+ if (value !== undefined)
756
+ Object.assign(track, { [key]: value });
757
+ }, { audio: true });
758
+ }
759
+ removeAudioTrack(trackId, options = {}) {
760
+ this.host._applyProduction("remove audio track and clips", [trackId], options.expectedVersion, d => {
761
+ const track = d.audioTracks.find(track => track.id === trackId);
762
+ if (!track)
763
+ throw new Error(`Audio track not found: ${trackId}`);
764
+ if (track.locked)
765
+ throw new Error(`Audio track is locked: ${trackId}`);
766
+ d.audioTracks = d.audioTracks.filter(track => track.id !== trackId);
767
+ }, { audio: true });
768
+ }
769
+ addAudioClip(trackId, clip, options = {}) {
770
+ let id = "";
771
+ this.host._applyProduction("add audio clip", [trackId], options.expectedVersion, (document, nextId) => {
772
+ const track = document.audioTracks.find((entry) => entry.id === trackId);
773
+ if (!track)
774
+ throw new Error(`Audio track not found: ${trackId}`);
775
+ if (track.locked)
776
+ throw new Error(`Audio track is locked: ${trackId}`);
777
+ const asset = document.assets.find((entry) => entry.id === clip.assetId);
778
+ if (!asset || asset.kind !== "audio")
779
+ throw new Error(`Audio asset not found: ${clip.assetId}`);
780
+ const proposed = audioClipSchema.parse({ ...clip, id: clip.id ?? "new clip" });
781
+ validateAudioFades(proposed);
782
+ id = clip.id ?? nextId("audio-clip");
783
+ track.clips.push({ ...proposed, id });
784
+ }, { audio: true });
785
+ return id;
786
+ }
787
+ updateAudioClip(trackId, clipId, changes, options = {}) {
788
+ this.host._applyProduction("update audio clip", [trackId, clipId], options.expectedVersion, (document) => {
789
+ const track = document.audioTracks.find((entry) => entry.id === trackId);
790
+ if (!track)
791
+ throw new Error(`Audio track not found: ${trackId}`);
792
+ if (track.locked)
793
+ throw new Error(`Audio track is locked: ${trackId}`);
794
+ const clip = track.clips.find((entry) => entry.id === clipId);
795
+ if (!clip)
796
+ throw new Error(`Audio clip not found: ${clipId}`);
797
+ const proposed = audioClipSchema.parse({ ...clip, ...changes, id: clip.id, assetId: clip.assetId });
798
+ validateAudioFades(proposed);
799
+ Object.assign(clip, proposed);
800
+ }, { audio: true });
801
+ }
802
+ removeAudioClip(trackId, clipId, options = {}) {
803
+ this.host._applyProduction("remove audio clip", [trackId, clipId], options.expectedVersion, d => {
804
+ const track = d.audioTracks.find(t => t.id === trackId);
805
+ if (!track || track.locked)
806
+ throw new Error("Missing or locked audio track");
807
+ const index = track.clips.findIndex(c => c.id === clipId);
808
+ if (index < 0)
809
+ throw new Error(`Audio clip not found: ${clipId}`);
810
+ track.clips.splice(index, 1);
811
+ }, { audio: true });
812
+ }
813
+ moveAudioClip(clipId, targetTrackId, options = {}) {
814
+ const owner = this.host._readAudioClip(clipId).trackId;
815
+ this.host._applyProduction("move audio clip", [owner, targetTrackId, clipId], options.expectedVersion, d => {
816
+ const source = d.audioTracks.find(track => track.id === owner), target = d.audioTracks.find(track => track.id === targetTrackId);
817
+ if (!target)
818
+ throw new Error(`Audio track not found: ${targetTrackId}`);
819
+ if (source.locked || target.locked)
820
+ throw new Error(`Audio track is locked: ${source.locked ? source.id : target.id}`);
821
+ const index = source.clips.findIndex(clip => clip.id === clipId), clip = source.clips[index];
822
+ const proposed = audioClipSchema.parse({ ...clip, startFrame: options.startFrame ?? clip.startFrame });
823
+ if (source === target)
824
+ source.clips[index] = proposed;
825
+ else {
826
+ source.clips.splice(index, 1);
827
+ target.clips.push(proposed);
828
+ }
829
+ }, { audio: true });
830
+ }
831
+ splitAudioClip(clipId, frame, options = {}) {
832
+ if (!Number.isSafeInteger(frame) || frame < 0)
833
+ throw new Error("Audio split frame must be a nonnegative safe integer");
834
+ const owner = this.host._readAudioClip(clipId).trackId;
835
+ let rightId = "";
836
+ this.host._applyProduction("split audio clip", [owner, clipId], options.expectedVersion, (document, nextId) => {
837
+ const track = document.audioTracks.find(track => track.id === owner);
838
+ if (track.locked)
839
+ throw new Error(`Audio track is locked: ${owner}`);
840
+ const index = track.clips.findIndex(clip => clip.id === clipId), clip = track.clips[index], end = clip.startFrame + clip.durationFrames;
841
+ if (!Number.isSafeInteger(end))
842
+ throw new Error("Audio clip end exceeds the safe integer frame range");
843
+ if (frame <= clip.startFrame || frame >= end)
844
+ throw new Error("Audio split frame must be strictly inside the clip");
845
+ const leftDuration = frame - clip.startFrame, rightDuration = end - frame;
846
+ if (leftDuration < clip.fadeInFrames || rightDuration < clip.fadeOutFrames)
847
+ throw new Error("Audio split crosses a fade; revise the fade or choose a frame between the fades");
848
+ const left = audioClipSchema.parse({ ...clip, durationFrames: leftDuration, fadeOutFrames: 0 });
849
+ const right = audioClipSchema.parse({ ...clip, startFrame: frame, sourceInFrame: clip.sourceInFrame + leftDuration, durationFrames: rightDuration, fadeInFrames: 0 });
850
+ validateAudioFades(left);
851
+ validateAudioFades(right);
852
+ rightId = nextId("audio-clip");
853
+ right.id = rightId;
854
+ track.clips.splice(index, 1, left, right);
855
+ }, { audio: true });
856
+ return rightId;
857
+ }
858
+ comment(body, anchor, options = {}) {
859
+ let id = "";
860
+ this.host._applyProduction("add review comment", Object.values(anchor).filter((value) => typeof value === "string"), options.expectedVersion, (document, nextId) => {
861
+ id = nextId("comment");
862
+ document.comments.push({ id, author: options.author ?? this.host.actor, body, status: "open", anchor: structuredClone(anchor), createdAt: new Date().toISOString() });
863
+ });
864
+ return id;
865
+ }
866
+ resolveComment(commentId, options = {}) {
867
+ this.host._applyProduction("resolve review comment", [commentId], options.expectedVersion, (document) => {
868
+ const comment = document.comments.find((entry) => entry.id === commentId);
869
+ if (!comment)
870
+ throw new Error(`Comment not found: ${commentId}`);
871
+ comment.status = "resolved";
872
+ comment.resolvedAt = new Date().toISOString();
873
+ });
874
+ }
875
+ lock(targetType, targetId, reason, options = {}) {
876
+ let id = "";
877
+ this.host._applyProduction("lock project target", [targetId], options.expectedVersion, (document, nextId) => {
878
+ if (document.locks.some((lock) => lock.targetType === targetType && lock.targetId === targetId))
879
+ throw new Error(`Target is already locked: ${targetId}`);
880
+ id = nextId("lock");
881
+ document.locks.push({ id, targetType, targetId, owner: this.host.actor, reason, createdAt: new Date().toISOString() });
882
+ });
883
+ return id;
884
+ }
885
+ unlock(lockId, options = {}) {
886
+ const lock = this.host._readLock(lockId);
887
+ if (lock.owner !== this.host.actor)
888
+ throw new Error(`Only ${lock.owner} can unlock ${lock.targetId}`);
889
+ this.host._applyProduction("unlock project target", [lock.targetId], options.expectedVersion, (document) => {
890
+ document.locks = document.locks.filter((entry) => entry.id !== lockId);
891
+ });
892
+ }
893
+ }
894
+ //# sourceMappingURL=production.js.map