@bwp-web/canvas 0.5.1 → 0.6.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/index.cjs CHANGED
@@ -35,6 +35,7 @@ __export(index_exports, {
35
35
  Rect: () => import_fabric19.Rect,
36
36
  createCircle: () => createCircle,
37
37
  createCircleAtPoint: () => createCircleAtPoint,
38
+ createHistoryTracker: () => createHistoryTracker,
38
39
  createPolygon: () => createPolygon,
39
40
  createPolygonAtPoint: () => createPolygonAtPoint,
40
41
  createPolygonFromDrag: () => createPolygonFromDrag,
@@ -115,7 +116,9 @@ function Canvas({
115
116
  height,
116
117
  className,
117
118
  style,
118
- onReady
119
+ onReady,
120
+ keyboardShortcuts,
121
+ fabricOptions
119
122
  }) {
120
123
  const canvasRef = (0, import_react.useRef)(null);
121
124
  const wrapperRef = (0, import_react.useRef)(null);
@@ -127,11 +130,12 @@ function Canvas({
127
130
  const initialWidth = isFixedSize ? width : wrapper.clientWidth || 800;
128
131
  const initialHeight = isFixedSize ? height : wrapper.clientHeight || 600;
129
132
  const fabricCanvas = new import_fabric2.Canvas(el, {
133
+ ...fabricOptions,
130
134
  width: initialWidth,
131
135
  height: initialHeight
132
136
  });
133
137
  onReady?.(fabricCanvas);
134
- const cleanupShortcuts = enableKeyboardShortcuts(fabricCanvas);
138
+ const cleanupShortcuts = keyboardShortcuts ? enableKeyboardShortcuts(fabricCanvas) : void 0;
135
139
  let observer;
136
140
  let rafId = 0;
137
141
  if (!isFixedSize) {
@@ -155,7 +159,7 @@ function Canvas({
155
159
  return () => {
156
160
  cancelAnimationFrame(rafId);
157
161
  observer?.disconnect();
158
- cleanupShortcuts();
162
+ cleanupShortcuts?.();
159
163
  fabricCanvas.dispose();
160
164
  };
161
165
  }, []);
@@ -164,7 +168,7 @@ function Canvas({
164
168
  }
165
169
 
166
170
  // src/hooks/useEditCanvas.ts
167
- var import_react2 = require("react");
171
+ var import_react3 = require("react");
168
172
  var import_fabric17 = require("fabric");
169
173
 
170
174
  // src/viewport.ts
@@ -173,7 +177,7 @@ var import_fabric3 = require("fabric");
173
177
  // src/constants.ts
174
178
  var DEFAULT_MIN_ZOOM = 0.2;
175
179
  var DEFAULT_MAX_ZOOM = 10;
176
- var DEFAULT_ZOOM_FACTOR = 1.03;
180
+ var DEFAULT_ZOOM_FACTOR = 0.999;
177
181
  var DEFAULT_ZOOM_STEP = 1.2;
178
182
  var DEFAULT_VIEWPORT_PADDING = 0.05;
179
183
  var BASE_CANVAS_SIZE = 1e3;
@@ -216,7 +220,7 @@ function setupWheelZoom(canvas, bounds, zoomFactor, isEnabled) {
216
220
  e.stopPropagation();
217
221
  const delta = e.deltaY;
218
222
  let zoom = canvas.getZoom();
219
- zoom = delta < 0 ? zoom * zoomFactor : zoom / zoomFactor;
223
+ zoom *= zoomFactor ** delta;
220
224
  zoom = Math.min(Math.max(zoom, bounds.minZoom), bounds.maxZoom);
221
225
  canvas.zoomToPoint(new import_fabric3.Point(e.offsetX, e.offsetY), zoom);
222
226
  };
@@ -324,8 +328,15 @@ function enablePanAndZoom(canvas, options) {
324
328
  const zoomFactor = options?.zoomFactor ?? DEFAULT_ZOOM_FACTOR;
325
329
  let mode = options?.initialMode ?? "select";
326
330
  let enabled = true;
331
+ let currentAnimRafId = null;
327
332
  const isEnabled = () => enabled;
328
333
  const getMode = () => mode;
334
+ function cancelAnimation() {
335
+ if (currentAnimRafId !== null) {
336
+ cancelAnimationFrame(currentAnimRafId);
337
+ currentAnimRafId = null;
338
+ }
339
+ }
329
340
  const handleWheel = setupWheelZoom(canvas, bounds, zoomFactor, isEnabled);
330
341
  const panHandlers = setupMousePan(canvas, getMode, isEnabled);
331
342
  const cleanupPinch = setupPinchZoom(canvas, bounds, isEnabled);
@@ -371,6 +382,7 @@ function enablePanAndZoom(canvas, options) {
371
382
  );
372
383
  },
373
384
  panToObject(object, panOpts) {
385
+ cancelAnimation();
374
386
  const zoom = canvas.getZoom();
375
387
  const objectCenter = object.getCenterPoint();
376
388
  const canvasCenterX = canvas.getWidth() / 2;
@@ -413,12 +425,37 @@ function enablePanAndZoom(canvas, options) {
413
425
  currentY
414
426
  ]);
415
427
  if (t < 1) {
416
- requestAnimationFrame(step);
428
+ currentAnimRafId = requestAnimationFrame(step);
429
+ } else {
430
+ currentAnimRafId = null;
417
431
  }
418
432
  }
419
- requestAnimationFrame(step);
433
+ currentAnimRafId = requestAnimationFrame(step);
434
+ },
435
+ zoomToFit(object, fitOpts) {
436
+ cancelAnimation();
437
+ const padding = fitOpts?.padding ?? 0.1;
438
+ const objWidth = (object.width ?? 0) * (object.scaleX ?? 1);
439
+ const objHeight = (object.height ?? 0) * (object.scaleY ?? 1);
440
+ if (!objWidth || !objHeight) return;
441
+ const canvasWidth = canvas.getWidth();
442
+ const canvasHeight = canvas.getHeight();
443
+ const availableWidth = canvasWidth * (1 - padding * 2);
444
+ const availableHeight = canvasHeight * (1 - padding * 2);
445
+ const zoom = Math.min(
446
+ Math.max(
447
+ Math.min(availableWidth / objWidth, availableHeight / objHeight),
448
+ bounds.minZoom
449
+ ),
450
+ bounds.maxZoom
451
+ );
452
+ const objectCenter = object.getCenterPoint();
453
+ const offsetX = canvasWidth / 2 - objectCenter.x * zoom;
454
+ const offsetY = canvasHeight / 2 - objectCenter.y * zoom;
455
+ canvas.setViewportTransform([zoom, 0, 0, zoom, offsetX, offsetY]);
420
456
  },
421
457
  cleanup() {
458
+ cancelAnimation();
422
459
  canvas.off("mouse:wheel", handleWheel);
423
460
  canvas.off("mouse:down", panHandlers.handleMouseDown);
424
461
  canvas.off("mouse:move", panHandlers.handleMouseMove);
@@ -431,6 +468,9 @@ function resetViewport(canvas) {
431
468
  canvas.setViewportTransform([1, 0, 0, 1, 0, 0]);
432
469
  }
433
470
 
471
+ // src/hooks/shared.ts
472
+ var import_react2 = require("react");
473
+
434
474
  // src/background.ts
435
475
  var import_fabric4 = require("fabric");
436
476
  function getBackgroundImage(canvas) {
@@ -561,7 +601,8 @@ function resizeImageUrl(url, options) {
561
601
  async function setBackgroundImage(canvas, url, options) {
562
602
  const prevContrast = options?.preserveContrast ? getBackgroundContrast(canvas) : void 0;
563
603
  let imageUrl = url;
564
- if (options !== void 0) {
604
+ const hasResizeOptions = options?.maxSize !== void 0 || options?.minSize !== void 0;
605
+ if (hasResizeOptions) {
565
606
  const result = await resizeImageUrl(url, options);
566
607
  imageUrl = result.url;
567
608
  }
@@ -579,29 +620,35 @@ function syncZoom(canvasRef, setZoom) {
579
620
  const canvas = canvasRef.current;
580
621
  if (canvas) setZoom(canvas.getZoom());
581
622
  }
582
- function createViewportActions(canvasRef, viewportRef, setZoom) {
583
- const resetViewport2 = () => {
584
- const canvas = canvasRef.current;
585
- if (!canvas) return;
586
- if (canvas.backgroundImage) {
587
- fitViewportToBackground(canvas);
588
- } else {
589
- resetViewport(canvas);
590
- }
591
- setZoom(canvas.getZoom());
592
- };
593
- const zoomIn = (step) => {
594
- viewportRef.current?.zoomIn(step);
595
- syncZoom(canvasRef, setZoom);
596
- };
597
- const zoomOut = (step) => {
598
- viewportRef.current?.zoomOut(step);
599
- syncZoom(canvasRef, setZoom);
600
- };
601
- const panToObject = (object, panOpts) => {
602
- viewportRef.current?.panToObject(object, panOpts);
603
- };
604
- return { resetViewport: resetViewport2, zoomIn, zoomOut, panToObject };
623
+ function useViewportActions(canvasRef, viewportRef, setZoom) {
624
+ return (0, import_react2.useMemo)(() => {
625
+ const resetViewport2 = () => {
626
+ const canvas = canvasRef.current;
627
+ if (!canvas) return;
628
+ if (canvas.backgroundImage) {
629
+ fitViewportToBackground(canvas);
630
+ } else {
631
+ resetViewport(canvas);
632
+ }
633
+ setZoom(canvas.getZoom());
634
+ };
635
+ const zoomIn = (step) => {
636
+ viewportRef.current?.zoomIn(step);
637
+ syncZoom(canvasRef, setZoom);
638
+ };
639
+ const zoomOut = (step) => {
640
+ viewportRef.current?.zoomOut(step);
641
+ syncZoom(canvasRef, setZoom);
642
+ };
643
+ const panToObject = (object, panOpts) => {
644
+ viewportRef.current?.panToObject(object, panOpts);
645
+ };
646
+ const zoomToFit = (object, fitOpts) => {
647
+ viewportRef.current?.zoomToFit(object, fitOpts);
648
+ syncZoom(canvasRef, setZoom);
649
+ };
650
+ return { resetViewport: resetViewport2, zoomIn, zoomOut, panToObject, zoomToFit };
651
+ }, []);
605
652
  }
606
653
  function resolveAlignmentEnabled(enableAlignment, alignmentProp) {
607
654
  if (enableAlignment !== void 0) return enableAlignment;
@@ -1660,7 +1707,7 @@ function createPolygonAtPoint(canvas, point, options) {
1660
1707
  canvas.requestRenderAll();
1661
1708
  return polygon;
1662
1709
  }
1663
- function createPolygonFromDrag(canvas, start, end, style) {
1710
+ function createPolygonFromDrag(canvas, start, end, options) {
1664
1711
  const width = Math.abs(end.x - start.x);
1665
1712
  const height = Math.abs(end.y - start.y);
1666
1713
  const left = Math.min(start.x, end.x) + width / 2;
@@ -1672,16 +1719,16 @@ function createPolygonFromDrag(canvas, start, end, style) {
1672
1719
  { x: width, y: height },
1673
1720
  { x: 0, y: height }
1674
1721
  ],
1675
- { ...DEFAULT_SHAPE_STYLE, left, top, ...style }
1722
+ { ...DEFAULT_SHAPE_STYLE, left, top, ...options }
1676
1723
  );
1677
1724
  canvas.add(polygon);
1678
1725
  canvas.requestRenderAll();
1679
1726
  return polygon;
1680
1727
  }
1681
- function createPolygonFromVertices(canvas, points, style) {
1728
+ function createPolygonFromVertices(canvas, points, options) {
1682
1729
  const polygon = new import_fabric13.Polygon(
1683
1730
  points.map((p) => ({ x: p.x, y: p.y })),
1684
- { ...DEFAULT_SHAPE_STYLE, ...style }
1731
+ { ...DEFAULT_SHAPE_STYLE, ...options }
1685
1732
  );
1686
1733
  canvas.add(polygon);
1687
1734
  canvas.requestRenderAll();
@@ -1805,14 +1852,14 @@ function enableDrawToCreate(canvas, options) {
1805
1852
  const finalize = () => {
1806
1853
  removePreviewElements();
1807
1854
  snapping.clearSnapResult();
1808
- const polygon = createPolygonFromVertices(canvas, points, options?.style);
1855
+ const obj = options?.factory ? options.factory(canvas, [...points]) : createPolygonFromVertices(canvas, points, options?.style);
1809
1856
  if (options?.data) {
1810
- polygon.data = options.data;
1857
+ obj.data = options.data;
1811
1858
  }
1812
1859
  canvas.selection = previousSelection;
1813
1860
  canvas.requestRenderAll();
1814
1861
  restoreViewport(options?.viewport);
1815
- options?.onCreated?.(polygon);
1862
+ options?.onCreated?.(obj);
1816
1863
  points.length = 0;
1817
1864
  };
1818
1865
  const handleMouseDown = (event) => {
@@ -2121,7 +2168,7 @@ function enableVertexEdit(canvas, polygon, options, onExit) {
2121
2168
  });
2122
2169
  canvas.discardActiveObject();
2123
2170
  canvas.requestRenderAll();
2124
- onExit?.();
2171
+ (options?.onExit ?? onExit)?.();
2125
2172
  }
2126
2173
  return cleanup;
2127
2174
  }
@@ -2130,7 +2177,7 @@ function enableVertexEdit(canvas, polygon, options, onExit) {
2130
2177
  var import_fabric16 = require("fabric");
2131
2178
  var strokeBaseMap = /* @__PURE__ */ new WeakMap();
2132
2179
  var borderRadiusBaseMap = /* @__PURE__ */ new WeakMap();
2133
- var VIEW_BORDER_RADIUS = 4;
2180
+ var DEFAULT_VIEW_BORDER_RADIUS = 4;
2134
2181
  function enableScaledStrokes(canvas) {
2135
2182
  function applyScaledStrokes() {
2136
2183
  const zoom = canvas.getZoom();
@@ -2155,13 +2202,14 @@ function enableScaledStrokes(canvas) {
2155
2202
  });
2156
2203
  };
2157
2204
  }
2158
- function enableScaledBorderRadius(canvas) {
2205
+ function enableScaledBorderRadius(canvas, options) {
2206
+ const radius = options?.radius ?? DEFAULT_VIEW_BORDER_RADIUS;
2159
2207
  function applyScaledBorderRadius() {
2160
2208
  canvas.forEachObject((obj) => {
2161
2209
  if (!(obj instanceof import_fabric16.Rect)) return;
2162
2210
  if (!borderRadiusBaseMap.has(obj)) return;
2163
- const rx = VIEW_BORDER_RADIUS / (obj.scaleX ?? 1);
2164
- const ry = VIEW_BORDER_RADIUS / (obj.scaleY ?? 1);
2211
+ const rx = radius / (obj.scaleX ?? 1);
2212
+ const ry = radius / (obj.scaleY ?? 1);
2165
2213
  obj.set({ rx, ry });
2166
2214
  });
2167
2215
  }
@@ -2224,6 +2272,20 @@ function serializeCanvas(canvas, options) {
2224
2272
  async function loadCanvas(canvas, json, options) {
2225
2273
  await canvas.loadFromJSON(json);
2226
2274
  canvas.backgroundColor = "";
2275
+ delete canvas.backgroundFilters;
2276
+ const bg = canvas.backgroundImage;
2277
+ if (bg instanceof import_fabric16.FabricImage) {
2278
+ if (bg.originX !== "center" || bg.originY !== "center") {
2279
+ const center = bg.getCenterPoint();
2280
+ bg.set({
2281
+ originX: "center",
2282
+ originY: "center",
2283
+ left: center.x,
2284
+ top: center.y
2285
+ });
2286
+ bg.setCoords();
2287
+ }
2288
+ }
2227
2289
  if (options?.filter) {
2228
2290
  const toRemove = [];
2229
2291
  canvas.forEachObject((obj) => {
@@ -2243,14 +2305,19 @@ async function loadCanvas(canvas, json, options) {
2243
2305
  obj.setCoords();
2244
2306
  });
2245
2307
  canvas.forEachObject((obj) => {
2308
+ const data = obj.data;
2309
+ if (data?.strokeWidthBase !== void 0) {
2310
+ delete data.strokeWidthBase;
2311
+ }
2246
2312
  obj.set(DEFAULT_CONTROL_STYLE);
2247
2313
  if (obj.shapeType === "circle" && obj instanceof import_fabric16.Rect) {
2248
2314
  restoreCircleConstraints(obj);
2249
2315
  }
2250
- if (obj instanceof import_fabric16.Rect && obj.shapeType !== "circle" && obj.data?.type !== "DEVICE") {
2316
+ const borderRadius = options?.borderRadius ?? DEFAULT_VIEW_BORDER_RADIUS;
2317
+ if (borderRadius !== false && obj instanceof import_fabric16.Rect && obj.shapeType !== "circle" && obj.data?.type !== "DEVICE") {
2251
2318
  borderRadiusBaseMap.set(obj, { rx: obj.rx ?? 0, ry: obj.ry ?? 0 });
2252
- const rx = VIEW_BORDER_RADIUS / (obj.scaleX ?? 1);
2253
- const ry = VIEW_BORDER_RADIUS / (obj.scaleY ?? 1);
2319
+ const rx = borderRadius / (obj.scaleX ?? 1);
2320
+ const ry = borderRadius / (obj.scaleY ?? 1);
2254
2321
  obj.set({ rx, ry });
2255
2322
  }
2256
2323
  });
@@ -2258,21 +2325,110 @@ async function loadCanvas(canvas, json, options) {
2258
2325
  return canvas.getObjects();
2259
2326
  }
2260
2327
 
2328
+ // src/history.ts
2329
+ function createHistoryTracker(canvas, options) {
2330
+ const maxSize = options?.maxSize ?? 50;
2331
+ const debounceMs = options?.debounce ?? 300;
2332
+ const snapshots = [];
2333
+ let currentIndex = -1;
2334
+ let isUndoRedo = false;
2335
+ let debounceTimer = null;
2336
+ function captureSnapshot() {
2337
+ if (isUndoRedo) return;
2338
+ const snapshot = serializeCanvas(canvas);
2339
+ if (currentIndex < snapshots.length - 1) {
2340
+ snapshots.length = currentIndex + 1;
2341
+ }
2342
+ snapshots.push(snapshot);
2343
+ if (snapshots.length > maxSize) {
2344
+ snapshots.shift();
2345
+ }
2346
+ currentIndex = snapshots.length - 1;
2347
+ }
2348
+ function debouncedCapture() {
2349
+ if (debounceTimer !== null) {
2350
+ clearTimeout(debounceTimer);
2351
+ }
2352
+ debounceTimer = setTimeout(() => {
2353
+ debounceTimer = null;
2354
+ captureSnapshot();
2355
+ }, debounceMs);
2356
+ }
2357
+ const onChange = () => {
2358
+ if (!isUndoRedo) debouncedCapture();
2359
+ };
2360
+ canvas.on("object:added", onChange);
2361
+ canvas.on("object:modified", onChange);
2362
+ canvas.on("object:removed", onChange);
2363
+ async function loadSnapshot(index) {
2364
+ if (index < 0 || index >= snapshots.length) return;
2365
+ isUndoRedo = true;
2366
+ currentIndex = index;
2367
+ try {
2368
+ await loadCanvas(canvas, snapshots[index]);
2369
+ } finally {
2370
+ isUndoRedo = false;
2371
+ }
2372
+ }
2373
+ return {
2374
+ async undo() {
2375
+ if (currentIndex <= 0) return;
2376
+ await loadSnapshot(currentIndex - 1);
2377
+ },
2378
+ async redo() {
2379
+ if (currentIndex >= snapshots.length - 1) return;
2380
+ await loadSnapshot(currentIndex + 1);
2381
+ },
2382
+ canUndo() {
2383
+ return currentIndex > 0;
2384
+ },
2385
+ canRedo() {
2386
+ return currentIndex < snapshots.length - 1;
2387
+ },
2388
+ pushSnapshot() {
2389
+ if (debounceTimer !== null) {
2390
+ clearTimeout(debounceTimer);
2391
+ debounceTimer = null;
2392
+ }
2393
+ captureSnapshot();
2394
+ },
2395
+ cleanup() {
2396
+ if (debounceTimer !== null) {
2397
+ clearTimeout(debounceTimer);
2398
+ debounceTimer = null;
2399
+ }
2400
+ canvas.off("object:added", onChange);
2401
+ canvas.off("object:modified", onChange);
2402
+ canvas.off("object:removed", onChange);
2403
+ snapshots.length = 0;
2404
+ currentIndex = -1;
2405
+ }
2406
+ };
2407
+ }
2408
+
2261
2409
  // src/hooks/useEditCanvas.ts
2262
2410
  function useEditCanvas(options) {
2263
- const canvasRef = (0, import_react2.useRef)(null);
2264
- const viewportRef = (0, import_react2.useRef)(null);
2265
- const alignmentCleanupRef = (0, import_react2.useRef)(null);
2266
- const rotationSnapCleanupRef = (0, import_react2.useRef)(null);
2267
- const modeCleanupRef = (0, import_react2.useRef)(null);
2268
- const vertexEditCleanupRef = (0, import_react2.useRef)(null);
2269
- const keyboardCleanupRef = (0, import_react2.useRef)(null);
2270
- const [zoom, setZoom] = (0, import_react2.useState)(1);
2271
- const [selected, setSelected] = (0, import_react2.useState)([]);
2272
- const [viewportMode, setViewportModeState] = (0, import_react2.useState)("select");
2273
- const [isEditingVertices, setIsEditingVertices] = (0, import_react2.useState)(false);
2274
- const [isDirty, setIsDirty] = (0, import_react2.useState)(false);
2275
- const setMode = (0, import_react2.useCallback)((setup) => {
2411
+ const canvasRef = (0, import_react3.useRef)(null);
2412
+ const viewportRef = (0, import_react3.useRef)(null);
2413
+ const alignmentCleanupRef = (0, import_react3.useRef)(null);
2414
+ const rotationSnapCleanupRef = (0, import_react3.useRef)(null);
2415
+ const modeCleanupRef = (0, import_react3.useRef)(null);
2416
+ const vertexEditCleanupRef = (0, import_react3.useRef)(null);
2417
+ const keyboardCleanupRef = (0, import_react3.useRef)(null);
2418
+ const historyRef = (0, import_react3.useRef)(null);
2419
+ const optionsRef = (0, import_react3.useRef)(options);
2420
+ optionsRef.current = options;
2421
+ const savedSelectabilityRef = (0, import_react3.useRef)(
2422
+ /* @__PURE__ */ new WeakMap()
2423
+ );
2424
+ const [zoom, setZoom] = (0, import_react3.useState)(1);
2425
+ const [selected, setSelected] = (0, import_react3.useState)([]);
2426
+ const [viewportMode, setViewportModeState] = (0, import_react3.useState)("select");
2427
+ const [isEditingVertices, setIsEditingVertices] = (0, import_react3.useState)(false);
2428
+ const [isDirty, setIsDirty] = (0, import_react3.useState)(false);
2429
+ const [canUndo, setCanUndo] = (0, import_react3.useState)(false);
2430
+ const [canRedo, setCanRedo] = (0, import_react3.useState)(false);
2431
+ const setMode = (0, import_react3.useCallback)((setup) => {
2276
2432
  vertexEditCleanupRef.current?.();
2277
2433
  vertexEditCleanupRef.current = null;
2278
2434
  setIsEditingVertices(false);
@@ -2283,10 +2439,17 @@ function useEditCanvas(options) {
2283
2439
  if (setup === null) {
2284
2440
  canvas.selection = true;
2285
2441
  canvas.forEachObject((obj) => {
2286
- obj.selectable = true;
2287
- obj.evented = true;
2442
+ const saved = savedSelectabilityRef.current.get(obj);
2443
+ obj.selectable = saved?.selectable ?? true;
2444
+ obj.evented = saved?.evented ?? true;
2288
2445
  });
2289
2446
  } else {
2447
+ canvas.forEachObject((obj) => {
2448
+ savedSelectabilityRef.current.set(obj, {
2449
+ selectable: obj.selectable,
2450
+ evented: obj.evented
2451
+ });
2452
+ });
2290
2453
  canvas.selection = false;
2291
2454
  canvas.forEachObject((obj) => {
2292
2455
  obj.selectable = false;
@@ -2298,37 +2461,41 @@ function useEditCanvas(options) {
2298
2461
  }
2299
2462
  }
2300
2463
  }, []);
2301
- const onReady = (0, import_react2.useCallback)(
2464
+ const onReady = (0, import_react3.useCallback)(
2302
2465
  (canvas) => {
2303
2466
  canvasRef.current = canvas;
2304
- if (options?.scaledStrokes !== false) {
2467
+ const opts = optionsRef.current;
2468
+ if (opts?.scaledStrokes !== false) {
2305
2469
  enableScaledStrokes(canvas);
2306
2470
  }
2307
- enableScaledBorderRadius(canvas);
2308
- if (options?.keyboardShortcuts !== false) {
2471
+ if (opts?.borderRadius !== false) {
2472
+ const borderRadiusOpts = typeof opts?.borderRadius === "number" ? { radius: opts.borderRadius } : void 0;
2473
+ enableScaledBorderRadius(canvas, borderRadiusOpts);
2474
+ }
2475
+ if (opts?.keyboardShortcuts !== false) {
2309
2476
  keyboardCleanupRef.current = enableKeyboardShortcuts(canvas);
2310
2477
  }
2311
- setCanvasAlignmentEnabled(canvas, options?.enableAlignment);
2312
- if (options?.panAndZoom !== false) {
2478
+ setCanvasAlignmentEnabled(canvas, opts?.enableAlignment);
2479
+ if (opts?.panAndZoom !== false) {
2313
2480
  viewportRef.current = enablePanAndZoom(
2314
2481
  canvas,
2315
- typeof options?.panAndZoom === "object" ? options.panAndZoom : void 0
2482
+ typeof opts?.panAndZoom === "object" ? opts.panAndZoom : void 0
2316
2483
  );
2317
2484
  }
2318
2485
  const alignmentEnabled = resolveAlignmentEnabled(
2319
- options?.enableAlignment,
2320
- options?.alignment
2486
+ opts?.enableAlignment,
2487
+ opts?.alignment
2321
2488
  );
2322
2489
  if (alignmentEnabled) {
2323
2490
  alignmentCleanupRef.current = enableObjectAlignment(
2324
2491
  canvas,
2325
- typeof options?.alignment === "object" ? options.alignment : void 0
2492
+ typeof opts?.alignment === "object" ? opts.alignment : void 0
2326
2493
  );
2327
2494
  }
2328
- if (options?.rotationSnap !== false) {
2495
+ if (opts?.rotationSnap !== false) {
2329
2496
  rotationSnapCleanupRef.current = enableRotationSnap(
2330
2497
  canvas,
2331
- typeof options?.rotationSnap === "object" ? options.rotationSnap : void 0
2498
+ typeof opts?.rotationSnap === "object" ? opts.rotationSnap : void 0
2332
2499
  );
2333
2500
  }
2334
2501
  canvas.on("mouse:wheel", () => {
@@ -2343,44 +2510,63 @@ function useEditCanvas(options) {
2343
2510
  canvas.on("selection:cleared", () => {
2344
2511
  setSelected([]);
2345
2512
  });
2346
- if (options?.trackChanges) {
2513
+ if (opts?.trackChanges) {
2347
2514
  canvas.on("object:added", () => setIsDirty(true));
2348
2515
  canvas.on("object:removed", () => setIsDirty(true));
2349
2516
  canvas.on("object:modified", () => setIsDirty(true));
2350
2517
  }
2351
- if (options?.vertexEdit !== false) {
2352
- const vertexOpts = typeof options?.vertexEdit === "object" ? options.vertexEdit : void 0;
2518
+ if (opts?.history) {
2519
+ const syncHistoryState = () => {
2520
+ const h = historyRef.current;
2521
+ if (!h) return;
2522
+ setTimeout(() => {
2523
+ setCanUndo(h.canUndo());
2524
+ setCanRedo(h.canRedo());
2525
+ }, 350);
2526
+ };
2527
+ canvas.on("object:added", syncHistoryState);
2528
+ canvas.on("object:removed", syncHistoryState);
2529
+ canvas.on("object:modified", syncHistoryState);
2530
+ }
2531
+ if (opts?.vertexEdit !== false) {
2532
+ const vertexOpts = typeof opts?.vertexEdit === "object" ? opts.vertexEdit : void 0;
2353
2533
  canvas.on("mouse:dblclick", (e) => {
2354
2534
  if (e.target && e.target instanceof import_fabric17.Polygon) {
2355
2535
  vertexEditCleanupRef.current?.();
2356
- vertexEditCleanupRef.current = enableVertexEdit(
2357
- canvas,
2358
- e.target,
2359
- vertexOpts,
2360
- () => {
2536
+ vertexEditCleanupRef.current = enableVertexEdit(canvas, e.target, {
2537
+ ...vertexOpts,
2538
+ onExit: () => {
2361
2539
  vertexEditCleanupRef.current = null;
2362
2540
  setIsEditingVertices(false);
2363
2541
  }
2364
- );
2542
+ });
2365
2543
  setIsEditingVertices(true);
2366
2544
  }
2367
2545
  });
2368
2546
  }
2369
- const onReadyResult = options?.onReady?.(canvas);
2370
- if (options?.autoFitToBackground !== false) {
2547
+ if (opts?.history) {
2548
+ const historyOpts = typeof opts.history === "object" ? opts.history : void 0;
2549
+ historyRef.current = createHistoryTracker(canvas, historyOpts);
2550
+ }
2551
+ const onReadyResult = opts?.onReady?.(canvas);
2552
+ if (opts?.autoFitToBackground !== false) {
2371
2553
  Promise.resolve(onReadyResult).then(() => {
2372
2554
  if (canvas.backgroundImage) {
2373
2555
  fitViewportToBackground(canvas);
2374
2556
  syncZoom(canvasRef, setZoom);
2375
2557
  }
2558
+ historyRef.current?.pushSnapshot();
2559
+ });
2560
+ } else {
2561
+ Promise.resolve(onReadyResult).then(() => {
2562
+ historyRef.current?.pushSnapshot();
2376
2563
  });
2377
2564
  }
2378
2565
  },
2379
2566
  // onReady and panAndZoom are intentionally excluded — we only initialize once
2380
- // eslint-disable-next-line react-hooks/exhaustive-deps
2381
2567
  []
2382
2568
  );
2383
- (0, import_react2.useEffect)(() => {
2569
+ (0, import_react3.useEffect)(() => {
2384
2570
  const canvas = canvasRef.current;
2385
2571
  if (!canvas) return;
2386
2572
  setCanvasAlignmentEnabled(canvas, options?.enableAlignment);
@@ -2398,28 +2584,24 @@ function useEditCanvas(options) {
2398
2584
  alignmentCleanupRef.current = null;
2399
2585
  }
2400
2586
  }, [options?.enableAlignment]);
2401
- const setViewportMode = (0, import_react2.useCallback)((mode) => {
2587
+ const setViewportMode = (0, import_react3.useCallback)((mode) => {
2402
2588
  viewportRef.current?.setMode(mode);
2403
2589
  setViewportModeState(mode);
2404
2590
  }, []);
2405
- const { resetViewport: resetViewport2, zoomIn, zoomOut, panToObject } = createViewportActions(
2406
- canvasRef,
2407
- viewportRef,
2408
- setZoom
2409
- );
2410
- const setBackground = (0, import_react2.useCallback)(
2591
+ const { resetViewport: resetViewport2, zoomIn, zoomOut, panToObject, zoomToFit } = useViewportActions(canvasRef, viewportRef, setZoom);
2592
+ const setBackground = (0, import_react3.useCallback)(
2411
2593
  async (url, bgOpts) => {
2412
2594
  const canvas = canvasRef.current;
2413
2595
  if (!canvas) throw new Error("Canvas not ready");
2414
- const resizeOpts = options?.backgroundResize !== false ? typeof options?.backgroundResize === "object" ? { ...options.backgroundResize, ...bgOpts } : { ...bgOpts } : bgOpts?.preserveContrast ? { preserveContrast: true } : void 0;
2596
+ const opts = optionsRef.current;
2597
+ const resizeOpts = opts?.backgroundResize !== false ? typeof opts?.backgroundResize === "object" ? { ...opts.backgroundResize, ...bgOpts } : { ...bgOpts } : bgOpts?.preserveContrast ? { preserveContrast: true } : void 0;
2415
2598
  const img = await setBackgroundImage(canvas, url, resizeOpts);
2416
- if (options?.autoFitToBackground !== false) {
2599
+ if (opts?.autoFitToBackground !== false) {
2417
2600
  fitViewportToBackground(canvas);
2418
2601
  syncZoom(canvasRef, setZoom);
2419
2602
  }
2420
2603
  return img;
2421
2604
  },
2422
- // eslint-disable-next-line react-hooks/exhaustive-deps
2423
2605
  []
2424
2606
  );
2425
2607
  return {
@@ -2444,7 +2626,9 @@ function useEditCanvas(options) {
2444
2626
  /** Zoom out from the canvas center. Default step: 0.2. */
2445
2627
  zoomOut,
2446
2628
  /** Pan the viewport to center on a specific object. */
2447
- panToObject
2629
+ panToObject,
2630
+ /** Zoom and pan to fit a specific object in the viewport. */
2631
+ zoomToFit
2448
2632
  },
2449
2633
  /** Whether vertex edit mode is currently active (reactive). */
2450
2634
  isEditingVertices,
@@ -2476,12 +2660,32 @@ function useEditCanvas(options) {
2476
2660
  /** Whether the canvas has been modified since the last `resetDirty()` call. Requires `trackChanges: true`. */
2477
2661
  isDirty,
2478
2662
  /** Reset the dirty flag (e.g., after a successful save). */
2479
- resetDirty: (0, import_react2.useCallback)(() => setIsDirty(false), [])
2663
+ resetDirty: (0, import_react3.useCallback)(() => setIsDirty(false), []),
2664
+ /** Undo the last change. Requires `history: true`. */
2665
+ undo: (0, import_react3.useCallback)(async () => {
2666
+ const h = historyRef.current;
2667
+ if (!h) return;
2668
+ await h.undo();
2669
+ setCanUndo(h.canUndo());
2670
+ setCanRedo(h.canRedo());
2671
+ }, []),
2672
+ /** Redo a previously undone change. Requires `history: true`. */
2673
+ redo: (0, import_react3.useCallback)(async () => {
2674
+ const h = historyRef.current;
2675
+ if (!h) return;
2676
+ await h.redo();
2677
+ setCanUndo(h.canUndo());
2678
+ setCanRedo(h.canRedo());
2679
+ }, []),
2680
+ /** Whether an undo operation is available (reactive). Requires `history: true`. */
2681
+ canUndo,
2682
+ /** Whether a redo operation is available (reactive). Requires `history: true`. */
2683
+ canRedo
2480
2684
  };
2481
2685
  }
2482
2686
 
2483
2687
  // src/hooks/useViewCanvas.ts
2484
- var import_react3 = require("react");
2688
+ var import_react4 = require("react");
2485
2689
  function lockCanvas(canvas) {
2486
2690
  canvas.selection = false;
2487
2691
  canvas.forEachObject((obj) => {
@@ -2489,18 +2693,24 @@ function lockCanvas(canvas) {
2489
2693
  });
2490
2694
  }
2491
2695
  function useViewCanvas(options) {
2492
- const canvasRef = (0, import_react3.useRef)(null);
2493
- const viewportRef = (0, import_react3.useRef)(null);
2494
- const [zoom, setZoom] = (0, import_react3.useState)(1);
2495
- const onReady = (0, import_react3.useCallback)(
2696
+ const canvasRef = (0, import_react4.useRef)(null);
2697
+ const viewportRef = (0, import_react4.useRef)(null);
2698
+ const optionsRef = (0, import_react4.useRef)(options);
2699
+ optionsRef.current = options;
2700
+ const [zoom, setZoom] = (0, import_react4.useState)(1);
2701
+ const onReady = (0, import_react4.useCallback)(
2496
2702
  (canvas) => {
2497
2703
  canvasRef.current = canvas;
2498
- if (options?.scaledStrokes !== false) {
2704
+ const opts = optionsRef.current;
2705
+ if (opts?.scaledStrokes !== false) {
2499
2706
  enableScaledStrokes(canvas);
2500
2707
  }
2501
- enableScaledBorderRadius(canvas);
2502
- if (options?.panAndZoom !== false) {
2503
- const panAndZoomOpts = typeof options?.panAndZoom === "object" ? options.panAndZoom : {};
2708
+ if (opts?.borderRadius !== false) {
2709
+ const borderRadiusOpts = typeof opts?.borderRadius === "number" ? { radius: opts.borderRadius } : void 0;
2710
+ enableScaledBorderRadius(canvas, borderRadiusOpts);
2711
+ }
2712
+ if (opts?.panAndZoom !== false) {
2713
+ const panAndZoomOpts = typeof opts?.panAndZoom === "object" ? opts.panAndZoom : {};
2504
2714
  viewportRef.current = enablePanAndZoom(canvas, {
2505
2715
  ...panAndZoomOpts,
2506
2716
  initialMode: "pan"
@@ -2513,8 +2723,8 @@ function useViewCanvas(options) {
2513
2723
  canvas.on("mouse:wheel", () => {
2514
2724
  setZoom(canvas.getZoom());
2515
2725
  });
2516
- const onReadyResult = options?.onReady?.(canvas);
2517
- if (options?.autoFitToBackground !== false) {
2726
+ const onReadyResult = opts?.onReady?.(canvas);
2727
+ if (opts?.autoFitToBackground !== false) {
2518
2728
  Promise.resolve(onReadyResult).then(() => {
2519
2729
  if (canvas.backgroundImage) {
2520
2730
  fitViewportToBackground(canvas);
@@ -2524,26 +2734,21 @@ function useViewCanvas(options) {
2524
2734
  }
2525
2735
  },
2526
2736
  // onReady and panAndZoom are intentionally excluded — we only initialize once
2527
- // eslint-disable-next-line react-hooks/exhaustive-deps
2528
2737
  []
2529
2738
  );
2530
- const { resetViewport: resetViewport2, zoomIn, zoomOut, panToObject } = createViewportActions(
2531
- canvasRef,
2532
- viewportRef,
2533
- setZoom
2534
- );
2739
+ const { resetViewport: resetViewport2, zoomIn, zoomOut, panToObject, zoomToFit } = useViewportActions(canvasRef, viewportRef, setZoom);
2535
2740
  const findObject = (id) => {
2536
2741
  const c = canvasRef.current;
2537
2742
  if (!c) return void 0;
2538
2743
  return c.getObjects().find((o) => o.data?.id === id);
2539
2744
  };
2540
- const setObjectStyle = (0, import_react3.useCallback)((id, style) => {
2745
+ const setObjectStyle = (0, import_react4.useCallback)((id, style) => {
2541
2746
  const obj = findObject(id);
2542
2747
  if (!obj) return;
2543
2748
  obj.set(style);
2544
2749
  canvasRef.current.requestRenderAll();
2545
2750
  }, []);
2546
- const setObjectStyles = (0, import_react3.useCallback)(
2751
+ const setObjectStyles = (0, import_react4.useCallback)(
2547
2752
  (styles) => {
2548
2753
  const c = canvasRef.current;
2549
2754
  if (!c) return;
@@ -2564,7 +2769,7 @@ function useViewCanvas(options) {
2564
2769
  },
2565
2770
  []
2566
2771
  );
2567
- const setObjectStyleByType = (0, import_react3.useCallback)(
2772
+ const setObjectStyleByType = (0, import_react4.useCallback)(
2568
2773
  (type, style) => {
2569
2774
  const c = canvasRef.current;
2570
2775
  if (!c) return;
@@ -2595,7 +2800,9 @@ function useViewCanvas(options) {
2595
2800
  /** Zoom out from the canvas center. Default step: 0.2. */
2596
2801
  zoomOut,
2597
2802
  /** Pan the viewport to center on a specific object. */
2598
- panToObject
2803
+ panToObject,
2804
+ /** Zoom and pan to fit a specific object in the viewport. */
2805
+ zoomToFit
2599
2806
  },
2600
2807
  /** Update a single object's visual style by its `data.id`. */
2601
2808
  setObjectStyle,
@@ -2607,11 +2814,11 @@ function useViewCanvas(options) {
2607
2814
  }
2608
2815
 
2609
2816
  // src/hooks/useCanvasEvents.ts
2610
- var import_react4 = require("react");
2817
+ var import_react5 = require("react");
2611
2818
  function useCanvasEvents(canvasRef, events) {
2612
- const eventsRef = (0, import_react4.useRef)(events);
2819
+ const eventsRef = (0, import_react5.useRef)(events);
2613
2820
  eventsRef.current = events;
2614
- (0, import_react4.useEffect)(() => {
2821
+ (0, import_react5.useEffect)(() => {
2615
2822
  const canvas = canvasRef.current;
2616
2823
  if (!canvas) return;
2617
2824
  const wrappers = /* @__PURE__ */ new Map();
@@ -2632,17 +2839,17 @@ function useCanvasEvents(canvasRef, events) {
2632
2839
  }
2633
2840
 
2634
2841
  // src/hooks/useCanvasTooltip.ts
2635
- var import_react5 = require("react");
2842
+ var import_react6 = require("react");
2636
2843
  function useCanvasTooltip(canvasRef, options) {
2637
- const [state, setState] = (0, import_react5.useState)({
2844
+ const [state, setState] = (0, import_react6.useState)({
2638
2845
  visible: false,
2639
2846
  content: null,
2640
2847
  position: { x: 0, y: 0 }
2641
2848
  });
2642
- const hoveredObjectRef = (0, import_react5.useRef)(null);
2643
- const optionsRef = (0, import_react5.useRef)(options);
2849
+ const hoveredObjectRef = (0, import_react6.useRef)(null);
2850
+ const optionsRef = (0, import_react6.useRef)(options);
2644
2851
  optionsRef.current = options;
2645
- (0, import_react5.useEffect)(() => {
2852
+ (0, import_react6.useEffect)(() => {
2646
2853
  const canvas = canvasRef.current;
2647
2854
  if (!canvas) return;
2648
2855
  function calculatePosition(target) {
@@ -2696,13 +2903,13 @@ function useCanvasTooltip(canvasRef, options) {
2696
2903
  }
2697
2904
 
2698
2905
  // src/hooks/useCanvasClick.ts
2699
- var import_react6 = require("react");
2906
+ var import_react7 = require("react");
2700
2907
  function useCanvasClick(canvasRef, onClick, options) {
2701
- const onClickRef = (0, import_react6.useRef)(onClick);
2908
+ const onClickRef = (0, import_react7.useRef)(onClick);
2702
2909
  onClickRef.current = onClick;
2703
- const optionsRef = (0, import_react6.useRef)(options);
2910
+ const optionsRef = (0, import_react7.useRef)(options);
2704
2911
  optionsRef.current = options;
2705
- (0, import_react6.useEffect)(() => {
2912
+ (0, import_react7.useEffect)(() => {
2706
2913
  const canvas = canvasRef.current;
2707
2914
  if (!canvas) return;
2708
2915
  let mouseDown = null;
@@ -2747,13 +2954,13 @@ function useCanvasClick(canvasRef, onClick, options) {
2747
2954
  }
2748
2955
 
2749
2956
  // src/hooks/useObjectOverlay.ts
2750
- var import_react7 = require("react");
2957
+ var import_react8 = require("react");
2751
2958
  var import_fabric18 = require("fabric");
2752
2959
  function useObjectOverlay(canvasRef, object, options) {
2753
- const containerRef = (0, import_react7.useRef)(null);
2754
- const optionsRef = (0, import_react7.useRef)(options);
2960
+ const containerRef = (0, import_react8.useRef)(null);
2961
+ const optionsRef = (0, import_react8.useRef)(options);
2755
2962
  optionsRef.current = options;
2756
- (0, import_react7.useEffect)(() => {
2963
+ (0, import_react8.useEffect)(() => {
2757
2964
  const canvas = canvasRef.current;
2758
2965
  if (!canvas || !object) return;
2759
2966
  function update() {
@@ -2768,36 +2975,41 @@ function useObjectOverlay(canvasRef, object, options) {
2768
2975
  const screenCoords = import_fabric18.util.transformPoint(center, vt);
2769
2976
  const screenWidth = actualWidth * zoom;
2770
2977
  const screenHeight = actualHeight * zoom;
2771
- el.style.left = `${screenCoords.x - screenWidth / 2}px`;
2772
- el.style.top = `${screenCoords.y - screenHeight / 2}px`;
2773
- el.style.width = `${screenWidth}px`;
2774
- el.style.height = `${screenHeight}px`;
2775
2978
  const angle = object.angle ?? 0;
2776
- el.style.rotate = angle !== 0 ? `${angle}deg` : "";
2777
2979
  const opts = optionsRef.current;
2778
- if (opts?.autoScaleContent) {
2779
- const contentScale = Math.min(screenWidth, screenHeight) / 100;
2780
- const clampedScale = Math.max(0.1, Math.min(contentScale, 2));
2781
- el.style.setProperty("--overlay-scale", String(clampedScale));
2782
- if (opts.textSelector) {
2783
- const textMinScale = opts.textMinScale ?? 0.5;
2980
+ if (opts?.autoScaleContent !== false) {
2981
+ el.style.left = `${screenCoords.x - actualWidth / 2}px`;
2982
+ el.style.top = `${screenCoords.y - actualHeight / 2}px`;
2983
+ el.style.width = `${actualWidth}px`;
2984
+ el.style.height = `${actualHeight}px`;
2985
+ el.style.transformOrigin = "center center";
2986
+ el.style.transform = angle !== 0 ? `scale(${zoom}) rotate(${angle}deg)` : `scale(${zoom})`;
2987
+ el.style.rotate = "";
2988
+ el.style.setProperty("--overlay-scale", String(zoom));
2989
+ if (opts?.textSelector) {
2990
+ const textMinScale = opts?.textMinScale ?? 0.5;
2784
2991
  const textEls = el.querySelectorAll(opts.textSelector);
2785
- const display = clampedScale < textMinScale ? "none" : "";
2992
+ const display = zoom < textMinScale ? "none" : "";
2786
2993
  textEls.forEach((t) => {
2787
2994
  t.style.display = display;
2788
2995
  });
2789
2996
  }
2997
+ } else {
2998
+ el.style.left = `${screenCoords.x - screenWidth / 2}px`;
2999
+ el.style.top = `${screenCoords.y - screenHeight / 2}px`;
3000
+ el.style.width = `${screenWidth}px`;
3001
+ el.style.height = `${screenHeight}px`;
3002
+ el.style.transform = "";
3003
+ el.style.rotate = angle !== 0 ? `${angle}deg` : "";
2790
3004
  }
2791
3005
  }
2792
3006
  update();
2793
3007
  canvas.on("after:render", update);
2794
- canvas.on("mouse:wheel", update);
2795
3008
  object.on("moving", update);
2796
3009
  object.on("scaling", update);
2797
3010
  object.on("rotating", update);
2798
3011
  return () => {
2799
3012
  canvas.off("after:render", update);
2800
- canvas.off("mouse:wheel", update);
2801
3013
  object.off("moving", update);
2802
3014
  object.off("scaling", update);
2803
3015
  object.off("rotating", update);
@@ -2825,6 +3037,7 @@ var import_fabric19 = require("fabric");
2825
3037
  Rect,
2826
3038
  createCircle,
2827
3039
  createCircleAtPoint,
3040
+ createHistoryTracker,
2828
3041
  createPolygon,
2829
3042
  createPolygonAtPoint,
2830
3043
  createPolygonFromDrag,