@tangle-network/agent-app 0.11.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/DesignCanvas-3JEEIT6Y.js +10 -0
- package/dist/DesignCanvas-3JEEIT6Y.js.map +1 -0
- package/dist/DesignCanvasEditor-37LPJIIR.js +9 -0
- package/dist/DesignCanvasEditor-37LPJIIR.js.map +1 -0
- package/dist/TimelineEditor-OXPJZDP2.js +12 -0
- package/dist/TimelineEditor-OXPJZDP2.js.map +1 -0
- package/dist/apply-Cp8c3K9D.d.ts +249 -0
- package/dist/chunk-2Q73HGDI.js +1743 -0
- package/dist/chunk-2Q73HGDI.js.map +1 -0
- package/dist/chunk-6UOE5CTA.js +1647 -0
- package/dist/chunk-6UOE5CTA.js.map +1 -0
- package/dist/chunk-7QCIYDGC.js +1119 -0
- package/dist/chunk-7QCIYDGC.js.map +1 -0
- package/dist/chunk-A76ZHWNF.js +194 -0
- package/dist/chunk-A76ZHWNF.js.map +1 -0
- package/dist/chunk-ABGSFUJQ.js +111 -0
- package/dist/chunk-ABGSFUJQ.js.map +1 -0
- package/dist/{chunk-4YTWB5MG.js → chunk-ETX4O4BB.js} +98 -1
- package/dist/chunk-ETX4O4BB.js.map +1 -0
- package/dist/chunk-F5KTWRO7.js +2276 -0
- package/dist/chunk-F5KTWRO7.js.map +1 -0
- package/dist/chunk-IHR6K3GF.js +2367 -0
- package/dist/chunk-IHR6K3GF.js.map +1 -0
- package/dist/chunk-JZAJE3JL.js +990 -0
- package/dist/chunk-JZAJE3JL.js.map +1 -0
- package/dist/chunk-ZYBWGSAZ.js +130 -0
- package/dist/chunk-ZYBWGSAZ.js.map +1 -0
- package/dist/design-canvas/drizzle.d.ts +569 -0
- package/dist/design-canvas/drizzle.js +183 -0
- package/dist/design-canvas/drizzle.js.map +1 -0
- package/dist/design-canvas/index.d.ts +261 -0
- package/dist/design-canvas/index.js +96 -0
- package/dist/design-canvas/index.js.map +1 -0
- package/dist/design-canvas-react/index.d.ts +916 -0
- package/dist/design-canvas-react/index.js +423 -0
- package/dist/design-canvas-react/index.js.map +1 -0
- package/dist/export-presets-Dl5Aa5xj.d.ts +284 -0
- package/dist/index.d.ts +11 -2
- package/dist/index.js +224 -6
- package/dist/mcp-CIupfjxV.d.ts +112 -0
- package/dist/mcp-rpc-DLw_r9PQ.d.ts +55 -0
- package/dist/model-BHLN208Z.d.ts +183 -0
- package/dist/runtime/index.d.ts +108 -1
- package/dist/runtime/index.js +7 -1
- package/dist/sequences/drizzle.d.ts +1244 -0
- package/dist/sequences/drizzle.js +368 -0
- package/dist/sequences/drizzle.js.map +1 -0
- package/dist/sequences/index.d.ts +331 -0
- package/dist/sequences/index.js +114 -0
- package/dist/sequences/index.js.map +1 -0
- package/dist/sequences-react/index.d.ts +752 -0
- package/dist/sequences-react/index.js +241 -0
- package/dist/sequences-react/index.js.map +1 -0
- package/dist/store-CUStmtdH.d.ts +64 -0
- package/dist/store-gckrNq-g.d.ts +242 -0
- package/dist/tools/index.d.ts +25 -108
- package/dist/tools/index.js +16 -6
- package/package.json +62 -2
- package/dist/chunk-4YTWB5MG.js.map +0 -1
- package/dist/chunk-OLCVUGGI.js +0 -137
- package/dist/chunk-OLCVUGGI.js.map +0 -1
|
@@ -0,0 +1,990 @@
|
|
|
1
|
+
// src/design-canvas/model.ts
|
|
2
|
+
var SCENE_SCHEMA_VERSION = 1;
|
|
3
|
+
var SCENE_ELEMENT_KINDS = [
|
|
4
|
+
"rect",
|
|
5
|
+
"ellipse",
|
|
6
|
+
"line",
|
|
7
|
+
"text",
|
|
8
|
+
"image",
|
|
9
|
+
"video",
|
|
10
|
+
"group"
|
|
11
|
+
];
|
|
12
|
+
function elementExtent(element) {
|
|
13
|
+
switch (element.kind) {
|
|
14
|
+
case "rect":
|
|
15
|
+
case "ellipse":
|
|
16
|
+
case "image":
|
|
17
|
+
case "video":
|
|
18
|
+
return { width: element.width, height: element.height };
|
|
19
|
+
case "text":
|
|
20
|
+
return {
|
|
21
|
+
width: element.width,
|
|
22
|
+
height: estimateTextHeight(element)
|
|
23
|
+
};
|
|
24
|
+
case "line": {
|
|
25
|
+
let maxX = 0;
|
|
26
|
+
let maxY = 0;
|
|
27
|
+
for (let i = 0; i < element.points.length; i += 2) {
|
|
28
|
+
maxX = Math.max(maxX, Math.abs(element.points[i]));
|
|
29
|
+
maxY = Math.max(maxY, Math.abs(element.points[i + 1]));
|
|
30
|
+
}
|
|
31
|
+
return { width: maxX, height: maxY };
|
|
32
|
+
}
|
|
33
|
+
case "group": {
|
|
34
|
+
let minX = 0, minY = 0, maxX = 0, maxY = 0;
|
|
35
|
+
for (const child of element.children) {
|
|
36
|
+
const aabb = elementAabb(child);
|
|
37
|
+
minX = Math.min(minX, aabb.x);
|
|
38
|
+
minY = Math.min(minY, aabb.y);
|
|
39
|
+
maxX = Math.max(maxX, aabb.x + aabb.width);
|
|
40
|
+
maxY = Math.max(maxY, aabb.y + aabb.height);
|
|
41
|
+
}
|
|
42
|
+
return { width: maxX - minX, height: maxY - minY };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function estimateTextHeight(element) {
|
|
47
|
+
const lines = element.text.length === 0 ? 1 : element.text.split("\n").length;
|
|
48
|
+
return lines * element.fontSize * element.lineHeight;
|
|
49
|
+
}
|
|
50
|
+
function elementAabb(element) {
|
|
51
|
+
const { width, height } = elementExtent(element);
|
|
52
|
+
if (element.rotation % 360 === 0) {
|
|
53
|
+
return { x: element.x, y: element.y, width, height };
|
|
54
|
+
}
|
|
55
|
+
const rad = element.rotation * Math.PI / 180;
|
|
56
|
+
const cos = Math.cos(rad);
|
|
57
|
+
const sin = Math.sin(rad);
|
|
58
|
+
const corners = [
|
|
59
|
+
[0, 0],
|
|
60
|
+
[width * cos, width * sin],
|
|
61
|
+
[-height * sin, height * cos],
|
|
62
|
+
[width * cos - height * sin, width * sin + height * cos]
|
|
63
|
+
];
|
|
64
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
65
|
+
for (const [cx, cy] of corners) {
|
|
66
|
+
minX = Math.min(minX, cx);
|
|
67
|
+
minY = Math.min(minY, cy);
|
|
68
|
+
maxX = Math.max(maxX, cx);
|
|
69
|
+
maxY = Math.max(maxY, cy);
|
|
70
|
+
}
|
|
71
|
+
return { x: element.x + minX, y: element.y + minY, width: maxX - minX, height: maxY - minY };
|
|
72
|
+
}
|
|
73
|
+
function boundsIntersect(a, b) {
|
|
74
|
+
return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
|
|
75
|
+
}
|
|
76
|
+
function requirePage(document, pageId) {
|
|
77
|
+
const page = document.pages.find((candidate) => candidate.id === pageId);
|
|
78
|
+
if (!page) throw new Error(`page ${pageId} not found in document`);
|
|
79
|
+
return page;
|
|
80
|
+
}
|
|
81
|
+
function findElement(page, elementId) {
|
|
82
|
+
const stack = [page.elements];
|
|
83
|
+
while (stack.length > 0) {
|
|
84
|
+
const owner = stack.pop();
|
|
85
|
+
for (let index = 0; index < owner.length; index += 1) {
|
|
86
|
+
const element = owner[index];
|
|
87
|
+
if (element.id === elementId) return { element, owner, index };
|
|
88
|
+
if (element.kind === "group") stack.push(element.children);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
function requireElement(page, elementId) {
|
|
94
|
+
const found = findElement(page, elementId);
|
|
95
|
+
if (!found) throw new Error(`element ${elementId} not found on page ${page.id}`);
|
|
96
|
+
return found;
|
|
97
|
+
}
|
|
98
|
+
function collectSlots(document) {
|
|
99
|
+
const slots = /* @__PURE__ */ new Map();
|
|
100
|
+
for (const page of document.pages) {
|
|
101
|
+
const stack = [...page.elements];
|
|
102
|
+
while (stack.length > 0) {
|
|
103
|
+
const element = stack.pop();
|
|
104
|
+
if (element.slot) {
|
|
105
|
+
if (slots.has(element.slot)) throw new Error(`duplicate slot name "${element.slot}"`);
|
|
106
|
+
slots.set(element.slot, { pageId: page.id, elementId: element.id, kind: element.kind });
|
|
107
|
+
}
|
|
108
|
+
if (element.kind === "group") stack.push(...element.children);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return slots;
|
|
112
|
+
}
|
|
113
|
+
function createEmptyDocument(title, page) {
|
|
114
|
+
return {
|
|
115
|
+
schemaVersion: SCENE_SCHEMA_VERSION,
|
|
116
|
+
title,
|
|
117
|
+
pages: [createPage(page ?? {}, "page-1")],
|
|
118
|
+
settings: { dpi: 96 },
|
|
119
|
+
metadata: {}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function createPage(options, id) {
|
|
123
|
+
const width = options.width ?? 1080;
|
|
124
|
+
const height = options.height ?? 1080;
|
|
125
|
+
assertPositiveFinite(width, "page width");
|
|
126
|
+
assertPositiveFinite(height, "page height");
|
|
127
|
+
return {
|
|
128
|
+
id,
|
|
129
|
+
name: options.name ?? "Page",
|
|
130
|
+
width,
|
|
131
|
+
height,
|
|
132
|
+
background: options.background ?? "#ffffff",
|
|
133
|
+
bleed: null,
|
|
134
|
+
guides: { vertical: [], horizontal: [] },
|
|
135
|
+
elements: []
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function assertPositiveFinite(value, label) {
|
|
139
|
+
if (!Number.isFinite(value) || value <= 0) throw new Error(`${label} must be a positive finite number`);
|
|
140
|
+
}
|
|
141
|
+
function assertFinite(value, label) {
|
|
142
|
+
if (!Number.isFinite(value)) throw new Error(`${label} must be a finite number`);
|
|
143
|
+
}
|
|
144
|
+
var COLOR_PATTERN = /^(#[0-9a-fA-F]{3,8}|rgba?\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*(,\s*(0|1|0?\.\d+)\s*)?\)|transparent)$/;
|
|
145
|
+
function assertColor(value, label) {
|
|
146
|
+
if (!COLOR_PATTERN.test(value)) throw new Error(`${label} must be a hex/rgb(a) color or 'transparent', got "${value}"`);
|
|
147
|
+
}
|
|
148
|
+
function assertSceneMediaSrc(value, label) {
|
|
149
|
+
if (/^https?:\/\//i.test(value) || /^\/api\//.test(value)) return;
|
|
150
|
+
throw new Error(`${label} must be an http(s) URL or a rooted /api/ path, got "${value}"`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/design-canvas/validate.ts
|
|
154
|
+
function validateSceneOperations(document, operations) {
|
|
155
|
+
operations.forEach((operation, index) => {
|
|
156
|
+
try {
|
|
157
|
+
validateSceneOperation(document, operation);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
160
|
+
throw new Error(`operation ${index + 1} (${operation.type}): ${reason}`);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
function validateSceneOperation(document, operation) {
|
|
165
|
+
switch (operation.type) {
|
|
166
|
+
case "add_element":
|
|
167
|
+
return validateAddElement(document, operation);
|
|
168
|
+
case "set_attrs":
|
|
169
|
+
return validateSetAttrs(document, operation);
|
|
170
|
+
case "reorder_element":
|
|
171
|
+
return validateReorderElement(document, operation);
|
|
172
|
+
case "delete_element":
|
|
173
|
+
return validateDeleteElement(document, operation);
|
|
174
|
+
case "group_elements":
|
|
175
|
+
return validateGroupElements(document, operation);
|
|
176
|
+
case "ungroup_element":
|
|
177
|
+
return validateUngroupElement(document, operation);
|
|
178
|
+
case "add_page":
|
|
179
|
+
return validateAddPage(operation);
|
|
180
|
+
case "duplicate_page":
|
|
181
|
+
return validateDuplicatePage(document, operation);
|
|
182
|
+
case "delete_page":
|
|
183
|
+
return validateDeletePage(document, operation);
|
|
184
|
+
case "reorder_page":
|
|
185
|
+
return validateReorderPage(document, operation);
|
|
186
|
+
case "set_page_props":
|
|
187
|
+
return validateSetPageProps(document, operation);
|
|
188
|
+
case "set_page_guides":
|
|
189
|
+
return validateSetPageGuides(document, operation);
|
|
190
|
+
case "bind_slot":
|
|
191
|
+
return validateBindSlot(document, operation);
|
|
192
|
+
case "apply_data":
|
|
193
|
+
return validateApplyData(document, operation);
|
|
194
|
+
case "set_document_title":
|
|
195
|
+
return validateSetDocumentTitle(operation);
|
|
196
|
+
default: {
|
|
197
|
+
const unknown = operation;
|
|
198
|
+
throw new Error(`unsupported operation type ${JSON.stringify(unknown.type)}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function validateAddElement(document, op) {
|
|
203
|
+
const page = requirePage(document, op.pageId);
|
|
204
|
+
assertUniqueIdDocumentWide(document, op.element.id);
|
|
205
|
+
if (op.parentGroupId !== void 0) {
|
|
206
|
+
const { element: parent } = requireElement(page, op.parentGroupId);
|
|
207
|
+
if (parent.kind !== "group") {
|
|
208
|
+
throw new Error(`parentGroupId "${op.parentGroupId}" is a ${parent.kind}, not a group`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (op.index !== void 0) {
|
|
212
|
+
const owner = op.parentGroupId ? (() => {
|
|
213
|
+
const { element: g } = requireElement(page, op.parentGroupId);
|
|
214
|
+
return g.children;
|
|
215
|
+
})() : page.elements;
|
|
216
|
+
if (op.index < 0 || op.index > owner.length) {
|
|
217
|
+
throw new Error(`index ${op.index} out of range (owner has ${owner.length} elements)`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
validateElementAttrs(op.element.kind, op.element, true);
|
|
221
|
+
}
|
|
222
|
+
function validateSetAttrs(document, op) {
|
|
223
|
+
const page = requirePage(document, op.pageId);
|
|
224
|
+
const { element } = requireElement(page, op.elementId);
|
|
225
|
+
const isUnlockOnly = Object.keys(op.attrs).length === 1 && op.attrs.locked === false;
|
|
226
|
+
if (element.locked && !isUnlockOnly) {
|
|
227
|
+
throw new Error(`element "${op.elementId}" is locked; unlock it first (pass attrs: {locked: false}) before making other changes`);
|
|
228
|
+
}
|
|
229
|
+
validateElementAttrs(element.kind, op.attrs, false);
|
|
230
|
+
}
|
|
231
|
+
function validateReorderElement(document, op) {
|
|
232
|
+
const page = requirePage(document, op.pageId);
|
|
233
|
+
const { element, owner } = requireElement(page, op.elementId);
|
|
234
|
+
if (element.locked) {
|
|
235
|
+
throw new Error(`element "${op.elementId}" is locked; unlock it before reordering`);
|
|
236
|
+
}
|
|
237
|
+
if (op.toIndex < 0 || op.toIndex >= owner.length) {
|
|
238
|
+
throw new Error(`toIndex ${op.toIndex} out of range (owner has ${owner.length} elements)`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function validateDeleteElement(document, op) {
|
|
242
|
+
const page = requirePage(document, op.pageId);
|
|
243
|
+
const { element } = requireElement(page, op.elementId);
|
|
244
|
+
if (element.locked) {
|
|
245
|
+
throw new Error(`element "${op.elementId}" is locked; unlock it before deleting`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function validateGroupElements(document, op) {
|
|
249
|
+
if (op.elementIds.length < 2) {
|
|
250
|
+
throw new Error(`group_elements requires \u2265 2 element ids (got ${op.elementIds.length})`);
|
|
251
|
+
}
|
|
252
|
+
assertUniqueIdDocumentWide(document, op.groupId);
|
|
253
|
+
const page = requirePage(document, op.pageId);
|
|
254
|
+
const owners = op.elementIds.map((id) => {
|
|
255
|
+
const { element, owner } = requireElement(page, id);
|
|
256
|
+
if (element.locked) throw new Error(`element "${id}" is locked; unlock before grouping`);
|
|
257
|
+
return owner;
|
|
258
|
+
});
|
|
259
|
+
const firstOwner = owners[0];
|
|
260
|
+
for (let i = 1; i < owners.length; i++) {
|
|
261
|
+
if (owners[i] !== firstOwner) {
|
|
262
|
+
throw new Error(`elements are not siblings \u2014 they must all share the same parent (page root or one group)`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function validateUngroupElement(document, op) {
|
|
267
|
+
const page = requirePage(document, op.pageId);
|
|
268
|
+
const { element } = requireElement(page, op.groupId);
|
|
269
|
+
if (element.kind !== "group") {
|
|
270
|
+
throw new Error(`element "${op.groupId}" is a ${element.kind}, not a group`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function validateAddPage(op) {
|
|
274
|
+
const opts = op.options;
|
|
275
|
+
if (!opts) return;
|
|
276
|
+
if (opts.width !== void 0) assertPositiveFinite(opts.width, "page width");
|
|
277
|
+
if (opts.height !== void 0) assertPositiveFinite(opts.height, "page height");
|
|
278
|
+
if (opts.background !== void 0) assertColor(opts.background, "page background");
|
|
279
|
+
}
|
|
280
|
+
function validateDuplicatePage(document, op) {
|
|
281
|
+
requirePage(document, op.sourcePageId);
|
|
282
|
+
const existing = document.pages.find((p) => p.id === op.pageId);
|
|
283
|
+
if (existing) throw new Error(`pageId "${op.pageId}" already exists in the document`);
|
|
284
|
+
}
|
|
285
|
+
function validateDeletePage(document, op) {
|
|
286
|
+
requirePage(document, op.pageId);
|
|
287
|
+
if (document.pages.length === 1) {
|
|
288
|
+
throw new Error("cannot delete the last remaining page");
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
function validateReorderPage(document, op) {
|
|
292
|
+
requirePage(document, op.pageId);
|
|
293
|
+
if (op.toIndex < 0 || op.toIndex >= document.pages.length) {
|
|
294
|
+
throw new Error(`toIndex ${op.toIndex} out of range (document has ${document.pages.length} pages)`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function validateSetPageProps(document, op) {
|
|
298
|
+
requirePage(document, op.pageId);
|
|
299
|
+
if (op.width !== void 0) assertPositiveFinite(op.width, "page width");
|
|
300
|
+
if (op.height !== void 0) assertPositiveFinite(op.height, "page height");
|
|
301
|
+
if (op.background !== void 0) assertColor(op.background, "page background");
|
|
302
|
+
if (op.bleed != null) {
|
|
303
|
+
assertNonNegativeFinite(op.bleed.top, "bleed.top");
|
|
304
|
+
assertNonNegativeFinite(op.bleed.right, "bleed.right");
|
|
305
|
+
assertNonNegativeFinite(op.bleed.bottom, "bleed.bottom");
|
|
306
|
+
assertNonNegativeFinite(op.bleed.left, "bleed.left");
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function validateSetPageGuides(document, op) {
|
|
310
|
+
requirePage(document, op.pageId);
|
|
311
|
+
for (const pos of op.guides.vertical) {
|
|
312
|
+
if (!Number.isFinite(pos)) throw new Error(`guide position ${pos} is not finite`);
|
|
313
|
+
}
|
|
314
|
+
for (const pos of op.guides.horizontal) {
|
|
315
|
+
if (!Number.isFinite(pos)) throw new Error(`guide position ${pos} is not finite`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function validateBindSlot(document, op) {
|
|
319
|
+
const page = requirePage(document, op.pageId);
|
|
320
|
+
requireElement(page, op.elementId);
|
|
321
|
+
if (op.slot === null) return;
|
|
322
|
+
for (const p of document.pages) {
|
|
323
|
+
const stack = [...p.elements];
|
|
324
|
+
while (stack.length > 0) {
|
|
325
|
+
const el = stack.pop();
|
|
326
|
+
if (el.slot === op.slot && el.id !== op.elementId) {
|
|
327
|
+
throw new Error(`slot "${op.slot}" is already bound to element "${el.id}" on page "${p.id}"`);
|
|
328
|
+
}
|
|
329
|
+
if (el.kind === "group") stack.push(...el.children);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
function validateApplyData(document, op) {
|
|
334
|
+
const slots = collectSlots(document);
|
|
335
|
+
for (const [slotName, value] of Object.entries(op.bindings)) {
|
|
336
|
+
const slot = slots.get(slotName);
|
|
337
|
+
if (!slot) {
|
|
338
|
+
throw new Error(`slot "${slotName}" does not exist in the document`);
|
|
339
|
+
}
|
|
340
|
+
validateSlotValue(slotName, slot.kind, value);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
function validateSetDocumentTitle(op) {
|
|
344
|
+
if (op.title.trim().length === 0) throw new Error("title must be non-empty");
|
|
345
|
+
}
|
|
346
|
+
var BASE_ATTRS = /* @__PURE__ */ new Set([
|
|
347
|
+
"name",
|
|
348
|
+
"x",
|
|
349
|
+
"y",
|
|
350
|
+
"rotation",
|
|
351
|
+
"opacity",
|
|
352
|
+
"locked",
|
|
353
|
+
"visible",
|
|
354
|
+
"slot"
|
|
355
|
+
]);
|
|
356
|
+
var KIND_ATTRS = {
|
|
357
|
+
rect: /* @__PURE__ */ new Set(["width", "height", "fill", "stroke", "strokeWidth", "cornerRadius"]),
|
|
358
|
+
ellipse: /* @__PURE__ */ new Set(["width", "height", "fill", "stroke", "strokeWidth"]),
|
|
359
|
+
line: /* @__PURE__ */ new Set(["points", "stroke", "strokeWidth", "dash"]),
|
|
360
|
+
text: /* @__PURE__ */ new Set(["text", "width", "fontFamily", "fontSize", "fontStyle", "fill", "align", "lineHeight", "letterSpacing"]),
|
|
361
|
+
image: /* @__PURE__ */ new Set(["width", "height", "src", "fit"]),
|
|
362
|
+
video: /* @__PURE__ */ new Set(["width", "height", "src", "posterSrc"]),
|
|
363
|
+
group: /* @__PURE__ */ new Set([])
|
|
364
|
+
};
|
|
365
|
+
var FONT_STYLES = /* @__PURE__ */ new Set(["normal", "bold", "italic", "bold italic"]);
|
|
366
|
+
var ALIGN_VALUES = /* @__PURE__ */ new Set(["left", "center", "right"]);
|
|
367
|
+
var FIT_VALUES = /* @__PURE__ */ new Set(["fill", "cover", "contain"]);
|
|
368
|
+
function validateElementAttrs(kind, attrs, isConstruction) {
|
|
369
|
+
const allowed = KIND_ATTRS[kind];
|
|
370
|
+
for (const key of Object.keys(attrs)) {
|
|
371
|
+
if (key === "id" || key === "kind" || key === "children") continue;
|
|
372
|
+
if (!BASE_ATTRS.has(key) && !allowed.has(key)) {
|
|
373
|
+
throw new Error(`attribute "${key}" is not valid for a ${kind} element`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (attrs.opacity !== void 0) {
|
|
377
|
+
if (typeof attrs.opacity !== "number" || !Number.isFinite(attrs.opacity) || attrs.opacity < 0 || attrs.opacity > 1) {
|
|
378
|
+
throw new Error("opacity must be a number in [0, 1]");
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (attrs.x !== void 0) assertFinite(attrs.x, "x");
|
|
382
|
+
if (attrs.y !== void 0) assertFinite(attrs.y, "y");
|
|
383
|
+
if (attrs.rotation !== void 0) assertFinite(attrs.rotation, "rotation");
|
|
384
|
+
switch (kind) {
|
|
385
|
+
case "rect":
|
|
386
|
+
case "ellipse":
|
|
387
|
+
case "image":
|
|
388
|
+
case "video":
|
|
389
|
+
if (attrs.width !== void 0) assertPositiveFinite(attrs.width, "width");
|
|
390
|
+
if (attrs.height !== void 0) assertPositiveFinite(attrs.height, "height");
|
|
391
|
+
break;
|
|
392
|
+
case "text":
|
|
393
|
+
if (attrs.width !== void 0) assertPositiveFinite(attrs.width, "width");
|
|
394
|
+
if (attrs.fontSize !== void 0) assertPositiveFinite(attrs.fontSize, "fontSize");
|
|
395
|
+
if (attrs.lineHeight !== void 0) {
|
|
396
|
+
if (typeof attrs.lineHeight !== "number" || !Number.isFinite(attrs.lineHeight) || attrs.lineHeight <= 0) {
|
|
397
|
+
throw new Error("lineHeight must be a positive finite number");
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (attrs.fontStyle !== void 0 && !FONT_STYLES.has(attrs.fontStyle)) {
|
|
401
|
+
throw new Error(`fontStyle must be one of: ${[...FONT_STYLES].join(", ")}`);
|
|
402
|
+
}
|
|
403
|
+
if (attrs.align !== void 0 && !ALIGN_VALUES.has(attrs.align)) {
|
|
404
|
+
throw new Error(`align must be one of: ${[...ALIGN_VALUES].join(", ")}`);
|
|
405
|
+
}
|
|
406
|
+
break;
|
|
407
|
+
case "line":
|
|
408
|
+
if (attrs.points !== void 0) {
|
|
409
|
+
if (!Array.isArray(attrs.points) || attrs.points.length < 4 || attrs.points.length % 2 !== 0) {
|
|
410
|
+
throw new Error("points must be an even-length array with at least 4 numbers (2 points)");
|
|
411
|
+
}
|
|
412
|
+
for (let i = 0; i < attrs.points.length; i++) {
|
|
413
|
+
if (!Number.isFinite(attrs.points[i])) {
|
|
414
|
+
throw new Error(`points[${i}] is not finite`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
if (attrs.strokeWidth !== void 0) assertPositiveFinite(attrs.strokeWidth, "strokeWidth");
|
|
419
|
+
break;
|
|
420
|
+
case "group":
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
if (attrs.fill !== void 0) assertColor(attrs.fill, "fill");
|
|
424
|
+
if (attrs.stroke !== void 0) assertColor(attrs.stroke, "stroke");
|
|
425
|
+
if (attrs.strokeWidth !== void 0 && kind !== "line") {
|
|
426
|
+
assertPositiveFinite(attrs.strokeWidth, "strokeWidth");
|
|
427
|
+
}
|
|
428
|
+
if (attrs.src !== void 0) assertSceneMediaSrc(attrs.src, "src");
|
|
429
|
+
if (attrs.posterSrc !== void 0) {
|
|
430
|
+
assertSceneMediaSrc(attrs.posterSrc, "posterSrc");
|
|
431
|
+
}
|
|
432
|
+
if (attrs.fit !== void 0 && !FIT_VALUES.has(attrs.fit)) {
|
|
433
|
+
throw new Error(`fit must be one of: ${[...FIT_VALUES].join(", ")}`);
|
|
434
|
+
}
|
|
435
|
+
if (isConstruction) {
|
|
436
|
+
validateRequiredConstructionAttrs(kind, attrs);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
function validateRequiredConstructionAttrs(kind, attrs) {
|
|
440
|
+
switch (kind) {
|
|
441
|
+
case "rect":
|
|
442
|
+
requireAttrPresent(attrs, "width", kind);
|
|
443
|
+
requireAttrPresent(attrs, "height", kind);
|
|
444
|
+
requireAttrPresent(attrs, "fill", kind);
|
|
445
|
+
break;
|
|
446
|
+
case "ellipse":
|
|
447
|
+
requireAttrPresent(attrs, "width", kind);
|
|
448
|
+
requireAttrPresent(attrs, "height", kind);
|
|
449
|
+
requireAttrPresent(attrs, "fill", kind);
|
|
450
|
+
break;
|
|
451
|
+
case "line":
|
|
452
|
+
requireAttrPresent(attrs, "points", kind);
|
|
453
|
+
requireAttrPresent(attrs, "stroke", kind);
|
|
454
|
+
requireAttrPresent(attrs, "strokeWidth", kind);
|
|
455
|
+
break;
|
|
456
|
+
case "text":
|
|
457
|
+
requireAttrPresent(attrs, "text", kind);
|
|
458
|
+
requireAttrPresent(attrs, "width", kind);
|
|
459
|
+
requireAttrPresent(attrs, "fontFamily", kind);
|
|
460
|
+
requireAttrPresent(attrs, "fontSize", kind);
|
|
461
|
+
requireAttrPresent(attrs, "fontStyle", kind);
|
|
462
|
+
requireAttrPresent(attrs, "fill", kind);
|
|
463
|
+
requireAttrPresent(attrs, "align", kind);
|
|
464
|
+
requireAttrPresent(attrs, "lineHeight", kind);
|
|
465
|
+
requireAttrPresent(attrs, "letterSpacing", kind);
|
|
466
|
+
break;
|
|
467
|
+
case "image":
|
|
468
|
+
requireAttrPresent(attrs, "width", kind);
|
|
469
|
+
requireAttrPresent(attrs, "height", kind);
|
|
470
|
+
requireAttrPresent(attrs, "src", kind);
|
|
471
|
+
requireAttrPresent(attrs, "fit", kind);
|
|
472
|
+
break;
|
|
473
|
+
case "video":
|
|
474
|
+
requireAttrPresent(attrs, "width", kind);
|
|
475
|
+
requireAttrPresent(attrs, "height", kind);
|
|
476
|
+
requireAttrPresent(attrs, "src", kind);
|
|
477
|
+
break;
|
|
478
|
+
case "group":
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
function requireAttrPresent(attrs, key, kind) {
|
|
483
|
+
if (attrs[key] === void 0) {
|
|
484
|
+
throw new Error(`${key} is required when constructing a ${kind} element`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
function validateSlotValue(slotName, elementKind, value) {
|
|
488
|
+
switch (elementKind) {
|
|
489
|
+
case "text":
|
|
490
|
+
return;
|
|
491
|
+
case "image":
|
|
492
|
+
case "video":
|
|
493
|
+
try {
|
|
494
|
+
assertSceneMediaSrc(value, `slot "${slotName}" value`);
|
|
495
|
+
} catch (e) {
|
|
496
|
+
throw new Error(
|
|
497
|
+
`slot "${slotName}" is bound to a ${elementKind} element \u2014 value must be an http(s) URL or a rooted /api/ path (got "${value}")`
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
return;
|
|
501
|
+
case "rect":
|
|
502
|
+
case "ellipse":
|
|
503
|
+
case "line":
|
|
504
|
+
case "group":
|
|
505
|
+
try {
|
|
506
|
+
assertColor(value, `slot "${slotName}" value`);
|
|
507
|
+
} catch {
|
|
508
|
+
throw new Error(
|
|
509
|
+
`slot "${slotName}" is bound to a ${elementKind} element \u2014 value must be a color (hex/rgb(a)/transparent) for fill/stroke recolor (got "${value}")`
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
function assertUniqueIdDocumentWide(document, id) {
|
|
516
|
+
for (const page of document.pages) {
|
|
517
|
+
if (page.id === id) throw new Error(`id "${id}" is already used by a page`);
|
|
518
|
+
const stack = [...page.elements];
|
|
519
|
+
while (stack.length > 0) {
|
|
520
|
+
const el = stack.pop();
|
|
521
|
+
if (el.id === id) throw new Error(`id "${id}" is already used by element on page "${page.id}"`);
|
|
522
|
+
if (el.kind === "group") stack.push(...el.children);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
function assertNonNegativeFinite(value, label) {
|
|
527
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
528
|
+
throw new Error(`${label} must be a non-negative finite number`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// src/design-canvas/apply.ts
|
|
533
|
+
function applySceneOperations(document, operations, options) {
|
|
534
|
+
const opts = options ?? { mintId: makeCounter() };
|
|
535
|
+
const doc = deepCloneDocument(document);
|
|
536
|
+
const results = [];
|
|
537
|
+
for (const operation of operations) {
|
|
538
|
+
results.push(applyOneOperation(doc, operation, opts));
|
|
539
|
+
}
|
|
540
|
+
return options !== void 0 ? { document: doc, results } : doc;
|
|
541
|
+
}
|
|
542
|
+
function applySceneOperation(document, operation) {
|
|
543
|
+
return applySceneOperations(document, [operation]);
|
|
544
|
+
}
|
|
545
|
+
function makeCounter() {
|
|
546
|
+
let n = 0;
|
|
547
|
+
return () => `minted-${n += 1}`;
|
|
548
|
+
}
|
|
549
|
+
function isStaleRevError(err) {
|
|
550
|
+
return err instanceof Error && /stale rev/i.test(err.message);
|
|
551
|
+
}
|
|
552
|
+
async function storeApplyScenePlan(store, plan, opts) {
|
|
553
|
+
let { document, rev } = await store.getDocument();
|
|
554
|
+
validateSceneOperations(document, plan.operations);
|
|
555
|
+
let applied = applySceneOperations(document, plan.operations, { mintId: opts.mintId });
|
|
556
|
+
let record;
|
|
557
|
+
try {
|
|
558
|
+
record = await store.saveDocument(applied.document, rev);
|
|
559
|
+
} catch (firstError) {
|
|
560
|
+
if (!isStaleRevError(firstError)) throw firstError;
|
|
561
|
+
const refreshed = await store.getDocument();
|
|
562
|
+
validateSceneOperations(refreshed.document, plan.operations);
|
|
563
|
+
applied = applySceneOperations(refreshed.document, plan.operations, { mintId: opts.mintId });
|
|
564
|
+
try {
|
|
565
|
+
record = await store.saveDocument(applied.document, refreshed.rev);
|
|
566
|
+
} catch (secondError) {
|
|
567
|
+
const reason = secondError instanceof Error ? secondError.message : String(secondError);
|
|
568
|
+
throw new Error(`storeApplyScenePlan: stale rev persists after retry \u2014 ${reason}`);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
const opTypeCounts = {};
|
|
572
|
+
for (const op of plan.operations) {
|
|
573
|
+
opTypeCounts[op.type] = (opTypeCounts[op.type] ?? 0) + 1;
|
|
574
|
+
}
|
|
575
|
+
await store.recordDecision({
|
|
576
|
+
kind: opts.actorKind,
|
|
577
|
+
instruction: plan.summary,
|
|
578
|
+
metadata: { opTypeCounts, operationCount: plan.operations.length }
|
|
579
|
+
});
|
|
580
|
+
return { record, results: applied.results };
|
|
581
|
+
}
|
|
582
|
+
function applyOneOperation(doc, operation, options) {
|
|
583
|
+
switch (operation.type) {
|
|
584
|
+
case "add_element":
|
|
585
|
+
return applyAddElement(doc, operation);
|
|
586
|
+
case "set_attrs":
|
|
587
|
+
return applySetAttrs(doc, operation);
|
|
588
|
+
case "reorder_element":
|
|
589
|
+
return applyReorderElement(doc, operation);
|
|
590
|
+
case "delete_element":
|
|
591
|
+
return applyDeleteElement(doc, operation);
|
|
592
|
+
case "group_elements":
|
|
593
|
+
return applyGroupElements(doc, operation);
|
|
594
|
+
case "ungroup_element":
|
|
595
|
+
return applyUngroupElement(doc, operation);
|
|
596
|
+
case "add_page":
|
|
597
|
+
return applyAddPage(doc, operation);
|
|
598
|
+
case "duplicate_page":
|
|
599
|
+
return applyDuplicatePage(doc, operation, options);
|
|
600
|
+
case "delete_page":
|
|
601
|
+
return applyDeletePage(doc, operation);
|
|
602
|
+
case "reorder_page":
|
|
603
|
+
return applyReorderPage(doc, operation);
|
|
604
|
+
case "set_page_props":
|
|
605
|
+
return applySetPageProps(doc, operation);
|
|
606
|
+
case "set_page_guides":
|
|
607
|
+
return applySetPageGuides(doc, operation);
|
|
608
|
+
case "bind_slot":
|
|
609
|
+
return applyBindSlot(doc, operation);
|
|
610
|
+
case "apply_data":
|
|
611
|
+
return applyApplyData(doc, operation);
|
|
612
|
+
case "set_document_title":
|
|
613
|
+
return applySetDocumentTitle(doc, operation);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
function applyAddElement(doc, op) {
|
|
617
|
+
const page = requirePage(doc, op.pageId);
|
|
618
|
+
const owner = op.parentGroupId !== void 0 ? (() => {
|
|
619
|
+
const { element: g } = requireElement(page, op.parentGroupId);
|
|
620
|
+
return g.children;
|
|
621
|
+
})() : page.elements;
|
|
622
|
+
const index = op.index !== void 0 ? op.index : owner.length;
|
|
623
|
+
owner.splice(index, 0, op.element);
|
|
624
|
+
return { kind: "element", pageId: op.pageId, element: op.element };
|
|
625
|
+
}
|
|
626
|
+
function applySetAttrs(doc, op) {
|
|
627
|
+
const page = requirePage(doc, op.pageId);
|
|
628
|
+
const { element, owner, index } = requireElement(page, op.elementId);
|
|
629
|
+
const patched = { ...element, ...op.attrs };
|
|
630
|
+
owner[index] = patched;
|
|
631
|
+
return { kind: "element", pageId: op.pageId, element: patched };
|
|
632
|
+
}
|
|
633
|
+
function applyReorderElement(doc, op) {
|
|
634
|
+
const page = requirePage(doc, op.pageId);
|
|
635
|
+
const { element, owner, index } = requireElement(page, op.elementId);
|
|
636
|
+
owner.splice(index, 1);
|
|
637
|
+
owner.splice(op.toIndex, 0, element);
|
|
638
|
+
return { kind: "element", pageId: op.pageId, element };
|
|
639
|
+
}
|
|
640
|
+
function applyDeleteElement(doc, op) {
|
|
641
|
+
const page = requirePage(doc, op.pageId);
|
|
642
|
+
const { element, owner, index } = requireElement(page, op.elementId);
|
|
643
|
+
owner.splice(index, 1);
|
|
644
|
+
return { kind: "element", pageId: op.pageId, element };
|
|
645
|
+
}
|
|
646
|
+
function applyGroupElements(doc, op) {
|
|
647
|
+
const page = requirePage(doc, op.pageId);
|
|
648
|
+
const members = op.elementIds.map((id) => requireElement(page, id));
|
|
649
|
+
let minX = Infinity, minY = Infinity;
|
|
650
|
+
for (const { element } of members) {
|
|
651
|
+
const aabb = elementAabb(element);
|
|
652
|
+
if (aabb.x < minX) minX = aabb.x;
|
|
653
|
+
if (aabb.y < minY) minY = aabb.y;
|
|
654
|
+
}
|
|
655
|
+
const owner = members[0].owner;
|
|
656
|
+
const sortedByIndex = [...members].sort((a, b) => a.index - b.index);
|
|
657
|
+
const children = sortedByIndex.map(({ element }) => ({
|
|
658
|
+
...element,
|
|
659
|
+
x: element.x - minX,
|
|
660
|
+
y: element.y - minY
|
|
661
|
+
}));
|
|
662
|
+
for (const { index } of [...sortedByIndex].reverse()) {
|
|
663
|
+
owner.splice(index, 1);
|
|
664
|
+
}
|
|
665
|
+
const insertAt = sortedByIndex[0].index;
|
|
666
|
+
const group = {
|
|
667
|
+
id: op.groupId,
|
|
668
|
+
kind: "group",
|
|
669
|
+
name: op.name ?? "Group",
|
|
670
|
+
x: minX,
|
|
671
|
+
y: minY,
|
|
672
|
+
rotation: 0,
|
|
673
|
+
opacity: 1,
|
|
674
|
+
locked: false,
|
|
675
|
+
visible: true,
|
|
676
|
+
children
|
|
677
|
+
};
|
|
678
|
+
owner.splice(insertAt, 0, group);
|
|
679
|
+
return { kind: "element", pageId: op.pageId, element: group };
|
|
680
|
+
}
|
|
681
|
+
function applyUngroupElement(doc, op) {
|
|
682
|
+
const page = requirePage(doc, op.pageId);
|
|
683
|
+
const { element: groupEl, owner, index: groupIndex } = requireElement(page, op.groupId);
|
|
684
|
+
const group = groupEl;
|
|
685
|
+
const promoted = group.children.map((child) => ({
|
|
686
|
+
...child,
|
|
687
|
+
x: child.x + group.x,
|
|
688
|
+
y: child.y + group.y
|
|
689
|
+
}));
|
|
690
|
+
owner.splice(groupIndex, 1, ...promoted);
|
|
691
|
+
return { kind: "element", pageId: op.pageId, element: groupEl };
|
|
692
|
+
}
|
|
693
|
+
function applyAddPage(doc, op) {
|
|
694
|
+
const opts = op.options ?? {};
|
|
695
|
+
const page = createPage(opts, op.pageId);
|
|
696
|
+
const index = op.index !== void 0 ? op.index : doc.pages.length;
|
|
697
|
+
doc.pages.splice(index, 0, page);
|
|
698
|
+
return { kind: "page", page };
|
|
699
|
+
}
|
|
700
|
+
function applyDuplicatePage(doc, op, options) {
|
|
701
|
+
const source = requirePage(doc, op.sourcePageId);
|
|
702
|
+
const copy = JSON.parse(JSON.stringify(source));
|
|
703
|
+
copy.id = op.pageId;
|
|
704
|
+
remintElementIds(copy.elements, options.mintId);
|
|
705
|
+
doc.pages.push(copy);
|
|
706
|
+
return { kind: "page", page: copy };
|
|
707
|
+
}
|
|
708
|
+
function remintElementIds(elements, mintId) {
|
|
709
|
+
for (let i = 0; i < elements.length; i++) {
|
|
710
|
+
const el = elements[i];
|
|
711
|
+
const newEl = { ...el, id: mintId() };
|
|
712
|
+
if (newEl.kind === "group") {
|
|
713
|
+
remintElementIds(newEl.children, mintId);
|
|
714
|
+
}
|
|
715
|
+
elements[i] = newEl;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
function applyDeletePage(doc, op) {
|
|
719
|
+
if (doc.pages.length <= 1) throw new Error("delete_page: cannot delete the last page");
|
|
720
|
+
const index = doc.pages.findIndex((p) => p.id === op.pageId);
|
|
721
|
+
if (index < 0) throw new Error(`page ${op.pageId} not found`);
|
|
722
|
+
const [page] = doc.pages.splice(index, 1);
|
|
723
|
+
return { kind: "page", page };
|
|
724
|
+
}
|
|
725
|
+
function applyReorderPage(doc, op) {
|
|
726
|
+
const index = doc.pages.findIndex((p) => p.id === op.pageId);
|
|
727
|
+
if (index < 0) throw new Error(`page ${op.pageId} not found`);
|
|
728
|
+
const [page] = doc.pages.splice(index, 1);
|
|
729
|
+
doc.pages.splice(op.toIndex, 0, page);
|
|
730
|
+
return { kind: "page", page };
|
|
731
|
+
}
|
|
732
|
+
function applySetPageProps(doc, op) {
|
|
733
|
+
const page = requirePage(doc, op.pageId);
|
|
734
|
+
if (op.name !== void 0) page.name = op.name;
|
|
735
|
+
if (op.width !== void 0) page.width = op.width;
|
|
736
|
+
if (op.height !== void 0) page.height = op.height;
|
|
737
|
+
if (op.background !== void 0) page.background = op.background;
|
|
738
|
+
if (op.bleed !== void 0) page.bleed = op.bleed;
|
|
739
|
+
return { kind: "page", page };
|
|
740
|
+
}
|
|
741
|
+
function applySetPageGuides(doc, op) {
|
|
742
|
+
const page = requirePage(doc, op.pageId);
|
|
743
|
+
page.guides = op.guides;
|
|
744
|
+
return { kind: "page", page };
|
|
745
|
+
}
|
|
746
|
+
function applyBindSlot(doc, op) {
|
|
747
|
+
const page = requirePage(doc, op.pageId);
|
|
748
|
+
const { element, owner, index } = requireElement(page, op.elementId);
|
|
749
|
+
const patched = op.slot === null ? omitSlot(element) : { ...element, slot: op.slot };
|
|
750
|
+
owner[index] = patched;
|
|
751
|
+
return { kind: "element", pageId: op.pageId, element: patched };
|
|
752
|
+
}
|
|
753
|
+
function omitSlot(element) {
|
|
754
|
+
const { slot: _slot, ...rest } = element;
|
|
755
|
+
return rest;
|
|
756
|
+
}
|
|
757
|
+
function applyApplyData(doc, op) {
|
|
758
|
+
const slots = collectSlots(doc);
|
|
759
|
+
for (const [slotName, value] of Object.entries(op.bindings)) {
|
|
760
|
+
const slot = slots.get(slotName);
|
|
761
|
+
if (!slot) throw new Error(`slot "${slotName}" not found in document`);
|
|
762
|
+
const page = requirePage(doc, slot.pageId);
|
|
763
|
+
const { element, owner, index } = requireElement(page, slot.elementId);
|
|
764
|
+
owner[index] = applySlotValue(element, value);
|
|
765
|
+
}
|
|
766
|
+
return { kind: "document" };
|
|
767
|
+
}
|
|
768
|
+
function applySlotValue(element, value) {
|
|
769
|
+
switch (element.kind) {
|
|
770
|
+
case "text":
|
|
771
|
+
return { ...element, text: value };
|
|
772
|
+
case "image":
|
|
773
|
+
case "video":
|
|
774
|
+
assertSceneMediaSrc(value, "slot value");
|
|
775
|
+
return { ...element, src: value };
|
|
776
|
+
case "rect":
|
|
777
|
+
case "ellipse":
|
|
778
|
+
assertColor(value, "slot value");
|
|
779
|
+
return { ...element, fill: value };
|
|
780
|
+
case "line":
|
|
781
|
+
assertColor(value, "slot value");
|
|
782
|
+
return { ...element, stroke: value };
|
|
783
|
+
case "group":
|
|
784
|
+
assertColor(value, "slot value");
|
|
785
|
+
return recolorGroupChildren(element, value);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
function recolorGroupChildren(group, color) {
|
|
789
|
+
const children = group.children.map((child) => {
|
|
790
|
+
switch (child.kind) {
|
|
791
|
+
case "rect":
|
|
792
|
+
case "ellipse":
|
|
793
|
+
return { ...child, fill: color };
|
|
794
|
+
case "line":
|
|
795
|
+
return { ...child, stroke: color };
|
|
796
|
+
case "text":
|
|
797
|
+
return { ...child, fill: color };
|
|
798
|
+
case "image":
|
|
799
|
+
case "video":
|
|
800
|
+
return child;
|
|
801
|
+
case "group":
|
|
802
|
+
return recolorGroupChildren(child, color);
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
return { ...group, children };
|
|
806
|
+
}
|
|
807
|
+
function applySetDocumentTitle(doc, op) {
|
|
808
|
+
doc.title = op.title;
|
|
809
|
+
return { kind: "document" };
|
|
810
|
+
}
|
|
811
|
+
function deepCloneDocument(document) {
|
|
812
|
+
return JSON.parse(JSON.stringify(document));
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// src/design-canvas/export-presets.ts
|
|
816
|
+
var SIZE_PRESETS = [
|
|
817
|
+
// Social
|
|
818
|
+
{ id: "instagram-square", label: "Instagram \u2014 Square", category: "social", width: 1080, height: 1080 },
|
|
819
|
+
{ id: "instagram-portrait", label: "Instagram \u2014 Portrait", category: "social", width: 1080, height: 1350 },
|
|
820
|
+
{ id: "instagram-story", label: "Instagram Story", category: "social", width: 1080, height: 1920 },
|
|
821
|
+
{ id: "twitter-post", label: "X / Twitter Post", category: "social", width: 1200, height: 675 },
|
|
822
|
+
{ id: "linkedin-post", label: "LinkedIn Post", category: "social", width: 1200, height: 627 },
|
|
823
|
+
{ id: "facebook-post", label: "Facebook Post", category: "social", width: 1200, height: 630 },
|
|
824
|
+
{ id: "youtube-thumbnail", label: "YouTube Thumbnail", category: "social", width: 1280, height: 720 },
|
|
825
|
+
{ id: "og-image", label: "Open Graph Image", category: "social", width: 1200, height: 630 },
|
|
826
|
+
// Presentation
|
|
827
|
+
{ id: "slide-16-9", label: "Slide \u2014 16:9", category: "presentation", width: 1920, height: 1080 },
|
|
828
|
+
{ id: "slide-4-3", label: "Slide \u2014 4:3", category: "presentation", width: 1024, height: 768 },
|
|
829
|
+
// Print (96 DPI px equivalents of A4, Letter — products may scale at export)
|
|
830
|
+
{ id: "a4-landscape", label: "A4 Landscape", category: "print", width: 1123, height: 794 },
|
|
831
|
+
{ id: "a4-portrait", label: "A4 Portrait", category: "print", width: 794, height: 1123 },
|
|
832
|
+
{ id: "us-letter-landscape", label: "US Letter Landscape", category: "print", width: 1100, height: 850 },
|
|
833
|
+
{ id: "us-letter-portrait", label: "US Letter Portrait", category: "print", width: 850, height: 1100 }
|
|
834
|
+
];
|
|
835
|
+
function findPreset(id) {
|
|
836
|
+
return SIZE_PRESETS.find((p) => p.id === id) ?? null;
|
|
837
|
+
}
|
|
838
|
+
function matchPreset(width, height) {
|
|
839
|
+
return SIZE_PRESETS.find((p) => p.width === width && p.height === height) ?? null;
|
|
840
|
+
}
|
|
841
|
+
var EXPORT_PRESETS = {
|
|
842
|
+
"instagram-square": {
|
|
843
|
+
name: "Instagram square (1080\xD71080)",
|
|
844
|
+
pixelRatio: 1,
|
|
845
|
+
outputWidth: 1080,
|
|
846
|
+
outputHeight: 1080,
|
|
847
|
+
includeBleed: false,
|
|
848
|
+
format: "jpeg"
|
|
849
|
+
},
|
|
850
|
+
"instagram-portrait": {
|
|
851
|
+
name: "Instagram portrait (1080\xD71350)",
|
|
852
|
+
pixelRatio: 1,
|
|
853
|
+
outputWidth: 1080,
|
|
854
|
+
outputHeight: 1350,
|
|
855
|
+
includeBleed: false,
|
|
856
|
+
format: "jpeg"
|
|
857
|
+
},
|
|
858
|
+
"twitter-card": {
|
|
859
|
+
name: "Twitter/X card (1200\xD7675)",
|
|
860
|
+
pixelRatio: 1,
|
|
861
|
+
outputWidth: 1200,
|
|
862
|
+
outputHeight: 675,
|
|
863
|
+
includeBleed: false,
|
|
864
|
+
format: "jpeg"
|
|
865
|
+
},
|
|
866
|
+
"og-image": {
|
|
867
|
+
name: "OG image (1200\xD7630)",
|
|
868
|
+
pixelRatio: 1,
|
|
869
|
+
outputWidth: 1200,
|
|
870
|
+
outputHeight: 630,
|
|
871
|
+
includeBleed: false,
|
|
872
|
+
format: "png"
|
|
873
|
+
},
|
|
874
|
+
"print-a4": {
|
|
875
|
+
name: "Print A4 (300 dpi)",
|
|
876
|
+
pixelRatio: 3.125,
|
|
877
|
+
outputWidth: null,
|
|
878
|
+
outputHeight: null,
|
|
879
|
+
includeBleed: true,
|
|
880
|
+
format: "png"
|
|
881
|
+
},
|
|
882
|
+
"screen-2x": {
|
|
883
|
+
name: "Screen @2\xD7",
|
|
884
|
+
pixelRatio: 2,
|
|
885
|
+
outputWidth: null,
|
|
886
|
+
outputHeight: null,
|
|
887
|
+
includeBleed: false,
|
|
888
|
+
format: "png"
|
|
889
|
+
}
|
|
890
|
+
};
|
|
891
|
+
function bleedAwareExportBounds(page, includeBleed) {
|
|
892
|
+
if (!includeBleed || page.bleed === null) {
|
|
893
|
+
return { x: 0, y: 0, width: page.width, height: page.height };
|
|
894
|
+
}
|
|
895
|
+
const bleed = page.bleed;
|
|
896
|
+
return {
|
|
897
|
+
x: -bleed.left,
|
|
898
|
+
y: -bleed.top,
|
|
899
|
+
width: page.width + bleed.left + bleed.right,
|
|
900
|
+
height: page.height + bleed.top + bleed.bottom
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
function scaleForPreset(preset, cropRect) {
|
|
904
|
+
if (preset.outputWidth !== null) {
|
|
905
|
+
if (cropRect.width <= 0) {
|
|
906
|
+
throw new Error(`export crop width must be positive, got ${cropRect.width}`);
|
|
907
|
+
}
|
|
908
|
+
return preset.outputWidth / cropRect.width;
|
|
909
|
+
}
|
|
910
|
+
return preset.pixelRatio;
|
|
911
|
+
}
|
|
912
|
+
var CHANNEL_PRESETS = [
|
|
913
|
+
{ id: "square_1080", label: "Square (1080\xD71080)", width: 1080, height: 1080 },
|
|
914
|
+
{ id: "portrait_1080x1350", label: "Portrait (1080\xD71350)", width: 1080, height: 1350 },
|
|
915
|
+
{ id: "story_1080x1920", label: "Story (1080\xD71920)", width: 1080, height: 1920 },
|
|
916
|
+
{ id: "landscape_1200x628", label: "Landscape (1200\xD7628)", width: 1200, height: 628 },
|
|
917
|
+
{ id: "wide_1920x1080", label: "Wide (1920\xD71080)", width: 1920, height: 1080 },
|
|
918
|
+
{ id: "og_1200x630", label: "Open Graph (1200\xD7630)", width: 1200, height: 630 },
|
|
919
|
+
{ id: "a4_print_2480x3508", label: "A4 Print (2480\xD73508 \xB7 300 dpi)", width: 2480, height: 3508 }
|
|
920
|
+
];
|
|
921
|
+
function requireChannelPreset(id) {
|
|
922
|
+
const found = CHANNEL_PRESETS.find((p) => p.id === id);
|
|
923
|
+
if (!found) {
|
|
924
|
+
throw new Error(
|
|
925
|
+
`unknown channel preset "${id}" \u2014 valid ids: ${CHANNEL_PRESETS.map((p) => p.id).join(", ")}`
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
return found;
|
|
929
|
+
}
|
|
930
|
+
function scalePageForChannelPreset(page, channelPreset) {
|
|
931
|
+
if (page.width <= 0 || page.height <= 0) {
|
|
932
|
+
throw new Error(`page dimensions must be positive; got ${page.width}\xD7${page.height}`);
|
|
933
|
+
}
|
|
934
|
+
const scaleX = channelPreset.width / page.width;
|
|
935
|
+
const scaleY = channelPreset.height / page.height;
|
|
936
|
+
const pixelRatio = Math.min(scaleX, scaleY);
|
|
937
|
+
const renderedW = page.width * pixelRatio;
|
|
938
|
+
const renderedH = page.height * pixelRatio;
|
|
939
|
+
const offsetX = (channelPreset.width - renderedW) / 2 / pixelRatio;
|
|
940
|
+
const offsetY = (channelPreset.height - renderedH) / 2 / pixelRatio;
|
|
941
|
+
return { pixelRatio, offsetX, offsetY, fit: "contain" };
|
|
942
|
+
}
|
|
943
|
+
function bleedAwareExportRect(page) {
|
|
944
|
+
if (page.bleed === null) {
|
|
945
|
+
return { x: 0, y: 0, width: page.width, height: page.height };
|
|
946
|
+
}
|
|
947
|
+
const bleed = page.bleed;
|
|
948
|
+
return {
|
|
949
|
+
x: -bleed.left,
|
|
950
|
+
y: -bleed.top,
|
|
951
|
+
width: page.width + bleed.left + bleed.right,
|
|
952
|
+
height: page.height + bleed.top + bleed.bottom
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
export {
|
|
957
|
+
SCENE_SCHEMA_VERSION,
|
|
958
|
+
SCENE_ELEMENT_KINDS,
|
|
959
|
+
elementExtent,
|
|
960
|
+
estimateTextHeight,
|
|
961
|
+
elementAabb,
|
|
962
|
+
boundsIntersect,
|
|
963
|
+
requirePage,
|
|
964
|
+
findElement,
|
|
965
|
+
requireElement,
|
|
966
|
+
collectSlots,
|
|
967
|
+
createEmptyDocument,
|
|
968
|
+
createPage,
|
|
969
|
+
assertPositiveFinite,
|
|
970
|
+
assertFinite,
|
|
971
|
+
assertColor,
|
|
972
|
+
assertSceneMediaSrc,
|
|
973
|
+
validateSceneOperations,
|
|
974
|
+
validateSceneOperation,
|
|
975
|
+
validateSlotValue,
|
|
976
|
+
applySceneOperations,
|
|
977
|
+
applySceneOperation,
|
|
978
|
+
storeApplyScenePlan,
|
|
979
|
+
SIZE_PRESETS,
|
|
980
|
+
findPreset,
|
|
981
|
+
matchPreset,
|
|
982
|
+
EXPORT_PRESETS,
|
|
983
|
+
bleedAwareExportBounds,
|
|
984
|
+
scaleForPreset,
|
|
985
|
+
CHANNEL_PRESETS,
|
|
986
|
+
requireChannelPreset,
|
|
987
|
+
scalePageForChannelPreset,
|
|
988
|
+
bleedAwareExportRect
|
|
989
|
+
};
|
|
990
|
+
//# sourceMappingURL=chunk-JZAJE3JL.js.map
|