@bwp-web/canvas 0.6.1 → 0.7.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.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(() => {
@@ -2934,18 +2976,24 @@ function useCanvasClick(canvasRef, onClick, options) {
2934
2976
  }, [canvasRef]);
2935
2977
  }
2936
2978
 
2937
- // src/hooks/useObjectOverlay.ts
2979
+ // src/overlay/ObjectOverlay.tsx
2938
2980
  import { useEffect as useEffect6, useRef as useRef7 } from "react";
2981
+ import { Stack } from "@mui/material";
2939
2982
  import { util as util4 } from "fabric";
2940
- function useObjectOverlay(canvasRef, object, options) {
2941
- const containerRef = useRef7(null);
2942
- const optionsRef = useRef7(options);
2943
- optionsRef.current = options;
2983
+ import { jsx as jsx2 } from "react/jsx-runtime";
2984
+ function ObjectOverlay({
2985
+ canvasRef,
2986
+ object,
2987
+ sx,
2988
+ children,
2989
+ ...rest
2990
+ }) {
2991
+ const stackRef = useRef7(null);
2944
2992
  useEffect6(() => {
2945
2993
  const canvas = canvasRef.current;
2946
2994
  if (!canvas || !object) return;
2947
2995
  function update() {
2948
- const el = containerRef.current;
2996
+ const el = stackRef.current;
2949
2997
  if (!el || !canvas || !object) return;
2950
2998
  const zoom = canvas.getZoom();
2951
2999
  const vt = canvas.viewportTransform;
@@ -2957,32 +3005,11 @@ function useObjectOverlay(canvasRef, object, options) {
2957
3005
  const screenWidth = actualWidth * zoom;
2958
3006
  const screenHeight = actualHeight * zoom;
2959
3007
  const angle = object.angle ?? 0;
2960
- const opts = optionsRef.current;
2961
- if (opts?.autoScaleContent !== false) {
2962
- el.style.left = `${screenCoords.x - actualWidth / 2}px`;
2963
- el.style.top = `${screenCoords.y - actualHeight / 2}px`;
2964
- el.style.width = `${actualWidth}px`;
2965
- el.style.height = `${actualHeight}px`;
2966
- el.style.transformOrigin = "center center";
2967
- el.style.transform = angle !== 0 ? `scale(${zoom}) rotate(${angle}deg)` : `scale(${zoom})`;
2968
- el.style.rotate = "";
2969
- el.style.setProperty("--overlay-scale", String(zoom));
2970
- if (opts?.textSelector) {
2971
- const textMinScale = opts?.textMinScale ?? 0.5;
2972
- const textEls = el.querySelectorAll(opts.textSelector);
2973
- const display = zoom < textMinScale ? "none" : "";
2974
- textEls.forEach((t) => {
2975
- t.style.display = display;
2976
- });
2977
- }
2978
- } else {
2979
- el.style.left = `${screenCoords.x - screenWidth / 2}px`;
2980
- el.style.top = `${screenCoords.y - screenHeight / 2}px`;
2981
- el.style.width = `${screenWidth}px`;
2982
- el.style.height = `${screenHeight}px`;
2983
- el.style.transform = "";
2984
- el.style.rotate = angle !== 0 ? `${angle}deg` : "";
2985
- }
3008
+ el.style.left = `${screenCoords.x - screenWidth / 2}px`;
3009
+ el.style.top = `${screenCoords.y - screenHeight / 2}px`;
3010
+ el.style.width = `${screenWidth}px`;
3011
+ el.style.height = `${screenHeight}px`;
3012
+ el.style.transform = angle !== 0 ? `rotate(${angle}deg)` : "";
2986
3013
  }
2987
3014
  update();
2988
3015
  canvas.on("after:render", update);
@@ -2996,7 +3023,168 @@ function useObjectOverlay(canvasRef, object, options) {
2996
3023
  object.off("rotating", update);
2997
3024
  };
2998
3025
  }, [canvasRef, object]);
2999
- return containerRef;
3026
+ if (!object) return null;
3027
+ return /* @__PURE__ */ jsx2(
3028
+ Stack,
3029
+ {
3030
+ ref: stackRef,
3031
+ sx: {
3032
+ position: "absolute",
3033
+ pointerEvents: "none",
3034
+ alignItems: "center",
3035
+ justifyContent: "center",
3036
+ zIndex: 1,
3037
+ overflow: "hidden",
3038
+ ...sx
3039
+ },
3040
+ ...rest,
3041
+ children
3042
+ }
3043
+ );
3044
+ }
3045
+
3046
+ // src/overlay/OverlayContent.tsx
3047
+ import { Stack as Stack2 } from "@mui/material";
3048
+ import { useEffect as useEffect7, useRef as useRef8 } from "react";
3049
+ import { jsx as jsx3 } from "react/jsx-runtime";
3050
+ function OverlayContent({
3051
+ children,
3052
+ padding = 4,
3053
+ maxScale = 2,
3054
+ sx,
3055
+ ...rest
3056
+ }) {
3057
+ const outerRef = useRef8(null);
3058
+ const innerRef = useRef8(null);
3059
+ useEffect7(() => {
3060
+ const outer = outerRef.current;
3061
+ const inner = innerRef.current;
3062
+ if (!outer || !inner) return;
3063
+ function fit() {
3064
+ if (!outer || !inner) return;
3065
+ const containerW = outer.clientWidth;
3066
+ const containerH = outer.clientHeight;
3067
+ const natW = inner.scrollWidth;
3068
+ const natH = inner.scrollHeight;
3069
+ if (natW === 0 || natH === 0 || containerW <= 0 || containerH <= 0) {
3070
+ inner.style.transform = "";
3071
+ inner.style.removeProperty("--overlay-scale");
3072
+ return;
3073
+ }
3074
+ const scale = Math.min(
3075
+ containerW / (natW + padding * 2),
3076
+ containerH / (natH + padding * 2),
3077
+ maxScale
3078
+ );
3079
+ inner.style.transform = `scale(${scale})`;
3080
+ inner.style.setProperty("--overlay-scale", String(scale));
3081
+ }
3082
+ const observer = new ResizeObserver(fit);
3083
+ observer.observe(outer);
3084
+ observer.observe(inner);
3085
+ fit();
3086
+ return () => observer.disconnect();
3087
+ }, [padding, maxScale]);
3088
+ return /* @__PURE__ */ jsx3(
3089
+ Stack2,
3090
+ {
3091
+ ref: outerRef,
3092
+ sx: {
3093
+ width: "100%",
3094
+ height: "100%",
3095
+ alignItems: "center",
3096
+ justifyContent: "center",
3097
+ overflow: "hidden",
3098
+ ...sx
3099
+ },
3100
+ ...rest,
3101
+ children: /* @__PURE__ */ jsx3(
3102
+ Stack2,
3103
+ {
3104
+ ref: innerRef,
3105
+ sx: {
3106
+ transformOrigin: "center center",
3107
+ flexShrink: 0,
3108
+ width: "max-content"
3109
+ },
3110
+ children
3111
+ }
3112
+ )
3113
+ }
3114
+ );
3115
+ }
3116
+
3117
+ // src/overlay/FixedSizeContent.tsx
3118
+ import { Stack as Stack3 } from "@mui/material";
3119
+ import { useEffect as useEffect8, useRef as useRef9 } from "react";
3120
+ import { jsx as jsx4 } from "react/jsx-runtime";
3121
+ function FixedSizeContent({
3122
+ children,
3123
+ hideOnOverflow = true,
3124
+ truncationPadding = 4,
3125
+ sx,
3126
+ ...rest
3127
+ }) {
3128
+ const ref = useRef9(null);
3129
+ const totalContentHeightRef = useRef9(0);
3130
+ useEffect8(() => {
3131
+ const el = ref.current;
3132
+ if (!el) return;
3133
+ let clipAncestor = el.parentElement;
3134
+ while (clipAncestor) {
3135
+ if (getComputedStyle(clipAncestor).overflow === "hidden") break;
3136
+ clipAncestor = clipAncestor.parentElement;
3137
+ }
3138
+ if (!clipAncestor) return;
3139
+ const ancestor = clipAncestor;
3140
+ totalContentHeightRef.current = el.parentElement?.scrollHeight ?? 0;
3141
+ function check() {
3142
+ requestAnimationFrame(() => {
3143
+ if (!el) return;
3144
+ const containerRect = ancestor.getBoundingClientRect();
3145
+ el.style.maxWidth = `${Math.max(0, containerRect.width - truncationPadding * 2)}px`;
3146
+ if (!hideOnOverflow) return;
3147
+ const fits = containerRect.height >= totalContentHeightRef.current;
3148
+ if (fits && el.style.display === "none") {
3149
+ el.style.display = "";
3150
+ totalContentHeightRef.current = el.parentElement?.scrollHeight ?? 0;
3151
+ if (containerRect.height < totalContentHeightRef.current) {
3152
+ el.style.display = "none";
3153
+ }
3154
+ } else if (!fits && el.style.display !== "none") {
3155
+ el.style.display = "none";
3156
+ }
3157
+ if (el.style.display !== "none") {
3158
+ totalContentHeightRef.current = el.parentElement?.scrollHeight ?? 0;
3159
+ }
3160
+ });
3161
+ }
3162
+ const observer = new ResizeObserver(check);
3163
+ observer.observe(ancestor);
3164
+ check();
3165
+ return () => observer.disconnect();
3166
+ }, [hideOnOverflow, truncationPadding]);
3167
+ return /* @__PURE__ */ jsx4(
3168
+ Stack3,
3169
+ {
3170
+ ref,
3171
+ sx: {
3172
+ transform: "scale(calc(1 / var(--overlay-scale, 1)))",
3173
+ transformOrigin: "center center",
3174
+ flexShrink: 0,
3175
+ width: "max-content",
3176
+ overflow: "hidden",
3177
+ "& > *": {
3178
+ maxWidth: "100%",
3179
+ overflow: "hidden",
3180
+ textOverflow: "ellipsis"
3181
+ },
3182
+ ...sx
3183
+ },
3184
+ ...rest,
3185
+ children
3186
+ }
3187
+ );
3000
3188
  }
3001
3189
 
3002
3190
  // src/index.ts
@@ -3020,6 +3208,9 @@ export {
3020
3208
  Canvas2 as FabricCanvas,
3021
3209
  FabricImage3 as FabricImage,
3022
3210
  FabricObject5 as FabricObject,
3211
+ FixedSizeContent,
3212
+ ObjectOverlay,
3213
+ OverlayContent,
3023
3214
  Point9 as Point,
3024
3215
  Polygon5 as Polygon,
3025
3216
  Rect6 as Rect,
@@ -3065,7 +3256,6 @@ export {
3065
3256
  useCanvasEvents,
3066
3257
  useCanvasTooltip,
3067
3258
  useEditCanvas,
3068
- useObjectOverlay,
3069
3259
  useViewCanvas,
3070
3260
  util5 as util
3071
3261
  };