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