@bwp-web/canvas 0.6.1 → 0.6.2

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.js CHANGED
@@ -102,6 +102,10 @@ var DEFAULT_VIEWPORT_PADDING = 0.05;
102
102
  var BASE_CANVAS_SIZE = 1e3;
103
103
  var DEFAULT_SNAP_MARGIN = 6;
104
104
  var DEFAULT_ANGLE_SNAP_INTERVAL = 15;
105
+ function computeSnapMargin(canvasWidth, canvasHeight, zoom, baseMargin = DEFAULT_SNAP_MARGIN, scaleWithCanvasSize = true) {
106
+ const sizeScale = scaleWithCanvasSize ? Math.max(canvasWidth || 800, canvasHeight || 600) / BASE_CANVAS_SIZE : 1;
107
+ return baseMargin * sizeScale / zoom;
108
+ }
105
109
  var MIN_DRAG_SIZE = 3;
106
110
  var POLYGON_CLOSE_THRESHOLD = 10;
107
111
  var DEFAULT_IMAGE_MAX_SIZE = 4096;
@@ -309,23 +313,23 @@ function enablePanAndZoom(canvas, options) {
309
313
  const targetX = canvasCenterX - objectCenter.x * zoom;
310
314
  const targetY = canvasCenterY - objectCenter.y * zoom;
311
315
  if (!panOpts?.animate) {
312
- const vt2 = canvas.viewportTransform;
313
- if (!vt2) return;
316
+ const viewportTransform2 = canvas.viewportTransform;
317
+ if (!viewportTransform2) return;
314
318
  canvas.setViewportTransform([
315
- vt2[0],
316
- vt2[1],
317
- vt2[2],
318
- vt2[3],
319
+ viewportTransform2[0],
320
+ viewportTransform2[1],
321
+ viewportTransform2[2],
322
+ viewportTransform2[3],
319
323
  targetX,
320
324
  targetY
321
325
  ]);
322
326
  return;
323
327
  }
324
328
  const duration = panOpts.duration ?? 300;
325
- const vt = canvas.viewportTransform;
326
- if (!vt) return;
327
- const startX = vt[4];
328
- const startY = vt[5];
329
+ const viewportTransform = canvas.viewportTransform;
330
+ if (!viewportTransform) return;
331
+ const startX = viewportTransform[4];
332
+ const startY = viewportTransform[5];
329
333
  const startTime = performance.now();
330
334
  function step(now) {
331
335
  const elapsed = now - startTime;
@@ -333,13 +337,13 @@ function enablePanAndZoom(canvas, options) {
333
337
  const eased = 1 - Math.pow(1 - t, 3);
334
338
  const currentX = startX + (targetX - startX) * eased;
335
339
  const currentY = startY + (targetY - startY) * eased;
336
- const currentVt = canvas.viewportTransform;
337
- if (!currentVt) return;
340
+ const currentTransform = canvas.viewportTransform;
341
+ if (!currentTransform) return;
338
342
  canvas.setViewportTransform([
339
- currentVt[0],
340
- currentVt[1],
341
- currentVt[2],
342
- currentVt[3],
343
+ currentTransform[0],
344
+ currentTransform[1],
345
+ currentTransform[2],
346
+ currentTransform[3],
343
347
  currentX,
344
348
  currentY
345
349
  ]);
@@ -526,6 +530,7 @@ async function setBackgroundImage(canvas, url, options) {
526
530
  imageUrl = result.url;
527
531
  }
528
532
  const img = await FabricImage.fromURL(imageUrl, { crossOrigin: "anonymous" });
533
+ img.set({ left: img.width / 2, top: img.height / 2 });
529
534
  canvas.backgroundImage = img;
530
535
  if (prevContrast !== void 0 && prevContrast !== 1) {
531
536
  setBackgroundContrast(canvas, prevContrast);
@@ -723,63 +728,6 @@ registerSnapPointExtractor(
723
728
  // src/alignment/objectAlignment.ts
724
729
  import { util as util2 } from "fabric";
725
730
 
726
- // src/alignment/objectAlignmentRendering.ts
727
- function drawAlignmentLine(config, origin, target) {
728
- const ctx = config.canvas.getTopContext();
729
- const vt = config.canvas.viewportTransform;
730
- const zoom = config.canvas.getZoom();
731
- ctx.save();
732
- ctx.transform(...vt);
733
- ctx.lineWidth = config.width / zoom;
734
- if (config.lineDash) ctx.setLineDash(config.lineDash);
735
- ctx.strokeStyle = config.color;
736
- ctx.beginPath();
737
- ctx.moveTo(origin.x, origin.y);
738
- ctx.lineTo(target.x, target.y);
739
- ctx.stroke();
740
- if (config.lineDash) ctx.setLineDash([]);
741
- drawXMarker(ctx, origin, config.xSize / zoom);
742
- drawXMarker(ctx, target, config.xSize / zoom);
743
- ctx.restore();
744
- }
745
- function drawXMarker(ctx, point, size) {
746
- ctx.save();
747
- ctx.translate(point.x, point.y);
748
- ctx.beginPath();
749
- ctx.moveTo(-size, -size);
750
- ctx.lineTo(size, size);
751
- ctx.moveTo(size, -size);
752
- ctx.lineTo(-size, size);
753
- ctx.stroke();
754
- ctx.restore();
755
- }
756
- function drawMarkerList(config, lines) {
757
- const ctx = config.canvas.getTopContext();
758
- const vt = config.canvas.viewportTransform;
759
- const zoom = config.canvas.getZoom();
760
- const markerSize = config.xSize / zoom;
761
- ctx.save();
762
- ctx.transform(...vt);
763
- ctx.lineWidth = config.width / zoom;
764
- ctx.strokeStyle = config.color;
765
- for (const item of lines) drawXMarker(ctx, item.target, markerSize);
766
- ctx.restore();
767
- }
768
- function drawVerticalAlignmentLines(config, lines) {
769
- for (const v of lines) {
770
- const { origin, target } = JSON.parse(v);
771
- const from = { x: target.x, y: origin.y };
772
- drawAlignmentLine(config, from, target);
773
- }
774
- }
775
- function drawHorizontalAlignmentLines(config, lines) {
776
- for (const h of lines) {
777
- const { origin, target } = JSON.parse(h);
778
- const from = { x: origin.x, y: target.y };
779
- drawAlignmentLine(config, from, target);
780
- }
781
- }
782
-
783
731
  // src/alignment/objectAlignmentMath.ts
784
732
  var OPPOSITE_ORIGIN_MAP = {
785
733
  tl: ["right", "bottom"],
@@ -903,6 +851,61 @@ function collectHorizontalSnapOffset(props) {
903
851
  }
904
852
 
905
853
  // src/alignment/objectAlignment.ts
854
+ function drawXMarker(ctx, point, size) {
855
+ ctx.save();
856
+ ctx.translate(point.x, point.y);
857
+ ctx.beginPath();
858
+ ctx.moveTo(-size, -size);
859
+ ctx.lineTo(size, size);
860
+ ctx.moveTo(size, -size);
861
+ ctx.lineTo(-size, size);
862
+ ctx.stroke();
863
+ ctx.restore();
864
+ }
865
+ function drawAlignmentLine(config, origin, target) {
866
+ const ctx = config.canvas.getTopContext();
867
+ const vt = config.canvas.viewportTransform;
868
+ const zoom = config.canvas.getZoom();
869
+ ctx.save();
870
+ ctx.transform(...vt);
871
+ ctx.lineWidth = config.width / zoom;
872
+ if (config.lineDash) ctx.setLineDash(config.lineDash);
873
+ ctx.strokeStyle = config.color;
874
+ ctx.beginPath();
875
+ ctx.moveTo(origin.x, origin.y);
876
+ ctx.lineTo(target.x, target.y);
877
+ ctx.stroke();
878
+ if (config.lineDash) ctx.setLineDash([]);
879
+ drawXMarker(ctx, origin, config.xSize / zoom);
880
+ drawXMarker(ctx, target, config.xSize / zoom);
881
+ ctx.restore();
882
+ }
883
+ function drawMarkerList(config, lines) {
884
+ const ctx = config.canvas.getTopContext();
885
+ const vt = config.canvas.viewportTransform;
886
+ const zoom = config.canvas.getZoom();
887
+ const markerSize = config.xSize / zoom;
888
+ ctx.save();
889
+ ctx.transform(...vt);
890
+ ctx.lineWidth = config.width / zoom;
891
+ ctx.strokeStyle = config.color;
892
+ for (const item of lines) drawXMarker(ctx, item.target, markerSize);
893
+ ctx.restore();
894
+ }
895
+ function drawVerticalAlignmentLines(config, lines) {
896
+ for (const v of lines) {
897
+ const { origin, target } = JSON.parse(v);
898
+ const from = { x: target.x, y: origin.y };
899
+ drawAlignmentLine(config, from, target);
900
+ }
901
+ }
902
+ function drawHorizontalAlignmentLines(config, lines) {
903
+ for (const h of lines) {
904
+ const { origin, target } = JSON.parse(h);
905
+ const from = { x: origin.x, y: target.y };
906
+ drawAlignmentLine(config, from, target);
907
+ }
908
+ }
906
909
  function adjustCornerForFlip(corner, target) {
907
910
  let adjusted = corner;
908
911
  if (target.flipX) {
@@ -957,9 +960,13 @@ var ObjectAlignmentGuides = class {
957
960
  }
958
961
  // --- Margin calculation ---
959
962
  computeMargin() {
960
- const zoom = this.canvas.getZoom();
961
- const sizeScale = this.scaleWithCanvasSize ? Math.max(this.canvas.width ?? 800, this.canvas.height ?? 600) / BASE_CANVAS_SIZE : 1;
962
- return this.margin * sizeScale / zoom;
963
+ return computeSnapMargin(
964
+ this.canvas.width ?? 800,
965
+ this.canvas.height ?? 600,
966
+ this.canvas.getZoom(),
967
+ this.margin,
968
+ this.scaleWithCanvasSize
969
+ );
963
970
  }
964
971
  // --- Snap point caching ---
965
972
  getCachedSnapPoints(object) {
@@ -1095,10 +1102,13 @@ function enableRotationSnap(canvas, options) {
1095
1102
  // src/alignment/cursorSnapping.ts
1096
1103
  import { Point as Point5 } from "fabric";
1097
1104
  function snapCursorPoint(canvas, rawPoint, options) {
1098
- const zoom = canvas.getZoom();
1099
- const scaleWithSize = options?.scaleWithCanvasSize !== false;
1100
- const sizeScale = scaleWithSize ? Math.max(canvas.width ?? 800, canvas.height ?? 600) / BASE_CANVAS_SIZE : 1;
1101
- const margin = (options?.margin ?? DEFAULT_SNAP_MARGIN) * sizeScale / zoom;
1105
+ const margin = computeSnapMargin(
1106
+ canvas.width ?? 800,
1107
+ canvas.height ?? 600,
1108
+ canvas.getZoom(),
1109
+ options?.margin ?? DEFAULT_SNAP_MARGIN,
1110
+ options?.scaleWithCanvasSize !== false
1111
+ );
1102
1112
  const exclude = options?.exclude ?? /* @__PURE__ */ new Set();
1103
1113
  let targetPoints;
1104
1114
  if (options?.targetPoints) {
@@ -2161,20 +2171,7 @@ function enableScaledBorderRadius(canvas, options) {
2161
2171
  function getBaseStrokeWidth(obj) {
2162
2172
  return strokeBaseMap.get(obj) ?? obj.strokeWidth ?? 0;
2163
2173
  }
2164
- function serializeCanvas(canvas, options) {
2165
- const properties = [
2166
- "data",
2167
- "shapeType",
2168
- // Control styling — absent from Fabric's default toObject output
2169
- "borderColor",
2170
- "cornerColor",
2171
- "cornerStrokeColor",
2172
- "transparentCorners",
2173
- // Interaction locks — absent from Fabric's default toObject output
2174
- "lockRotation",
2175
- "lockUniScaling",
2176
- ...options?.properties ?? []
2177
- ];
2174
+ function prepareStrokeWidths(canvas) {
2178
2175
  const scaledWidths = /* @__PURE__ */ new Map();
2179
2176
  canvas.forEachObject((obj) => {
2180
2177
  const base = strokeBaseMap.get(obj);
@@ -2183,6 +2180,11 @@ function serializeCanvas(canvas, options) {
2183
2180
  obj.strokeWidth = base;
2184
2181
  }
2185
2182
  });
2183
+ return () => scaledWidths.forEach((scaled, obj) => {
2184
+ obj.strokeWidth = scaled;
2185
+ });
2186
+ }
2187
+ function prepareBorderRadii(canvas) {
2186
2188
  const appliedRadii = /* @__PURE__ */ new Map();
2187
2189
  canvas.forEachObject((obj) => {
2188
2190
  if (!(obj instanceof Rect5)) return;
@@ -2192,6 +2194,11 @@ function serializeCanvas(canvas, options) {
2192
2194
  obj.set({ rx: base.rx, ry: base.ry });
2193
2195
  }
2194
2196
  });
2197
+ return () => appliedRadii.forEach((radii, obj) => {
2198
+ obj.set(radii);
2199
+ });
2200
+ }
2201
+ function prepareObjectOrigins(canvas) {
2195
2202
  const savedOrigins = /* @__PURE__ */ new Map();
2196
2203
  canvas.forEachObject((obj) => {
2197
2204
  if (obj.originX === "left" && obj.originY === "top") return;
@@ -2202,20 +2209,36 @@ function serializeCanvas(canvas, options) {
2202
2209
  top: obj.top ?? 0
2203
2210
  });
2204
2211
  const leftTop = obj.getPositionByOrigin("left", "top");
2205
- obj.set({ originX: "left", originY: "top", left: leftTop.x, top: leftTop.y });
2212
+ obj.set({
2213
+ originX: "left",
2214
+ originY: "top",
2215
+ left: leftTop.x,
2216
+ top: leftTop.y
2217
+ });
2206
2218
  });
2219
+ return () => savedOrigins.forEach((saved, obj) => {
2220
+ obj.set(saved);
2221
+ });
2222
+ }
2223
+ function prepareBackgroundOrigin(canvas) {
2207
2224
  const bg = canvas.backgroundImage;
2208
- let savedBgOrigin = null;
2209
- if (bg instanceof FabricImage2 && (bg.originX !== "left" || bg.originY !== "top")) {
2210
- savedBgOrigin = {
2211
- originX: bg.originX,
2212
- originY: bg.originY,
2213
- left: bg.left ?? 0,
2214
- top: bg.top ?? 0
2225
+ if (!(bg instanceof FabricImage2) || bg.originX === "left" && bg.originY === "top") {
2226
+ return () => {
2215
2227
  };
2216
- const leftTop = bg.getPositionByOrigin("left", "top");
2217
- bg.set({ originX: "left", originY: "top", left: leftTop.x, top: leftTop.y });
2218
2228
  }
2229
+ const saved = {
2230
+ originX: bg.originX,
2231
+ originY: bg.originY,
2232
+ left: bg.left ?? 0,
2233
+ top: bg.top ?? 0
2234
+ };
2235
+ const leftTop = bg.getPositionByOrigin("left", "top");
2236
+ bg.set({ originX: "left", originY: "top", left: leftTop.x, top: leftTop.y });
2237
+ return () => {
2238
+ bg.set(saved);
2239
+ };
2240
+ }
2241
+ function prepareStrokeWidthBaseData(canvas) {
2219
2242
  const savedData = /* @__PURE__ */ new Map();
2220
2243
  canvas.forEachObject((obj) => {
2221
2244
  const base = strokeBaseMap.get(obj) ?? obj.strokeWidth;
@@ -2227,33 +2250,53 @@ function serializeCanvas(canvas, options) {
2227
2250
  };
2228
2251
  }
2229
2252
  });
2253
+ return () => savedData.forEach((originalData, obj) => {
2254
+ obj.data = originalData;
2255
+ });
2256
+ }
2257
+ function serializeCanvas(canvas, options) {
2258
+ const properties = [
2259
+ "data",
2260
+ "shapeType",
2261
+ // Control styling — absent from Fabric's default toObject output
2262
+ "borderColor",
2263
+ "cornerColor",
2264
+ "cornerStrokeColor",
2265
+ "transparentCorners",
2266
+ // Interaction locks — absent from Fabric's default toObject output
2267
+ "lockRotation",
2268
+ "lockUniScaling",
2269
+ ...options?.properties ?? []
2270
+ ];
2271
+ const restoreStrokeWidths = prepareStrokeWidths(canvas);
2272
+ const restoreBorderRadii = prepareBorderRadii(canvas);
2273
+ const restoreOrigins = prepareObjectOrigins(canvas);
2274
+ const restoreBgOrigin = prepareBackgroundOrigin(canvas);
2275
+ const restoreData = prepareStrokeWidthBaseData(canvas);
2230
2276
  const json = canvas.toObject(properties);
2231
2277
  delete json.backgroundColor;
2232
2278
  json.backgroundFilters = {
2233
2279
  opacity: getBackgroundContrast(canvas),
2234
2280
  inverted: getBackgroundInverted(canvas)
2235
2281
  };
2236
- scaledWidths.forEach((scaled, obj) => {
2237
- obj.strokeWidth = scaled;
2238
- });
2239
- appliedRadii.forEach((radii, obj) => {
2240
- obj.set({ rx: radii.rx, ry: radii.ry });
2241
- });
2242
- savedOrigins.forEach((saved, obj) => {
2243
- obj.set(saved);
2244
- });
2245
- if (savedBgOrigin && bg instanceof FabricImage2) {
2246
- bg.set(savedBgOrigin);
2282
+ if (canvas.lockLightMode !== void 0) {
2283
+ json.lockLightMode = canvas.lockLightMode;
2247
2284
  }
2248
- savedData.forEach((originalData, obj) => {
2249
- obj.data = originalData;
2250
- });
2285
+ restoreStrokeWidths();
2286
+ restoreBorderRadii();
2287
+ restoreOrigins();
2288
+ restoreBgOrigin();
2289
+ restoreData();
2251
2290
  return json;
2252
2291
  }
2253
2292
  async function loadCanvas(canvas, json, options) {
2254
2293
  await canvas.loadFromJSON(json);
2255
2294
  canvas.backgroundColor = "";
2256
2295
  delete canvas.backgroundFilters;
2296
+ const rawCanvas = canvas;
2297
+ if (rawCanvas.lockLightMode !== void 0) {
2298
+ canvas.lockLightMode = rawCanvas.lockLightMode;
2299
+ }
2257
2300
  const bg = canvas.backgroundImage;
2258
2301
  if (bg instanceof FabricImage2) {
2259
2302
  if (bg.originX !== "center" || bg.originY !== "center") {
@@ -2446,105 +2489,104 @@ function useEditCanvas(options) {
2446
2489
  (canvas) => {
2447
2490
  canvasRef.current = canvas;
2448
2491
  const opts = optionsRef.current;
2449
- if (opts?.scaledStrokes !== false) {
2450
- enableScaledStrokes(canvas);
2451
- }
2452
- if (opts?.borderRadius !== false) {
2453
- const borderRadiusOpts = typeof opts?.borderRadius === "number" ? { radius: opts.borderRadius } : void 0;
2454
- enableScaledBorderRadius(canvas, borderRadiusOpts);
2455
- }
2456
- if (opts?.keyboardShortcuts !== false) {
2457
- keyboardCleanupRef.current = enableKeyboardShortcuts(canvas);
2458
- }
2459
- setCanvasAlignmentEnabled(canvas, opts?.enableAlignment);
2460
- if (opts?.panAndZoom !== false) {
2461
- viewportRef.current = enablePanAndZoom(
2462
- canvas,
2463
- typeof opts?.panAndZoom === "object" ? opts.panAndZoom : void 0
2464
- );
2465
- }
2466
- const alignmentEnabled = resolveAlignmentEnabled(
2467
- opts?.enableAlignment,
2468
- opts?.alignment
2469
- );
2470
- if (alignmentEnabled) {
2471
- alignmentCleanupRef.current = enableObjectAlignment(
2472
- canvas,
2473
- typeof opts?.alignment === "object" ? opts.alignment : void 0
2474
- );
2475
- }
2476
- if (opts?.rotationSnap !== false) {
2477
- rotationSnapCleanupRef.current = enableRotationSnap(
2478
- canvas,
2479
- typeof opts?.rotationSnap === "object" ? opts.rotationSnap : void 0
2492
+ setupFeatures();
2493
+ setupEventListeners();
2494
+ invokeOnReady();
2495
+ function setupFeatures() {
2496
+ if (opts?.scaledStrokes !== false) {
2497
+ enableScaledStrokes(canvas);
2498
+ }
2499
+ if (opts?.borderRadius !== false) {
2500
+ const borderRadiusOpts = typeof opts?.borderRadius === "number" ? { radius: opts.borderRadius } : void 0;
2501
+ enableScaledBorderRadius(canvas, borderRadiusOpts);
2502
+ }
2503
+ if (opts?.keyboardShortcuts !== false) {
2504
+ keyboardCleanupRef.current = enableKeyboardShortcuts(canvas);
2505
+ }
2506
+ setCanvasAlignmentEnabled(canvas, opts?.enableAlignment);
2507
+ if (opts?.panAndZoom !== false) {
2508
+ viewportRef.current = enablePanAndZoom(
2509
+ canvas,
2510
+ typeof opts?.panAndZoom === "object" ? opts.panAndZoom : void 0
2511
+ );
2512
+ }
2513
+ const alignmentEnabled = resolveAlignmentEnabled(
2514
+ opts?.enableAlignment,
2515
+ opts?.alignment
2480
2516
  );
2517
+ if (alignmentEnabled) {
2518
+ alignmentCleanupRef.current = enableObjectAlignment(
2519
+ canvas,
2520
+ typeof opts?.alignment === "object" ? opts.alignment : void 0
2521
+ );
2522
+ }
2523
+ if (opts?.rotationSnap !== false) {
2524
+ rotationSnapCleanupRef.current = enableRotationSnap(
2525
+ canvas,
2526
+ typeof opts?.rotationSnap === "object" ? opts.rotationSnap : void 0
2527
+ );
2528
+ }
2529
+ if (opts?.history) {
2530
+ const historyOpts = typeof opts.history === "object" ? opts.history : void 0;
2531
+ historyRef.current = createHistoryTracker(canvas, historyOpts);
2532
+ }
2481
2533
  }
2482
- canvas.on("mouse:wheel", () => {
2483
- setZoom(canvas.getZoom());
2484
- });
2485
- canvas.on("selection:created", (e) => {
2486
- setSelected(e.selected ?? []);
2487
- });
2488
- canvas.on("selection:updated", (e) => {
2489
- setSelected(e.selected ?? []);
2490
- });
2491
- canvas.on("selection:cleared", () => {
2492
- setSelected([]);
2493
- });
2494
- if (opts?.trackChanges) {
2495
- canvas.on("object:added", () => setIsDirty(true));
2496
- canvas.on("object:removed", () => setIsDirty(true));
2497
- canvas.on("object:modified", () => setIsDirty(true));
2498
- }
2499
- if (opts?.history) {
2500
- const syncHistoryState = () => {
2501
- const h = historyRef.current;
2502
- if (!h) return;
2503
- setTimeout(() => {
2504
- setCanUndo(h.canUndo());
2505
- setCanRedo(h.canRedo());
2506
- }, 350);
2507
- };
2508
- canvas.on("object:added", syncHistoryState);
2509
- canvas.on("object:removed", syncHistoryState);
2510
- canvas.on("object:modified", syncHistoryState);
2511
- }
2512
- if (opts?.vertexEdit !== false) {
2513
- const vertexOpts = typeof opts?.vertexEdit === "object" ? opts.vertexEdit : void 0;
2514
- canvas.on("mouse:dblclick", (e) => {
2515
- if (e.target && e.target instanceof Polygon4) {
2516
- vertexEditCleanupRef.current?.();
2517
- vertexEditCleanupRef.current = enableVertexEdit(canvas, e.target, {
2518
- ...vertexOpts,
2519
- onExit: () => {
2520
- vertexEditCleanupRef.current = null;
2521
- setIsEditingVertices(false);
2522
- }
2523
- });
2524
- setIsEditingVertices(true);
2525
- }
2526
- });
2527
- }
2528
- if (opts?.history) {
2529
- const historyOpts = typeof opts.history === "object" ? opts.history : void 0;
2530
- historyRef.current = createHistoryTracker(canvas, historyOpts);
2534
+ function setupEventListeners() {
2535
+ canvas.on("mouse:wheel", () => setZoom(canvas.getZoom()));
2536
+ canvas.on("selection:created", (e) => setSelected(e.selected ?? []));
2537
+ canvas.on("selection:updated", (e) => setSelected(e.selected ?? []));
2538
+ canvas.on("selection:cleared", () => setSelected([]));
2539
+ if (opts?.trackChanges) {
2540
+ canvas.on("object:added", () => setIsDirty(true));
2541
+ canvas.on("object:removed", () => setIsDirty(true));
2542
+ canvas.on("object:modified", () => setIsDirty(true));
2543
+ }
2544
+ if (opts?.history) {
2545
+ const syncHistoryState = () => {
2546
+ const h = historyRef.current;
2547
+ if (!h) return;
2548
+ setTimeout(() => {
2549
+ setCanUndo(h.canUndo());
2550
+ setCanRedo(h.canRedo());
2551
+ }, 350);
2552
+ };
2553
+ canvas.on("object:added", syncHistoryState);
2554
+ canvas.on("object:removed", syncHistoryState);
2555
+ canvas.on("object:modified", syncHistoryState);
2556
+ }
2557
+ if (opts?.vertexEdit !== false) {
2558
+ const vertexOpts = typeof opts?.vertexEdit === "object" ? opts.vertexEdit : void 0;
2559
+ canvas.on("mouse:dblclick", (e) => {
2560
+ if (e.target && e.target instanceof Polygon4) {
2561
+ vertexEditCleanupRef.current?.();
2562
+ vertexEditCleanupRef.current = enableVertexEdit(
2563
+ canvas,
2564
+ e.target,
2565
+ {
2566
+ ...vertexOpts,
2567
+ onExit: () => {
2568
+ vertexEditCleanupRef.current = null;
2569
+ setIsEditingVertices(false);
2570
+ }
2571
+ }
2572
+ );
2573
+ setIsEditingVertices(true);
2574
+ }
2575
+ });
2576
+ }
2531
2577
  }
2532
- const onReadyResult = opts?.onReady?.(canvas);
2533
- if (opts?.autoFitToBackground !== false) {
2578
+ function invokeOnReady() {
2579
+ const onReadyResult = opts?.onReady?.(canvas);
2534
2580
  Promise.resolve(onReadyResult).then(() => {
2535
- if (canvas.backgroundImage) {
2581
+ if (opts?.autoFitToBackground !== false && canvas.backgroundImage) {
2536
2582
  fitViewportToBackground(canvas);
2537
2583
  syncZoom(canvasRef, setZoom);
2538
2584
  }
2539
2585
  historyRef.current?.pushSnapshot();
2540
2586
  });
2541
- } else {
2542
- Promise.resolve(onReadyResult).then(() => {
2543
- historyRef.current?.pushSnapshot();
2544
- });
2545
2587
  }
2546
2588
  },
2547
- // onReady and panAndZoom are intentionally excluded — we only initialize once
2589
+ // Dependency array intentionally empty — we only initialize once on mount
2548
2590
  []
2549
2591
  );
2550
2592
  useEffect2(() => {