@bwp-web/canvas 0.4.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/Canvas/Canvas.d.ts +11 -0
- package/dist/Canvas/Canvas.d.ts.map +1 -0
- package/dist/Canvas/index.d.ts +3 -0
- package/dist/Canvas/index.d.ts.map +1 -0
- package/dist/alignment/cursorSnapping.d.ts +55 -0
- package/dist/alignment/cursorSnapping.d.ts.map +1 -0
- package/dist/alignment/index.d.ts +9 -0
- package/dist/alignment/index.d.ts.map +1 -0
- package/dist/alignment/objectAlignment.d.ts +27 -0
- package/dist/alignment/objectAlignment.d.ts.map +1 -0
- package/dist/alignment/objectAlignmentMath.d.ts +35 -0
- package/dist/alignment/objectAlignmentMath.d.ts.map +1 -0
- package/dist/alignment/objectAlignmentRendering.d.ts +21 -0
- package/dist/alignment/objectAlignmentRendering.d.ts.map +1 -0
- package/dist/alignment/objectAlignmentUtils.d.ts +44 -0
- package/dist/alignment/objectAlignmentUtils.d.ts.map +1 -0
- package/dist/alignment/rotationSnap.d.ts +16 -0
- package/dist/alignment/rotationSnap.d.ts.map +1 -0
- package/dist/alignment/snapPoints.d.ts +18 -0
- package/dist/alignment/snapPoints.d.ts.map +1 -0
- package/dist/background.d.ts +79 -0
- package/dist/background.d.ts.map +1 -0
- package/dist/constants.d.ts +34 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/fabricAugmentation.d.ts +15 -0
- package/dist/fabricAugmentation.d.ts.map +1 -0
- package/dist/hooks/index.d.ts +5 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/shared.d.ts +20 -0
- package/dist/hooks/shared.d.ts.map +1 -0
- package/dist/hooks/useEditCanvas.d.ts +126 -0
- package/dist/hooks/useEditCanvas.d.ts.map +1 -0
- package/dist/hooks/useViewCanvas.d.ts +66 -0
- package/dist/hooks/useViewCanvas.d.ts.map +1 -0
- package/dist/index.cjs +2424 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2365 -0
- package/dist/index.js.map +1 -0
- package/dist/interactions/clickToCreate.d.ts +10 -0
- package/dist/interactions/clickToCreate.d.ts.map +1 -0
- package/dist/interactions/dragToCreate.d.ts +20 -0
- package/dist/interactions/dragToCreate.d.ts.map +1 -0
- package/dist/interactions/drawToCreate.d.ts +28 -0
- package/dist/interactions/drawToCreate.d.ts.map +1 -0
- package/dist/interactions/index.d.ts +9 -0
- package/dist/interactions/index.d.ts.map +1 -0
- package/dist/interactions/interactionSnapping.d.ts +34 -0
- package/dist/interactions/interactionSnapping.d.ts.map +1 -0
- package/dist/interactions/shared.d.ts +13 -0
- package/dist/interactions/shared.d.ts.map +1 -0
- package/dist/interactions/vertexEdit.d.ts +18 -0
- package/dist/interactions/vertexEdit.d.ts.map +1 -0
- package/dist/keyboard.d.ts +13 -0
- package/dist/keyboard.d.ts.map +1 -0
- package/dist/serialization.d.ts +52 -0
- package/dist/serialization.d.ts.map +1 -0
- package/dist/shapes/circle.d.ts +36 -0
- package/dist/shapes/circle.d.ts.map +1 -0
- package/dist/shapes/index.d.ts +4 -0
- package/dist/shapes/index.d.ts.map +1 -0
- package/dist/shapes/polygon.d.ts +37 -0
- package/dist/shapes/polygon.d.ts.map +1 -0
- package/dist/shapes/rectangle.d.ts +31 -0
- package/dist/shapes/rectangle.d.ts.map +1 -0
- package/dist/styles.d.ts +46 -0
- package/dist/styles.d.ts.map +1 -0
- package/dist/types.d.ts +48 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/viewport.d.ts +51 -0
- package/dist/viewport.d.ts.map +1 -0
- package/package.json +51 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2365 @@
|
|
|
1
|
+
// src/Canvas/Canvas.tsx
|
|
2
|
+
import { Canvas as FabricCanvas } from "fabric";
|
|
3
|
+
import { useEffect, useRef } from "react";
|
|
4
|
+
|
|
5
|
+
// src/keyboard.ts
|
|
6
|
+
function deleteObjects(canvas, ...objects) {
|
|
7
|
+
canvas.remove(...objects);
|
|
8
|
+
canvas.requestRenderAll();
|
|
9
|
+
}
|
|
10
|
+
function enableKeyboardShortcuts(canvas) {
|
|
11
|
+
const handleKeyDown = (e) => {
|
|
12
|
+
if (e.key === "Escape" || e.key === "Delete" || e.key === "Backspace") {
|
|
13
|
+
const active = canvas.getActiveObjects();
|
|
14
|
+
if (active.length > 0) {
|
|
15
|
+
canvas.discardActiveObject();
|
|
16
|
+
deleteObjects(canvas, ...active);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
21
|
+
return () => {
|
|
22
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/Canvas/Canvas.tsx
|
|
27
|
+
import { jsx } from "react/jsx-runtime";
|
|
28
|
+
function Canvas({
|
|
29
|
+
width = 800,
|
|
30
|
+
height = 600,
|
|
31
|
+
className,
|
|
32
|
+
style,
|
|
33
|
+
onReady
|
|
34
|
+
}) {
|
|
35
|
+
const canvasRef = useRef(null);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const el = canvasRef.current;
|
|
38
|
+
if (!el) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const fabricCanvas = new FabricCanvas(el, { width, height });
|
|
42
|
+
onReady?.(fabricCanvas);
|
|
43
|
+
const cleanupShortcuts = enableKeyboardShortcuts(fabricCanvas);
|
|
44
|
+
return () => {
|
|
45
|
+
cleanupShortcuts();
|
|
46
|
+
fabricCanvas.dispose();
|
|
47
|
+
};
|
|
48
|
+
}, []);
|
|
49
|
+
return /* @__PURE__ */ jsx("div", { className, style, children: /* @__PURE__ */ jsx("canvas", { ref: canvasRef }) });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/hooks/useEditCanvas.ts
|
|
53
|
+
import { useCallback, useEffect as useEffect2, useRef as useRef2, useState } from "react";
|
|
54
|
+
import { Polygon as Polygon4 } from "fabric";
|
|
55
|
+
|
|
56
|
+
// src/viewport.ts
|
|
57
|
+
import { Point } from "fabric";
|
|
58
|
+
|
|
59
|
+
// src/constants.ts
|
|
60
|
+
var DEFAULT_MIN_ZOOM = 0.2;
|
|
61
|
+
var DEFAULT_MAX_ZOOM = 10;
|
|
62
|
+
var DEFAULT_ZOOM_FACTOR = 1.03;
|
|
63
|
+
var DEFAULT_ZOOM_STEP = 0.2;
|
|
64
|
+
var DEFAULT_VIEWPORT_PADDING = 0.05;
|
|
65
|
+
var BASE_CANVAS_SIZE = 1e3;
|
|
66
|
+
var DEFAULT_SNAP_MARGIN = 6;
|
|
67
|
+
var DEFAULT_ANGLE_SNAP_INTERVAL = 15;
|
|
68
|
+
var MIN_DRAG_SIZE = 3;
|
|
69
|
+
var POLYGON_CLOSE_THRESHOLD = 10;
|
|
70
|
+
var DEFAULT_IMAGE_MAX_SIZE = 4096;
|
|
71
|
+
var DEFAULT_IMAGE_MIN_SIZE = 480;
|
|
72
|
+
var DEFAULT_VERTEX_HANDLE_RADIUS = 6;
|
|
73
|
+
var DEFAULT_VERTEX_HANDLE_FILL = "#ffffff";
|
|
74
|
+
var DEFAULT_VERTEX_HANDLE_STROKE = "#2196f3";
|
|
75
|
+
var DEFAULT_VERTEX_HANDLE_STROKE_WIDTH = 2;
|
|
76
|
+
|
|
77
|
+
// src/viewport.ts
|
|
78
|
+
function getPointerXY(e) {
|
|
79
|
+
if (e instanceof MouseEvent) return { x: e.clientX, y: e.clientY };
|
|
80
|
+
if (typeof TouchEvent !== "undefined" && e instanceof TouchEvent && e.touches.length > 0) {
|
|
81
|
+
return { x: e.touches[0].clientX, y: e.touches[0].clientY };
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
function touchDistance(touches) {
|
|
86
|
+
const dx = touches[0].clientX - touches[1].clientX;
|
|
87
|
+
const dy = touches[0].clientY - touches[1].clientY;
|
|
88
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
89
|
+
}
|
|
90
|
+
function touchMidpoint(touches, el) {
|
|
91
|
+
const rect = el.getBoundingClientRect();
|
|
92
|
+
return new Point(
|
|
93
|
+
(touches[0].clientX + touches[1].clientX) / 2 - rect.left,
|
|
94
|
+
(touches[0].clientY + touches[1].clientY) / 2 - rect.top
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
function setupWheelZoom(canvas, bounds, zoomFactor, isEnabled) {
|
|
98
|
+
const handleWheel = (opt) => {
|
|
99
|
+
if (!isEnabled()) return;
|
|
100
|
+
const e = opt.e;
|
|
101
|
+
e.preventDefault();
|
|
102
|
+
e.stopPropagation();
|
|
103
|
+
const delta = e.deltaY;
|
|
104
|
+
let zoom = canvas.getZoom();
|
|
105
|
+
zoom = delta < 0 ? zoom * zoomFactor : zoom / zoomFactor;
|
|
106
|
+
zoom = Math.min(Math.max(zoom, bounds.minZoom), bounds.maxZoom);
|
|
107
|
+
canvas.zoomToPoint(new Point(e.offsetX, e.offsetY), zoom);
|
|
108
|
+
};
|
|
109
|
+
canvas.on("mouse:wheel", handleWheel);
|
|
110
|
+
return handleWheel;
|
|
111
|
+
}
|
|
112
|
+
function setupMousePan(canvas, getMode, isEnabled) {
|
|
113
|
+
let isPanning = false;
|
|
114
|
+
let lastPanX = 0;
|
|
115
|
+
let lastPanY = 0;
|
|
116
|
+
let didDisableSelection = false;
|
|
117
|
+
const handleMouseDown = (opt) => {
|
|
118
|
+
if (!isEnabled()) return;
|
|
119
|
+
const pos = getPointerXY(opt.e);
|
|
120
|
+
if (!pos) return;
|
|
121
|
+
const e = opt.e;
|
|
122
|
+
const mode = getMode();
|
|
123
|
+
const isMiddleButton = e instanceof MouseEvent && e.button === 1;
|
|
124
|
+
const isModifiedSelect = e instanceof MouseEvent && mode === "select" && (e.metaKey || e.ctrlKey);
|
|
125
|
+
const shouldPan = mode === "pan" || isMiddleButton || isModifiedSelect || mode === "select" && !opt.target;
|
|
126
|
+
if (shouldPan) {
|
|
127
|
+
isPanning = true;
|
|
128
|
+
lastPanX = pos.x;
|
|
129
|
+
lastPanY = pos.y;
|
|
130
|
+
if (canvas.selection) {
|
|
131
|
+
didDisableSelection = true;
|
|
132
|
+
canvas.selection = false;
|
|
133
|
+
}
|
|
134
|
+
canvas.setCursor("grab");
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
const handleMouseMove = (opt) => {
|
|
138
|
+
if (!isPanning) return;
|
|
139
|
+
const pos = getPointerXY(opt.e);
|
|
140
|
+
if (!pos) return;
|
|
141
|
+
const dx = pos.x - lastPanX;
|
|
142
|
+
const dy = pos.y - lastPanY;
|
|
143
|
+
lastPanX = pos.x;
|
|
144
|
+
lastPanY = pos.y;
|
|
145
|
+
canvas.relativePan(new Point(dx, dy));
|
|
146
|
+
canvas.setCursor("grab");
|
|
147
|
+
};
|
|
148
|
+
const handleMouseUp = () => {
|
|
149
|
+
if (isPanning) {
|
|
150
|
+
isPanning = false;
|
|
151
|
+
if (didDisableSelection) {
|
|
152
|
+
canvas.selection = true;
|
|
153
|
+
didDisableSelection = false;
|
|
154
|
+
}
|
|
155
|
+
canvas.setCursor(getMode() === "pan" ? "grab" : "default");
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
canvas.on("mouse:down", handleMouseDown);
|
|
159
|
+
canvas.on("mouse:move", handleMouseMove);
|
|
160
|
+
canvas.on("mouse:up", handleMouseUp);
|
|
161
|
+
return { handleMouseDown, handleMouseMove, handleMouseUp };
|
|
162
|
+
}
|
|
163
|
+
function setupPinchZoom(canvas, bounds, isEnabled) {
|
|
164
|
+
const canvasEl = canvas.getElement();
|
|
165
|
+
if (typeof TouchEvent === "undefined") return () => {
|
|
166
|
+
};
|
|
167
|
+
let lastPinchDist = 0;
|
|
168
|
+
const onTouchStart = (e) => {
|
|
169
|
+
if (e.touches.length === 2) {
|
|
170
|
+
e.preventDefault();
|
|
171
|
+
lastPinchDist = touchDistance(e.touches);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
const onTouchMove = (e) => {
|
|
175
|
+
if (!isEnabled() || e.touches.length !== 2) return;
|
|
176
|
+
e.preventDefault();
|
|
177
|
+
const dist = touchDistance(e.touches);
|
|
178
|
+
if (lastPinchDist === 0) {
|
|
179
|
+
lastPinchDist = dist;
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const ratio = dist / lastPinchDist;
|
|
183
|
+
lastPinchDist = dist;
|
|
184
|
+
const mid = touchMidpoint(e.touches, canvasEl);
|
|
185
|
+
canvas.zoomToPoint(
|
|
186
|
+
mid,
|
|
187
|
+
Math.min(
|
|
188
|
+
Math.max(canvas.getZoom() * ratio, bounds.minZoom),
|
|
189
|
+
bounds.maxZoom
|
|
190
|
+
)
|
|
191
|
+
);
|
|
192
|
+
};
|
|
193
|
+
const onTouchEnd = (e) => {
|
|
194
|
+
if (e.touches.length < 2) lastPinchDist = 0;
|
|
195
|
+
};
|
|
196
|
+
canvasEl.addEventListener("touchstart", onTouchStart, { passive: false });
|
|
197
|
+
canvasEl.addEventListener("touchmove", onTouchMove, { passive: false });
|
|
198
|
+
canvasEl.addEventListener("touchend", onTouchEnd);
|
|
199
|
+
return () => {
|
|
200
|
+
canvasEl.removeEventListener("touchstart", onTouchStart);
|
|
201
|
+
canvasEl.removeEventListener("touchmove", onTouchMove);
|
|
202
|
+
canvasEl.removeEventListener("touchend", onTouchEnd);
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function enablePanAndZoom(canvas, options) {
|
|
206
|
+
const bounds = {
|
|
207
|
+
minZoom: options?.minZoom ?? DEFAULT_MIN_ZOOM,
|
|
208
|
+
maxZoom: options?.maxZoom ?? DEFAULT_MAX_ZOOM
|
|
209
|
+
};
|
|
210
|
+
const zoomFactor = options?.zoomFactor ?? DEFAULT_ZOOM_FACTOR;
|
|
211
|
+
let mode = options?.initialMode ?? "select";
|
|
212
|
+
let enabled = true;
|
|
213
|
+
const isEnabled = () => enabled;
|
|
214
|
+
const getMode = () => mode;
|
|
215
|
+
const handleWheel = setupWheelZoom(canvas, bounds, zoomFactor, isEnabled);
|
|
216
|
+
const panHandlers = setupMousePan(canvas, getMode, isEnabled);
|
|
217
|
+
const cleanupPinch = setupPinchZoom(canvas, bounds, isEnabled);
|
|
218
|
+
return {
|
|
219
|
+
setMode(newMode) {
|
|
220
|
+
mode = newMode;
|
|
221
|
+
if (newMode === "pan") {
|
|
222
|
+
canvas.selection = false;
|
|
223
|
+
canvas.forEachObject((obj) => {
|
|
224
|
+
obj.selectable = false;
|
|
225
|
+
obj.evented = false;
|
|
226
|
+
});
|
|
227
|
+
canvas.discardActiveObject();
|
|
228
|
+
canvas.setCursor("grab");
|
|
229
|
+
} else {
|
|
230
|
+
canvas.selection = true;
|
|
231
|
+
canvas.forEachObject((obj) => {
|
|
232
|
+
obj.selectable = true;
|
|
233
|
+
obj.evented = true;
|
|
234
|
+
});
|
|
235
|
+
canvas.setCursor("default");
|
|
236
|
+
}
|
|
237
|
+
canvas.requestRenderAll();
|
|
238
|
+
},
|
|
239
|
+
getMode() {
|
|
240
|
+
return mode;
|
|
241
|
+
},
|
|
242
|
+
setEnabled(value) {
|
|
243
|
+
enabled = value;
|
|
244
|
+
},
|
|
245
|
+
zoomIn(step = DEFAULT_ZOOM_STEP) {
|
|
246
|
+
const z = Math.min(canvas.getZoom() + step, bounds.maxZoom);
|
|
247
|
+
canvas.zoomToPoint(
|
|
248
|
+
new Point(canvas.getWidth() / 2, canvas.getHeight() / 2),
|
|
249
|
+
z
|
|
250
|
+
);
|
|
251
|
+
},
|
|
252
|
+
zoomOut(step = DEFAULT_ZOOM_STEP) {
|
|
253
|
+
const z = Math.max(canvas.getZoom() - step, bounds.minZoom);
|
|
254
|
+
canvas.zoomToPoint(
|
|
255
|
+
new Point(canvas.getWidth() / 2, canvas.getHeight() / 2),
|
|
256
|
+
z
|
|
257
|
+
);
|
|
258
|
+
},
|
|
259
|
+
cleanup() {
|
|
260
|
+
canvas.off("mouse:wheel", handleWheel);
|
|
261
|
+
canvas.off("mouse:down", panHandlers.handleMouseDown);
|
|
262
|
+
canvas.off("mouse:move", panHandlers.handleMouseMove);
|
|
263
|
+
canvas.off("mouse:up", panHandlers.handleMouseUp);
|
|
264
|
+
cleanupPinch();
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
function resetViewport(canvas) {
|
|
269
|
+
canvas.setViewportTransform([1, 0, 0, 1, 0, 0]);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// src/background.ts
|
|
273
|
+
import { FabricImage, filters } from "fabric";
|
|
274
|
+
function getBackgroundImage(canvas) {
|
|
275
|
+
return canvas.backgroundImage;
|
|
276
|
+
}
|
|
277
|
+
function fitViewportToBackground(canvas, options) {
|
|
278
|
+
const bg = getBackgroundImage(canvas);
|
|
279
|
+
if (!bg) return;
|
|
280
|
+
const bgWidth = bg.getScaledWidth();
|
|
281
|
+
const bgHeight = bg.getScaledHeight();
|
|
282
|
+
if (!bgWidth || !bgHeight) return;
|
|
283
|
+
const center = bg.getCenterPoint();
|
|
284
|
+
const imgLeft = center.x - bgWidth / 2;
|
|
285
|
+
const imgTop = center.y - bgHeight / 2;
|
|
286
|
+
const padding = options?.padding ?? DEFAULT_VIEWPORT_PADDING;
|
|
287
|
+
const canvasWidth = canvas.getWidth();
|
|
288
|
+
const canvasHeight = canvas.getHeight();
|
|
289
|
+
const availableWidth = canvasWidth * (1 - padding * 2);
|
|
290
|
+
const availableHeight = canvasHeight * (1 - padding * 2);
|
|
291
|
+
const scale = Math.min(availableWidth / bgWidth, availableHeight / bgHeight);
|
|
292
|
+
const scaledW = bgWidth * scale;
|
|
293
|
+
const scaledH = bgHeight * scale;
|
|
294
|
+
const offsetX = (canvasWidth - scaledW) / 2 - imgLeft * scale;
|
|
295
|
+
const offsetY = (canvasHeight - scaledH) / 2 - imgTop * scale;
|
|
296
|
+
canvas.setViewportTransform([scale, 0, 0, scale, offsetX, offsetY]);
|
|
297
|
+
canvas.requestRenderAll();
|
|
298
|
+
}
|
|
299
|
+
function setBackgroundOpacity(canvas, opacity) {
|
|
300
|
+
const bg = getBackgroundImage(canvas);
|
|
301
|
+
if (!bg) return;
|
|
302
|
+
bg.set("opacity", Math.min(1, Math.max(0, opacity)));
|
|
303
|
+
canvas.requestRenderAll();
|
|
304
|
+
}
|
|
305
|
+
function getBackgroundOpacity(canvas) {
|
|
306
|
+
const bg = getBackgroundImage(canvas);
|
|
307
|
+
return bg?.opacity ?? 1;
|
|
308
|
+
}
|
|
309
|
+
function setBackgroundInverted(canvas, inverted) {
|
|
310
|
+
const bg = getBackgroundImage(canvas);
|
|
311
|
+
if (!bg) return;
|
|
312
|
+
const currentFilters = bg.filters ?? [];
|
|
313
|
+
const hasInvert = currentFilters.some((f) => f instanceof filters.Invert);
|
|
314
|
+
if (inverted && !hasInvert) {
|
|
315
|
+
bg.filters = [...currentFilters, new filters.Invert()];
|
|
316
|
+
bg.applyFilters();
|
|
317
|
+
} else if (!inverted && hasInvert) {
|
|
318
|
+
bg.filters = currentFilters.filter((f) => !(f instanceof filters.Invert));
|
|
319
|
+
bg.applyFilters();
|
|
320
|
+
}
|
|
321
|
+
canvas.requestRenderAll();
|
|
322
|
+
}
|
|
323
|
+
function getBackgroundInverted(canvas) {
|
|
324
|
+
const bg = getBackgroundImage(canvas);
|
|
325
|
+
if (!bg?.filters) return false;
|
|
326
|
+
return bg.filters.some((f) => f instanceof filters.Invert);
|
|
327
|
+
}
|
|
328
|
+
function resizeImageUrl(url, options) {
|
|
329
|
+
const maxSize = options?.maxSize ?? DEFAULT_IMAGE_MAX_SIZE;
|
|
330
|
+
const minSize = options?.minSize ?? DEFAULT_IMAGE_MIN_SIZE;
|
|
331
|
+
return new Promise((resolve, reject) => {
|
|
332
|
+
const img = new Image();
|
|
333
|
+
img.crossOrigin = "anonymous";
|
|
334
|
+
img.onload = () => {
|
|
335
|
+
const { naturalWidth: w, naturalHeight: h } = img;
|
|
336
|
+
if (w < minSize && h < minSize) {
|
|
337
|
+
reject(
|
|
338
|
+
new Error(
|
|
339
|
+
`Image is too small (${w}\xD7${h}). Minimum size is ${minSize}px on either dimension.`
|
|
340
|
+
)
|
|
341
|
+
);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
const needsResize = w > maxSize || h > maxSize;
|
|
345
|
+
if (!needsResize) {
|
|
346
|
+
resolve({ url, width: w, height: h, wasResized: false });
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const scale = Math.min(maxSize / w, maxSize / h);
|
|
350
|
+
const newW = Math.round(w * scale);
|
|
351
|
+
const newH = Math.round(h * scale);
|
|
352
|
+
const tempCanvas = document.createElement("canvas");
|
|
353
|
+
tempCanvas.width = newW;
|
|
354
|
+
tempCanvas.height = newH;
|
|
355
|
+
const ctx = tempCanvas.getContext("2d");
|
|
356
|
+
if (!ctx) {
|
|
357
|
+
reject(new Error("Could not get 2D context for image resize."));
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
ctx.drawImage(img, 0, 0, newW, newH);
|
|
361
|
+
resolve({
|
|
362
|
+
url: tempCanvas.toDataURL("image/png"),
|
|
363
|
+
width: newW,
|
|
364
|
+
height: newH,
|
|
365
|
+
wasResized: true
|
|
366
|
+
});
|
|
367
|
+
};
|
|
368
|
+
img.onerror = () => reject(new Error(`Failed to load image: ${url}`));
|
|
369
|
+
img.src = url;
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
async function setBackgroundImage(canvas, url, resize) {
|
|
373
|
+
let imageUrl = url;
|
|
374
|
+
if (resize !== void 0) {
|
|
375
|
+
const result = await resizeImageUrl(url, resize);
|
|
376
|
+
imageUrl = result.url;
|
|
377
|
+
}
|
|
378
|
+
const img = await FabricImage.fromURL(imageUrl, { crossOrigin: "anonymous" });
|
|
379
|
+
canvas.backgroundImage = img;
|
|
380
|
+
canvas.requestRenderAll();
|
|
381
|
+
return img;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// src/hooks/shared.ts
|
|
385
|
+
function syncZoom(canvasRef, setZoom) {
|
|
386
|
+
const canvas = canvasRef.current;
|
|
387
|
+
if (canvas) setZoom(canvas.getZoom());
|
|
388
|
+
}
|
|
389
|
+
function createViewportActions(canvasRef, viewportRef, setZoom) {
|
|
390
|
+
const resetViewport2 = () => {
|
|
391
|
+
const canvas = canvasRef.current;
|
|
392
|
+
if (!canvas) return;
|
|
393
|
+
if (canvas.backgroundImage) {
|
|
394
|
+
fitViewportToBackground(canvas);
|
|
395
|
+
} else {
|
|
396
|
+
resetViewport(canvas);
|
|
397
|
+
}
|
|
398
|
+
setZoom(canvas.getZoom());
|
|
399
|
+
};
|
|
400
|
+
const zoomIn = (step) => {
|
|
401
|
+
viewportRef.current?.zoomIn(step);
|
|
402
|
+
syncZoom(canvasRef, setZoom);
|
|
403
|
+
};
|
|
404
|
+
const zoomOut = (step) => {
|
|
405
|
+
viewportRef.current?.zoomOut(step);
|
|
406
|
+
syncZoom(canvasRef, setZoom);
|
|
407
|
+
};
|
|
408
|
+
return { resetViewport: resetViewport2, zoomIn, zoomOut };
|
|
409
|
+
}
|
|
410
|
+
function resolveAlignmentEnabled(enableAlignment, alignmentProp) {
|
|
411
|
+
if (enableAlignment !== void 0) return enableAlignment;
|
|
412
|
+
return alignmentProp !== false;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// src/alignment/snapPoints.ts
|
|
416
|
+
import { Point as Point3, Polygon, Rect, util } from "fabric";
|
|
417
|
+
|
|
418
|
+
// src/alignment/objectAlignmentUtils.ts
|
|
419
|
+
import {
|
|
420
|
+
ActiveSelection,
|
|
421
|
+
Group,
|
|
422
|
+
Point as Point2
|
|
423
|
+
} from "fabric";
|
|
424
|
+
function getStrokeFreeCoords(obj) {
|
|
425
|
+
const m = obj.calcTransformMatrix();
|
|
426
|
+
const w = obj.width / 2;
|
|
427
|
+
const h = obj.height / 2;
|
|
428
|
+
return [
|
|
429
|
+
new Point2(-w, -h).transform(m),
|
|
430
|
+
// tl
|
|
431
|
+
new Point2(w, -h).transform(m),
|
|
432
|
+
// tr
|
|
433
|
+
new Point2(w, h).transform(m),
|
|
434
|
+
// br
|
|
435
|
+
new Point2(-w, h).transform(m)
|
|
436
|
+
// bl
|
|
437
|
+
];
|
|
438
|
+
}
|
|
439
|
+
function getAbsoluteDistance(a, b) {
|
|
440
|
+
return Math.abs(a - b);
|
|
441
|
+
}
|
|
442
|
+
function findNearestOnAxis(point, list, axis) {
|
|
443
|
+
let distance = Infinity;
|
|
444
|
+
let matches = [];
|
|
445
|
+
for (const item of list) {
|
|
446
|
+
const d = getAbsoluteDistance(point[axis], item[axis]);
|
|
447
|
+
if (distance > d) {
|
|
448
|
+
matches = [];
|
|
449
|
+
distance = d;
|
|
450
|
+
}
|
|
451
|
+
if (distance === d) {
|
|
452
|
+
matches.push(item);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return { distance, matches };
|
|
456
|
+
}
|
|
457
|
+
function getBoundingPointMap(target) {
|
|
458
|
+
const coords = getStrokeFreeCoords(target);
|
|
459
|
+
return {
|
|
460
|
+
tl: coords[0],
|
|
461
|
+
tr: coords[1],
|
|
462
|
+
br: coords[2],
|
|
463
|
+
bl: coords[3],
|
|
464
|
+
mt: coords[0].add(coords[1]).scalarDivide(2),
|
|
465
|
+
mr: coords[1].add(coords[2]).scalarDivide(2),
|
|
466
|
+
mb: coords[2].add(coords[3]).scalarDivide(2),
|
|
467
|
+
ml: coords[3].add(coords[0]).scalarDivide(2)
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
function getOppositeCornerMap(target) {
|
|
471
|
+
const aCoords = target.aCoords ?? target.calcACoords();
|
|
472
|
+
return {
|
|
473
|
+
tl: aCoords.br,
|
|
474
|
+
tr: aCoords.bl,
|
|
475
|
+
br: aCoords.tl,
|
|
476
|
+
bl: aCoords.tr,
|
|
477
|
+
mt: aCoords.br.add(aCoords.bl).scalarDivide(2),
|
|
478
|
+
mr: aCoords.bl.add(aCoords.tl).scalarDivide(2),
|
|
479
|
+
mb: aCoords.tl.add(aCoords.tr).scalarDivide(2),
|
|
480
|
+
ml: aCoords.tr.add(aCoords.br).scalarDivide(2)
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
function getAlignmentTargets(target) {
|
|
484
|
+
const objects = /* @__PURE__ */ new Set();
|
|
485
|
+
const canvas = target.canvas;
|
|
486
|
+
if (!canvas) return objects;
|
|
487
|
+
const children = target instanceof ActiveSelection ? target.getObjects() : [target];
|
|
488
|
+
canvas.forEachObject((o) => {
|
|
489
|
+
if (!o.isOnScreen() || !o.visible) return;
|
|
490
|
+
if (o.constructor === Group) {
|
|
491
|
+
collectGroupChildren(objects, o);
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
objects.add(o);
|
|
495
|
+
});
|
|
496
|
+
for (const child of children) {
|
|
497
|
+
if (child.constructor === Group) {
|
|
498
|
+
for (const gc of child.getObjects()) objects.delete(gc);
|
|
499
|
+
} else {
|
|
500
|
+
objects.delete(child);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return objects;
|
|
504
|
+
}
|
|
505
|
+
function collectGroupChildren(objects, group) {
|
|
506
|
+
for (const child of group.getObjects()) {
|
|
507
|
+
if (!child.visible) continue;
|
|
508
|
+
if (child.constructor === Group) {
|
|
509
|
+
collectGroupChildren(objects, child);
|
|
510
|
+
} else {
|
|
511
|
+
objects.add(child);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// src/alignment/snapPoints.ts
|
|
517
|
+
var registry = [];
|
|
518
|
+
function registerSnapPointExtractor(matcher, extractor) {
|
|
519
|
+
registry.unshift({ matcher, extractor });
|
|
520
|
+
}
|
|
521
|
+
function getSnapPoints(object) {
|
|
522
|
+
for (const { matcher, extractor } of registry) {
|
|
523
|
+
if (matcher(object)) {
|
|
524
|
+
return extractor(object);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return getDefaultSnapPoints(object);
|
|
528
|
+
}
|
|
529
|
+
function getDefaultSnapPoints(object) {
|
|
530
|
+
const coords = getStrokeFreeCoords(object);
|
|
531
|
+
return [...coords, object.getCenterPoint()];
|
|
532
|
+
}
|
|
533
|
+
registerSnapPointExtractor(
|
|
534
|
+
(obj) => obj instanceof Rect,
|
|
535
|
+
(obj) => {
|
|
536
|
+
const [tl, tr, br, bl] = getStrokeFreeCoords(obj);
|
|
537
|
+
const mt = tl.add(tr).scalarDivide(2);
|
|
538
|
+
const mr = tr.add(br).scalarDivide(2);
|
|
539
|
+
const mb = br.add(bl).scalarDivide(2);
|
|
540
|
+
const ml = bl.add(tl).scalarDivide(2);
|
|
541
|
+
return [tl, tr, br, bl, mt, mr, mb, ml, obj.getCenterPoint()];
|
|
542
|
+
}
|
|
543
|
+
);
|
|
544
|
+
registerSnapPointExtractor(
|
|
545
|
+
(obj) => obj instanceof Polygon,
|
|
546
|
+
(obj) => {
|
|
547
|
+
const polygon = obj;
|
|
548
|
+
const matrix = polygon.calcTransformMatrix();
|
|
549
|
+
const points = polygon.points.map((pt) => {
|
|
550
|
+
const local = new Point3(
|
|
551
|
+
pt.x - polygon.pathOffset.x,
|
|
552
|
+
pt.y - polygon.pathOffset.y
|
|
553
|
+
);
|
|
554
|
+
return util.transformPoint(local, matrix);
|
|
555
|
+
});
|
|
556
|
+
points.push(polygon.getCenterPoint());
|
|
557
|
+
return points;
|
|
558
|
+
}
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
// src/alignment/objectAlignment.ts
|
|
562
|
+
import { util as util2 } from "fabric";
|
|
563
|
+
|
|
564
|
+
// src/alignment/objectAlignmentRendering.ts
|
|
565
|
+
function drawAlignmentLine(config, origin, target) {
|
|
566
|
+
const ctx = config.canvas.getTopContext();
|
|
567
|
+
const vt = config.canvas.viewportTransform;
|
|
568
|
+
const zoom = config.canvas.getZoom();
|
|
569
|
+
ctx.save();
|
|
570
|
+
ctx.transform(...vt);
|
|
571
|
+
ctx.lineWidth = config.width / zoom;
|
|
572
|
+
if (config.lineDash) ctx.setLineDash(config.lineDash);
|
|
573
|
+
ctx.strokeStyle = config.color;
|
|
574
|
+
ctx.beginPath();
|
|
575
|
+
ctx.moveTo(origin.x, origin.y);
|
|
576
|
+
ctx.lineTo(target.x, target.y);
|
|
577
|
+
ctx.stroke();
|
|
578
|
+
if (config.lineDash) ctx.setLineDash([]);
|
|
579
|
+
drawXMarker(ctx, origin, config.xSize / zoom);
|
|
580
|
+
drawXMarker(ctx, target, config.xSize / zoom);
|
|
581
|
+
ctx.restore();
|
|
582
|
+
}
|
|
583
|
+
function drawXMarker(ctx, point, size) {
|
|
584
|
+
ctx.save();
|
|
585
|
+
ctx.translate(point.x, point.y);
|
|
586
|
+
ctx.beginPath();
|
|
587
|
+
ctx.moveTo(-size, -size);
|
|
588
|
+
ctx.lineTo(size, size);
|
|
589
|
+
ctx.moveTo(size, -size);
|
|
590
|
+
ctx.lineTo(-size, size);
|
|
591
|
+
ctx.stroke();
|
|
592
|
+
ctx.restore();
|
|
593
|
+
}
|
|
594
|
+
function drawMarkerList(config, lines) {
|
|
595
|
+
const ctx = config.canvas.getTopContext();
|
|
596
|
+
const vt = config.canvas.viewportTransform;
|
|
597
|
+
const zoom = config.canvas.getZoom();
|
|
598
|
+
const markerSize = config.xSize / zoom;
|
|
599
|
+
ctx.save();
|
|
600
|
+
ctx.transform(...vt);
|
|
601
|
+
ctx.lineWidth = config.width / zoom;
|
|
602
|
+
ctx.strokeStyle = config.color;
|
|
603
|
+
for (const item of lines) drawXMarker(ctx, item.target, markerSize);
|
|
604
|
+
ctx.restore();
|
|
605
|
+
}
|
|
606
|
+
function drawVerticalAlignmentLines(config, lines) {
|
|
607
|
+
for (const v of lines) {
|
|
608
|
+
const { origin, target } = JSON.parse(v);
|
|
609
|
+
const from = { x: target.x, y: origin.y };
|
|
610
|
+
drawAlignmentLine(config, from, target);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
function drawHorizontalAlignmentLines(config, lines) {
|
|
614
|
+
for (const h of lines) {
|
|
615
|
+
const { origin, target } = JSON.parse(h);
|
|
616
|
+
const from = { x: origin.x, y: target.y };
|
|
617
|
+
drawAlignmentLine(config, from, target);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// src/alignment/objectAlignmentMath.ts
|
|
622
|
+
var OPPOSITE_ORIGIN_MAP = {
|
|
623
|
+
tl: ["right", "bottom"],
|
|
624
|
+
tr: ["left", "bottom"],
|
|
625
|
+
br: ["left", "top"],
|
|
626
|
+
bl: ["right", "top"],
|
|
627
|
+
mt: ["center", "bottom"],
|
|
628
|
+
mr: ["left", "center"],
|
|
629
|
+
mb: ["center", "top"],
|
|
630
|
+
ml: ["right", "center"]
|
|
631
|
+
};
|
|
632
|
+
function collectMovingAlignmentLines(target, points, margin) {
|
|
633
|
+
const list = [...getStrokeFreeCoords(target)];
|
|
634
|
+
list.push(target.getCenterPoint());
|
|
635
|
+
const opts = { target, list, points, margin };
|
|
636
|
+
const verticalLines = collectMovingAxisMatches({ ...opts, axis: "x" });
|
|
637
|
+
const horizontalLines = collectMovingAxisMatches({ ...opts, axis: "y" });
|
|
638
|
+
return { verticalLines, horizontalLines };
|
|
639
|
+
}
|
|
640
|
+
function collectMovingAxisMatches(props) {
|
|
641
|
+
const { target, list, points, margin, axis } = props;
|
|
642
|
+
const result = [];
|
|
643
|
+
const distances = [];
|
|
644
|
+
let min = Infinity;
|
|
645
|
+
for (const item of list) {
|
|
646
|
+
const nearest = findNearestOnAxis(item, points, axis);
|
|
647
|
+
distances.push(nearest);
|
|
648
|
+
if (min > nearest.distance) min = nearest.distance;
|
|
649
|
+
}
|
|
650
|
+
if (min > margin) return result;
|
|
651
|
+
let snapped = false;
|
|
652
|
+
for (let i = 0; i < list.length; i++) {
|
|
653
|
+
if (distances[i].distance !== min) continue;
|
|
654
|
+
for (const item of distances[i].matches) {
|
|
655
|
+
result.push({ origin: list[i], target: item });
|
|
656
|
+
}
|
|
657
|
+
if (snapped) continue;
|
|
658
|
+
snapped = true;
|
|
659
|
+
const snapOffset = distances[i].matches[0][axis] - list[i][axis];
|
|
660
|
+
list.forEach((item) => {
|
|
661
|
+
item[axis] += snapOffset;
|
|
662
|
+
});
|
|
663
|
+
const center = target.getCenterPoint();
|
|
664
|
+
center[axis] += snapOffset;
|
|
665
|
+
target.setXY(center, "center", "center");
|
|
666
|
+
target.setCoords();
|
|
667
|
+
}
|
|
668
|
+
return result;
|
|
669
|
+
}
|
|
670
|
+
function collectVerticalSnapOffset(props) {
|
|
671
|
+
const {
|
|
672
|
+
target,
|
|
673
|
+
isScale,
|
|
674
|
+
isUniform,
|
|
675
|
+
corner,
|
|
676
|
+
point,
|
|
677
|
+
diagonalPoint,
|
|
678
|
+
list,
|
|
679
|
+
isCenter,
|
|
680
|
+
margin
|
|
681
|
+
} = props;
|
|
682
|
+
const { distance, matches } = findNearestOnAxis(point, list, "x");
|
|
683
|
+
if (distance > margin) return [];
|
|
684
|
+
let snapOffset = matches[matches.length - 1].x - point.x;
|
|
685
|
+
const dirX = corner.includes("l") ? -1 : 1;
|
|
686
|
+
snapOffset *= dirX;
|
|
687
|
+
const { width, height, scaleX, scaleY } = target;
|
|
688
|
+
const scaleWidth = scaleX * width;
|
|
689
|
+
const sx = (snapOffset + scaleWidth) / scaleWidth;
|
|
690
|
+
if (sx === 0) return [];
|
|
691
|
+
if (isScale) {
|
|
692
|
+
target.set("scaleX", scaleX * sx);
|
|
693
|
+
if (isUniform) target.set("scaleY", scaleY * sx);
|
|
694
|
+
} else {
|
|
695
|
+
target.set("width", width * sx);
|
|
696
|
+
if (isUniform) target.set("height", height * sx);
|
|
697
|
+
}
|
|
698
|
+
if (isCenter) {
|
|
699
|
+
target.setRelativeXY(diagonalPoint, "center", "center");
|
|
700
|
+
} else {
|
|
701
|
+
target.setRelativeXY(diagonalPoint, ...OPPOSITE_ORIGIN_MAP[corner]);
|
|
702
|
+
}
|
|
703
|
+
target.setCoords();
|
|
704
|
+
return matches.map((t) => ({ origin: point, target: t }));
|
|
705
|
+
}
|
|
706
|
+
function collectHorizontalSnapOffset(props) {
|
|
707
|
+
const {
|
|
708
|
+
target,
|
|
709
|
+
isScale,
|
|
710
|
+
isUniform,
|
|
711
|
+
corner,
|
|
712
|
+
point,
|
|
713
|
+
diagonalPoint,
|
|
714
|
+
list,
|
|
715
|
+
isCenter,
|
|
716
|
+
margin
|
|
717
|
+
} = props;
|
|
718
|
+
const { distance, matches } = findNearestOnAxis(point, list, "y");
|
|
719
|
+
if (distance > margin) return [];
|
|
720
|
+
let snapOffset = matches[matches.length - 1].y - point.y;
|
|
721
|
+
const dirY = corner.includes("t") ? -1 : 1;
|
|
722
|
+
snapOffset *= dirY;
|
|
723
|
+
const { width, height, scaleX, scaleY } = target;
|
|
724
|
+
const scaleHeight = scaleY * height;
|
|
725
|
+
const sy = (snapOffset + scaleHeight) / scaleHeight;
|
|
726
|
+
if (sy === 0) return [];
|
|
727
|
+
if (isScale) {
|
|
728
|
+
target.set("scaleY", scaleY * sy);
|
|
729
|
+
if (isUniform) target.set("scaleX", scaleX * sy);
|
|
730
|
+
} else {
|
|
731
|
+
target.set("height", height * sy);
|
|
732
|
+
if (isUniform) target.set("width", width * sy);
|
|
733
|
+
}
|
|
734
|
+
if (isCenter) {
|
|
735
|
+
target.setRelativeXY(diagonalPoint, "center", "center");
|
|
736
|
+
} else {
|
|
737
|
+
target.setRelativeXY(diagonalPoint, ...OPPOSITE_ORIGIN_MAP[corner]);
|
|
738
|
+
}
|
|
739
|
+
target.setCoords();
|
|
740
|
+
return matches.map((t) => ({ origin: point, target: t }));
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// src/alignment/objectAlignment.ts
|
|
744
|
+
function adjustCornerForFlip(corner, target) {
|
|
745
|
+
let adjusted = corner;
|
|
746
|
+
if (target.flipX) {
|
|
747
|
+
if (adjusted.includes("l")) adjusted = adjusted.replace("l", "r");
|
|
748
|
+
else if (adjusted.includes("r")) adjusted = adjusted.replace("r", "l");
|
|
749
|
+
}
|
|
750
|
+
if (target.flipY) {
|
|
751
|
+
if (adjusted.includes("t")) adjusted = adjusted.replace("t", "b");
|
|
752
|
+
else if (adjusted.includes("b")) adjusted = adjusted.replace("b", "t");
|
|
753
|
+
}
|
|
754
|
+
return adjusted;
|
|
755
|
+
}
|
|
756
|
+
var ObjectAlignmentGuides = class {
|
|
757
|
+
canvas;
|
|
758
|
+
margin;
|
|
759
|
+
scaleWithCanvasSize;
|
|
760
|
+
renderConfig;
|
|
761
|
+
horizontalLines = /* @__PURE__ */ new Set();
|
|
762
|
+
verticalLines = /* @__PURE__ */ new Set();
|
|
763
|
+
cacheMap = /* @__PURE__ */ new Map();
|
|
764
|
+
markersOnly = false;
|
|
765
|
+
constructor(canvas, opts) {
|
|
766
|
+
this.canvas = canvas;
|
|
767
|
+
this.margin = opts?.margin ?? DEFAULT_SNAP_MARGIN;
|
|
768
|
+
this.scaleWithCanvasSize = opts?.scaleWithCanvasSize ?? true;
|
|
769
|
+
this.renderConfig = {
|
|
770
|
+
canvas,
|
|
771
|
+
width: opts?.width ?? 1,
|
|
772
|
+
color: opts?.color ?? "rgba(255,0,0,0.9)",
|
|
773
|
+
xSize: opts?.xSize ?? 2.4,
|
|
774
|
+
lineDash: opts?.lineDash
|
|
775
|
+
};
|
|
776
|
+
this.mouseUp = this.mouseUp.bind(this);
|
|
777
|
+
this.onMoving = this.onMoving.bind(this);
|
|
778
|
+
this.onScalingOrResizing = this.onScalingOrResizing.bind(this);
|
|
779
|
+
this.beforeRender = this.beforeRender.bind(this);
|
|
780
|
+
this.afterRender = this.afterRender.bind(this);
|
|
781
|
+
canvas.on("mouse:up", this.mouseUp);
|
|
782
|
+
canvas.on("object:moving", this.onMoving);
|
|
783
|
+
canvas.on("object:scaling", this.onScalingOrResizing);
|
|
784
|
+
canvas.on("object:resizing", this.onScalingOrResizing);
|
|
785
|
+
canvas.on("before:render", this.beforeRender);
|
|
786
|
+
canvas.on("after:render", this.afterRender);
|
|
787
|
+
}
|
|
788
|
+
dispose() {
|
|
789
|
+
this.canvas.off("mouse:up", this.mouseUp);
|
|
790
|
+
this.canvas.off("object:moving", this.onMoving);
|
|
791
|
+
this.canvas.off("object:scaling", this.onScalingOrResizing);
|
|
792
|
+
this.canvas.off("object:resizing", this.onScalingOrResizing);
|
|
793
|
+
this.canvas.off("before:render", this.beforeRender);
|
|
794
|
+
this.canvas.off("after:render", this.afterRender);
|
|
795
|
+
}
|
|
796
|
+
// --- Margin calculation ---
|
|
797
|
+
computeMargin() {
|
|
798
|
+
const zoom = this.canvas.getZoom();
|
|
799
|
+
const sizeScale = this.scaleWithCanvasSize ? Math.max(this.canvas.width ?? 800, this.canvas.height ?? 600) / BASE_CANVAS_SIZE : 1;
|
|
800
|
+
return this.margin * sizeScale / zoom;
|
|
801
|
+
}
|
|
802
|
+
// --- Snap point caching ---
|
|
803
|
+
getCachedSnapPoints(object) {
|
|
804
|
+
const cacheKey = [
|
|
805
|
+
object.calcTransformMatrix().toString(),
|
|
806
|
+
object.width,
|
|
807
|
+
object.height,
|
|
808
|
+
"points" in object && Array.isArray(object.points) ? object.points.length : 0
|
|
809
|
+
].join();
|
|
810
|
+
const cached = this.cacheMap.get(cacheKey);
|
|
811
|
+
if (cached) return cached;
|
|
812
|
+
const value = getSnapPoints(object);
|
|
813
|
+
this.cacheMap.set(cacheKey, value);
|
|
814
|
+
return value;
|
|
815
|
+
}
|
|
816
|
+
collectSnapPointsFromTargets(target) {
|
|
817
|
+
const objects = getAlignmentTargets(target);
|
|
818
|
+
const points = [];
|
|
819
|
+
for (const obj of objects) points.push(...this.getCachedSnapPoints(obj));
|
|
820
|
+
return points;
|
|
821
|
+
}
|
|
822
|
+
// --- Event handlers ---
|
|
823
|
+
mouseUp() {
|
|
824
|
+
this.verticalLines.clear();
|
|
825
|
+
this.horizontalLines.clear();
|
|
826
|
+
this.cacheMap.clear();
|
|
827
|
+
this.canvas.requestRenderAll();
|
|
828
|
+
}
|
|
829
|
+
onMoving(e) {
|
|
830
|
+
const target = e.target;
|
|
831
|
+
target.setCoords();
|
|
832
|
+
this.markersOnly = false;
|
|
833
|
+
this.verticalLines.clear();
|
|
834
|
+
this.horizontalLines.clear();
|
|
835
|
+
const points = this.collectSnapPointsFromTargets(target);
|
|
836
|
+
const margin = this.computeMargin();
|
|
837
|
+
const { verticalLines, horizontalLines } = collectMovingAlignmentLines(
|
|
838
|
+
target,
|
|
839
|
+
points,
|
|
840
|
+
margin
|
|
841
|
+
);
|
|
842
|
+
for (const l of verticalLines) this.verticalLines.add(JSON.stringify(l));
|
|
843
|
+
for (const l of horizontalLines)
|
|
844
|
+
this.horizontalLines.add(JSON.stringify(l));
|
|
845
|
+
}
|
|
846
|
+
onScalingOrResizing(e) {
|
|
847
|
+
const target = e.target;
|
|
848
|
+
target.setCoords();
|
|
849
|
+
const isScale = String(e.transform.action).startsWith("scale");
|
|
850
|
+
this.verticalLines.clear();
|
|
851
|
+
this.horizontalLines.clear();
|
|
852
|
+
const corner = adjustCornerForFlip(e.transform.corner, target);
|
|
853
|
+
const pointMap = getBoundingPointMap(target);
|
|
854
|
+
if (!(corner in pointMap)) return;
|
|
855
|
+
this.markersOnly = corner.includes("m");
|
|
856
|
+
if (this.markersOnly) {
|
|
857
|
+
if (target.getTotalAngle() % 90 !== 0) return;
|
|
858
|
+
}
|
|
859
|
+
const oppositeMap = getOppositeCornerMap(target);
|
|
860
|
+
const point = pointMap[corner];
|
|
861
|
+
let diagonalPoint = oppositeMap[corner];
|
|
862
|
+
const isCenter = e.transform.original.originX === "center" && e.transform.original.originY === "center";
|
|
863
|
+
if (isCenter) {
|
|
864
|
+
const p = target.group ? point.transform(
|
|
865
|
+
util2.invertTransform(target.group.calcTransformMatrix())
|
|
866
|
+
) : point;
|
|
867
|
+
diagonalPoint = diagonalPoint.add(p).scalarDivide(2);
|
|
868
|
+
}
|
|
869
|
+
const uniformIsToggled = e.e[this.canvas.uniScaleKey];
|
|
870
|
+
let isUniform = this.canvas.uniformScaling && !uniformIsToggled || !this.canvas.uniformScaling && uniformIsToggled;
|
|
871
|
+
if (this.markersOnly) isUniform = false;
|
|
872
|
+
const allPoints = this.collectSnapPointsFromTargets(target);
|
|
873
|
+
const margin = this.computeMargin();
|
|
874
|
+
const props = {
|
|
875
|
+
target,
|
|
876
|
+
point,
|
|
877
|
+
diagonalPoint,
|
|
878
|
+
corner,
|
|
879
|
+
list: allPoints,
|
|
880
|
+
isScale,
|
|
881
|
+
isUniform,
|
|
882
|
+
isCenter,
|
|
883
|
+
margin
|
|
884
|
+
};
|
|
885
|
+
const skipVertical = this.markersOnly && (corner.includes("t") || corner.includes("b"));
|
|
886
|
+
const skipHorizontal = this.markersOnly && (corner.includes("l") || corner.includes("r"));
|
|
887
|
+
const vList = skipVertical ? [] : collectVerticalSnapOffset(props);
|
|
888
|
+
if (vList.length > 0) {
|
|
889
|
+
const updatedPointMap = getBoundingPointMap(target);
|
|
890
|
+
if (corner in updatedPointMap) props.point = updatedPointMap[corner];
|
|
891
|
+
}
|
|
892
|
+
const hList = skipHorizontal ? [] : collectHorizontalSnapOffset(props);
|
|
893
|
+
for (const l of vList) this.verticalLines.add(JSON.stringify(l));
|
|
894
|
+
for (const l of hList) this.horizontalLines.add(JSON.stringify(l));
|
|
895
|
+
}
|
|
896
|
+
// --- Render ---
|
|
897
|
+
beforeRender() {
|
|
898
|
+
this.canvas.clearContext(this.canvas.contextTop);
|
|
899
|
+
}
|
|
900
|
+
afterRender() {
|
|
901
|
+
if (this.markersOnly) {
|
|
902
|
+
const lines = [];
|
|
903
|
+
for (const v of this.verticalLines)
|
|
904
|
+
lines.push(JSON.parse(v));
|
|
905
|
+
for (const h of this.horizontalLines)
|
|
906
|
+
lines.push(JSON.parse(h));
|
|
907
|
+
drawMarkerList(this.renderConfig, lines);
|
|
908
|
+
} else {
|
|
909
|
+
drawVerticalAlignmentLines(this.renderConfig, this.verticalLines);
|
|
910
|
+
drawHorizontalAlignmentLines(this.renderConfig, this.horizontalLines);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
};
|
|
914
|
+
function enableObjectAlignment(canvas, options) {
|
|
915
|
+
const alignment = new ObjectAlignmentGuides(canvas, options);
|
|
916
|
+
return () => alignment.dispose();
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// src/alignment/rotationSnap.ts
|
|
920
|
+
function snapToInterval(angle, interval) {
|
|
921
|
+
return Math.round(angle / interval) * interval;
|
|
922
|
+
}
|
|
923
|
+
function enableRotationSnap(canvas, options) {
|
|
924
|
+
const interval = options?.interval ?? DEFAULT_ANGLE_SNAP_INTERVAL;
|
|
925
|
+
const onRotating = (e) => {
|
|
926
|
+
if (!("shiftKey" in e.e) || !e.e.shiftKey) return;
|
|
927
|
+
e.target.angle = snapToInterval(e.target.angle, interval);
|
|
928
|
+
};
|
|
929
|
+
canvas.on("object:rotating", onRotating);
|
|
930
|
+
return () => canvas.off("object:rotating", onRotating);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// src/alignment/cursorSnapping.ts
|
|
934
|
+
import { Point as Point5 } from "fabric";
|
|
935
|
+
function snapCursorPoint(canvas, rawPoint, options) {
|
|
936
|
+
const zoom = canvas.getZoom();
|
|
937
|
+
const scaleWithSize = options?.scaleWithCanvasSize !== false;
|
|
938
|
+
const sizeScale = scaleWithSize ? Math.max(canvas.width ?? 800, canvas.height ?? 600) / BASE_CANVAS_SIZE : 1;
|
|
939
|
+
const margin = (options?.margin ?? DEFAULT_SNAP_MARGIN) * sizeScale / zoom;
|
|
940
|
+
const exclude = options?.exclude ?? /* @__PURE__ */ new Set();
|
|
941
|
+
let targetPoints;
|
|
942
|
+
if (options?.targetPoints) {
|
|
943
|
+
targetPoints = options.targetPoints;
|
|
944
|
+
} else {
|
|
945
|
+
targetPoints = [];
|
|
946
|
+
canvas.forEachObject((obj) => {
|
|
947
|
+
if (!obj.visible || !obj.isOnScreen()) return;
|
|
948
|
+
if (exclude.has(obj)) return;
|
|
949
|
+
targetPoints.push(...getSnapPoints(obj));
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
let bestDx = Infinity;
|
|
953
|
+
let bestDy = Infinity;
|
|
954
|
+
let snapTargetsX = [];
|
|
955
|
+
let snapTargetsY = [];
|
|
956
|
+
for (const tp of targetPoints) {
|
|
957
|
+
const dx = Math.abs(rawPoint.x - tp.x);
|
|
958
|
+
const dy = Math.abs(rawPoint.y - tp.y);
|
|
959
|
+
if (dx < bestDx) {
|
|
960
|
+
bestDx = dx;
|
|
961
|
+
snapTargetsX = [];
|
|
962
|
+
}
|
|
963
|
+
if (dx === bestDx) {
|
|
964
|
+
snapTargetsX.push(tp);
|
|
965
|
+
}
|
|
966
|
+
if (dy < bestDy) {
|
|
967
|
+
bestDy = dy;
|
|
968
|
+
snapTargetsY = [];
|
|
969
|
+
}
|
|
970
|
+
if (dy === bestDy) {
|
|
971
|
+
snapTargetsY.push(tp);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
const snapX = bestDx <= margin && snapTargetsX.length > 0;
|
|
975
|
+
const snapY = bestDy <= margin && snapTargetsY.length > 0;
|
|
976
|
+
return {
|
|
977
|
+
point: new Point5(
|
|
978
|
+
snapX ? snapTargetsX[0].x : rawPoint.x,
|
|
979
|
+
snapY ? snapTargetsY[0].y : rawPoint.y
|
|
980
|
+
),
|
|
981
|
+
snapped: snapX || snapY,
|
|
982
|
+
snapX,
|
|
983
|
+
snapY,
|
|
984
|
+
alignTargetsX: snapX ? snapTargetsX : void 0,
|
|
985
|
+
alignTargetsY: snapY ? snapTargetsY : void 0
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
function drawCursorGuidelines(canvas, snapResult, style) {
|
|
989
|
+
if (!snapResult.snapped) return;
|
|
990
|
+
const ctx = canvas.getTopContext();
|
|
991
|
+
const vt = canvas.viewportTransform;
|
|
992
|
+
const zoom = canvas.getZoom();
|
|
993
|
+
const color = style?.color ?? "rgba(255,0,0,0.9)";
|
|
994
|
+
const width = style?.width ?? 1;
|
|
995
|
+
const xSize = (style?.xSize ?? 2.4) / zoom;
|
|
996
|
+
ctx.save();
|
|
997
|
+
ctx.transform(vt[0], vt[1], vt[2], vt[3], vt[4], vt[5]);
|
|
998
|
+
ctx.lineWidth = width / zoom;
|
|
999
|
+
ctx.strokeStyle = color;
|
|
1000
|
+
if (snapResult.snapX && snapResult.alignTargetsX) {
|
|
1001
|
+
const to = snapResult.point;
|
|
1002
|
+
for (const from of snapResult.alignTargetsX) {
|
|
1003
|
+
ctx.beginPath();
|
|
1004
|
+
ctx.moveTo(from.x, from.y);
|
|
1005
|
+
ctx.lineTo(to.x, to.y);
|
|
1006
|
+
ctx.stroke();
|
|
1007
|
+
drawXMarker2(ctx, from, xSize);
|
|
1008
|
+
}
|
|
1009
|
+
drawXMarker2(ctx, new Point5(to.x, to.y), xSize);
|
|
1010
|
+
}
|
|
1011
|
+
if (snapResult.snapY && snapResult.alignTargetsY) {
|
|
1012
|
+
const to = snapResult.point;
|
|
1013
|
+
for (const from of snapResult.alignTargetsY) {
|
|
1014
|
+
ctx.beginPath();
|
|
1015
|
+
ctx.moveTo(from.x, from.y);
|
|
1016
|
+
ctx.lineTo(to.x, to.y);
|
|
1017
|
+
ctx.stroke();
|
|
1018
|
+
drawXMarker2(ctx, from, xSize);
|
|
1019
|
+
}
|
|
1020
|
+
drawXMarker2(ctx, new Point5(to.x, to.y), xSize);
|
|
1021
|
+
}
|
|
1022
|
+
ctx.restore();
|
|
1023
|
+
}
|
|
1024
|
+
function drawXMarker2(ctx, point, size) {
|
|
1025
|
+
ctx.beginPath();
|
|
1026
|
+
ctx.moveTo(point.x - size, point.y - size);
|
|
1027
|
+
ctx.lineTo(point.x + size, point.y + size);
|
|
1028
|
+
ctx.moveTo(point.x + size, point.y - size);
|
|
1029
|
+
ctx.lineTo(point.x - size, point.y + size);
|
|
1030
|
+
ctx.stroke();
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// src/interactions/shared.ts
|
|
1034
|
+
function restoreViewport(viewport) {
|
|
1035
|
+
if (!viewport) return;
|
|
1036
|
+
viewport.setEnabled(true);
|
|
1037
|
+
viewport.setMode("select");
|
|
1038
|
+
}
|
|
1039
|
+
function createShiftKeyTracker(onChange) {
|
|
1040
|
+
let shiftHeld = false;
|
|
1041
|
+
const onKeyDown = (e) => {
|
|
1042
|
+
if (e.key === "Shift" && !shiftHeld) {
|
|
1043
|
+
shiftHeld = true;
|
|
1044
|
+
onChange?.(true);
|
|
1045
|
+
}
|
|
1046
|
+
};
|
|
1047
|
+
const onKeyUp = (e) => {
|
|
1048
|
+
if (e.key === "Shift" && shiftHeld) {
|
|
1049
|
+
shiftHeld = false;
|
|
1050
|
+
onChange?.(false);
|
|
1051
|
+
}
|
|
1052
|
+
};
|
|
1053
|
+
document.addEventListener("keydown", onKeyDown);
|
|
1054
|
+
document.addEventListener("keyup", onKeyUp);
|
|
1055
|
+
return {
|
|
1056
|
+
get held() {
|
|
1057
|
+
return shiftHeld;
|
|
1058
|
+
},
|
|
1059
|
+
cleanup() {
|
|
1060
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
1061
|
+
document.removeEventListener("keyup", onKeyUp);
|
|
1062
|
+
}
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
// src/interactions/clickToCreate.ts
|
|
1067
|
+
function enableClickToCreate(canvas, factory, options) {
|
|
1068
|
+
options?.viewport?.setEnabled(false);
|
|
1069
|
+
const handleMouseDown = (event) => {
|
|
1070
|
+
const obj = factory(canvas, event.scenePoint);
|
|
1071
|
+
restoreViewport(options?.viewport);
|
|
1072
|
+
options?.onCreated?.(obj);
|
|
1073
|
+
};
|
|
1074
|
+
canvas.on("mouse:down", handleMouseDown);
|
|
1075
|
+
return () => {
|
|
1076
|
+
canvas.off("mouse:down", handleMouseDown);
|
|
1077
|
+
restoreViewport(options?.viewport);
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// src/interactions/dragToCreate.ts
|
|
1082
|
+
import { Rect as Rect2 } from "fabric";
|
|
1083
|
+
|
|
1084
|
+
// src/styles.ts
|
|
1085
|
+
import { alpha } from "@mui/material/styles";
|
|
1086
|
+
import { biampTheme } from "@bwp-web/styles";
|
|
1087
|
+
var { palette } = biampTheme();
|
|
1088
|
+
var DEFAULT_CONTROL_STYLE = {
|
|
1089
|
+
borderColor: palette.info.main,
|
|
1090
|
+
cornerColor: palette.info.main,
|
|
1091
|
+
cornerStrokeColor: palette.info.main,
|
|
1092
|
+
transparentCorners: true
|
|
1093
|
+
};
|
|
1094
|
+
var DEFAULT_SHAPE_STYLE = {
|
|
1095
|
+
fill: alpha(palette.info.main, 0.3),
|
|
1096
|
+
stroke: palette.info.main,
|
|
1097
|
+
strokeWidth: 2.5,
|
|
1098
|
+
strokeUniform: true,
|
|
1099
|
+
...DEFAULT_CONTROL_STYLE
|
|
1100
|
+
};
|
|
1101
|
+
var DEFAULT_CIRCLE_STYLE = {
|
|
1102
|
+
fill: palette.info.main,
|
|
1103
|
+
stroke: palette.info.main,
|
|
1104
|
+
strokeWidth: 2.5,
|
|
1105
|
+
strokeUniform: true,
|
|
1106
|
+
...DEFAULT_CONTROL_STYLE
|
|
1107
|
+
};
|
|
1108
|
+
var DEFAULT_DRAG_SHAPE_STYLE = {
|
|
1109
|
+
fill: alpha(palette.info.main, 0.1),
|
|
1110
|
+
stroke: palette.info.main,
|
|
1111
|
+
strokeWidth: 2.5,
|
|
1112
|
+
strokeUniform: true,
|
|
1113
|
+
strokeDashArray: [5, 5]
|
|
1114
|
+
};
|
|
1115
|
+
var DEFAULT_GUIDELINE_SHAPE_STYLE = {
|
|
1116
|
+
fill: alpha(palette.info.main, 0.1),
|
|
1117
|
+
stroke: alpha(palette.info.main, 0.5),
|
|
1118
|
+
strokeWidth: 2.5,
|
|
1119
|
+
strokeUniform: true,
|
|
1120
|
+
strokeDashArray: [5, 5]
|
|
1121
|
+
};
|
|
1122
|
+
var DEFAULT_ALIGNMENT_STYLE = {
|
|
1123
|
+
color: "rgba(255, 0, 0, 0.9)",
|
|
1124
|
+
width: 1,
|
|
1125
|
+
xSize: 2.4
|
|
1126
|
+
};
|
|
1127
|
+
|
|
1128
|
+
// src/interactions/interactionSnapping.ts
|
|
1129
|
+
import { Point as Point6 } from "fabric";
|
|
1130
|
+
var canvasAlignmentState = /* @__PURE__ */ new WeakMap();
|
|
1131
|
+
function setCanvasAlignmentEnabled(canvas, enabled) {
|
|
1132
|
+
canvasAlignmentState.set(canvas, enabled);
|
|
1133
|
+
}
|
|
1134
|
+
function createInteractionSnapping(canvas, options, getAdditionalTargets) {
|
|
1135
|
+
const canvasAlignment = canvasAlignmentState.get(canvas);
|
|
1136
|
+
const snapEnabled = options?.enableAlignment !== void 0 ? options.enableAlignment : canvasAlignment !== void 0 ? canvasAlignment : options?.snapping !== false;
|
|
1137
|
+
const snapMargin = typeof options?.snapping === "object" ? options.snapping.margin : void 0;
|
|
1138
|
+
const guidelineStyle = typeof options?.snapping === "object" ? options.snapping.guidelineStyle : void 0;
|
|
1139
|
+
const excludeSet = /* @__PURE__ */ new Set();
|
|
1140
|
+
let cachedTargetPoints = null;
|
|
1141
|
+
let lastSnapResult = null;
|
|
1142
|
+
function getTargetPoints() {
|
|
1143
|
+
if (cachedTargetPoints) return cachedTargetPoints;
|
|
1144
|
+
cachedTargetPoints = [];
|
|
1145
|
+
canvas.forEachObject((obj) => {
|
|
1146
|
+
if (!obj.visible) return;
|
|
1147
|
+
if (excludeSet.has(obj)) return;
|
|
1148
|
+
cachedTargetPoints.push(...getSnapPoints(obj));
|
|
1149
|
+
});
|
|
1150
|
+
return cachedTargetPoints;
|
|
1151
|
+
}
|
|
1152
|
+
function getAllTargetPoints() {
|
|
1153
|
+
const base = getTargetPoints();
|
|
1154
|
+
if (!getAdditionalTargets) return base;
|
|
1155
|
+
const additional = getAdditionalTargets();
|
|
1156
|
+
return additional.length > 0 ? [...base, ...additional] : base;
|
|
1157
|
+
}
|
|
1158
|
+
const invalidateCache = () => {
|
|
1159
|
+
cachedTargetPoints = null;
|
|
1160
|
+
};
|
|
1161
|
+
const beforeRender = () => {
|
|
1162
|
+
canvas.clearContext(canvas.getTopContext());
|
|
1163
|
+
};
|
|
1164
|
+
const afterRender = () => {
|
|
1165
|
+
if (lastSnapResult) {
|
|
1166
|
+
drawCursorGuidelines(canvas, lastSnapResult, guidelineStyle);
|
|
1167
|
+
}
|
|
1168
|
+
};
|
|
1169
|
+
if (snapEnabled) {
|
|
1170
|
+
canvas.on("object:added", invalidateCache);
|
|
1171
|
+
canvas.on("object:removed", invalidateCache);
|
|
1172
|
+
canvas.on("before:render", beforeRender);
|
|
1173
|
+
canvas.on("after:render", afterRender);
|
|
1174
|
+
}
|
|
1175
|
+
function snap(rawX, rawY) {
|
|
1176
|
+
if (!snapEnabled) return { x: rawX, y: rawY };
|
|
1177
|
+
const result = snapCursorPoint(canvas, new Point6(rawX, rawY), {
|
|
1178
|
+
margin: snapMargin,
|
|
1179
|
+
exclude: excludeSet,
|
|
1180
|
+
targetPoints: getAllTargetPoints()
|
|
1181
|
+
});
|
|
1182
|
+
return { x: result.point.x, y: result.point.y };
|
|
1183
|
+
}
|
|
1184
|
+
function snapWithGuidelines(rawX, rawY) {
|
|
1185
|
+
if (!snapEnabled) {
|
|
1186
|
+
lastSnapResult = null;
|
|
1187
|
+
return { x: rawX, y: rawY };
|
|
1188
|
+
}
|
|
1189
|
+
lastSnapResult = snapCursorPoint(canvas, new Point6(rawX, rawY), {
|
|
1190
|
+
margin: snapMargin,
|
|
1191
|
+
exclude: excludeSet,
|
|
1192
|
+
targetPoints: getAllTargetPoints()
|
|
1193
|
+
});
|
|
1194
|
+
return { x: lastSnapResult.point.x, y: lastSnapResult.point.y };
|
|
1195
|
+
}
|
|
1196
|
+
return {
|
|
1197
|
+
enabled: snapEnabled,
|
|
1198
|
+
snap,
|
|
1199
|
+
snapWithGuidelines,
|
|
1200
|
+
clearSnapResult() {
|
|
1201
|
+
lastSnapResult = null;
|
|
1202
|
+
},
|
|
1203
|
+
excludeSet,
|
|
1204
|
+
cleanup() {
|
|
1205
|
+
if (snapEnabled) {
|
|
1206
|
+
canvas.off("object:added", invalidateCache);
|
|
1207
|
+
canvas.off("object:removed", invalidateCache);
|
|
1208
|
+
canvas.off("before:render", beforeRender);
|
|
1209
|
+
canvas.off("after:render", afterRender);
|
|
1210
|
+
canvas.clearContext(canvas.getTopContext());
|
|
1211
|
+
}
|
|
1212
|
+
lastSnapResult = null;
|
|
1213
|
+
}
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// src/interactions/dragToCreate.ts
|
|
1218
|
+
function enableDragToCreate(canvas, factory, options) {
|
|
1219
|
+
let isDrawing = false;
|
|
1220
|
+
let startX = 0;
|
|
1221
|
+
let startY = 0;
|
|
1222
|
+
let lastEndX = 0;
|
|
1223
|
+
let lastEndY = 0;
|
|
1224
|
+
let previewRect = null;
|
|
1225
|
+
let previousSelection;
|
|
1226
|
+
const snapping = createInteractionSnapping(canvas, options);
|
|
1227
|
+
const shiftTracker = createShiftKeyTracker(() => {
|
|
1228
|
+
updatePreview(lastEndX, lastEndY);
|
|
1229
|
+
});
|
|
1230
|
+
options?.viewport?.setEnabled(false);
|
|
1231
|
+
const shouldConstrain = () => !!(options?.constrainToSquare || shiftTracker.held);
|
|
1232
|
+
const computeDimensions = (endX, endY) => {
|
|
1233
|
+
let width = Math.max(0, endX - startX);
|
|
1234
|
+
let height = Math.max(0, endY - startY);
|
|
1235
|
+
if (shouldConstrain()) {
|
|
1236
|
+
const size = Math.max(width, height);
|
|
1237
|
+
width = size;
|
|
1238
|
+
height = size;
|
|
1239
|
+
}
|
|
1240
|
+
return { width, height };
|
|
1241
|
+
};
|
|
1242
|
+
const updatePreview = (endX, endY) => {
|
|
1243
|
+
lastEndX = endX;
|
|
1244
|
+
lastEndY = endY;
|
|
1245
|
+
if (!isDrawing || !previewRect) return;
|
|
1246
|
+
const { width, height } = computeDimensions(endX, endY);
|
|
1247
|
+
previewRect.set({
|
|
1248
|
+
left: startX + width / 2,
|
|
1249
|
+
top: startY + height / 2,
|
|
1250
|
+
width,
|
|
1251
|
+
height
|
|
1252
|
+
});
|
|
1253
|
+
previewRect.setCoords();
|
|
1254
|
+
canvas.requestRenderAll();
|
|
1255
|
+
};
|
|
1256
|
+
const handleMouseDown = (event) => {
|
|
1257
|
+
isDrawing = true;
|
|
1258
|
+
const snapped = snapping.snap(event.scenePoint.x, event.scenePoint.y);
|
|
1259
|
+
startX = snapped.x;
|
|
1260
|
+
startY = snapped.y;
|
|
1261
|
+
lastEndX = startX;
|
|
1262
|
+
lastEndY = startY;
|
|
1263
|
+
previousSelection = canvas.selection;
|
|
1264
|
+
canvas.selection = false;
|
|
1265
|
+
previewRect = new Rect2({
|
|
1266
|
+
...DEFAULT_GUIDELINE_SHAPE_STYLE,
|
|
1267
|
+
left: startX,
|
|
1268
|
+
top: startY,
|
|
1269
|
+
width: 0,
|
|
1270
|
+
height: 0,
|
|
1271
|
+
...options?.previewStyle,
|
|
1272
|
+
selectable: false,
|
|
1273
|
+
evented: false
|
|
1274
|
+
});
|
|
1275
|
+
snapping.excludeSet.add(previewRect);
|
|
1276
|
+
canvas.add(previewRect);
|
|
1277
|
+
};
|
|
1278
|
+
const handleMouseMove = (event) => {
|
|
1279
|
+
if (!isDrawing || !previewRect) return;
|
|
1280
|
+
const { x: endX, y: endY } = snapping.snapWithGuidelines(
|
|
1281
|
+
event.scenePoint.x,
|
|
1282
|
+
event.scenePoint.y
|
|
1283
|
+
);
|
|
1284
|
+
updatePreview(endX, endY);
|
|
1285
|
+
};
|
|
1286
|
+
const handleMouseUp = () => {
|
|
1287
|
+
if (!isDrawing || !previewRect) return;
|
|
1288
|
+
isDrawing = false;
|
|
1289
|
+
snapping.clearSnapResult();
|
|
1290
|
+
canvas.selection = previousSelection;
|
|
1291
|
+
const { width, height } = computeDimensions(lastEndX, lastEndY);
|
|
1292
|
+
snapping.excludeSet.delete(previewRect);
|
|
1293
|
+
canvas.remove(previewRect);
|
|
1294
|
+
if (width < MIN_DRAG_SIZE && height < MIN_DRAG_SIZE) {
|
|
1295
|
+
canvas.requestRenderAll();
|
|
1296
|
+
previewRect = null;
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
const obj = factory(canvas, { startX, startY, width, height });
|
|
1300
|
+
restoreViewport(options?.viewport);
|
|
1301
|
+
options?.onCreated?.(obj);
|
|
1302
|
+
previewRect = null;
|
|
1303
|
+
};
|
|
1304
|
+
canvas.on("mouse:down", handleMouseDown);
|
|
1305
|
+
canvas.on("mouse:move", handleMouseMove);
|
|
1306
|
+
canvas.on("mouse:up", handleMouseUp);
|
|
1307
|
+
return () => {
|
|
1308
|
+
canvas.off("mouse:down", handleMouseDown);
|
|
1309
|
+
canvas.off("mouse:move", handleMouseMove);
|
|
1310
|
+
canvas.off("mouse:up", handleMouseUp);
|
|
1311
|
+
shiftTracker.cleanup();
|
|
1312
|
+
snapping.cleanup();
|
|
1313
|
+
if (isDrawing && previewRect) {
|
|
1314
|
+
canvas.remove(previewRect);
|
|
1315
|
+
canvas.requestRenderAll();
|
|
1316
|
+
}
|
|
1317
|
+
restoreViewport(options?.viewport);
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
// src/interactions/drawToCreate.ts
|
|
1322
|
+
import {
|
|
1323
|
+
Circle,
|
|
1324
|
+
Line,
|
|
1325
|
+
Point as Point7
|
|
1326
|
+
} from "fabric";
|
|
1327
|
+
|
|
1328
|
+
// src/shapes/rectangle.ts
|
|
1329
|
+
import { Rect as Rect3 } from "fabric";
|
|
1330
|
+
function createRectangle(canvas, options) {
|
|
1331
|
+
const rect = new Rect3({ ...DEFAULT_SHAPE_STYLE, ...options });
|
|
1332
|
+
canvas.add(rect);
|
|
1333
|
+
canvas.requestRenderAll();
|
|
1334
|
+
return rect;
|
|
1335
|
+
}
|
|
1336
|
+
function createRectangleAtPoint(canvas, point, options) {
|
|
1337
|
+
const { width, height, ...style } = options;
|
|
1338
|
+
const rect = new Rect3({
|
|
1339
|
+
...DEFAULT_SHAPE_STYLE,
|
|
1340
|
+
left: point.x,
|
|
1341
|
+
top: point.y,
|
|
1342
|
+
width,
|
|
1343
|
+
height,
|
|
1344
|
+
...style
|
|
1345
|
+
});
|
|
1346
|
+
canvas.add(rect);
|
|
1347
|
+
canvas.requestRenderAll();
|
|
1348
|
+
return rect;
|
|
1349
|
+
}
|
|
1350
|
+
function editRectangle(canvas, rect, changes) {
|
|
1351
|
+
rect.set(changes);
|
|
1352
|
+
rect.setCoords();
|
|
1353
|
+
canvas.requestRenderAll();
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// src/shapes/circle.ts
|
|
1357
|
+
import { Rect as Rect4 } from "fabric";
|
|
1358
|
+
var CIRCLE_CONSTRAINTS = {
|
|
1359
|
+
lockRotation: true,
|
|
1360
|
+
lockUniScaling: true
|
|
1361
|
+
};
|
|
1362
|
+
var HIDDEN_CIRCLE_CONTROLS = {
|
|
1363
|
+
mt: false,
|
|
1364
|
+
mb: false,
|
|
1365
|
+
ml: false,
|
|
1366
|
+
mr: false,
|
|
1367
|
+
mtr: false
|
|
1368
|
+
};
|
|
1369
|
+
function applyCircleConstraints(rect) {
|
|
1370
|
+
rect.shapeType = "circle";
|
|
1371
|
+
rect.setControlsVisibility(HIDDEN_CIRCLE_CONTROLS);
|
|
1372
|
+
}
|
|
1373
|
+
function restoreCircleConstraints(rect) {
|
|
1374
|
+
rect.set(CIRCLE_CONSTRAINTS);
|
|
1375
|
+
rect.setControlsVisibility(HIDDEN_CIRCLE_CONTROLS);
|
|
1376
|
+
}
|
|
1377
|
+
function createCircle(canvas, options) {
|
|
1378
|
+
const { size, ...rest } = options;
|
|
1379
|
+
const rect = new Rect4({
|
|
1380
|
+
...DEFAULT_CIRCLE_STYLE,
|
|
1381
|
+
...CIRCLE_CONSTRAINTS,
|
|
1382
|
+
width: size,
|
|
1383
|
+
height: size,
|
|
1384
|
+
rx: size / 2,
|
|
1385
|
+
ry: size / 2,
|
|
1386
|
+
...rest
|
|
1387
|
+
});
|
|
1388
|
+
applyCircleConstraints(rect);
|
|
1389
|
+
canvas.add(rect);
|
|
1390
|
+
canvas.requestRenderAll();
|
|
1391
|
+
return rect;
|
|
1392
|
+
}
|
|
1393
|
+
function createCircleAtPoint(canvas, point, options) {
|
|
1394
|
+
const { size, ...style } = options;
|
|
1395
|
+
const rect = new Rect4({
|
|
1396
|
+
...DEFAULT_CIRCLE_STYLE,
|
|
1397
|
+
...CIRCLE_CONSTRAINTS,
|
|
1398
|
+
left: point.x,
|
|
1399
|
+
top: point.y,
|
|
1400
|
+
width: size,
|
|
1401
|
+
height: size,
|
|
1402
|
+
rx: size / 2,
|
|
1403
|
+
ry: size / 2,
|
|
1404
|
+
...style
|
|
1405
|
+
});
|
|
1406
|
+
applyCircleConstraints(rect);
|
|
1407
|
+
canvas.add(rect);
|
|
1408
|
+
canvas.requestRenderAll();
|
|
1409
|
+
return rect;
|
|
1410
|
+
}
|
|
1411
|
+
function editCircle(canvas, rect, changes) {
|
|
1412
|
+
const { size, ...rest } = changes;
|
|
1413
|
+
if (size !== void 0) {
|
|
1414
|
+
rect.set({
|
|
1415
|
+
width: size,
|
|
1416
|
+
height: size,
|
|
1417
|
+
rx: size / 2,
|
|
1418
|
+
ry: size / 2,
|
|
1419
|
+
...rest
|
|
1420
|
+
});
|
|
1421
|
+
} else {
|
|
1422
|
+
rect.set(rest);
|
|
1423
|
+
}
|
|
1424
|
+
rect.setCoords();
|
|
1425
|
+
canvas.requestRenderAll();
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
// src/shapes/polygon.ts
|
|
1429
|
+
import { Polygon as Polygon2 } from "fabric";
|
|
1430
|
+
function createPolygon(canvas, options) {
|
|
1431
|
+
const { points, ...rest } = options;
|
|
1432
|
+
const polygon = new Polygon2(points, { ...DEFAULT_SHAPE_STYLE, ...rest });
|
|
1433
|
+
canvas.add(polygon);
|
|
1434
|
+
canvas.requestRenderAll();
|
|
1435
|
+
return polygon;
|
|
1436
|
+
}
|
|
1437
|
+
function createPolygonAtPoint(canvas, point, options) {
|
|
1438
|
+
const { width, height, ...style } = options;
|
|
1439
|
+
const polygon = new Polygon2(
|
|
1440
|
+
[
|
|
1441
|
+
{ x: 0, y: 0 },
|
|
1442
|
+
{ x: width, y: 0 },
|
|
1443
|
+
{ x: width, y: height },
|
|
1444
|
+
{ x: 0, y: height }
|
|
1445
|
+
],
|
|
1446
|
+
{ ...DEFAULT_SHAPE_STYLE, left: point.x, top: point.y, ...style }
|
|
1447
|
+
);
|
|
1448
|
+
canvas.add(polygon);
|
|
1449
|
+
canvas.requestRenderAll();
|
|
1450
|
+
return polygon;
|
|
1451
|
+
}
|
|
1452
|
+
function createPolygonFromDrag(canvas, start, end, style) {
|
|
1453
|
+
const width = Math.abs(end.x - start.x);
|
|
1454
|
+
const height = Math.abs(end.y - start.y);
|
|
1455
|
+
const left = Math.min(start.x, end.x) + width / 2;
|
|
1456
|
+
const top = Math.min(start.y, end.y) + height / 2;
|
|
1457
|
+
const polygon = new Polygon2(
|
|
1458
|
+
[
|
|
1459
|
+
{ x: 0, y: 0 },
|
|
1460
|
+
{ x: width, y: 0 },
|
|
1461
|
+
{ x: width, y: height },
|
|
1462
|
+
{ x: 0, y: height }
|
|
1463
|
+
],
|
|
1464
|
+
{ ...DEFAULT_SHAPE_STYLE, left, top, ...style }
|
|
1465
|
+
);
|
|
1466
|
+
canvas.add(polygon);
|
|
1467
|
+
canvas.requestRenderAll();
|
|
1468
|
+
return polygon;
|
|
1469
|
+
}
|
|
1470
|
+
function createPolygonFromVertices(canvas, points, style) {
|
|
1471
|
+
const polygon = new Polygon2(
|
|
1472
|
+
points.map((p) => ({ x: p.x, y: p.y })),
|
|
1473
|
+
{ ...DEFAULT_SHAPE_STYLE, ...style }
|
|
1474
|
+
);
|
|
1475
|
+
canvas.add(polygon);
|
|
1476
|
+
canvas.requestRenderAll();
|
|
1477
|
+
return polygon;
|
|
1478
|
+
}
|
|
1479
|
+
function editPolygon(canvas, polygon, changes) {
|
|
1480
|
+
const { points, ...rest } = changes;
|
|
1481
|
+
if (points) {
|
|
1482
|
+
polygon.points = points;
|
|
1483
|
+
polygon.setDimensions();
|
|
1484
|
+
}
|
|
1485
|
+
polygon.set(rest);
|
|
1486
|
+
polygon.setCoords();
|
|
1487
|
+
canvas.requestRenderAll();
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
// src/interactions/drawToCreate.ts
|
|
1491
|
+
function snapAngleToInterval(point, ref, intervalDeg) {
|
|
1492
|
+
const dx = point.x - ref.x;
|
|
1493
|
+
const dy = point.y - ref.y;
|
|
1494
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
1495
|
+
if (dist === 0) return point;
|
|
1496
|
+
const radInterval = intervalDeg * Math.PI / 180;
|
|
1497
|
+
const snappedAngle = Math.round(Math.atan2(dy, dx) / radInterval) * radInterval;
|
|
1498
|
+
return {
|
|
1499
|
+
x: ref.x + Math.cos(snappedAngle) * dist,
|
|
1500
|
+
y: ref.y + Math.sin(snappedAngle) * dist
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
function snapAlongRay(angleSnapped, ref, doSnap) {
|
|
1504
|
+
const cursorSnapped = doSnap(angleSnapped.x, angleSnapped.y);
|
|
1505
|
+
const snappedX = cursorSnapped.x !== angleSnapped.x;
|
|
1506
|
+
const snappedY = cursorSnapped.y !== angleSnapped.y;
|
|
1507
|
+
if (!snappedX && !snappedY) return angleSnapped;
|
|
1508
|
+
const rayDx = angleSnapped.x - ref.x;
|
|
1509
|
+
const rayDy = angleSnapped.y - ref.y;
|
|
1510
|
+
const candidates = [];
|
|
1511
|
+
if (snappedX && Math.abs(rayDx) > 1e-9) {
|
|
1512
|
+
const t = (cursorSnapped.x - ref.x) / rayDx;
|
|
1513
|
+
const onRayY = ref.y + t * rayDy;
|
|
1514
|
+
candidates.push({
|
|
1515
|
+
x: cursorSnapped.x,
|
|
1516
|
+
y: onRayY,
|
|
1517
|
+
dist: Math.abs(onRayY - angleSnapped.y)
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
if (snappedY && Math.abs(rayDy) > 1e-9) {
|
|
1521
|
+
const t = (cursorSnapped.y - ref.y) / rayDy;
|
|
1522
|
+
const onRayX = ref.x + t * rayDx;
|
|
1523
|
+
candidates.push({
|
|
1524
|
+
x: onRayX,
|
|
1525
|
+
y: cursorSnapped.y,
|
|
1526
|
+
dist: Math.abs(onRayX - angleSnapped.x)
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
if (candidates.length === 0) return angleSnapped;
|
|
1530
|
+
candidates.sort((a, b) => a.dist - b.dist);
|
|
1531
|
+
const best = candidates[0];
|
|
1532
|
+
doSnap(best.x, best.y);
|
|
1533
|
+
return best;
|
|
1534
|
+
}
|
|
1535
|
+
function enableDrawToCreate(canvas, options) {
|
|
1536
|
+
let exited = false;
|
|
1537
|
+
const angleSnapEnabled = options?.angleSnap !== false;
|
|
1538
|
+
const angleInterval = typeof options?.angleSnap === "object" ? options.angleSnap.interval ?? DEFAULT_ANGLE_SNAP_INTERVAL : DEFAULT_ANGLE_SNAP_INTERVAL;
|
|
1539
|
+
const shiftTracker = createShiftKeyTracker();
|
|
1540
|
+
const points = [];
|
|
1541
|
+
const markers = [];
|
|
1542
|
+
const edgeLines = [];
|
|
1543
|
+
let trackingLine = null;
|
|
1544
|
+
let closingLine = null;
|
|
1545
|
+
let previousSelection;
|
|
1546
|
+
const snapping = createInteractionSnapping(
|
|
1547
|
+
canvas,
|
|
1548
|
+
options,
|
|
1549
|
+
() => points.map((p) => new Point7(p.x, p.y))
|
|
1550
|
+
);
|
|
1551
|
+
function trackPreviewElement(obj) {
|
|
1552
|
+
snapping.excludeSet.add(obj);
|
|
1553
|
+
}
|
|
1554
|
+
function untrackPreviewElement(obj) {
|
|
1555
|
+
snapping.excludeSet.delete(obj);
|
|
1556
|
+
}
|
|
1557
|
+
options?.viewport?.setEnabled(false);
|
|
1558
|
+
const lineStyle = {
|
|
1559
|
+
stroke: options?.style?.stroke ?? DEFAULT_DRAG_SHAPE_STYLE.stroke,
|
|
1560
|
+
strokeWidth: options?.style?.strokeWidth ?? DEFAULT_DRAG_SHAPE_STYLE.strokeWidth,
|
|
1561
|
+
strokeUniform: true,
|
|
1562
|
+
selectable: false,
|
|
1563
|
+
evented: false
|
|
1564
|
+
};
|
|
1565
|
+
const guideLineStyle = {
|
|
1566
|
+
stroke: options?.style?.stroke ?? DEFAULT_GUIDELINE_SHAPE_STYLE.stroke,
|
|
1567
|
+
strokeWidth: options?.style?.strokeWidth ?? DEFAULT_GUIDELINE_SHAPE_STYLE.strokeWidth,
|
|
1568
|
+
strokeUniform: true,
|
|
1569
|
+
selectable: false,
|
|
1570
|
+
evented: false
|
|
1571
|
+
};
|
|
1572
|
+
const removePreviewElements = () => {
|
|
1573
|
+
for (const marker of markers) {
|
|
1574
|
+
canvas.remove(marker);
|
|
1575
|
+
untrackPreviewElement(marker);
|
|
1576
|
+
}
|
|
1577
|
+
markers.length = 0;
|
|
1578
|
+
for (const line of edgeLines) {
|
|
1579
|
+
canvas.remove(line);
|
|
1580
|
+
untrackPreviewElement(line);
|
|
1581
|
+
}
|
|
1582
|
+
edgeLines.length = 0;
|
|
1583
|
+
if (trackingLine) {
|
|
1584
|
+
canvas.remove(trackingLine);
|
|
1585
|
+
untrackPreviewElement(trackingLine);
|
|
1586
|
+
trackingLine = null;
|
|
1587
|
+
}
|
|
1588
|
+
if (closingLine) {
|
|
1589
|
+
canvas.remove(closingLine);
|
|
1590
|
+
untrackPreviewElement(closingLine);
|
|
1591
|
+
closingLine = null;
|
|
1592
|
+
}
|
|
1593
|
+
};
|
|
1594
|
+
const finalize = () => {
|
|
1595
|
+
removePreviewElements();
|
|
1596
|
+
snapping.clearSnapResult();
|
|
1597
|
+
const polygon = createPolygonFromVertices(canvas, points, options?.style);
|
|
1598
|
+
canvas.selection = previousSelection;
|
|
1599
|
+
canvas.requestRenderAll();
|
|
1600
|
+
restoreViewport(options?.viewport);
|
|
1601
|
+
options?.onCreated?.(polygon);
|
|
1602
|
+
points.length = 0;
|
|
1603
|
+
};
|
|
1604
|
+
const handleMouseDown = (event) => {
|
|
1605
|
+
let { x, y } = event.scenePoint;
|
|
1606
|
+
if (angleSnapEnabled && shiftTracker.held && points.length > 0) {
|
|
1607
|
+
const ref = points[points.length - 1];
|
|
1608
|
+
const angleSnapped = snapAngleToInterval({ x, y }, ref, angleInterval);
|
|
1609
|
+
({ x, y } = snapAlongRay(
|
|
1610
|
+
angleSnapped,
|
|
1611
|
+
ref,
|
|
1612
|
+
(sx, sy) => snapping.snap(sx, sy)
|
|
1613
|
+
));
|
|
1614
|
+
} else {
|
|
1615
|
+
({ x, y } = snapping.snap(x, y));
|
|
1616
|
+
}
|
|
1617
|
+
snapping.clearSnapResult();
|
|
1618
|
+
if (points.length >= 3) {
|
|
1619
|
+
const dx = x - points[0].x;
|
|
1620
|
+
const dy = y - points[0].y;
|
|
1621
|
+
if (Math.sqrt(dx * dx + dy * dy) <= POLYGON_CLOSE_THRESHOLD) {
|
|
1622
|
+
finalize();
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
if (points.length === 0) {
|
|
1627
|
+
previousSelection = canvas.selection;
|
|
1628
|
+
canvas.selection = false;
|
|
1629
|
+
}
|
|
1630
|
+
points.push({ x, y });
|
|
1631
|
+
const marker = new Circle({
|
|
1632
|
+
left: x,
|
|
1633
|
+
top: y,
|
|
1634
|
+
radius: 4,
|
|
1635
|
+
fill: DEFAULT_SHAPE_STYLE.stroke,
|
|
1636
|
+
stroke: DEFAULT_SHAPE_STYLE.stroke,
|
|
1637
|
+
strokeWidth: 1,
|
|
1638
|
+
strokeUniform: true,
|
|
1639
|
+
selectable: false,
|
|
1640
|
+
evented: false
|
|
1641
|
+
});
|
|
1642
|
+
markers.push(marker);
|
|
1643
|
+
trackPreviewElement(marker);
|
|
1644
|
+
canvas.add(marker);
|
|
1645
|
+
if (points.length >= 2) {
|
|
1646
|
+
const prev = points[points.length - 2];
|
|
1647
|
+
const edge = new Line([prev.x, prev.y, x, y], lineStyle);
|
|
1648
|
+
edgeLines.push(edge);
|
|
1649
|
+
trackPreviewElement(edge);
|
|
1650
|
+
canvas.add(edge);
|
|
1651
|
+
}
|
|
1652
|
+
canvas.requestRenderAll();
|
|
1653
|
+
};
|
|
1654
|
+
const handleMouseMove = (event) => {
|
|
1655
|
+
if (points.length === 0) return;
|
|
1656
|
+
const lastPoint = points[points.length - 1];
|
|
1657
|
+
let { x, y } = event.scenePoint;
|
|
1658
|
+
if (angleSnapEnabled && shiftTracker.held) {
|
|
1659
|
+
const angleSnapped = snapAngleToInterval(
|
|
1660
|
+
{ x, y },
|
|
1661
|
+
lastPoint,
|
|
1662
|
+
angleInterval
|
|
1663
|
+
);
|
|
1664
|
+
({ x, y } = snapAlongRay(
|
|
1665
|
+
angleSnapped,
|
|
1666
|
+
lastPoint,
|
|
1667
|
+
(sx, sy) => snapping.snapWithGuidelines(sx, sy)
|
|
1668
|
+
));
|
|
1669
|
+
} else {
|
|
1670
|
+
({ x, y } = snapping.snapWithGuidelines(x, y));
|
|
1671
|
+
}
|
|
1672
|
+
if (trackingLine) {
|
|
1673
|
+
untrackPreviewElement(trackingLine);
|
|
1674
|
+
canvas.remove(trackingLine);
|
|
1675
|
+
}
|
|
1676
|
+
trackingLine = new Line([lastPoint.x, lastPoint.y, x, y], {
|
|
1677
|
+
...guideLineStyle,
|
|
1678
|
+
strokeDashArray: [5, 5]
|
|
1679
|
+
});
|
|
1680
|
+
trackPreviewElement(trackingLine);
|
|
1681
|
+
canvas.add(trackingLine);
|
|
1682
|
+
if (closingLine) {
|
|
1683
|
+
untrackPreviewElement(closingLine);
|
|
1684
|
+
canvas.remove(closingLine);
|
|
1685
|
+
closingLine = null;
|
|
1686
|
+
}
|
|
1687
|
+
if (points.length >= 3) {
|
|
1688
|
+
closingLine = new Line([x, y, points[0].x, points[0].y], {
|
|
1689
|
+
...guideLineStyle,
|
|
1690
|
+
strokeDashArray: [5, 5]
|
|
1691
|
+
});
|
|
1692
|
+
trackPreviewElement(closingLine);
|
|
1693
|
+
canvas.add(closingLine);
|
|
1694
|
+
}
|
|
1695
|
+
canvas.requestRenderAll();
|
|
1696
|
+
};
|
|
1697
|
+
const handleKeyDown = (e) => {
|
|
1698
|
+
if (e.key === "Escape" || e.key === "Backspace") {
|
|
1699
|
+
e.stopImmediatePropagation();
|
|
1700
|
+
e.preventDefault();
|
|
1701
|
+
cleanup("cancel");
|
|
1702
|
+
}
|
|
1703
|
+
};
|
|
1704
|
+
canvas.on("mouse:down", handleMouseDown);
|
|
1705
|
+
canvas.on("mouse:move", handleMouseMove);
|
|
1706
|
+
document.addEventListener("keydown", handleKeyDown, true);
|
|
1707
|
+
function cleanup(reason) {
|
|
1708
|
+
if (exited) return;
|
|
1709
|
+
exited = true;
|
|
1710
|
+
canvas.off("mouse:down", handleMouseDown);
|
|
1711
|
+
canvas.off("mouse:move", handleMouseMove);
|
|
1712
|
+
document.removeEventListener("keydown", handleKeyDown, true);
|
|
1713
|
+
shiftTracker.cleanup();
|
|
1714
|
+
snapping.cleanup();
|
|
1715
|
+
removePreviewElements();
|
|
1716
|
+
if (points.length > 0) {
|
|
1717
|
+
canvas.selection = previousSelection;
|
|
1718
|
+
}
|
|
1719
|
+
points.length = 0;
|
|
1720
|
+
canvas.requestRenderAll();
|
|
1721
|
+
restoreViewport(options?.viewport);
|
|
1722
|
+
if (reason === "cancel") {
|
|
1723
|
+
options?.onCancel?.();
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
return () => cleanup();
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
// src/interactions/vertexEdit.ts
|
|
1730
|
+
import {
|
|
1731
|
+
Point as Point8,
|
|
1732
|
+
util as util3
|
|
1733
|
+
} from "fabric";
|
|
1734
|
+
function localPointToScene(polygon, point) {
|
|
1735
|
+
const matrix = polygon.calcTransformMatrix();
|
|
1736
|
+
const localPoint = new Point8(
|
|
1737
|
+
point.x - polygon.pathOffset.x,
|
|
1738
|
+
point.y - polygon.pathOffset.y
|
|
1739
|
+
);
|
|
1740
|
+
return util3.transformPoint(localPoint, matrix);
|
|
1741
|
+
}
|
|
1742
|
+
function scenePointToLocal(polygon, scenePoint) {
|
|
1743
|
+
const matrix = polygon.calcTransformMatrix();
|
|
1744
|
+
const invMatrix = util3.invertTransform(matrix);
|
|
1745
|
+
const localPoint = util3.transformPoint(scenePoint, invMatrix);
|
|
1746
|
+
return {
|
|
1747
|
+
x: localPoint.x + polygon.pathOffset.x,
|
|
1748
|
+
y: localPoint.y + polygon.pathOffset.y
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
function sceneToScreen(scenePoint, canvas) {
|
|
1752
|
+
return util3.transformPoint(scenePoint, canvas.viewportTransform);
|
|
1753
|
+
}
|
|
1754
|
+
function screenToScene(screenX, screenY, canvas) {
|
|
1755
|
+
const inv = util3.invertTransform(canvas.viewportTransform);
|
|
1756
|
+
return util3.transformPoint(new Point8(screenX, screenY), inv);
|
|
1757
|
+
}
|
|
1758
|
+
function createHandleElement(radius, fill, stroke, strokeWidth) {
|
|
1759
|
+
const el = document.createElement("div");
|
|
1760
|
+
const size = radius * 2;
|
|
1761
|
+
el.style.cssText = [
|
|
1762
|
+
"position: absolute",
|
|
1763
|
+
`width: ${size}px`,
|
|
1764
|
+
`height: ${size}px`,
|
|
1765
|
+
`margin-left: ${-radius}px`,
|
|
1766
|
+
`margin-top: ${-radius}px`,
|
|
1767
|
+
"border-radius: 50%",
|
|
1768
|
+
`background: ${fill}`,
|
|
1769
|
+
`border: ${strokeWidth}px solid ${stroke}`,
|
|
1770
|
+
"box-sizing: border-box",
|
|
1771
|
+
"pointer-events: auto",
|
|
1772
|
+
"cursor: grab",
|
|
1773
|
+
"touch-action: none"
|
|
1774
|
+
].join("; ");
|
|
1775
|
+
return el;
|
|
1776
|
+
}
|
|
1777
|
+
function positionHandle(handle, scenePoint, canvas) {
|
|
1778
|
+
const screen = sceneToScreen(scenePoint, canvas);
|
|
1779
|
+
handle.style.left = `${screen.x}px`;
|
|
1780
|
+
handle.style.top = `${screen.y}px`;
|
|
1781
|
+
}
|
|
1782
|
+
function enableVertexEdit(canvas, polygon, options, onExit) {
|
|
1783
|
+
let exited = false;
|
|
1784
|
+
let draggingIndex = null;
|
|
1785
|
+
const handleRadius = options?.handleRadius ?? DEFAULT_VERTEX_HANDLE_RADIUS;
|
|
1786
|
+
const handleFill = options?.handleFill ?? DEFAULT_VERTEX_HANDLE_FILL;
|
|
1787
|
+
const handleStroke = options?.handleStroke ?? DEFAULT_VERTEX_HANDLE_STROKE;
|
|
1788
|
+
const handleStrokeWidth = options?.handleStrokeWidth ?? DEFAULT_VERTEX_HANDLE_STROKE_WIDTH;
|
|
1789
|
+
const previousState = {
|
|
1790
|
+
selectable: polygon.selectable,
|
|
1791
|
+
evented: polygon.evented,
|
|
1792
|
+
hasControls: polygon.hasControls,
|
|
1793
|
+
canvasSelection: canvas.selection
|
|
1794
|
+
};
|
|
1795
|
+
const prevObjectStates = /* @__PURE__ */ new Map();
|
|
1796
|
+
polygon.selectable = false;
|
|
1797
|
+
polygon.evented = false;
|
|
1798
|
+
polygon.hasControls = false;
|
|
1799
|
+
canvas.selection = false;
|
|
1800
|
+
canvas.discardActiveObject();
|
|
1801
|
+
canvas.forEachObject((obj) => {
|
|
1802
|
+
if (obj !== polygon) {
|
|
1803
|
+
prevObjectStates.set(obj, {
|
|
1804
|
+
selectable: obj.selectable,
|
|
1805
|
+
evented: obj.evented
|
|
1806
|
+
});
|
|
1807
|
+
obj.selectable = false;
|
|
1808
|
+
obj.evented = false;
|
|
1809
|
+
}
|
|
1810
|
+
});
|
|
1811
|
+
const snapping = createInteractionSnapping(canvas, void 0, () => {
|
|
1812
|
+
if (draggingIndex === null) return [];
|
|
1813
|
+
return polygon.points.filter((_, i) => i !== draggingIndex).map((pt) => localPointToScene(polygon, pt));
|
|
1814
|
+
});
|
|
1815
|
+
snapping.excludeSet.add(polygon);
|
|
1816
|
+
const container = document.createElement("div");
|
|
1817
|
+
container.style.cssText = "position: absolute; inset: 0; pointer-events: none; overflow: hidden;";
|
|
1818
|
+
canvas.wrapperEl.appendChild(container);
|
|
1819
|
+
const handles = [];
|
|
1820
|
+
const points = polygon.points;
|
|
1821
|
+
for (let i = 0; i < points.length; i++) {
|
|
1822
|
+
const handle = createHandleElement(
|
|
1823
|
+
handleRadius,
|
|
1824
|
+
handleFill,
|
|
1825
|
+
handleStroke,
|
|
1826
|
+
handleStrokeWidth
|
|
1827
|
+
);
|
|
1828
|
+
positionHandle(handle, localPointToScene(polygon, points[i]), canvas);
|
|
1829
|
+
container.appendChild(handle);
|
|
1830
|
+
handles.push(handle);
|
|
1831
|
+
handle.addEventListener("pointerdown", (e) => {
|
|
1832
|
+
if (exited) return;
|
|
1833
|
+
e.preventDefault();
|
|
1834
|
+
e.stopPropagation();
|
|
1835
|
+
draggingIndex = i;
|
|
1836
|
+
handle.setPointerCapture(e.pointerId);
|
|
1837
|
+
handle.style.cursor = "grabbing";
|
|
1838
|
+
});
|
|
1839
|
+
handle.addEventListener("pointermove", (e) => {
|
|
1840
|
+
if (draggingIndex !== i) return;
|
|
1841
|
+
const wrapperRect = canvas.wrapperEl.getBoundingClientRect();
|
|
1842
|
+
const canvasRelX = e.clientX - wrapperRect.left;
|
|
1843
|
+
const canvasRelY = e.clientY - wrapperRect.top;
|
|
1844
|
+
const rawScene = screenToScene(canvasRelX, canvasRelY, canvas);
|
|
1845
|
+
const snapped = snapping.snapWithGuidelines(rawScene.x, rawScene.y);
|
|
1846
|
+
const scenePoint = new Point8(snapped.x, snapped.y);
|
|
1847
|
+
const anchorIdx = i === 0 ? 1 : 0;
|
|
1848
|
+
const anchorBefore = localPointToScene(
|
|
1849
|
+
polygon,
|
|
1850
|
+
polygon.points[anchorIdx]
|
|
1851
|
+
);
|
|
1852
|
+
const localPoint = scenePointToLocal(polygon, scenePoint);
|
|
1853
|
+
polygon.points[i] = localPoint;
|
|
1854
|
+
polygon.setDimensions();
|
|
1855
|
+
const anchorAfter = localPointToScene(polygon, polygon.points[anchorIdx]);
|
|
1856
|
+
polygon.left += anchorBefore.x - anchorAfter.x;
|
|
1857
|
+
polygon.top += anchorBefore.y - anchorAfter.y;
|
|
1858
|
+
polygon.dirty = true;
|
|
1859
|
+
polygon.setCoords();
|
|
1860
|
+
repositionAllHandles();
|
|
1861
|
+
canvas.requestRenderAll();
|
|
1862
|
+
});
|
|
1863
|
+
handle.addEventListener("pointerup", (e) => {
|
|
1864
|
+
if (draggingIndex !== i) return;
|
|
1865
|
+
handle.releasePointerCapture(e.pointerId);
|
|
1866
|
+
draggingIndex = null;
|
|
1867
|
+
handle.style.cursor = "grab";
|
|
1868
|
+
snapping.clearSnapResult();
|
|
1869
|
+
canvas.requestRenderAll();
|
|
1870
|
+
});
|
|
1871
|
+
}
|
|
1872
|
+
function repositionAllHandles() {
|
|
1873
|
+
const pts = polygon.points;
|
|
1874
|
+
for (let j = 0; j < pts.length; j++) {
|
|
1875
|
+
positionHandle(handles[j], localPointToScene(polygon, pts[j]), canvas);
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
const afterRender = () => {
|
|
1879
|
+
if (draggingIndex === null) {
|
|
1880
|
+
repositionAllHandles();
|
|
1881
|
+
}
|
|
1882
|
+
};
|
|
1883
|
+
canvas.on("after:render", afterRender);
|
|
1884
|
+
const handleKeyDown = (e) => {
|
|
1885
|
+
if (e.key === "Escape") {
|
|
1886
|
+
e.stopImmediatePropagation();
|
|
1887
|
+
cleanup();
|
|
1888
|
+
}
|
|
1889
|
+
};
|
|
1890
|
+
document.addEventListener("keydown", handleKeyDown, true);
|
|
1891
|
+
const handleMouseDown = () => {
|
|
1892
|
+
cleanup();
|
|
1893
|
+
};
|
|
1894
|
+
canvas.on("mouse:down", handleMouseDown);
|
|
1895
|
+
function cleanup() {
|
|
1896
|
+
if (exited) return;
|
|
1897
|
+
exited = true;
|
|
1898
|
+
snapping.cleanup();
|
|
1899
|
+
canvas.off("after:render", afterRender);
|
|
1900
|
+
canvas.off("mouse:down", handleMouseDown);
|
|
1901
|
+
document.removeEventListener("keydown", handleKeyDown, true);
|
|
1902
|
+
container.remove();
|
|
1903
|
+
polygon.selectable = previousState.selectable;
|
|
1904
|
+
polygon.evented = previousState.evented;
|
|
1905
|
+
polygon.hasControls = previousState.hasControls;
|
|
1906
|
+
canvas.selection = previousState.canvasSelection;
|
|
1907
|
+
prevObjectStates.forEach((state, obj) => {
|
|
1908
|
+
obj.selectable = state.selectable;
|
|
1909
|
+
obj.evented = state.evented;
|
|
1910
|
+
});
|
|
1911
|
+
canvas.discardActiveObject();
|
|
1912
|
+
canvas.requestRenderAll();
|
|
1913
|
+
onExit?.();
|
|
1914
|
+
}
|
|
1915
|
+
return cleanup;
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
// src/serialization.ts
|
|
1919
|
+
import { Rect as Rect5 } from "fabric";
|
|
1920
|
+
var strokeBaseMap = /* @__PURE__ */ new WeakMap();
|
|
1921
|
+
function enableScaledStrokes(canvas) {
|
|
1922
|
+
function applyScaledStrokes() {
|
|
1923
|
+
const zoom = canvas.getZoom();
|
|
1924
|
+
canvas.forEachObject((obj) => {
|
|
1925
|
+
if (!obj.strokeWidth && obj.strokeWidth !== 0) return;
|
|
1926
|
+
if (!strokeBaseMap.has(obj)) {
|
|
1927
|
+
strokeBaseMap.set(obj, obj.strokeWidth ?? 0);
|
|
1928
|
+
}
|
|
1929
|
+
const base = strokeBaseMap.get(obj);
|
|
1930
|
+
if (base === 0) return;
|
|
1931
|
+
obj.strokeWidth = base / zoom;
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
1934
|
+
canvas.on("before:render", applyScaledStrokes);
|
|
1935
|
+
return () => {
|
|
1936
|
+
canvas.off("before:render", applyScaledStrokes);
|
|
1937
|
+
canvas.forEachObject((obj) => {
|
|
1938
|
+
const base = strokeBaseMap.get(obj);
|
|
1939
|
+
if (base !== void 0) {
|
|
1940
|
+
obj.strokeWidth = base;
|
|
1941
|
+
}
|
|
1942
|
+
});
|
|
1943
|
+
};
|
|
1944
|
+
}
|
|
1945
|
+
function getBaseStrokeWidth(obj) {
|
|
1946
|
+
return strokeBaseMap.get(obj) ?? obj.strokeWidth ?? 0;
|
|
1947
|
+
}
|
|
1948
|
+
function serializeCanvas(canvas, options) {
|
|
1949
|
+
const properties = [
|
|
1950
|
+
"data",
|
|
1951
|
+
"shapeType",
|
|
1952
|
+
// Control styling — absent from Fabric's default toObject output
|
|
1953
|
+
"borderColor",
|
|
1954
|
+
"cornerColor",
|
|
1955
|
+
"cornerStrokeColor",
|
|
1956
|
+
"transparentCorners",
|
|
1957
|
+
// Interaction locks — absent from Fabric's default toObject output
|
|
1958
|
+
"lockRotation",
|
|
1959
|
+
"lockUniScaling",
|
|
1960
|
+
...options?.properties ?? []
|
|
1961
|
+
];
|
|
1962
|
+
const scaledWidths = /* @__PURE__ */ new Map();
|
|
1963
|
+
canvas.forEachObject((obj) => {
|
|
1964
|
+
const base = strokeBaseMap.get(obj);
|
|
1965
|
+
if (base !== void 0 && obj.strokeWidth !== base) {
|
|
1966
|
+
scaledWidths.set(obj, obj.strokeWidth ?? 0);
|
|
1967
|
+
obj.strokeWidth = base;
|
|
1968
|
+
}
|
|
1969
|
+
});
|
|
1970
|
+
const json = canvas.toObject(properties);
|
|
1971
|
+
scaledWidths.forEach((scaled, obj) => {
|
|
1972
|
+
obj.strokeWidth = scaled;
|
|
1973
|
+
});
|
|
1974
|
+
return json;
|
|
1975
|
+
}
|
|
1976
|
+
async function loadCanvas(canvas, json) {
|
|
1977
|
+
await canvas.loadFromJSON(json);
|
|
1978
|
+
canvas.forEachObject((obj) => {
|
|
1979
|
+
obj.set(DEFAULT_CONTROL_STYLE);
|
|
1980
|
+
if (obj.shapeType === "circle" && obj instanceof Rect5) {
|
|
1981
|
+
restoreCircleConstraints(obj);
|
|
1982
|
+
}
|
|
1983
|
+
});
|
|
1984
|
+
canvas.requestRenderAll();
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
// src/hooks/useEditCanvas.ts
|
|
1988
|
+
function useEditCanvas(options) {
|
|
1989
|
+
const canvasRef = useRef2(null);
|
|
1990
|
+
const viewportRef = useRef2(null);
|
|
1991
|
+
const alignmentCleanupRef = useRef2(null);
|
|
1992
|
+
const rotationSnapCleanupRef = useRef2(null);
|
|
1993
|
+
const modeCleanupRef = useRef2(null);
|
|
1994
|
+
const vertexEditCleanupRef = useRef2(null);
|
|
1995
|
+
const keyboardCleanupRef = useRef2(null);
|
|
1996
|
+
const [zoom, setZoom] = useState(1);
|
|
1997
|
+
const [selected, setSelected] = useState([]);
|
|
1998
|
+
const [viewportMode, setViewportModeState] = useState("select");
|
|
1999
|
+
const [isEditingVertices, setIsEditingVertices] = useState(false);
|
|
2000
|
+
const setMode = useCallback((setup) => {
|
|
2001
|
+
vertexEditCleanupRef.current?.();
|
|
2002
|
+
vertexEditCleanupRef.current = null;
|
|
2003
|
+
setIsEditingVertices(false);
|
|
2004
|
+
modeCleanupRef.current?.();
|
|
2005
|
+
modeCleanupRef.current = null;
|
|
2006
|
+
const canvas = canvasRef.current;
|
|
2007
|
+
if (!canvas) return;
|
|
2008
|
+
if (setup === null) {
|
|
2009
|
+
canvas.selection = true;
|
|
2010
|
+
canvas.forEachObject((obj) => {
|
|
2011
|
+
obj.selectable = true;
|
|
2012
|
+
obj.evented = true;
|
|
2013
|
+
});
|
|
2014
|
+
} else {
|
|
2015
|
+
canvas.selection = false;
|
|
2016
|
+
canvas.forEachObject((obj) => {
|
|
2017
|
+
obj.selectable = false;
|
|
2018
|
+
obj.evented = false;
|
|
2019
|
+
});
|
|
2020
|
+
const cleanup = setup(canvas, viewportRef.current ?? void 0);
|
|
2021
|
+
if (cleanup) {
|
|
2022
|
+
modeCleanupRef.current = cleanup;
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
}, []);
|
|
2026
|
+
const onReady = useCallback(
|
|
2027
|
+
(canvas) => {
|
|
2028
|
+
canvasRef.current = canvas;
|
|
2029
|
+
if (options?.scaledStrokes !== false) {
|
|
2030
|
+
enableScaledStrokes(canvas);
|
|
2031
|
+
}
|
|
2032
|
+
if (options?.keyboardShortcuts !== false) {
|
|
2033
|
+
keyboardCleanupRef.current = enableKeyboardShortcuts(canvas);
|
|
2034
|
+
}
|
|
2035
|
+
setCanvasAlignmentEnabled(canvas, options?.enableAlignment);
|
|
2036
|
+
if (options?.panAndZoom !== false) {
|
|
2037
|
+
viewportRef.current = enablePanAndZoom(
|
|
2038
|
+
canvas,
|
|
2039
|
+
typeof options?.panAndZoom === "object" ? options.panAndZoom : void 0
|
|
2040
|
+
);
|
|
2041
|
+
}
|
|
2042
|
+
const alignmentEnabled = resolveAlignmentEnabled(
|
|
2043
|
+
options?.enableAlignment,
|
|
2044
|
+
options?.alignment
|
|
2045
|
+
);
|
|
2046
|
+
if (alignmentEnabled) {
|
|
2047
|
+
alignmentCleanupRef.current = enableObjectAlignment(
|
|
2048
|
+
canvas,
|
|
2049
|
+
typeof options?.alignment === "object" ? options.alignment : void 0
|
|
2050
|
+
);
|
|
2051
|
+
}
|
|
2052
|
+
if (options?.rotationSnap !== false) {
|
|
2053
|
+
rotationSnapCleanupRef.current = enableRotationSnap(
|
|
2054
|
+
canvas,
|
|
2055
|
+
typeof options?.rotationSnap === "object" ? options.rotationSnap : void 0
|
|
2056
|
+
);
|
|
2057
|
+
}
|
|
2058
|
+
canvas.on("mouse:wheel", () => {
|
|
2059
|
+
setZoom(canvas.getZoom());
|
|
2060
|
+
});
|
|
2061
|
+
canvas.on("selection:created", (e) => {
|
|
2062
|
+
setSelected(e.selected ?? []);
|
|
2063
|
+
});
|
|
2064
|
+
canvas.on("selection:updated", (e) => {
|
|
2065
|
+
setSelected(e.selected ?? []);
|
|
2066
|
+
});
|
|
2067
|
+
canvas.on("selection:cleared", () => {
|
|
2068
|
+
setSelected([]);
|
|
2069
|
+
});
|
|
2070
|
+
if (options?.vertexEdit !== false) {
|
|
2071
|
+
const vertexOpts = typeof options?.vertexEdit === "object" ? options.vertexEdit : void 0;
|
|
2072
|
+
canvas.on("mouse:dblclick", (e) => {
|
|
2073
|
+
if (e.target && e.target instanceof Polygon4) {
|
|
2074
|
+
vertexEditCleanupRef.current?.();
|
|
2075
|
+
vertexEditCleanupRef.current = enableVertexEdit(
|
|
2076
|
+
canvas,
|
|
2077
|
+
e.target,
|
|
2078
|
+
vertexOpts,
|
|
2079
|
+
() => {
|
|
2080
|
+
vertexEditCleanupRef.current = null;
|
|
2081
|
+
setIsEditingVertices(false);
|
|
2082
|
+
}
|
|
2083
|
+
);
|
|
2084
|
+
setIsEditingVertices(true);
|
|
2085
|
+
}
|
|
2086
|
+
});
|
|
2087
|
+
}
|
|
2088
|
+
const onReadyResult = options?.onReady?.(canvas);
|
|
2089
|
+
if (options?.autoFitToBackground !== false) {
|
|
2090
|
+
Promise.resolve(onReadyResult).then(() => {
|
|
2091
|
+
if (canvas.backgroundImage) {
|
|
2092
|
+
fitViewportToBackground(canvas);
|
|
2093
|
+
syncZoom(canvasRef, setZoom);
|
|
2094
|
+
}
|
|
2095
|
+
});
|
|
2096
|
+
}
|
|
2097
|
+
},
|
|
2098
|
+
// onReady and panAndZoom are intentionally excluded — we only initialize once
|
|
2099
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2100
|
+
[]
|
|
2101
|
+
);
|
|
2102
|
+
useEffect2(() => {
|
|
2103
|
+
const canvas = canvasRef.current;
|
|
2104
|
+
if (!canvas) return;
|
|
2105
|
+
setCanvasAlignmentEnabled(canvas, options?.enableAlignment);
|
|
2106
|
+
const shouldEnable = resolveAlignmentEnabled(
|
|
2107
|
+
options?.enableAlignment,
|
|
2108
|
+
options?.alignment
|
|
2109
|
+
);
|
|
2110
|
+
if (shouldEnable && !alignmentCleanupRef.current) {
|
|
2111
|
+
alignmentCleanupRef.current = enableObjectAlignment(
|
|
2112
|
+
canvas,
|
|
2113
|
+
typeof options?.alignment === "object" ? options.alignment : void 0
|
|
2114
|
+
);
|
|
2115
|
+
} else if (!shouldEnable && alignmentCleanupRef.current) {
|
|
2116
|
+
alignmentCleanupRef.current();
|
|
2117
|
+
alignmentCleanupRef.current = null;
|
|
2118
|
+
}
|
|
2119
|
+
}, [options?.enableAlignment]);
|
|
2120
|
+
const setViewportMode = useCallback((mode) => {
|
|
2121
|
+
viewportRef.current?.setMode(mode);
|
|
2122
|
+
setViewportModeState(mode);
|
|
2123
|
+
}, []);
|
|
2124
|
+
const { resetViewport: resetViewport2, zoomIn, zoomOut } = createViewportActions(
|
|
2125
|
+
canvasRef,
|
|
2126
|
+
viewportRef,
|
|
2127
|
+
setZoom
|
|
2128
|
+
);
|
|
2129
|
+
const setBackground = useCallback(async (url) => {
|
|
2130
|
+
const canvas = canvasRef.current;
|
|
2131
|
+
if (!canvas) throw new Error("Canvas not ready");
|
|
2132
|
+
const resizeOpts = options?.backgroundResize !== false ? typeof options?.backgroundResize === "object" ? options.backgroundResize : {} : void 0;
|
|
2133
|
+
const img = await setBackgroundImage(canvas, url, resizeOpts);
|
|
2134
|
+
if (options?.autoFitToBackground !== false) {
|
|
2135
|
+
fitViewportToBackground(canvas);
|
|
2136
|
+
syncZoom(canvasRef, setZoom);
|
|
2137
|
+
}
|
|
2138
|
+
return img;
|
|
2139
|
+
}, []);
|
|
2140
|
+
return {
|
|
2141
|
+
/** Pass this to `<Canvas onReady={...} />` */
|
|
2142
|
+
onReady,
|
|
2143
|
+
/** Ref to the underlying Fabric canvas instance. */
|
|
2144
|
+
canvasRef,
|
|
2145
|
+
/** Current zoom level (reactive). */
|
|
2146
|
+
zoom,
|
|
2147
|
+
/** Currently selected objects (reactive). */
|
|
2148
|
+
selected,
|
|
2149
|
+
/** Viewport controls. */
|
|
2150
|
+
viewport: {
|
|
2151
|
+
/** Current viewport mode (reactive). */
|
|
2152
|
+
mode: viewportMode,
|
|
2153
|
+
/** Switch between 'select' and 'pan' viewport modes. */
|
|
2154
|
+
setMode: setViewportMode,
|
|
2155
|
+
/** Reset viewport to default (no pan, zoom = 1), or fit to background if one is set. */
|
|
2156
|
+
reset: resetViewport2,
|
|
2157
|
+
/** Zoom in toward the canvas center. Default step: 0.2. */
|
|
2158
|
+
zoomIn,
|
|
2159
|
+
/** Zoom out from the canvas center. Default step: 0.2. */
|
|
2160
|
+
zoomOut
|
|
2161
|
+
},
|
|
2162
|
+
/** Whether vertex edit mode is currently active (reactive). */
|
|
2163
|
+
isEditingVertices,
|
|
2164
|
+
/**
|
|
2165
|
+
* Activate an interaction mode or return to select mode.
|
|
2166
|
+
*
|
|
2167
|
+
* Pass a setup function to activate a creation mode:
|
|
2168
|
+
* ```ts
|
|
2169
|
+
* canvas.setMode((c, viewport) =>
|
|
2170
|
+
* enableClickToCreate(c, factory, { viewport })
|
|
2171
|
+
* );
|
|
2172
|
+
* ```
|
|
2173
|
+
*
|
|
2174
|
+
* Pass `null` to deactivate and return to select mode:
|
|
2175
|
+
* ```ts
|
|
2176
|
+
* canvas.setMode(null);
|
|
2177
|
+
* ```
|
|
2178
|
+
*/
|
|
2179
|
+
setMode,
|
|
2180
|
+
/**
|
|
2181
|
+
* Set a background image from a URL. Automatically resizes if the image
|
|
2182
|
+
* exceeds the configured limits (opt out via `backgroundResize: false`),
|
|
2183
|
+
* and fits the viewport after setting if `autoFitToBackground` is enabled.
|
|
2184
|
+
*/
|
|
2185
|
+
setBackground
|
|
2186
|
+
};
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
// src/hooks/useViewCanvas.ts
|
|
2190
|
+
import { useCallback as useCallback2, useRef as useRef3, useState as useState2 } from "react";
|
|
2191
|
+
import { Rect as Rect6 } from "fabric";
|
|
2192
|
+
var VIEW_BORDER_RADIUS = 4;
|
|
2193
|
+
function lockCanvas(canvas) {
|
|
2194
|
+
canvas.selection = false;
|
|
2195
|
+
canvas.forEachObject((obj) => {
|
|
2196
|
+
obj.selectable = false;
|
|
2197
|
+
obj.evented = false;
|
|
2198
|
+
if (obj instanceof Rect6 && obj.shapeType !== "circle" && obj.data?.type !== "DEVICE") {
|
|
2199
|
+
const rx = VIEW_BORDER_RADIUS / (obj.scaleX ?? 1);
|
|
2200
|
+
const ry = VIEW_BORDER_RADIUS / (obj.scaleY ?? 1);
|
|
2201
|
+
obj.set({ rx, ry });
|
|
2202
|
+
}
|
|
2203
|
+
});
|
|
2204
|
+
}
|
|
2205
|
+
function useViewCanvas(options) {
|
|
2206
|
+
const canvasRef = useRef3(null);
|
|
2207
|
+
const viewportRef = useRef3(null);
|
|
2208
|
+
const [zoom, setZoom] = useState2(1);
|
|
2209
|
+
const onReady = useCallback2(
|
|
2210
|
+
(canvas) => {
|
|
2211
|
+
canvasRef.current = canvas;
|
|
2212
|
+
if (options?.scaledStrokes !== false) {
|
|
2213
|
+
enableScaledStrokes(canvas);
|
|
2214
|
+
}
|
|
2215
|
+
if (options?.panAndZoom !== false) {
|
|
2216
|
+
const panAndZoomOpts = typeof options?.panAndZoom === "object" ? options.panAndZoom : {};
|
|
2217
|
+
viewportRef.current = enablePanAndZoom(canvas, {
|
|
2218
|
+
...panAndZoomOpts,
|
|
2219
|
+
initialMode: "pan"
|
|
2220
|
+
});
|
|
2221
|
+
}
|
|
2222
|
+
lockCanvas(canvas);
|
|
2223
|
+
canvas.on("object:added", () => {
|
|
2224
|
+
lockCanvas(canvas);
|
|
2225
|
+
});
|
|
2226
|
+
canvas.on("mouse:wheel", () => {
|
|
2227
|
+
setZoom(canvas.getZoom());
|
|
2228
|
+
});
|
|
2229
|
+
const onReadyResult = options?.onReady?.(canvas);
|
|
2230
|
+
if (options?.autoFitToBackground !== false) {
|
|
2231
|
+
Promise.resolve(onReadyResult).then(() => {
|
|
2232
|
+
if (canvas.backgroundImage) {
|
|
2233
|
+
fitViewportToBackground(canvas);
|
|
2234
|
+
syncZoom(canvasRef, setZoom);
|
|
2235
|
+
}
|
|
2236
|
+
});
|
|
2237
|
+
}
|
|
2238
|
+
},
|
|
2239
|
+
// onReady and panAndZoom are intentionally excluded — we only initialize once
|
|
2240
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2241
|
+
[]
|
|
2242
|
+
);
|
|
2243
|
+
const { resetViewport: resetViewport2, zoomIn, zoomOut } = createViewportActions(
|
|
2244
|
+
canvasRef,
|
|
2245
|
+
viewportRef,
|
|
2246
|
+
setZoom
|
|
2247
|
+
);
|
|
2248
|
+
const findObject = (id) => {
|
|
2249
|
+
const c = canvasRef.current;
|
|
2250
|
+
if (!c) return void 0;
|
|
2251
|
+
return c.getObjects().find((o) => o.data?.id === id);
|
|
2252
|
+
};
|
|
2253
|
+
const setObjectStyle = useCallback2((id, style) => {
|
|
2254
|
+
const obj = findObject(id);
|
|
2255
|
+
if (!obj) return;
|
|
2256
|
+
obj.set(style);
|
|
2257
|
+
canvasRef.current.requestRenderAll();
|
|
2258
|
+
}, []);
|
|
2259
|
+
const setObjectStyles = useCallback2(
|
|
2260
|
+
(styles) => {
|
|
2261
|
+
const c = canvasRef.current;
|
|
2262
|
+
if (!c) return;
|
|
2263
|
+
const objects = c.getObjects();
|
|
2264
|
+
const idMap = /* @__PURE__ */ new Map();
|
|
2265
|
+
for (const obj of objects) {
|
|
2266
|
+
if (obj.data?.id) idMap.set(obj.data.id, obj);
|
|
2267
|
+
}
|
|
2268
|
+
let updated = false;
|
|
2269
|
+
for (const [id, style] of Object.entries(styles)) {
|
|
2270
|
+
const obj = idMap.get(id);
|
|
2271
|
+
if (obj) {
|
|
2272
|
+
obj.set(style);
|
|
2273
|
+
updated = true;
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
if (updated) c.requestRenderAll();
|
|
2277
|
+
},
|
|
2278
|
+
[]
|
|
2279
|
+
);
|
|
2280
|
+
const setObjectStyleByType = useCallback2(
|
|
2281
|
+
(type, style) => {
|
|
2282
|
+
const c = canvasRef.current;
|
|
2283
|
+
if (!c) return;
|
|
2284
|
+
let updated = false;
|
|
2285
|
+
for (const obj of c.getObjects()) {
|
|
2286
|
+
if (obj.data?.type === type) {
|
|
2287
|
+
obj.set(style);
|
|
2288
|
+
updated = true;
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
if (updated) c.requestRenderAll();
|
|
2292
|
+
},
|
|
2293
|
+
[]
|
|
2294
|
+
);
|
|
2295
|
+
return {
|
|
2296
|
+
/** Pass this to `<Canvas onReady={...} />` */
|
|
2297
|
+
onReady,
|
|
2298
|
+
/** Ref to the underlying Fabric canvas instance. */
|
|
2299
|
+
canvasRef,
|
|
2300
|
+
/** Current zoom level (reactive). */
|
|
2301
|
+
zoom,
|
|
2302
|
+
/** Viewport controls. */
|
|
2303
|
+
viewport: {
|
|
2304
|
+
/** Reset viewport to default (no pan, zoom = 1), or fit to background if one is set. */
|
|
2305
|
+
reset: resetViewport2,
|
|
2306
|
+
/** Zoom in toward the canvas center. Default step: 0.2. */
|
|
2307
|
+
zoomIn,
|
|
2308
|
+
/** Zoom out from the canvas center. Default step: 0.2. */
|
|
2309
|
+
zoomOut
|
|
2310
|
+
},
|
|
2311
|
+
/** Update a single object's visual style by its `data.id`. */
|
|
2312
|
+
setObjectStyle,
|
|
2313
|
+
/** Batch-update multiple objects' visual styles in one render. Keyed by `data.id`. */
|
|
2314
|
+
setObjectStyles,
|
|
2315
|
+
/** Apply a visual style to all objects whose `data.type` matches. */
|
|
2316
|
+
setObjectStyleByType
|
|
2317
|
+
};
|
|
2318
|
+
}
|
|
2319
|
+
export {
|
|
2320
|
+
Canvas,
|
|
2321
|
+
DEFAULT_ALIGNMENT_STYLE,
|
|
2322
|
+
DEFAULT_CIRCLE_STYLE,
|
|
2323
|
+
DEFAULT_CONTROL_STYLE,
|
|
2324
|
+
DEFAULT_DRAG_SHAPE_STYLE,
|
|
2325
|
+
DEFAULT_GUIDELINE_SHAPE_STYLE,
|
|
2326
|
+
DEFAULT_SHAPE_STYLE,
|
|
2327
|
+
createCircle,
|
|
2328
|
+
createCircleAtPoint,
|
|
2329
|
+
createPolygon,
|
|
2330
|
+
createPolygonAtPoint,
|
|
2331
|
+
createPolygonFromDrag,
|
|
2332
|
+
createPolygonFromVertices,
|
|
2333
|
+
createRectangle,
|
|
2334
|
+
createRectangleAtPoint,
|
|
2335
|
+
deleteObjects,
|
|
2336
|
+
editCircle,
|
|
2337
|
+
editPolygon,
|
|
2338
|
+
editRectangle,
|
|
2339
|
+
enableClickToCreate,
|
|
2340
|
+
enableDragToCreate,
|
|
2341
|
+
enableDrawToCreate,
|
|
2342
|
+
enableKeyboardShortcuts,
|
|
2343
|
+
enableObjectAlignment,
|
|
2344
|
+
enablePanAndZoom,
|
|
2345
|
+
enableRotationSnap,
|
|
2346
|
+
enableScaledStrokes,
|
|
2347
|
+
enableVertexEdit,
|
|
2348
|
+
fitViewportToBackground,
|
|
2349
|
+
getBackgroundInverted,
|
|
2350
|
+
getBackgroundOpacity,
|
|
2351
|
+
getBaseStrokeWidth,
|
|
2352
|
+
getSnapPoints,
|
|
2353
|
+
loadCanvas,
|
|
2354
|
+
registerSnapPointExtractor,
|
|
2355
|
+
resetViewport,
|
|
2356
|
+
resizeImageUrl,
|
|
2357
|
+
serializeCanvas,
|
|
2358
|
+
setBackgroundImage,
|
|
2359
|
+
setBackgroundInverted,
|
|
2360
|
+
setBackgroundOpacity,
|
|
2361
|
+
snapCursorPoint,
|
|
2362
|
+
useEditCanvas,
|
|
2363
|
+
useViewCanvas
|
|
2364
|
+
};
|
|
2365
|
+
//# sourceMappingURL=index.js.map
|