@tangle-network/agent-app 0.12.0 → 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/chunk-2Q73HGDI.js +1743 -0
- package/dist/chunk-2Q73HGDI.js.map +1 -0
- package/dist/{chunk-3WAJWYKD.js → chunk-6UOE5CTA.js} +9 -92
- package/dist/{chunk-3WAJWYKD.js.map → chunk-6UOE5CTA.js.map} +1 -1
- 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-CF5DZELC.js → chunk-ABGSFUJQ.js} +2 -2
- package/dist/chunk-F5KTWRO7.js +2276 -0
- package/dist/chunk-F5KTWRO7.js.map +1 -0
- package/dist/chunk-JZAJE3JL.js +990 -0
- package/dist/chunk-JZAJE3JL.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 +5 -0
- package/dist/index.js +99 -3
- package/dist/mcp-rpc-DLw_r9PQ.d.ts +55 -0
- package/dist/model-BHLN208Z.d.ts +183 -0
- package/dist/sequences/index.d.ts +4 -0
- package/dist/sequences/index.js +2 -2
- package/dist/store-CUStmtdH.d.ts +64 -0
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.js +6 -2
- package/package.json +28 -3
- package/dist/chunk-IJZJWKUK.js +0 -77
- package/dist/chunk-IJZJWKUK.js.map +0 -1
- /package/dist/{chunk-CF5DZELC.js.map → chunk-ABGSFUJQ.js.map} +0 -0
|
@@ -0,0 +1,1743 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BleedTrimOverlay,
|
|
3
|
+
DesignCanvas,
|
|
4
|
+
addElementCommand,
|
|
5
|
+
createSceneCommandStack,
|
|
6
|
+
deleteElementCommand,
|
|
7
|
+
groupElementsCommand,
|
|
8
|
+
multiSetAttrsCommand,
|
|
9
|
+
setAttrsCommand,
|
|
10
|
+
ungroupElementCommand
|
|
11
|
+
} from "./chunk-F5KTWRO7.js";
|
|
12
|
+
import {
|
|
13
|
+
boundsIntersect,
|
|
14
|
+
elementAabb,
|
|
15
|
+
findElement
|
|
16
|
+
} from "./chunk-JZAJE3JL.js";
|
|
17
|
+
|
|
18
|
+
// src/design-canvas-react/components/Workspace.tsx
|
|
19
|
+
import {
|
|
20
|
+
useCallback,
|
|
21
|
+
useEffect as useEffect4,
|
|
22
|
+
useLayoutEffect,
|
|
23
|
+
useMemo,
|
|
24
|
+
useRef as useRef4,
|
|
25
|
+
useState as useState2
|
|
26
|
+
} from "react";
|
|
27
|
+
import { Stage, Layer as Layer4, Rect as Rect2, Group as Group2 } from "react-konva";
|
|
28
|
+
|
|
29
|
+
// src/design-canvas-react/engine/snap.ts
|
|
30
|
+
var KIND_PRIORITY = {
|
|
31
|
+
"guide": 0,
|
|
32
|
+
"page-edge": 1,
|
|
33
|
+
"page-center": 2,
|
|
34
|
+
"element-edge": 3,
|
|
35
|
+
"element-center": 4,
|
|
36
|
+
"grid": 5
|
|
37
|
+
};
|
|
38
|
+
function createSnapEngine() {
|
|
39
|
+
return {
|
|
40
|
+
collectTargets(state, excludeIds) {
|
|
41
|
+
const page = state.document.pages.find((p) => p.id === state.activePageId);
|
|
42
|
+
if (!page) throw new Error(`collectTargets: active page ${state.activePageId} not found`);
|
|
43
|
+
const vertical = [];
|
|
44
|
+
const horizontal = [];
|
|
45
|
+
const excludeSet = new Set(excludeIds);
|
|
46
|
+
vertical.push({ position: 0, kind: "page-edge" });
|
|
47
|
+
vertical.push({ position: page.width, kind: "page-edge" });
|
|
48
|
+
vertical.push({ position: page.width / 2, kind: "page-center" });
|
|
49
|
+
horizontal.push({ position: 0, kind: "page-edge" });
|
|
50
|
+
horizontal.push({ position: page.height, kind: "page-edge" });
|
|
51
|
+
horizontal.push({ position: page.height / 2, kind: "page-center" });
|
|
52
|
+
for (const pos of page.guides.vertical) {
|
|
53
|
+
vertical.push({ position: pos, kind: "guide" });
|
|
54
|
+
}
|
|
55
|
+
for (const pos of page.guides.horizontal) {
|
|
56
|
+
horizontal.push({ position: pos, kind: "guide" });
|
|
57
|
+
}
|
|
58
|
+
collectElementTargets(page.elements, excludeSet, vertical, horizontal);
|
|
59
|
+
return { vertical, horizontal };
|
|
60
|
+
},
|
|
61
|
+
apply(bounds, targets, thresholdPx, zoom) {
|
|
62
|
+
if (!Number.isFinite(zoom) || zoom <= 0) {
|
|
63
|
+
throw new Error(`snap.apply: zoom must be a positive finite number, got ${zoom}`);
|
|
64
|
+
}
|
|
65
|
+
if (!Number.isFinite(thresholdPx) || thresholdPx < 0) {
|
|
66
|
+
throw new Error(`snap.apply: thresholdPx must be a non-negative finite number, got ${thresholdPx}`);
|
|
67
|
+
}
|
|
68
|
+
const threshold = thresholdPx / zoom;
|
|
69
|
+
const snappedX = snapAxis(bounds.x, bounds.x + bounds.width / 2, bounds.x + bounds.width, targets.vertical, threshold);
|
|
70
|
+
const snappedY = snapAxis(bounds.y, bounds.y + bounds.height / 2, bounds.y + bounds.height, targets.horizontal, threshold);
|
|
71
|
+
return {
|
|
72
|
+
x: snappedX !== null ? snappedX.docPosition - snappedX.elementOffset : bounds.x,
|
|
73
|
+
y: snappedY !== null ? snappedY.docPosition - snappedY.elementOffset : bounds.y,
|
|
74
|
+
activeVertical: snappedX !== null ? snappedX.target : null,
|
|
75
|
+
activeHorizontal: snappedY !== null ? snappedY.target : null
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function collectGridTargets(bounds, gridSize, page, thresholdDocPx) {
|
|
81
|
+
if (!Number.isFinite(gridSize) || gridSize <= 0) {
|
|
82
|
+
throw new Error(`collectGridTargets: gridSize must be positive finite, got ${gridSize}`);
|
|
83
|
+
}
|
|
84
|
+
const vertical = [];
|
|
85
|
+
const horizontal = [];
|
|
86
|
+
const xMin = Math.floor((bounds.x - thresholdDocPx) / gridSize) * gridSize;
|
|
87
|
+
const xMax = Math.ceil((bounds.x + bounds.width + thresholdDocPx) / gridSize) * gridSize;
|
|
88
|
+
for (let x = xMin; x <= xMax; x += gridSize) {
|
|
89
|
+
if (x >= 0 && x <= page.width) vertical.push({ position: x, kind: "grid" });
|
|
90
|
+
}
|
|
91
|
+
const yMin = Math.floor((bounds.y - thresholdDocPx) / gridSize) * gridSize;
|
|
92
|
+
const yMax = Math.ceil((bounds.y + bounds.height + thresholdDocPx) / gridSize) * gridSize;
|
|
93
|
+
for (let y = yMin; y <= yMax; y += gridSize) {
|
|
94
|
+
if (y >= 0 && y <= page.height) horizontal.push({ position: y, kind: "grid" });
|
|
95
|
+
}
|
|
96
|
+
return { vertical, horizontal };
|
|
97
|
+
}
|
|
98
|
+
function snapAxis(start, center, end, targets, threshold) {
|
|
99
|
+
const candidates = [
|
|
100
|
+
{ value: start, offset: 0 },
|
|
101
|
+
{ value: center, offset: center - start },
|
|
102
|
+
{ value: end, offset: end - start }
|
|
103
|
+
];
|
|
104
|
+
let best = null;
|
|
105
|
+
let bestDistance = Infinity;
|
|
106
|
+
let bestPriority = Infinity;
|
|
107
|
+
for (const { value, offset } of candidates) {
|
|
108
|
+
for (const target of targets) {
|
|
109
|
+
const distance = Math.abs(target.position - value);
|
|
110
|
+
if (distance > threshold) continue;
|
|
111
|
+
const priority = KIND_PRIORITY[target.kind];
|
|
112
|
+
if (distance < bestDistance || distance === bestDistance && priority < bestPriority) {
|
|
113
|
+
bestDistance = distance;
|
|
114
|
+
bestPriority = priority;
|
|
115
|
+
best = { target, docPosition: target.position, elementOffset: offset };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return best;
|
|
120
|
+
}
|
|
121
|
+
function collectElementTargets(elements, excludeIds, vertical, horizontal) {
|
|
122
|
+
for (const el of elements) {
|
|
123
|
+
if (!el.visible || el.locked) continue;
|
|
124
|
+
if (excludeIds.has(el.id)) continue;
|
|
125
|
+
const aabb = elementAabb(el);
|
|
126
|
+
vertical.push({ position: aabb.x, kind: "element-edge" });
|
|
127
|
+
vertical.push({ position: aabb.x + aabb.width / 2, kind: "element-center" });
|
|
128
|
+
vertical.push({ position: aabb.x + aabb.width, kind: "element-edge" });
|
|
129
|
+
horizontal.push({ position: aabb.y, kind: "element-edge" });
|
|
130
|
+
horizontal.push({ position: aabb.y + aabb.height / 2, kind: "element-center" });
|
|
131
|
+
horizontal.push({ position: aabb.y + aabb.height, kind: "element-edge" });
|
|
132
|
+
if (el.kind === "group") {
|
|
133
|
+
collectElementTargets(el.children, excludeIds, vertical, horizontal);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/design-canvas-react/engine/zoom-pan.ts
|
|
139
|
+
function createZoomPanMath(config) {
|
|
140
|
+
const { minZoom, maxZoom } = config;
|
|
141
|
+
if (!Number.isFinite(minZoom) || minZoom <= 0) {
|
|
142
|
+
throw new Error(`minZoom must be a positive finite number, got ${minZoom}`);
|
|
143
|
+
}
|
|
144
|
+
if (!Number.isFinite(maxZoom) || maxZoom <= minZoom) {
|
|
145
|
+
throw new Error(`maxZoom must be finite and greater than minZoom (${minZoom}), got ${maxZoom}`);
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
minZoom,
|
|
149
|
+
maxZoom,
|
|
150
|
+
zoomAtPoint(state, factor, screenX, screenY) {
|
|
151
|
+
if (!Number.isFinite(factor) || factor <= 0) {
|
|
152
|
+
throw new Error(`zoomAtPoint: factor must be a positive finite number, got ${factor}`);
|
|
153
|
+
}
|
|
154
|
+
if (!Number.isFinite(screenX) || !Number.isFinite(screenY)) {
|
|
155
|
+
throw new Error(`zoomAtPoint: screenX/screenY must be finite numbers`);
|
|
156
|
+
}
|
|
157
|
+
if (!Number.isFinite(state.zoom) || state.zoom <= 0) {
|
|
158
|
+
throw new Error(`zoomAtPoint: state.zoom must be a positive finite number, got ${state.zoom}`);
|
|
159
|
+
}
|
|
160
|
+
const newZoom = Math.min(maxZoom, Math.max(minZoom, state.zoom * factor));
|
|
161
|
+
const docX = (screenX - state.panX) / state.zoom;
|
|
162
|
+
const docY = (screenY - state.panY) / state.zoom;
|
|
163
|
+
const panX = screenX - docX * newZoom;
|
|
164
|
+
const panY = screenY - docY * newZoom;
|
|
165
|
+
return { zoom: newZoom, panX, panY };
|
|
166
|
+
},
|
|
167
|
+
fitPage(page, viewport, paddingPx = 48) {
|
|
168
|
+
if (!Number.isFinite(page.width) || page.width <= 0) {
|
|
169
|
+
throw new Error(`fitPage: page.width must be a positive finite number, got ${page.width}`);
|
|
170
|
+
}
|
|
171
|
+
if (!Number.isFinite(page.height) || page.height <= 0) {
|
|
172
|
+
throw new Error(`fitPage: page.height must be a positive finite number, got ${page.height}`);
|
|
173
|
+
}
|
|
174
|
+
if (!Number.isFinite(viewport.width) || viewport.width <= 0) {
|
|
175
|
+
throw new Error(`fitPage: viewport.width must be a positive finite number, got ${viewport.width}`);
|
|
176
|
+
}
|
|
177
|
+
if (!Number.isFinite(viewport.height) || viewport.height <= 0) {
|
|
178
|
+
throw new Error(`fitPage: viewport.height must be a positive finite number, got ${viewport.height}`);
|
|
179
|
+
}
|
|
180
|
+
if (!Number.isFinite(paddingPx) || paddingPx < 0) {
|
|
181
|
+
throw new Error(`fitPage: paddingPx must be a non-negative finite number, got ${paddingPx}`);
|
|
182
|
+
}
|
|
183
|
+
const availW = viewport.width - paddingPx * 2;
|
|
184
|
+
const availH = viewport.height - paddingPx * 2;
|
|
185
|
+
const zoom = Math.min(maxZoom, Math.max(minZoom, Math.min(availW / page.width, availH / page.height)));
|
|
186
|
+
const panX = (viewport.width - page.width * zoom) / 2;
|
|
187
|
+
const panY = (viewport.height - page.height * zoom) / 2;
|
|
188
|
+
return { zoom, panX, panY };
|
|
189
|
+
},
|
|
190
|
+
documentToScreen(state, x, y) {
|
|
191
|
+
return { x: x * state.zoom + state.panX, y: y * state.zoom + state.panY };
|
|
192
|
+
},
|
|
193
|
+
screenToDocument(state, x, y) {
|
|
194
|
+
if (!Number.isFinite(state.zoom) || state.zoom === 0) {
|
|
195
|
+
throw new Error(`screenToDocument: zoom must be a non-zero finite number, got ${state.zoom}`);
|
|
196
|
+
}
|
|
197
|
+
return { x: (x - state.panX) / state.zoom, y: (y - state.panY) / state.zoom };
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// src/design-canvas-react/engine/selection.ts
|
|
203
|
+
function marqueeSelect(page, rect, opts = {}) {
|
|
204
|
+
assertValidBounds(rect, "marqueeSelect rect");
|
|
205
|
+
const result = [];
|
|
206
|
+
collectMarqueeIds(page.elements, rect, opts.requireFullContainment ?? false, result);
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
function collectMarqueeIds(elements, rect, requireContainment, result) {
|
|
210
|
+
for (const el of elements) {
|
|
211
|
+
if (!el.visible || el.locked) continue;
|
|
212
|
+
const aabb = elementAabb(el);
|
|
213
|
+
if (el.kind === "group") {
|
|
214
|
+
if (boundsIntersect(rect, aabb)) {
|
|
215
|
+
collectMarqueeIds(el.children, rect, requireContainment, result);
|
|
216
|
+
}
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const hit = requireContainment ? boundsContain(rect, aabb) : boundsIntersect(rect, aabb);
|
|
220
|
+
if (hit) result.push(el.id);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function boundsContain(outer, inner) {
|
|
224
|
+
return inner.x >= outer.x && inner.y >= outer.y && inner.x + inner.width <= outer.x + outer.width && inner.y + inner.height <= outer.y + outer.height;
|
|
225
|
+
}
|
|
226
|
+
var NUDGE_NORMAL_PX = 1;
|
|
227
|
+
var NUDGE_SHIFT_PX = 10;
|
|
228
|
+
function nudgeDelta(key, shift) {
|
|
229
|
+
const step = shift ? NUDGE_SHIFT_PX : NUDGE_NORMAL_PX;
|
|
230
|
+
switch (key) {
|
|
231
|
+
case "ArrowLeft":
|
|
232
|
+
return { dx: -step, dy: 0 };
|
|
233
|
+
case "ArrowRight":
|
|
234
|
+
return { dx: step, dy: 0 };
|
|
235
|
+
case "ArrowUp":
|
|
236
|
+
return { dx: 0, dy: -step };
|
|
237
|
+
case "ArrowDown":
|
|
238
|
+
return { dx: 0, dy: step };
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
var DUPLICATE_OFFSET = { dx: 10, dy: 10 };
|
|
242
|
+
function assertValidBounds(bounds, label) {
|
|
243
|
+
if (!Number.isFinite(bounds.x) || !Number.isFinite(bounds.y) || !Number.isFinite(bounds.width) || !Number.isFinite(bounds.height)) {
|
|
244
|
+
throw new Error(`${label}: all fields must be finite numbers`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// src/design-canvas-react/components/ElementNode.tsx
|
|
249
|
+
import { useEffect, useRef, useState } from "react";
|
|
250
|
+
import { Group, Rect, Ellipse, Line, Text, Image as KonvaImage } from "react-konva";
|
|
251
|
+
|
|
252
|
+
// src/design-canvas-react/components/transform-math.ts
|
|
253
|
+
function bakeRectTransform(node) {
|
|
254
|
+
return {
|
|
255
|
+
x: node.x,
|
|
256
|
+
y: node.y,
|
|
257
|
+
width: Math.abs(node.width * node.scaleX),
|
|
258
|
+
height: Math.abs(node.height * node.scaleY),
|
|
259
|
+
rotation: node.rotation
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function bakeLineTransform(node) {
|
|
263
|
+
const points = node.points.map((v, i) => i % 2 === 0 ? v * node.scaleX : v * node.scaleY);
|
|
264
|
+
return {
|
|
265
|
+
x: node.x,
|
|
266
|
+
y: node.y,
|
|
267
|
+
// Width/height for a line derive from its points; baking is for points only.
|
|
268
|
+
// We still return them so callers have a uniform shape.
|
|
269
|
+
width: node.width * Math.abs(node.scaleX),
|
|
270
|
+
height: node.height * Math.abs(node.scaleY),
|
|
271
|
+
rotation: node.rotation,
|
|
272
|
+
points
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
function bakeTextTransform(node) {
|
|
276
|
+
return {
|
|
277
|
+
x: node.x,
|
|
278
|
+
y: node.y,
|
|
279
|
+
width: Math.abs(node.width * node.scaleX),
|
|
280
|
+
// Height is excluded — it re-derives from content.
|
|
281
|
+
height: node.height,
|
|
282
|
+
rotation: node.rotation,
|
|
283
|
+
fontSize: Math.max(1, node.fontSize * Math.abs(node.scaleY))
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
function ellipseCenterFromTopLeft(topLeft) {
|
|
287
|
+
return {
|
|
288
|
+
x: topLeft.x + topLeft.width / 2,
|
|
289
|
+
y: topLeft.y + topLeft.height / 2,
|
|
290
|
+
radiusX: topLeft.width / 2,
|
|
291
|
+
radiusY: topLeft.height / 2
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
function ellipseTopLeftFromCenter(center) {
|
|
295
|
+
return {
|
|
296
|
+
x: center.x - center.radiusX,
|
|
297
|
+
y: center.y - center.radiusY,
|
|
298
|
+
width: center.radiusX * 2,
|
|
299
|
+
height: center.radiusY * 2
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
function normalizeMarquee(startX, startY, endX, endY) {
|
|
303
|
+
const x = Math.min(startX, endX);
|
|
304
|
+
const y = Math.min(startY, endY);
|
|
305
|
+
return {
|
|
306
|
+
x,
|
|
307
|
+
y,
|
|
308
|
+
width: Math.abs(endX - startX),
|
|
309
|
+
height: Math.abs(endY - startY)
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
function computeTextOverlayPosition(input) {
|
|
313
|
+
const left = input.panX + input.elementX * input.zoom + input.stageLeft;
|
|
314
|
+
const top = input.panY + input.elementY * input.zoom + input.stageTop;
|
|
315
|
+
return {
|
|
316
|
+
left,
|
|
317
|
+
top,
|
|
318
|
+
width: input.elementWidth * input.zoom,
|
|
319
|
+
fontSize: input.elementFontSize * input.zoom
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
var SNAP_ANGLES_DEG = [0, 45, 90, 135, 180, 225, 270, 315, 360];
|
|
323
|
+
function snapRotation(angleDeg, thresholdDeg = 5) {
|
|
324
|
+
const normalized = (angleDeg % 360 + 360) % 360;
|
|
325
|
+
let best = normalized;
|
|
326
|
+
let bestDist = Infinity;
|
|
327
|
+
for (const snap of SNAP_ANGLES_DEG) {
|
|
328
|
+
const dist = Math.abs(normalized - snap);
|
|
329
|
+
if (dist < bestDist) {
|
|
330
|
+
bestDist = dist;
|
|
331
|
+
best = snap % 360;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return bestDist <= thresholdDeg ? best : normalized;
|
|
335
|
+
}
|
|
336
|
+
function nudgeDelta2(key, shift) {
|
|
337
|
+
const step = shift ? 10 : 1;
|
|
338
|
+
switch (key) {
|
|
339
|
+
case "ArrowLeft":
|
|
340
|
+
return { dx: -step, dy: 0 };
|
|
341
|
+
case "ArrowRight":
|
|
342
|
+
return { dx: step, dy: 0 };
|
|
343
|
+
case "ArrowUp":
|
|
344
|
+
return { dx: 0, dy: -step };
|
|
345
|
+
case "ArrowDown":
|
|
346
|
+
return { dx: 0, dy: step };
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
function gridVisible(gridSize, zoom, minScreenPx = 4) {
|
|
350
|
+
return gridSize * zoom >= minScreenPx;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// src/design-canvas-react/components/ElementNode.tsx
|
|
354
|
+
import { jsx } from "react/jsx-runtime";
|
|
355
|
+
var IMAGE_CACHE_MAX = 256;
|
|
356
|
+
var imageCache = /* @__PURE__ */ new Map();
|
|
357
|
+
function imageCacheSet(src, img) {
|
|
358
|
+
if (imageCache.size >= IMAGE_CACHE_MAX) {
|
|
359
|
+
const oldest = imageCache.keys().next().value;
|
|
360
|
+
if (oldest !== void 0) imageCache.delete(oldest);
|
|
361
|
+
}
|
|
362
|
+
imageCache.set(src, img);
|
|
363
|
+
}
|
|
364
|
+
function useImage(src) {
|
|
365
|
+
const [, setVersion] = useState(0);
|
|
366
|
+
useEffect(() => {
|
|
367
|
+
if (imageCache.has(src)) return;
|
|
368
|
+
const img = new window.Image();
|
|
369
|
+
img.crossOrigin = "anonymous";
|
|
370
|
+
img.onload = () => {
|
|
371
|
+
imageCacheSet(src, img);
|
|
372
|
+
setVersion((v) => v + 1);
|
|
373
|
+
};
|
|
374
|
+
img.onerror = () => {
|
|
375
|
+
imageCacheSet(src, img);
|
|
376
|
+
setVersion((v) => v + 1);
|
|
377
|
+
};
|
|
378
|
+
img.src = src;
|
|
379
|
+
}, [src]);
|
|
380
|
+
return imageCache.get(src) ?? null;
|
|
381
|
+
}
|
|
382
|
+
function ElementNode(props) {
|
|
383
|
+
const { element } = props;
|
|
384
|
+
if (!element.visible) return null;
|
|
385
|
+
switch (element.kind) {
|
|
386
|
+
case "rect":
|
|
387
|
+
return /* @__PURE__ */ jsx(RectNode, { ...props, element });
|
|
388
|
+
case "ellipse":
|
|
389
|
+
return /* @__PURE__ */ jsx(EllipseNode, { ...props, element });
|
|
390
|
+
case "line":
|
|
391
|
+
return /* @__PURE__ */ jsx(LineNode, { ...props, element });
|
|
392
|
+
case "text":
|
|
393
|
+
return /* @__PURE__ */ jsx(TextNode, { ...props, element });
|
|
394
|
+
case "image":
|
|
395
|
+
return /* @__PURE__ */ jsx(ImageNode, { ...props, element });
|
|
396
|
+
case "video":
|
|
397
|
+
return /* @__PURE__ */ jsx(VideoNode, { ...props, element });
|
|
398
|
+
case "group":
|
|
399
|
+
return /* @__PURE__ */ jsx(GroupNode, { ...props, element });
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
function useDragBindings(props, originX, originY) {
|
|
403
|
+
const { element, onClick, onDragStart, onDragMove, onDragEnd, onDoubleClick } = props;
|
|
404
|
+
const isDraggable = !element.locked && !!onDragEnd;
|
|
405
|
+
const originRef = useRef({ x: originX, y: originY });
|
|
406
|
+
originRef.current = { x: originX, y: originY };
|
|
407
|
+
return {
|
|
408
|
+
draggable: isDraggable,
|
|
409
|
+
// locked elements still receive click for selection; listening must be true.
|
|
410
|
+
listening: true,
|
|
411
|
+
onDragStart: isDraggable ? () => {
|
|
412
|
+
onDragStart?.(element.id);
|
|
413
|
+
} : void 0,
|
|
414
|
+
onDragMove: isDraggable ? (e) => {
|
|
415
|
+
onDragMove?.(element.id, e.target.x() - originRef.current.x, e.target.y() - originRef.current.y);
|
|
416
|
+
} : void 0,
|
|
417
|
+
onDragEnd: isDraggable ? (e) => {
|
|
418
|
+
onDragEnd?.(element.id, e.target.x(), e.target.y());
|
|
419
|
+
} : void 0,
|
|
420
|
+
onClick: () => onClick?.(element.id),
|
|
421
|
+
onDblClick: () => onDoubleClick?.(element.id)
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
function RectNode({ element, ...rest }) {
|
|
425
|
+
const drag = useDragBindings({ element, ...rest }, element.x, element.y);
|
|
426
|
+
return /* @__PURE__ */ jsx(
|
|
427
|
+
Rect,
|
|
428
|
+
{
|
|
429
|
+
name: element.id,
|
|
430
|
+
x: element.x,
|
|
431
|
+
y: element.y,
|
|
432
|
+
width: element.width,
|
|
433
|
+
height: element.height,
|
|
434
|
+
rotation: element.rotation,
|
|
435
|
+
opacity: element.opacity,
|
|
436
|
+
fill: element.fill,
|
|
437
|
+
stroke: element.stroke,
|
|
438
|
+
strokeWidth: element.strokeWidth,
|
|
439
|
+
cornerRadius: element.cornerRadius ?? 0,
|
|
440
|
+
...drag
|
|
441
|
+
}
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
function EllipseNode({ element, ...rest }) {
|
|
445
|
+
const { x: cx, y: cy, radiusX, radiusY } = ellipseCenterFromTopLeft(element);
|
|
446
|
+
const drag = useDragBindings({ element, ...rest }, element.x, element.y);
|
|
447
|
+
return /* @__PURE__ */ jsx(
|
|
448
|
+
Ellipse,
|
|
449
|
+
{
|
|
450
|
+
name: element.id,
|
|
451
|
+
x: cx,
|
|
452
|
+
y: cy,
|
|
453
|
+
radiusX,
|
|
454
|
+
radiusY,
|
|
455
|
+
rotation: element.rotation,
|
|
456
|
+
opacity: element.opacity,
|
|
457
|
+
fill: element.fill,
|
|
458
|
+
stroke: element.stroke,
|
|
459
|
+
strokeWidth: element.strokeWidth,
|
|
460
|
+
...drag
|
|
461
|
+
}
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
function LineNode({ element, ...rest }) {
|
|
465
|
+
const drag = useDragBindings({ element, ...rest }, element.x, element.y);
|
|
466
|
+
return /* @__PURE__ */ jsx(
|
|
467
|
+
Line,
|
|
468
|
+
{
|
|
469
|
+
name: element.id,
|
|
470
|
+
x: element.x,
|
|
471
|
+
y: element.y,
|
|
472
|
+
points: element.points,
|
|
473
|
+
rotation: element.rotation,
|
|
474
|
+
opacity: element.opacity,
|
|
475
|
+
stroke: element.stroke,
|
|
476
|
+
strokeWidth: element.strokeWidth,
|
|
477
|
+
dash: element.dash,
|
|
478
|
+
...drag
|
|
479
|
+
}
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
var FONT_STYLE_MAP = {
|
|
483
|
+
"normal": { fontStyle: "normal", fontVariant: "normal", fontWeight: "normal" },
|
|
484
|
+
"bold": { fontStyle: "normal", fontVariant: "normal", fontWeight: "bold" },
|
|
485
|
+
"italic": { fontStyle: "italic", fontVariant: "normal", fontWeight: "normal" },
|
|
486
|
+
"bold italic": { fontStyle: "italic", fontVariant: "normal", fontWeight: "bold" }
|
|
487
|
+
};
|
|
488
|
+
function TextNode({ element, ...rest }) {
|
|
489
|
+
const drag = useDragBindings({ element, ...rest }, element.x, element.y);
|
|
490
|
+
const { fontStyle, fontWeight } = FONT_STYLE_MAP[element.fontStyle];
|
|
491
|
+
return /* @__PURE__ */ jsx(
|
|
492
|
+
Text,
|
|
493
|
+
{
|
|
494
|
+
name: element.id,
|
|
495
|
+
x: element.x,
|
|
496
|
+
y: element.y,
|
|
497
|
+
width: element.width,
|
|
498
|
+
rotation: element.rotation,
|
|
499
|
+
opacity: element.opacity,
|
|
500
|
+
text: element.text,
|
|
501
|
+
fontFamily: element.fontFamily,
|
|
502
|
+
fontSize: element.fontSize,
|
|
503
|
+
fontStyle: `${fontStyle} ${fontWeight}`.trim(),
|
|
504
|
+
fill: element.fill,
|
|
505
|
+
align: element.align,
|
|
506
|
+
lineHeight: element.lineHeight,
|
|
507
|
+
letterSpacing: element.letterSpacing,
|
|
508
|
+
...drag
|
|
509
|
+
}
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
var BROKEN_FILL = "#e5e7eb";
|
|
513
|
+
var BROKEN_STROKE = "#9ca3af";
|
|
514
|
+
function ImageNode({ element, ...rest }) {
|
|
515
|
+
const img = useImage(element.src);
|
|
516
|
+
const drag = useDragBindings({ element, ...rest }, element.x, element.y);
|
|
517
|
+
if (!img || !img.complete || img.naturalWidth === 0) {
|
|
518
|
+
return /* @__PURE__ */ jsx(
|
|
519
|
+
Rect,
|
|
520
|
+
{
|
|
521
|
+
name: element.id,
|
|
522
|
+
x: element.x,
|
|
523
|
+
y: element.y,
|
|
524
|
+
width: element.width,
|
|
525
|
+
height: element.height,
|
|
526
|
+
rotation: element.rotation,
|
|
527
|
+
opacity: element.opacity,
|
|
528
|
+
fill: BROKEN_FILL,
|
|
529
|
+
stroke: BROKEN_STROKE,
|
|
530
|
+
strokeWidth: 1,
|
|
531
|
+
...drag
|
|
532
|
+
}
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
let cropProps = {};
|
|
536
|
+
if (element.fit === "cover") {
|
|
537
|
+
const srcAspect = img.naturalWidth / img.naturalHeight;
|
|
538
|
+
const dstAspect = element.width / element.height;
|
|
539
|
+
if (srcAspect > dstAspect) {
|
|
540
|
+
const visibleW = img.naturalHeight * dstAspect;
|
|
541
|
+
cropProps = {
|
|
542
|
+
crop: {
|
|
543
|
+
x: (img.naturalWidth - visibleW) / 2,
|
|
544
|
+
y: 0,
|
|
545
|
+
width: visibleW,
|
|
546
|
+
height: img.naturalHeight
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
} else {
|
|
550
|
+
const visibleH = img.naturalWidth / dstAspect;
|
|
551
|
+
cropProps = {
|
|
552
|
+
crop: {
|
|
553
|
+
x: 0,
|
|
554
|
+
y: (img.naturalHeight - visibleH) / 2,
|
|
555
|
+
width: img.naturalWidth,
|
|
556
|
+
height: visibleH
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return /* @__PURE__ */ jsx(
|
|
562
|
+
KonvaImage,
|
|
563
|
+
{
|
|
564
|
+
name: element.id,
|
|
565
|
+
x: element.x,
|
|
566
|
+
y: element.y,
|
|
567
|
+
width: element.width,
|
|
568
|
+
height: element.height,
|
|
569
|
+
rotation: element.rotation,
|
|
570
|
+
opacity: element.opacity,
|
|
571
|
+
image: img,
|
|
572
|
+
...cropProps,
|
|
573
|
+
...drag
|
|
574
|
+
}
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
function VideoNode({ element, ...rest }) {
|
|
578
|
+
const img = useImage(element.posterSrc ?? "");
|
|
579
|
+
const drag = useDragBindings({ element, ...rest }, element.x, element.y);
|
|
580
|
+
if (!element.posterSrc || !img || !img.complete || img.naturalWidth === 0) {
|
|
581
|
+
return /* @__PURE__ */ jsx(
|
|
582
|
+
Rect,
|
|
583
|
+
{
|
|
584
|
+
name: element.id,
|
|
585
|
+
x: element.x,
|
|
586
|
+
y: element.y,
|
|
587
|
+
width: element.width,
|
|
588
|
+
height: element.height,
|
|
589
|
+
rotation: element.rotation,
|
|
590
|
+
opacity: element.opacity,
|
|
591
|
+
fill: "#1f2937",
|
|
592
|
+
stroke: "#374151",
|
|
593
|
+
strokeWidth: 1,
|
|
594
|
+
...drag
|
|
595
|
+
}
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
return /* @__PURE__ */ jsx(
|
|
599
|
+
KonvaImage,
|
|
600
|
+
{
|
|
601
|
+
name: element.id,
|
|
602
|
+
x: element.x,
|
|
603
|
+
y: element.y,
|
|
604
|
+
width: element.width,
|
|
605
|
+
height: element.height,
|
|
606
|
+
rotation: element.rotation,
|
|
607
|
+
opacity: element.opacity,
|
|
608
|
+
image: img,
|
|
609
|
+
...drag
|
|
610
|
+
}
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
function GroupNode({ element, ...rest }) {
|
|
614
|
+
const drag = useDragBindings({ element, ...rest }, element.x, element.y);
|
|
615
|
+
return /* @__PURE__ */ jsx(
|
|
616
|
+
Group,
|
|
617
|
+
{
|
|
618
|
+
name: element.id,
|
|
619
|
+
x: element.x,
|
|
620
|
+
y: element.y,
|
|
621
|
+
rotation: element.rotation,
|
|
622
|
+
opacity: element.opacity,
|
|
623
|
+
...drag,
|
|
624
|
+
children: element.children.map((child) => /* @__PURE__ */ jsx(ElementNode, { ...rest, element: child }, child.id))
|
|
625
|
+
}
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// src/design-canvas-react/components/SelectionLayer.tsx
|
|
630
|
+
import { useEffect as useEffect2, useRef as useRef2 } from "react";
|
|
631
|
+
import { Layer, Transformer } from "react-konva";
|
|
632
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
633
|
+
var MIN_SIZE = 4;
|
|
634
|
+
function SelectionLayer({
|
|
635
|
+
stageRef,
|
|
636
|
+
selectedIds,
|
|
637
|
+
selectedElements,
|
|
638
|
+
canWrite,
|
|
639
|
+
onTransformEnd,
|
|
640
|
+
pageId
|
|
641
|
+
}) {
|
|
642
|
+
const trRef = useRef2(null);
|
|
643
|
+
useEffect2(() => {
|
|
644
|
+
const tr = trRef.current;
|
|
645
|
+
const stage = stageRef.current;
|
|
646
|
+
if (!tr || !stage) return;
|
|
647
|
+
const nodes = [];
|
|
648
|
+
for (const id of selectedIds) {
|
|
649
|
+
const node = stage.findOne(`[name="${id}"]`);
|
|
650
|
+
if (node) nodes.push(node);
|
|
651
|
+
}
|
|
652
|
+
tr.nodes(nodes);
|
|
653
|
+
tr.getLayer()?.batchDraw();
|
|
654
|
+
}, [selectedIds, stageRef]);
|
|
655
|
+
function handleTransformEnd() {
|
|
656
|
+
const tr = trRef.current;
|
|
657
|
+
if (!tr) return;
|
|
658
|
+
const nodes = tr.nodes();
|
|
659
|
+
if (nodes.length === 0) return;
|
|
660
|
+
const entries = [];
|
|
661
|
+
for (const node of nodes) {
|
|
662
|
+
const elementId = node.name();
|
|
663
|
+
const element = selectedElements.find((e) => e.id === elementId);
|
|
664
|
+
if (!element) continue;
|
|
665
|
+
const priorAttrs = elementToPriorAttrs(element);
|
|
666
|
+
const finalAttrs = bakeNodeToAttrs(node, element);
|
|
667
|
+
if (!finalAttrs) continue;
|
|
668
|
+
entries.push({ pageId, elementId, attrs: finalAttrs, priorAttrs });
|
|
669
|
+
}
|
|
670
|
+
if (entries.length > 0) onTransformEnd(entries);
|
|
671
|
+
}
|
|
672
|
+
const multiSelect = selectedIds.length > 1;
|
|
673
|
+
return /* @__PURE__ */ jsx2(Layer, { name: "overlay:selection", listening: false, children: /* @__PURE__ */ jsx2(
|
|
674
|
+
Transformer,
|
|
675
|
+
{
|
|
676
|
+
ref: trRef,
|
|
677
|
+
name: "overlay:transformer",
|
|
678
|
+
keepRatio: multiSelect,
|
|
679
|
+
rotationSnaps: [0, 45, 90, 135, 180, 225, 270, 315],
|
|
680
|
+
rotationSnapTolerance: 5,
|
|
681
|
+
boundBoxFunc: (oldBox, newBox) => {
|
|
682
|
+
if (newBox.width < MIN_SIZE || newBox.height < MIN_SIZE) return oldBox;
|
|
683
|
+
return newBox;
|
|
684
|
+
},
|
|
685
|
+
listening: canWrite,
|
|
686
|
+
onTransformEnd: canWrite ? handleTransformEnd : void 0
|
|
687
|
+
}
|
|
688
|
+
) });
|
|
689
|
+
}
|
|
690
|
+
function elementToPriorAttrs(element) {
|
|
691
|
+
const base = {
|
|
692
|
+
x: element.x,
|
|
693
|
+
y: element.y,
|
|
694
|
+
rotation: element.rotation
|
|
695
|
+
};
|
|
696
|
+
switch (element.kind) {
|
|
697
|
+
case "rect":
|
|
698
|
+
case "image":
|
|
699
|
+
case "video":
|
|
700
|
+
return { ...base, width: element.width, height: element.height };
|
|
701
|
+
case "ellipse":
|
|
702
|
+
return { ...base, width: element.width, height: element.height };
|
|
703
|
+
case "line":
|
|
704
|
+
return { ...base, points: element.points.slice() };
|
|
705
|
+
case "text":
|
|
706
|
+
return { ...base, width: element.width, fontSize: element.fontSize };
|
|
707
|
+
case "group":
|
|
708
|
+
return { ...base, width: void 0, height: void 0 };
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
function bakeNodeToAttrs(node, element) {
|
|
712
|
+
const rawRotation = node.rotation();
|
|
713
|
+
const snappedRotation = snapRotation(rawRotation, 5);
|
|
714
|
+
const baseNode = {
|
|
715
|
+
x: node.x(),
|
|
716
|
+
y: node.y(),
|
|
717
|
+
width: node.width(),
|
|
718
|
+
height: node.height(),
|
|
719
|
+
scaleX: node.scaleX(),
|
|
720
|
+
scaleY: node.scaleY(),
|
|
721
|
+
rotation: snappedRotation
|
|
722
|
+
};
|
|
723
|
+
switch (element.kind) {
|
|
724
|
+
case "rect":
|
|
725
|
+
case "image":
|
|
726
|
+
case "video":
|
|
727
|
+
case "group": {
|
|
728
|
+
const baked = bakeRectTransform(baseNode);
|
|
729
|
+
return {
|
|
730
|
+
x: baked.x,
|
|
731
|
+
y: baked.y,
|
|
732
|
+
width: Math.max(MIN_SIZE, baked.width),
|
|
733
|
+
height: Math.max(MIN_SIZE, baked.height),
|
|
734
|
+
rotation: baked.rotation
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
case "ellipse": {
|
|
738
|
+
const baked = bakeRectTransform(baseNode);
|
|
739
|
+
const topLeft = ellipseTopLeftFromCenter({
|
|
740
|
+
x: baked.x,
|
|
741
|
+
y: baked.y,
|
|
742
|
+
// radiusX/radiusY = half of baked width/height
|
|
743
|
+
radiusX: baked.width / 2,
|
|
744
|
+
radiusY: baked.height / 2
|
|
745
|
+
});
|
|
746
|
+
return {
|
|
747
|
+
x: topLeft.x,
|
|
748
|
+
y: topLeft.y,
|
|
749
|
+
width: Math.max(MIN_SIZE, topLeft.width),
|
|
750
|
+
height: Math.max(MIN_SIZE, topLeft.height),
|
|
751
|
+
rotation: baked.rotation
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
case "line": {
|
|
755
|
+
const points = node.points();
|
|
756
|
+
const baked = bakeLineTransform({ ...baseNode, points });
|
|
757
|
+
return {
|
|
758
|
+
x: baked.x,
|
|
759
|
+
y: baked.y,
|
|
760
|
+
rotation: baked.rotation,
|
|
761
|
+
points: baked.points
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
case "text": {
|
|
765
|
+
const baked = bakeTextTransform({
|
|
766
|
+
...baseNode,
|
|
767
|
+
fontSize: element.fontSize
|
|
768
|
+
});
|
|
769
|
+
return {
|
|
770
|
+
x: baked.x,
|
|
771
|
+
y: baked.y,
|
|
772
|
+
width: Math.max(MIN_SIZE, baked.width),
|
|
773
|
+
rotation: baked.rotation,
|
|
774
|
+
fontSize: Math.max(1, baked.fontSize)
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// src/design-canvas-react/components/GridLayer.tsx
|
|
781
|
+
import { Layer as Layer2, Line as Line2 } from "react-konva";
|
|
782
|
+
import { jsx as jsx3, jsxs } from "react/jsx-runtime";
|
|
783
|
+
function GridLayer({
|
|
784
|
+
pageWidth,
|
|
785
|
+
pageHeight,
|
|
786
|
+
gridSize,
|
|
787
|
+
zoom,
|
|
788
|
+
color = "#c0c0c0",
|
|
789
|
+
opacity = 0.5
|
|
790
|
+
}) {
|
|
791
|
+
if (!gridVisible(gridSize, zoom, 4)) return null;
|
|
792
|
+
const verticals = [];
|
|
793
|
+
for (let x = gridSize; x < pageWidth; x += gridSize) {
|
|
794
|
+
verticals.push(x);
|
|
795
|
+
}
|
|
796
|
+
const horizontals = [];
|
|
797
|
+
for (let y = gridSize; y < pageHeight; y += gridSize) {
|
|
798
|
+
horizontals.push(y);
|
|
799
|
+
}
|
|
800
|
+
return /* @__PURE__ */ jsxs(Layer2, { name: "overlay:grid", listening: false, children: [
|
|
801
|
+
verticals.map((x) => /* @__PURE__ */ jsx3(
|
|
802
|
+
Line2,
|
|
803
|
+
{
|
|
804
|
+
name: "overlay:grid-line",
|
|
805
|
+
points: [x, 0, x, pageHeight],
|
|
806
|
+
stroke: color,
|
|
807
|
+
strokeWidth: 1 / zoom,
|
|
808
|
+
opacity,
|
|
809
|
+
listening: false,
|
|
810
|
+
perfectDrawEnabled: false
|
|
811
|
+
},
|
|
812
|
+
`v-${x}`
|
|
813
|
+
)),
|
|
814
|
+
horizontals.map((y) => /* @__PURE__ */ jsx3(
|
|
815
|
+
Line2,
|
|
816
|
+
{
|
|
817
|
+
name: "overlay:grid-line",
|
|
818
|
+
points: [0, y, pageWidth, y],
|
|
819
|
+
stroke: color,
|
|
820
|
+
strokeWidth: 1 / zoom,
|
|
821
|
+
opacity,
|
|
822
|
+
listening: false,
|
|
823
|
+
perfectDrawEnabled: false
|
|
824
|
+
},
|
|
825
|
+
`h-${y}`
|
|
826
|
+
))
|
|
827
|
+
] });
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// src/design-canvas-react/components/SnapGuidesOverlay.tsx
|
|
831
|
+
import { Layer as Layer3, Line as Line3 } from "react-konva";
|
|
832
|
+
import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
833
|
+
var KIND_COLOR = {
|
|
834
|
+
"grid": "#a0a0a0",
|
|
835
|
+
"guide": "#3b82f6",
|
|
836
|
+
"page-edge": "#f59e0b",
|
|
837
|
+
"page-center": "#f59e0b",
|
|
838
|
+
"element-edge": "#f43f5e",
|
|
839
|
+
"element-center": "#f43f5e"
|
|
840
|
+
};
|
|
841
|
+
function SnapGuidesOverlay({
|
|
842
|
+
pageWidth,
|
|
843
|
+
pageHeight,
|
|
844
|
+
activeVertical,
|
|
845
|
+
activeHorizontal,
|
|
846
|
+
zoom
|
|
847
|
+
}) {
|
|
848
|
+
if (!activeVertical && !activeHorizontal) return null;
|
|
849
|
+
const strokeWidth = 1 / zoom;
|
|
850
|
+
return /* @__PURE__ */ jsxs2(Layer3, { name: "overlay:snap", listening: false, children: [
|
|
851
|
+
activeVertical && /* @__PURE__ */ jsx4(
|
|
852
|
+
Line3,
|
|
853
|
+
{
|
|
854
|
+
name: "overlay:snap-vertical",
|
|
855
|
+
points: [activeVertical.position, -99999, activeVertical.position, 99999],
|
|
856
|
+
stroke: KIND_COLOR[activeVertical.kind],
|
|
857
|
+
strokeWidth,
|
|
858
|
+
dash: [4 / zoom, 3 / zoom],
|
|
859
|
+
listening: false,
|
|
860
|
+
perfectDrawEnabled: false
|
|
861
|
+
}
|
|
862
|
+
),
|
|
863
|
+
activeHorizontal && /* @__PURE__ */ jsx4(
|
|
864
|
+
Line3,
|
|
865
|
+
{
|
|
866
|
+
name: "overlay:snap-horizontal",
|
|
867
|
+
points: [-99999, activeHorizontal.position, 99999, activeHorizontal.position],
|
|
868
|
+
stroke: KIND_COLOR[activeHorizontal.kind],
|
|
869
|
+
strokeWidth,
|
|
870
|
+
dash: [4 / zoom, 3 / zoom],
|
|
871
|
+
listening: false,
|
|
872
|
+
perfectDrawEnabled: false
|
|
873
|
+
}
|
|
874
|
+
)
|
|
875
|
+
] });
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// src/design-canvas-react/components/InlineTextEditor.tsx
|
|
879
|
+
import { useEffect as useEffect3, useRef as useRef3 } from "react";
|
|
880
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
881
|
+
function InlineTextEditor({
|
|
882
|
+
element,
|
|
883
|
+
zoom,
|
|
884
|
+
panX,
|
|
885
|
+
panY,
|
|
886
|
+
stageRect,
|
|
887
|
+
onCommit,
|
|
888
|
+
onCancel
|
|
889
|
+
}) {
|
|
890
|
+
const ref = useRef3(null);
|
|
891
|
+
const pos = computeTextOverlayPosition({
|
|
892
|
+
elementX: element.x,
|
|
893
|
+
elementY: element.y,
|
|
894
|
+
elementWidth: element.width,
|
|
895
|
+
elementHeight: element.fontSize * element.lineHeight * 4,
|
|
896
|
+
// generous initial height
|
|
897
|
+
zoom,
|
|
898
|
+
panX,
|
|
899
|
+
panY,
|
|
900
|
+
stageLeft: stageRect.left,
|
|
901
|
+
stageTop: stageRect.top,
|
|
902
|
+
elementFontSize: element.fontSize
|
|
903
|
+
});
|
|
904
|
+
useEffect3(() => {
|
|
905
|
+
const el = ref.current;
|
|
906
|
+
if (!el) return;
|
|
907
|
+
el.focus();
|
|
908
|
+
el.select();
|
|
909
|
+
}, []);
|
|
910
|
+
function handleKeyDown(e) {
|
|
911
|
+
if (e.key === "Escape") {
|
|
912
|
+
e.preventDefault();
|
|
913
|
+
onCancel();
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
|
917
|
+
e.preventDefault();
|
|
918
|
+
onCommit(ref.current?.value ?? element.text);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
function handleBlur() {
|
|
922
|
+
onCommit(ref.current?.value ?? element.text);
|
|
923
|
+
}
|
|
924
|
+
const fontWeight = element.fontStyle.includes("bold") ? "bold" : "normal";
|
|
925
|
+
const fontStyle = element.fontStyle.includes("italic") ? "italic" : "normal";
|
|
926
|
+
return /* @__PURE__ */ jsx5(
|
|
927
|
+
"textarea",
|
|
928
|
+
{
|
|
929
|
+
ref,
|
|
930
|
+
defaultValue: element.text,
|
|
931
|
+
onKeyDown: handleKeyDown,
|
|
932
|
+
onBlur: handleBlur,
|
|
933
|
+
style: {
|
|
934
|
+
position: "absolute",
|
|
935
|
+
left: pos.left,
|
|
936
|
+
top: pos.top,
|
|
937
|
+
width: pos.width,
|
|
938
|
+
// Height auto-grows via CSS; min so single-line text has room.
|
|
939
|
+
minHeight: pos.fontSize * element.lineHeight * 1.5,
|
|
940
|
+
fontSize: pos.fontSize,
|
|
941
|
+
fontFamily: element.fontFamily,
|
|
942
|
+
fontWeight,
|
|
943
|
+
fontStyle,
|
|
944
|
+
textAlign: element.align,
|
|
945
|
+
lineHeight: element.lineHeight,
|
|
946
|
+
letterSpacing: element.letterSpacing * zoom,
|
|
947
|
+
color: element.fill,
|
|
948
|
+
background: "rgba(255,255,255,0.95)",
|
|
949
|
+
border: "2px solid #3b82f6",
|
|
950
|
+
borderRadius: 2,
|
|
951
|
+
padding: 2,
|
|
952
|
+
resize: "none",
|
|
953
|
+
outline: "none",
|
|
954
|
+
overflow: "hidden",
|
|
955
|
+
boxSizing: "border-box",
|
|
956
|
+
zIndex: 1e3
|
|
957
|
+
// Do NOT apply rotation — see V1 simplification note in module header.
|
|
958
|
+
},
|
|
959
|
+
"aria-label": `Editing text element "${element.name}"`
|
|
960
|
+
}
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// src/design-canvas-react/components/Workspace.tsx
|
|
965
|
+
import { jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
966
|
+
var SNAP_THRESHOLD_SCREEN_PX = 8;
|
|
967
|
+
var DUPLICATE_OFFSET2 = 10;
|
|
968
|
+
var ZOOM_FACTOR_WHEEL = 0.998;
|
|
969
|
+
var ZOOM_MIN = 0.05;
|
|
970
|
+
var ZOOM_MAX = 32;
|
|
971
|
+
var NO_MARQUEE = { active: false, startDocX: 0, startDocY: 0, endDocX: 0, endDocY: 0 };
|
|
972
|
+
function WorkspaceView({
|
|
973
|
+
canWrite,
|
|
974
|
+
onApplyOperations,
|
|
975
|
+
onSelectionChange,
|
|
976
|
+
renderAgentPanel,
|
|
977
|
+
renderSidePanel,
|
|
978
|
+
className,
|
|
979
|
+
stack,
|
|
980
|
+
activePage,
|
|
981
|
+
onFitRef
|
|
982
|
+
}) {
|
|
983
|
+
const [, setTick] = useState2(0);
|
|
984
|
+
const forceRender = useCallback(() => setTick((t) => t + 1), []);
|
|
985
|
+
useEffect4(() => stack.subscribe(forceRender), [stack, forceRender]);
|
|
986
|
+
const state = stack.getState();
|
|
987
|
+
const { document, activePageId, selectedElementIds, zoom, panX, panY, gridEnabled, gridSize, snapEnabled, showBleed } = state;
|
|
988
|
+
const zoomPanMath = useMemo(() => createZoomPanMath({ minZoom: ZOOM_MIN, maxZoom: ZOOM_MAX }), []);
|
|
989
|
+
const snapEngine = useMemo(() => createSnapEngine(), []);
|
|
990
|
+
const containerRef = useRef4(null);
|
|
991
|
+
const [containerSize, setContainerSize] = useState2({ width: 800, height: 600 });
|
|
992
|
+
useLayoutEffect(() => {
|
|
993
|
+
const el = containerRef.current;
|
|
994
|
+
if (!el) return;
|
|
995
|
+
const ro = new ResizeObserver((entries) => {
|
|
996
|
+
const entry = entries[0];
|
|
997
|
+
if (!entry) return;
|
|
998
|
+
const { width, height } = entry.contentRect;
|
|
999
|
+
setContainerSize({ width, height });
|
|
1000
|
+
});
|
|
1001
|
+
ro.observe(el);
|
|
1002
|
+
return () => ro.disconnect();
|
|
1003
|
+
}, []);
|
|
1004
|
+
const stageRef = useRef4(null);
|
|
1005
|
+
useEffect4(() => {
|
|
1006
|
+
if (!onFitRef) return;
|
|
1007
|
+
onFitRef.current = () => {
|
|
1008
|
+
const { width, height } = containerSize;
|
|
1009
|
+
if (width <= 0 || height <= 0) return;
|
|
1010
|
+
const view = zoomPanMath.fitPage(activePage, { width, height });
|
|
1011
|
+
stack.setView(view);
|
|
1012
|
+
};
|
|
1013
|
+
return () => {
|
|
1014
|
+
if (onFitRef.current !== null) onFitRef.current = null;
|
|
1015
|
+
};
|
|
1016
|
+
});
|
|
1017
|
+
const gestureRef = useRef4("idle");
|
|
1018
|
+
const panOriginRef = useRef4({ screenX: 0, screenY: 0, panX: 0, panY: 0 });
|
|
1019
|
+
const [marquee, setMarquee] = useState2(NO_MARQUEE);
|
|
1020
|
+
const marqueeRef = useRef4(NO_MARQUEE);
|
|
1021
|
+
const spaceHeldRef = useRef4(false);
|
|
1022
|
+
const dragOriginRef = useRef4(/* @__PURE__ */ new Map());
|
|
1023
|
+
const [activeSnap, setActiveSnap] = useState2(null);
|
|
1024
|
+
const [editingElementId, setEditingElementId] = useState2(null);
|
|
1025
|
+
const editingPreRef = useRef4("");
|
|
1026
|
+
const nudgeHeldRef = useRef4(null);
|
|
1027
|
+
async function persist(command) {
|
|
1028
|
+
if (!canWrite) return;
|
|
1029
|
+
stack.execute(command);
|
|
1030
|
+
try {
|
|
1031
|
+
const result = await onApplyOperations(command.operations());
|
|
1032
|
+
if (result.document) stack.reset(result.document);
|
|
1033
|
+
} catch {
|
|
1034
|
+
stack.rollback(command);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
function handleWheel(e) {
|
|
1038
|
+
e.preventDefault();
|
|
1039
|
+
const rect = containerRef.current?.getBoundingClientRect();
|
|
1040
|
+
if (!rect) return;
|
|
1041
|
+
const screenX = e.clientX - rect.left;
|
|
1042
|
+
const screenY = e.clientY - rect.top;
|
|
1043
|
+
const factor = Math.pow(ZOOM_FACTOR_WHEEL, e.deltaY);
|
|
1044
|
+
const next = zoomPanMath.zoomAtPoint({ zoom, panX, panY }, factor, screenX, screenY);
|
|
1045
|
+
stack.setView(next);
|
|
1046
|
+
}
|
|
1047
|
+
function handlePointerDown(e) {
|
|
1048
|
+
if (e.button === 1 || spaceHeldRef.current) {
|
|
1049
|
+
e.preventDefault();
|
|
1050
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
1051
|
+
gestureRef.current = "pan";
|
|
1052
|
+
panOriginRef.current = { screenX: e.clientX, screenY: e.clientY, panX, panY };
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
if (e.button !== 0) return;
|
|
1056
|
+
const stage = stageRef.current;
|
|
1057
|
+
const rect = containerRef.current?.getBoundingClientRect();
|
|
1058
|
+
if (!stage || !rect) return;
|
|
1059
|
+
const screenX = e.clientX - rect.left;
|
|
1060
|
+
const screenY = e.clientY - rect.top;
|
|
1061
|
+
const hitNode = stage.getIntersection({ x: screenX, y: screenY });
|
|
1062
|
+
if (hitNode && !hitNode.name().startsWith("overlay:") && hitNode.name() !== "page-background") {
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
e.preventDefault();
|
|
1066
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
1067
|
+
gestureRef.current = "marquee";
|
|
1068
|
+
const docPos = zoomPanMath.screenToDocument({ zoom, panX, panY }, screenX, screenY);
|
|
1069
|
+
const m = { active: true, startDocX: docPos.x, startDocY: docPos.y, endDocX: docPos.x, endDocY: docPos.y };
|
|
1070
|
+
marqueeRef.current = m;
|
|
1071
|
+
setMarquee(m);
|
|
1072
|
+
stack.setView({ selectedElementIds: [] });
|
|
1073
|
+
}
|
|
1074
|
+
function handlePointerMove(e) {
|
|
1075
|
+
const mode = gestureRef.current;
|
|
1076
|
+
const rect = containerRef.current?.getBoundingClientRect();
|
|
1077
|
+
if (!rect) return;
|
|
1078
|
+
if (mode === "pan") {
|
|
1079
|
+
const dx = e.clientX - panOriginRef.current.screenX;
|
|
1080
|
+
const dy = e.clientY - panOriginRef.current.screenY;
|
|
1081
|
+
stack.setView({ panX: panOriginRef.current.panX + dx, panY: panOriginRef.current.panY + dy });
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
if (mode === "marquee") {
|
|
1085
|
+
const screenX = e.clientX - rect.left;
|
|
1086
|
+
const screenY = e.clientY - rect.top;
|
|
1087
|
+
const docPos = zoomPanMath.screenToDocument({ zoom, panX, panY }, screenX, screenY);
|
|
1088
|
+
const m = { ...marqueeRef.current, active: true, endDocX: docPos.x, endDocY: docPos.y };
|
|
1089
|
+
marqueeRef.current = m;
|
|
1090
|
+
setMarquee(m);
|
|
1091
|
+
const normalized = normalizeMarquee(m.startDocX, m.startDocY, m.endDocX, m.endDocY);
|
|
1092
|
+
const ids = marqueeSelect(activePage, normalized);
|
|
1093
|
+
stack.setView({ selectedElementIds: ids });
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
function handlePointerUp(e) {
|
|
1097
|
+
gestureRef.current = "idle";
|
|
1098
|
+
setMarquee(NO_MARQUEE);
|
|
1099
|
+
marqueeRef.current = NO_MARQUEE;
|
|
1100
|
+
setActiveSnap(null);
|
|
1101
|
+
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
1102
|
+
}
|
|
1103
|
+
function handleElementDragStart(elementId) {
|
|
1104
|
+
if (!canWrite) return;
|
|
1105
|
+
const ids = selectedElementIds.includes(elementId) ? selectedElementIds : [elementId];
|
|
1106
|
+
const origins = /* @__PURE__ */ new Map();
|
|
1107
|
+
for (const id of ids) {
|
|
1108
|
+
const found = findElement(activePage, id);
|
|
1109
|
+
if (found) {
|
|
1110
|
+
const aabb = elementAabb(found.element);
|
|
1111
|
+
origins.set(id, { x: found.element.x, y: found.element.y, width: aabb.width, height: aabb.height });
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
dragOriginRef.current = origins;
|
|
1115
|
+
gestureRef.current = "drag";
|
|
1116
|
+
if (!selectedElementIds.includes(elementId)) {
|
|
1117
|
+
stack.setView({ selectedElementIds: [elementId] });
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
function handleElementDragMove(elementId, dx, dy) {
|
|
1121
|
+
if (!canWrite) return;
|
|
1122
|
+
const origin = dragOriginRef.current.get(elementId);
|
|
1123
|
+
if (!origin) return;
|
|
1124
|
+
const dragging = selectedElementIds.includes(elementId) ? selectedElementIds : [elementId];
|
|
1125
|
+
const proposedX = origin.x + dx;
|
|
1126
|
+
const proposedY = origin.y + dy;
|
|
1127
|
+
const snapBounds = { x: proposedX, y: proposedY, width: origin.width, height: origin.height };
|
|
1128
|
+
if (snapEnabled) {
|
|
1129
|
+
const targets = snapEngine.collectTargets(state, dragging);
|
|
1130
|
+
if (gridEnabled) {
|
|
1131
|
+
const thresholdDoc = SNAP_THRESHOLD_SCREEN_PX / zoom;
|
|
1132
|
+
const gt = collectGridTargets(snapBounds, gridSize, activePage, thresholdDoc);
|
|
1133
|
+
targets.vertical.push(...gt.vertical);
|
|
1134
|
+
targets.horizontal.push(...gt.horizontal);
|
|
1135
|
+
}
|
|
1136
|
+
const snapResult = snapEngine.apply(snapBounds, targets, SNAP_THRESHOLD_SCREEN_PX, zoom);
|
|
1137
|
+
setActiveSnap(snapResult);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
async function handleElementDragEnd(elementId, finalX, finalY) {
|
|
1141
|
+
if (!canWrite) return;
|
|
1142
|
+
gestureRef.current = "idle";
|
|
1143
|
+
setActiveSnap(null);
|
|
1144
|
+
const origin = dragOriginRef.current.get(elementId);
|
|
1145
|
+
if (!origin) return;
|
|
1146
|
+
const dx = finalX - origin.x;
|
|
1147
|
+
const dy = finalY - origin.y;
|
|
1148
|
+
const draggingIds = selectedElementIds.includes(elementId) ? selectedElementIds : [elementId];
|
|
1149
|
+
if (draggingIds.length === 1) {
|
|
1150
|
+
const el = findElement(activePage, elementId)?.element;
|
|
1151
|
+
if (!el) return;
|
|
1152
|
+
await persist(
|
|
1153
|
+
setAttrsCommand({
|
|
1154
|
+
pageId: activePageId,
|
|
1155
|
+
elementId,
|
|
1156
|
+
attrs: { x: finalX, y: finalY },
|
|
1157
|
+
priorAttrs: { x: el.x, y: el.y }
|
|
1158
|
+
})
|
|
1159
|
+
);
|
|
1160
|
+
} else {
|
|
1161
|
+
const entries = [];
|
|
1162
|
+
for (const id of draggingIds) {
|
|
1163
|
+
const el = findElement(activePage, id)?.element;
|
|
1164
|
+
const orig = dragOriginRef.current.get(id);
|
|
1165
|
+
if (!el || !orig) continue;
|
|
1166
|
+
entries.push({
|
|
1167
|
+
pageId: activePageId,
|
|
1168
|
+
elementId: id,
|
|
1169
|
+
attrs: { x: orig.x + dx, y: orig.y + dy },
|
|
1170
|
+
priorAttrs: { x: orig.x, y: orig.y }
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
if (entries.length > 0) await persist(multiSetAttrsCommand(entries));
|
|
1174
|
+
}
|
|
1175
|
+
dragOriginRef.current = /* @__PURE__ */ new Map();
|
|
1176
|
+
}
|
|
1177
|
+
function handleElementClick(elementId) {
|
|
1178
|
+
stack.setView({ selectedElementIds: [elementId] });
|
|
1179
|
+
}
|
|
1180
|
+
function handleElementDoubleClick(elementId) {
|
|
1181
|
+
if (!canWrite) return;
|
|
1182
|
+
const found = findElement(activePage, elementId);
|
|
1183
|
+
if (!found || found.element.kind !== "text") return;
|
|
1184
|
+
editingPreRef.current = found.element.text;
|
|
1185
|
+
setEditingElementId(elementId);
|
|
1186
|
+
}
|
|
1187
|
+
async function handleTextCommit(text) {
|
|
1188
|
+
const id = editingElementId;
|
|
1189
|
+
setEditingElementId(null);
|
|
1190
|
+
if (!id || !canWrite) return;
|
|
1191
|
+
const found = findElement(activePage, id);
|
|
1192
|
+
if (!found || found.element.kind !== "text") return;
|
|
1193
|
+
if (text === editingPreRef.current) return;
|
|
1194
|
+
await persist(
|
|
1195
|
+
setAttrsCommand({
|
|
1196
|
+
pageId: activePageId,
|
|
1197
|
+
elementId: id,
|
|
1198
|
+
attrs: { text },
|
|
1199
|
+
priorAttrs: { text: editingPreRef.current }
|
|
1200
|
+
})
|
|
1201
|
+
);
|
|
1202
|
+
}
|
|
1203
|
+
function handleTextCancel() {
|
|
1204
|
+
setEditingElementId(null);
|
|
1205
|
+
}
|
|
1206
|
+
async function handleTransformEnd(entries) {
|
|
1207
|
+
if (!canWrite || entries.length === 0) return;
|
|
1208
|
+
await persist(multiSetAttrsCommand(entries));
|
|
1209
|
+
}
|
|
1210
|
+
function handleKeyDown(e) {
|
|
1211
|
+
if (e.key === " ") {
|
|
1212
|
+
e.preventDefault();
|
|
1213
|
+
spaceHeldRef.current = true;
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
const mod = e.metaKey || e.ctrlKey;
|
|
1217
|
+
if (e.key === "Escape") {
|
|
1218
|
+
e.preventDefault();
|
|
1219
|
+
if (gestureRef.current !== "idle") {
|
|
1220
|
+
gestureRef.current = "idle";
|
|
1221
|
+
setMarquee(NO_MARQUEE);
|
|
1222
|
+
setActiveSnap(null);
|
|
1223
|
+
} else if (editingElementId) {
|
|
1224
|
+
setEditingElementId(null);
|
|
1225
|
+
} else {
|
|
1226
|
+
stack.setView({ selectedElementIds: [] });
|
|
1227
|
+
}
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
if (editingElementId) return;
|
|
1231
|
+
if ((e.key === "Delete" || e.key === "Backspace") && selectedElementIds.length > 0) {
|
|
1232
|
+
e.preventDefault();
|
|
1233
|
+
for (const id of [...selectedElementIds]) {
|
|
1234
|
+
try {
|
|
1235
|
+
persist(deleteElementCommand({ document, pageId: activePageId, elementId: id }));
|
|
1236
|
+
} catch {
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
stack.setView({ selectedElementIds: [] });
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
if (mod && e.key === "z" && !e.shiftKey) {
|
|
1243
|
+
e.preventDefault();
|
|
1244
|
+
if (stack.canUndo()) stack.undo();
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
if (mod && (e.key === "z" && e.shiftKey || e.key === "y")) {
|
|
1248
|
+
e.preventDefault();
|
|
1249
|
+
if (stack.canRedo()) stack.redo();
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
if (mod && e.key === "a") {
|
|
1253
|
+
e.preventDefault();
|
|
1254
|
+
const ids = activePage.elements.filter((el) => !el.locked && el.visible).map((el) => el.id);
|
|
1255
|
+
stack.setView({ selectedElementIds: ids });
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
if (mod && e.key === "d" && selectedElementIds.length > 0) {
|
|
1259
|
+
e.preventDefault();
|
|
1260
|
+
for (const id of selectedElementIds) {
|
|
1261
|
+
const found = findElement(activePage, id);
|
|
1262
|
+
if (!found) continue;
|
|
1263
|
+
const clone = {
|
|
1264
|
+
...structuredClone(found.element),
|
|
1265
|
+
id: crypto.randomUUID(),
|
|
1266
|
+
x: found.element.x + DUPLICATE_OFFSET2,
|
|
1267
|
+
y: found.element.y + DUPLICATE_OFFSET2
|
|
1268
|
+
};
|
|
1269
|
+
persist(addElementCommand({ pageId: activePageId, element: clone }));
|
|
1270
|
+
}
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
if (mod && !e.shiftKey && e.key === "g" && selectedElementIds.length >= 2) {
|
|
1274
|
+
e.preventDefault();
|
|
1275
|
+
persist(groupElementsCommand({
|
|
1276
|
+
document,
|
|
1277
|
+
pageId: activePageId,
|
|
1278
|
+
elementIds: selectedElementIds,
|
|
1279
|
+
groupId: crypto.randomUUID()
|
|
1280
|
+
}));
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
if (mod && e.shiftKey && e.key === "g" && selectedElementIds.length === 1) {
|
|
1284
|
+
e.preventDefault();
|
|
1285
|
+
const id = selectedElementIds[0];
|
|
1286
|
+
persist(ungroupElementCommand({ document, pageId: activePageId, groupId: id }));
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
if (["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(e.key) && selectedElementIds.length > 0) {
|
|
1290
|
+
e.preventDefault();
|
|
1291
|
+
const key = e.key;
|
|
1292
|
+
if (nudgeHeldRef.current && nudgeHeldRef.current.key !== key) {
|
|
1293
|
+
flushNudge();
|
|
1294
|
+
}
|
|
1295
|
+
if (!nudgeHeldRef.current) {
|
|
1296
|
+
const origins = /* @__PURE__ */ new Map();
|
|
1297
|
+
for (const id of selectedElementIds) {
|
|
1298
|
+
const found = findElement(activePage, id);
|
|
1299
|
+
if (found) origins.set(id, { x: found.element.x, y: found.element.y });
|
|
1300
|
+
}
|
|
1301
|
+
nudgeHeldRef.current = { key, ids: selectedElementIds.slice(), origin: origins };
|
|
1302
|
+
}
|
|
1303
|
+
const { dx, dy } = nudgeDelta2(key, e.shiftKey);
|
|
1304
|
+
const entries = [];
|
|
1305
|
+
for (const [id, orig] of nudgeHeldRef.current.origin) {
|
|
1306
|
+
const found = findElement(activePage, id);
|
|
1307
|
+
if (!found) continue;
|
|
1308
|
+
entries.push({
|
|
1309
|
+
pageId: activePageId,
|
|
1310
|
+
elementId: id,
|
|
1311
|
+
attrs: { x: found.element.x + dx, y: found.element.y + dy },
|
|
1312
|
+
priorAttrs: { x: found.element.x, y: found.element.y }
|
|
1313
|
+
});
|
|
1314
|
+
}
|
|
1315
|
+
if (entries.length > 0) {
|
|
1316
|
+
stack.execute(multiSetAttrsCommand(entries));
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
function handleKeyUp(e) {
|
|
1321
|
+
if (e.key === " ") {
|
|
1322
|
+
spaceHeldRef.current = false;
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
if (["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(e.key)) {
|
|
1326
|
+
flushNudge();
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
function flushNudge() {
|
|
1330
|
+
const held = nudgeHeldRef.current;
|
|
1331
|
+
if (!held) return;
|
|
1332
|
+
nudgeHeldRef.current = null;
|
|
1333
|
+
if (!canWrite || held.ids.length === 0) return;
|
|
1334
|
+
const entries = [];
|
|
1335
|
+
for (const id of held.ids) {
|
|
1336
|
+
const orig = held.origin.get(id);
|
|
1337
|
+
const found = findElement(activePage, id);
|
|
1338
|
+
if (!orig || !found) continue;
|
|
1339
|
+
entries.push({
|
|
1340
|
+
pageId: activePageId,
|
|
1341
|
+
elementId: id,
|
|
1342
|
+
attrs: { x: found.element.x, y: found.element.y },
|
|
1343
|
+
priorAttrs: { x: orig.x, y: orig.y }
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
if (entries.length > 0) {
|
|
1347
|
+
onApplyOperations(multiSetAttrsCommand(entries).operations()).catch(() => {
|
|
1348
|
+
for (const [id, orig] of held.origin) {
|
|
1349
|
+
const found = findElement(activePage, id);
|
|
1350
|
+
if (!found) continue;
|
|
1351
|
+
persist(setAttrsCommand({
|
|
1352
|
+
pageId: activePageId,
|
|
1353
|
+
elementId: id,
|
|
1354
|
+
attrs: { x: orig.x, y: orig.y },
|
|
1355
|
+
priorAttrs: { x: found.element.x, y: found.element.y }
|
|
1356
|
+
}));
|
|
1357
|
+
}
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
useEffect4(() => {
|
|
1362
|
+
if (!onSelectionChange) return;
|
|
1363
|
+
const elements = selectedElementIds.map((id) => findElement(activePage, id)?.element).filter((el) => !!el);
|
|
1364
|
+
onSelectionChange(elements);
|
|
1365
|
+
}, [selectedElementIds, activePage, onSelectionChange]);
|
|
1366
|
+
const selectedElements = useMemo(
|
|
1367
|
+
() => selectedElementIds.map((id) => findElement(activePage, id)?.element).filter((el) => !!el),
|
|
1368
|
+
[selectedElementIds, activePage]
|
|
1369
|
+
);
|
|
1370
|
+
const editingTextElement = editingElementId ? findElement(activePage, editingElementId)?.element ?? null : null;
|
|
1371
|
+
const stageRect = containerRef.current?.getBoundingClientRect() ?? { left: 0, top: 0 };
|
|
1372
|
+
const dpr = typeof window !== "undefined" ? window.devicePixelRatio : 1;
|
|
1373
|
+
const pageScreenX = panX;
|
|
1374
|
+
const pageScreenY = panY;
|
|
1375
|
+
const pageScreenW = activePage.width * zoom;
|
|
1376
|
+
const pageScreenH = activePage.height * zoom;
|
|
1377
|
+
const activeVerticalGuide = activeSnap?.activeVertical ?? null;
|
|
1378
|
+
const activeHorizontalGuide = activeSnap?.activeHorizontal ?? null;
|
|
1379
|
+
const normalizedMarquee = marquee.active ? normalizeMarquee(marquee.startDocX, marquee.startDocY, marquee.endDocX, marquee.endDocY) : null;
|
|
1380
|
+
return /* @__PURE__ */ jsxs3(
|
|
1381
|
+
"div",
|
|
1382
|
+
{
|
|
1383
|
+
className: `design-canvas-workspace relative overflow-hidden bg-[#1a1a1a] outline-none ${className ?? ""}`,
|
|
1384
|
+
ref: containerRef,
|
|
1385
|
+
tabIndex: 0,
|
|
1386
|
+
onWheel: handleWheel,
|
|
1387
|
+
onPointerDown: handlePointerDown,
|
|
1388
|
+
onPointerMove: handlePointerMove,
|
|
1389
|
+
onPointerUp: handlePointerUp,
|
|
1390
|
+
onKeyDown: handleKeyDown,
|
|
1391
|
+
onKeyUp: handleKeyUp,
|
|
1392
|
+
style: { cursor: spaceHeldRef.current ? "grab" : "default" },
|
|
1393
|
+
children: [
|
|
1394
|
+
renderSidePanel && /* @__PURE__ */ jsx6("div", { className: "absolute left-0 top-0 bottom-0 z-20 w-60 pointer-events-auto", children: renderSidePanel() }),
|
|
1395
|
+
/* @__PURE__ */ jsxs3(
|
|
1396
|
+
Stage,
|
|
1397
|
+
{
|
|
1398
|
+
ref: stageRef,
|
|
1399
|
+
width: containerSize.width,
|
|
1400
|
+
height: containerSize.height,
|
|
1401
|
+
pixelRatio: dpr,
|
|
1402
|
+
listening: true,
|
|
1403
|
+
children: [
|
|
1404
|
+
gridEnabled && /* @__PURE__ */ jsx6(
|
|
1405
|
+
GridLayer,
|
|
1406
|
+
{
|
|
1407
|
+
pageWidth: activePage.width,
|
|
1408
|
+
pageHeight: activePage.height,
|
|
1409
|
+
gridSize,
|
|
1410
|
+
zoom
|
|
1411
|
+
}
|
|
1412
|
+
),
|
|
1413
|
+
/* @__PURE__ */ jsx6(Layer4, { children: /* @__PURE__ */ jsxs3(Group2, { x: panX, y: panY, scaleX: zoom, scaleY: zoom, children: [
|
|
1414
|
+
/* @__PURE__ */ jsx6(
|
|
1415
|
+
Rect2,
|
|
1416
|
+
{
|
|
1417
|
+
name: "page-background",
|
|
1418
|
+
x: 0,
|
|
1419
|
+
y: 0,
|
|
1420
|
+
width: activePage.width,
|
|
1421
|
+
height: activePage.height,
|
|
1422
|
+
fill: activePage.background,
|
|
1423
|
+
shadowColor: "rgba(0,0,0,0.4)",
|
|
1424
|
+
shadowBlur: 24 / zoom,
|
|
1425
|
+
shadowOffset: { x: 0, y: 4 / zoom },
|
|
1426
|
+
listening: false
|
|
1427
|
+
}
|
|
1428
|
+
),
|
|
1429
|
+
/* @__PURE__ */ jsx6(
|
|
1430
|
+
Group2,
|
|
1431
|
+
{
|
|
1432
|
+
clipX: 0,
|
|
1433
|
+
clipY: 0,
|
|
1434
|
+
clipWidth: activePage.width,
|
|
1435
|
+
clipHeight: activePage.height,
|
|
1436
|
+
children: activePage.elements.map((element) => /* @__PURE__ */ jsx6(
|
|
1437
|
+
ElementNode,
|
|
1438
|
+
{
|
|
1439
|
+
element,
|
|
1440
|
+
isSelected: selectedElementIds.includes(element.id),
|
|
1441
|
+
zoom,
|
|
1442
|
+
onClick: handleElementClick,
|
|
1443
|
+
onDragStart: handleElementDragStart,
|
|
1444
|
+
onDragMove: handleElementDragMove,
|
|
1445
|
+
onDragEnd: handleElementDragEnd,
|
|
1446
|
+
onDoubleClick: handleElementDoubleClick
|
|
1447
|
+
},
|
|
1448
|
+
element.id
|
|
1449
|
+
))
|
|
1450
|
+
}
|
|
1451
|
+
)
|
|
1452
|
+
] }) }),
|
|
1453
|
+
(activeVerticalGuide || activeHorizontalGuide) && /* @__PURE__ */ jsx6(Layer4, { children: /* @__PURE__ */ jsx6(Group2, { x: panX, y: panY, scaleX: zoom, scaleY: zoom, children: /* @__PURE__ */ jsx6(
|
|
1454
|
+
SnapGuidesOverlay,
|
|
1455
|
+
{
|
|
1456
|
+
pageWidth: activePage.width,
|
|
1457
|
+
pageHeight: activePage.height,
|
|
1458
|
+
activeVertical: activeVerticalGuide,
|
|
1459
|
+
activeHorizontal: activeHorizontalGuide,
|
|
1460
|
+
zoom
|
|
1461
|
+
}
|
|
1462
|
+
) }) }),
|
|
1463
|
+
/* @__PURE__ */ jsx6(
|
|
1464
|
+
SelectionLayer,
|
|
1465
|
+
{
|
|
1466
|
+
stageRef,
|
|
1467
|
+
selectedIds: selectedElementIds,
|
|
1468
|
+
selectedElements,
|
|
1469
|
+
canWrite,
|
|
1470
|
+
onTransformEnd: handleTransformEnd,
|
|
1471
|
+
pageId: activePageId
|
|
1472
|
+
}
|
|
1473
|
+
)
|
|
1474
|
+
]
|
|
1475
|
+
}
|
|
1476
|
+
),
|
|
1477
|
+
showBleed && activePage.bleed && /* @__PURE__ */ jsx6(
|
|
1478
|
+
"div",
|
|
1479
|
+
{
|
|
1480
|
+
className: "pointer-events-none absolute",
|
|
1481
|
+
style: { left: pageScreenX, top: pageScreenY, width: pageScreenW, height: pageScreenH },
|
|
1482
|
+
children: /* @__PURE__ */ jsx6(
|
|
1483
|
+
BleedTrimOverlay,
|
|
1484
|
+
{
|
|
1485
|
+
pageWidthPx: pageScreenW,
|
|
1486
|
+
pageHeightPx: pageScreenH,
|
|
1487
|
+
bleed: {
|
|
1488
|
+
top: activePage.bleed.top * zoom,
|
|
1489
|
+
right: activePage.bleed.right * zoom,
|
|
1490
|
+
bottom: activePage.bleed.bottom * zoom,
|
|
1491
|
+
left: activePage.bleed.left * zoom
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
)
|
|
1495
|
+
}
|
|
1496
|
+
),
|
|
1497
|
+
normalizedMarquee && /* @__PURE__ */ jsx6(
|
|
1498
|
+
"div",
|
|
1499
|
+
{
|
|
1500
|
+
className: "pointer-events-none absolute border border-blue-400 bg-blue-400/10",
|
|
1501
|
+
style: {
|
|
1502
|
+
left: panX + normalizedMarquee.x * zoom,
|
|
1503
|
+
top: panY + normalizedMarquee.y * zoom,
|
|
1504
|
+
width: normalizedMarquee.width * zoom,
|
|
1505
|
+
height: normalizedMarquee.height * zoom
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
),
|
|
1509
|
+
editingTextElement && /* @__PURE__ */ jsx6(
|
|
1510
|
+
InlineTextEditor,
|
|
1511
|
+
{
|
|
1512
|
+
element: editingTextElement,
|
|
1513
|
+
zoom,
|
|
1514
|
+
panX,
|
|
1515
|
+
panY,
|
|
1516
|
+
stageRect: { left: stageRect.left, top: stageRect.top },
|
|
1517
|
+
onCommit: handleTextCommit,
|
|
1518
|
+
onCancel: handleTextCancel
|
|
1519
|
+
}
|
|
1520
|
+
),
|
|
1521
|
+
renderAgentPanel && /* @__PURE__ */ jsx6("div", { className: "absolute right-0 top-0 bottom-0 z-20 w-80 pointer-events-auto", children: renderAgentPanel({ selectedElements, activePageId }) })
|
|
1522
|
+
]
|
|
1523
|
+
}
|
|
1524
|
+
);
|
|
1525
|
+
}
|
|
1526
|
+
function Workspace(props) {
|
|
1527
|
+
const stackRef = useRef4(
|
|
1528
|
+
createSceneCommandStack(
|
|
1529
|
+
props.document,
|
|
1530
|
+
props.document.pages[0]?.id ?? ""
|
|
1531
|
+
)
|
|
1532
|
+
);
|
|
1533
|
+
const [, setTick] = useState2(0);
|
|
1534
|
+
const forceRender = useCallback(() => setTick((t) => t + 1), []);
|
|
1535
|
+
useEffect4(() => {
|
|
1536
|
+
return stackRef.current.subscribe(forceRender);
|
|
1537
|
+
}, [forceRender]);
|
|
1538
|
+
const prevRevRef = useRef4(props.rev);
|
|
1539
|
+
useEffect4(() => {
|
|
1540
|
+
if (props.rev !== prevRevRef.current) {
|
|
1541
|
+
prevRevRef.current = props.rev;
|
|
1542
|
+
stackRef.current.reset(props.document);
|
|
1543
|
+
}
|
|
1544
|
+
}, [props.rev, props.document]);
|
|
1545
|
+
const state = stackRef.current.getState();
|
|
1546
|
+
const activePage = state.document.pages.find((p) => p.id === state.activePageId) ?? state.document.pages[0];
|
|
1547
|
+
if (!activePage) return null;
|
|
1548
|
+
return /* @__PURE__ */ jsx6(
|
|
1549
|
+
WorkspaceView,
|
|
1550
|
+
{
|
|
1551
|
+
stack: stackRef.current,
|
|
1552
|
+
activePage,
|
|
1553
|
+
canWrite: props.canWrite,
|
|
1554
|
+
onApplyOperations: props.onApplyOperations,
|
|
1555
|
+
onSelectionChange: props.onSelectionChange,
|
|
1556
|
+
renderAgentPanel: props.renderAgentPanel,
|
|
1557
|
+
renderSidePanel: props.renderSidePanel,
|
|
1558
|
+
className: props.className
|
|
1559
|
+
}
|
|
1560
|
+
);
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
// src/design-canvas-react/components/DesignCanvasEditor.tsx
|
|
1564
|
+
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
1565
|
+
var THUMBNAIL_HEIGHT_PX = 96;
|
|
1566
|
+
var thumbnailCache = /* @__PURE__ */ new Map();
|
|
1567
|
+
var THUMBNAIL_CACHE_LIMIT = 200;
|
|
1568
|
+
function evictIfNeeded() {
|
|
1569
|
+
if (thumbnailCache.size < THUMBNAIL_CACHE_LIMIT) return;
|
|
1570
|
+
const toDrop = Math.floor(THUMBNAIL_CACHE_LIMIT * 0.2);
|
|
1571
|
+
let dropped = 0;
|
|
1572
|
+
for (const key of thumbnailCache.keys()) {
|
|
1573
|
+
if (dropped >= toDrop) break;
|
|
1574
|
+
thumbnailCache.delete(key);
|
|
1575
|
+
dropped++;
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
function cheapHash(value) {
|
|
1579
|
+
const s = JSON.stringify(value) ?? "";
|
|
1580
|
+
let h = 2166136261;
|
|
1581
|
+
for (let i = 0; i < s.length; i++) {
|
|
1582
|
+
h ^= s.charCodeAt(i);
|
|
1583
|
+
h = Math.imul(h, 16777619);
|
|
1584
|
+
}
|
|
1585
|
+
return (h >>> 0).toString(36);
|
|
1586
|
+
}
|
|
1587
|
+
async function renderPageThumbnail(page) {
|
|
1588
|
+
const cacheKey = `${page.id}:${cheapHash(page.elements)}`;
|
|
1589
|
+
const cached = thumbnailCache.get(cacheKey);
|
|
1590
|
+
if (cached !== void 0) return cached;
|
|
1591
|
+
let Konva;
|
|
1592
|
+
try {
|
|
1593
|
+
const mod = await import("konva");
|
|
1594
|
+
Konva = mod.default;
|
|
1595
|
+
} catch {
|
|
1596
|
+
return null;
|
|
1597
|
+
}
|
|
1598
|
+
const aspectRatio = page.width > 0 ? page.width / page.height : 1;
|
|
1599
|
+
const thumbH = THUMBNAIL_HEIGHT_PX;
|
|
1600
|
+
const thumbW = Math.round(thumbH * aspectRatio);
|
|
1601
|
+
const scale = page.height > 0 ? thumbH / page.height : 1;
|
|
1602
|
+
let stage = null;
|
|
1603
|
+
try {
|
|
1604
|
+
const container = globalThis.document?.createElement("div");
|
|
1605
|
+
if (!container) return null;
|
|
1606
|
+
container.style.position = "absolute";
|
|
1607
|
+
container.style.left = "-9999px";
|
|
1608
|
+
container.style.top = "-9999px";
|
|
1609
|
+
globalThis.document.body.appendChild(container);
|
|
1610
|
+
stage = new Konva.Stage({ container, width: thumbW, height: thumbH });
|
|
1611
|
+
const layer = new Konva.Layer();
|
|
1612
|
+
stage.add(layer);
|
|
1613
|
+
layer.add(new Konva.Rect({
|
|
1614
|
+
x: 0,
|
|
1615
|
+
y: 0,
|
|
1616
|
+
width: thumbW,
|
|
1617
|
+
height: thumbH,
|
|
1618
|
+
fill: page.background,
|
|
1619
|
+
listening: false
|
|
1620
|
+
}));
|
|
1621
|
+
const group = new Konva.Group({ x: 0, y: 0, scaleX: scale, scaleY: scale, listening: false });
|
|
1622
|
+
layer.add(group);
|
|
1623
|
+
paintElements(Konva, group, page.elements);
|
|
1624
|
+
const dataUrl = stage.toDataURL({ mimeType: "image/png", pixelRatio: 1 });
|
|
1625
|
+
evictIfNeeded();
|
|
1626
|
+
thumbnailCache.set(cacheKey, dataUrl);
|
|
1627
|
+
return dataUrl;
|
|
1628
|
+
} catch {
|
|
1629
|
+
return null;
|
|
1630
|
+
} finally {
|
|
1631
|
+
if (stage) {
|
|
1632
|
+
stage.destroy();
|
|
1633
|
+
const el = stage.container();
|
|
1634
|
+
el.parentNode?.removeChild(el);
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
function paintElements(Konva, parent, elements) {
|
|
1639
|
+
for (const el of elements) {
|
|
1640
|
+
if (!el.visible) continue;
|
|
1641
|
+
switch (el.kind) {
|
|
1642
|
+
case "rect":
|
|
1643
|
+
parent.add(new Konva.Rect({
|
|
1644
|
+
x: el.x,
|
|
1645
|
+
y: el.y,
|
|
1646
|
+
width: el.width,
|
|
1647
|
+
height: el.height,
|
|
1648
|
+
rotation: el.rotation,
|
|
1649
|
+
opacity: el.opacity,
|
|
1650
|
+
fill: el.fill ?? void 0,
|
|
1651
|
+
cornerRadius: el.cornerRadius ?? 0,
|
|
1652
|
+
listening: false
|
|
1653
|
+
}));
|
|
1654
|
+
break;
|
|
1655
|
+
case "ellipse":
|
|
1656
|
+
parent.add(new Konva.Ellipse({
|
|
1657
|
+
x: el.x + el.width / 2,
|
|
1658
|
+
y: el.y + el.height / 2,
|
|
1659
|
+
radiusX: el.width / 2,
|
|
1660
|
+
radiusY: el.height / 2,
|
|
1661
|
+
rotation: el.rotation,
|
|
1662
|
+
opacity: el.opacity,
|
|
1663
|
+
fill: el.fill ?? void 0,
|
|
1664
|
+
listening: false
|
|
1665
|
+
}));
|
|
1666
|
+
break;
|
|
1667
|
+
case "text":
|
|
1668
|
+
parent.add(new Konva.Text({
|
|
1669
|
+
x: el.x,
|
|
1670
|
+
y: el.y,
|
|
1671
|
+
width: el.width,
|
|
1672
|
+
rotation: el.rotation,
|
|
1673
|
+
opacity: el.opacity,
|
|
1674
|
+
text: el.text,
|
|
1675
|
+
fontSize: el.fontSize,
|
|
1676
|
+
fontFamily: el.fontFamily,
|
|
1677
|
+
fill: el.fill,
|
|
1678
|
+
align: el.align,
|
|
1679
|
+
listening: false
|
|
1680
|
+
}));
|
|
1681
|
+
break;
|
|
1682
|
+
case "group": {
|
|
1683
|
+
const g = new Konva.Group({
|
|
1684
|
+
x: el.x,
|
|
1685
|
+
y: el.y,
|
|
1686
|
+
rotation: el.rotation,
|
|
1687
|
+
opacity: el.opacity,
|
|
1688
|
+
listening: false
|
|
1689
|
+
});
|
|
1690
|
+
parent.add(g);
|
|
1691
|
+
paintElements(Konva, g, el.children);
|
|
1692
|
+
break;
|
|
1693
|
+
}
|
|
1694
|
+
// image and video: skip — async loading not viable in sync thumbnail render
|
|
1695
|
+
default:
|
|
1696
|
+
break;
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
function DesignCanvasEditor(props) {
|
|
1701
|
+
return /* @__PURE__ */ jsx7(
|
|
1702
|
+
DesignCanvas,
|
|
1703
|
+
{
|
|
1704
|
+
...props,
|
|
1705
|
+
renderWorkspace: (ctx) => {
|
|
1706
|
+
if (!ctx.activePage) return null;
|
|
1707
|
+
return /* @__PURE__ */ jsx7(
|
|
1708
|
+
WorkspaceView,
|
|
1709
|
+
{
|
|
1710
|
+
stack: ctx.stack,
|
|
1711
|
+
activePage: ctx.activePage,
|
|
1712
|
+
canWrite: ctx.canWrite,
|
|
1713
|
+
onApplyOperations: props.onApplyOperations,
|
|
1714
|
+
onSelectionChange: props.onSelectionChange,
|
|
1715
|
+
renderAgentPanel: props.renderAgentPanel,
|
|
1716
|
+
renderSidePanel: props.renderSidePanel,
|
|
1717
|
+
onFitRef: ctx.onFitRef
|
|
1718
|
+
}
|
|
1719
|
+
);
|
|
1720
|
+
},
|
|
1721
|
+
renderThumbnail: renderPageThumbnail
|
|
1722
|
+
}
|
|
1723
|
+
);
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
export {
|
|
1727
|
+
createSnapEngine,
|
|
1728
|
+
collectGridTargets,
|
|
1729
|
+
marqueeSelect,
|
|
1730
|
+
nudgeDelta,
|
|
1731
|
+
DUPLICATE_OFFSET,
|
|
1732
|
+
createZoomPanMath,
|
|
1733
|
+
bakeRectTransform,
|
|
1734
|
+
bakeLineTransform,
|
|
1735
|
+
bakeTextTransform,
|
|
1736
|
+
ElementNode,
|
|
1737
|
+
SelectionLayer,
|
|
1738
|
+
GridLayer,
|
|
1739
|
+
WorkspaceView,
|
|
1740
|
+
Workspace,
|
|
1741
|
+
DesignCanvasEditor
|
|
1742
|
+
};
|
|
1743
|
+
//# sourceMappingURL=chunk-2Q73HGDI.js.map
|