@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/Canvas/Canvas.d.ts +12 -1
- package/dist/Canvas/Canvas.d.ts.map +1 -1
- package/dist/background.d.ts.map +1 -1
- package/dist/constants.d.ts +2 -2
- package/dist/constants.d.ts.map +1 -1
- package/dist/fabricAugmentation.d.ts +3 -1
- package/dist/fabricAugmentation.d.ts.map +1 -1
- package/dist/history.d.ts +32 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/hooks/shared.d.ts +6 -4
- package/dist/hooks/shared.d.ts.map +1 -1
- package/dist/hooks/useEditCanvas.d.ts +22 -0
- package/dist/hooks/useEditCanvas.d.ts.map +1 -1
- package/dist/hooks/useObjectOverlay.d.ts +4 -3
- package/dist/hooks/useObjectOverlay.d.ts.map +1 -1
- package/dist/hooks/useViewCanvas.d.ts +7 -0
- package/dist/hooks/useViewCanvas.d.ts.map +1 -1
- package/dist/index.cjs +363 -150
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +326 -111
- package/dist/index.js.map +1 -1
- package/dist/interactions/drawToCreate.d.ts +8 -3
- package/dist/interactions/drawToCreate.d.ts.map +1 -1
- package/dist/interactions/vertexEdit.d.ts +5 -1
- package/dist/interactions/vertexEdit.d.ts.map +1 -1
- package/dist/serialization.d.ts +13 -3
- package/dist/serialization.d.ts.map +1 -1
- package/dist/shapes/polygon.d.ts +2 -2
- package/dist/shapes/polygon.d.ts.map +1 -1
- package/dist/types.d.ts +8 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/viewport.d.ts +12 -2
- package/dist/viewport.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -33,7 +33,9 @@ function Canvas({
|
|
|
33
33
|
height,
|
|
34
34
|
className,
|
|
35
35
|
style,
|
|
36
|
-
onReady
|
|
36
|
+
onReady,
|
|
37
|
+
keyboardShortcuts,
|
|
38
|
+
fabricOptions
|
|
37
39
|
}) {
|
|
38
40
|
const canvasRef = useRef(null);
|
|
39
41
|
const wrapperRef = useRef(null);
|
|
@@ -45,11 +47,12 @@ function Canvas({
|
|
|
45
47
|
const initialWidth = isFixedSize ? width : wrapper.clientWidth || 800;
|
|
46
48
|
const initialHeight = isFixedSize ? height : wrapper.clientHeight || 600;
|
|
47
49
|
const fabricCanvas = new FabricCanvas(el, {
|
|
50
|
+
...fabricOptions,
|
|
48
51
|
width: initialWidth,
|
|
49
52
|
height: initialHeight
|
|
50
53
|
});
|
|
51
54
|
onReady?.(fabricCanvas);
|
|
52
|
-
const cleanupShortcuts = enableKeyboardShortcuts(fabricCanvas);
|
|
55
|
+
const cleanupShortcuts = keyboardShortcuts ? enableKeyboardShortcuts(fabricCanvas) : void 0;
|
|
53
56
|
let observer;
|
|
54
57
|
let rafId = 0;
|
|
55
58
|
if (!isFixedSize) {
|
|
@@ -73,7 +76,7 @@ function Canvas({
|
|
|
73
76
|
return () => {
|
|
74
77
|
cancelAnimationFrame(rafId);
|
|
75
78
|
observer?.disconnect();
|
|
76
|
-
cleanupShortcuts();
|
|
79
|
+
cleanupShortcuts?.();
|
|
77
80
|
fabricCanvas.dispose();
|
|
78
81
|
};
|
|
79
82
|
}, []);
|
|
@@ -93,7 +96,7 @@ import {
|
|
|
93
96
|
// src/constants.ts
|
|
94
97
|
var DEFAULT_MIN_ZOOM = 0.2;
|
|
95
98
|
var DEFAULT_MAX_ZOOM = 10;
|
|
96
|
-
var DEFAULT_ZOOM_FACTOR =
|
|
99
|
+
var DEFAULT_ZOOM_FACTOR = 0.999;
|
|
97
100
|
var DEFAULT_ZOOM_STEP = 1.2;
|
|
98
101
|
var DEFAULT_VIEWPORT_PADDING = 0.05;
|
|
99
102
|
var BASE_CANVAS_SIZE = 1e3;
|
|
@@ -136,7 +139,7 @@ function setupWheelZoom(canvas, bounds, zoomFactor, isEnabled) {
|
|
|
136
139
|
e.stopPropagation();
|
|
137
140
|
const delta = e.deltaY;
|
|
138
141
|
let zoom = canvas.getZoom();
|
|
139
|
-
zoom
|
|
142
|
+
zoom *= zoomFactor ** delta;
|
|
140
143
|
zoom = Math.min(Math.max(zoom, bounds.minZoom), bounds.maxZoom);
|
|
141
144
|
canvas.zoomToPoint(new Point(e.offsetX, e.offsetY), zoom);
|
|
142
145
|
};
|
|
@@ -244,8 +247,15 @@ function enablePanAndZoom(canvas, options) {
|
|
|
244
247
|
const zoomFactor = options?.zoomFactor ?? DEFAULT_ZOOM_FACTOR;
|
|
245
248
|
let mode = options?.initialMode ?? "select";
|
|
246
249
|
let enabled = true;
|
|
250
|
+
let currentAnimRafId = null;
|
|
247
251
|
const isEnabled = () => enabled;
|
|
248
252
|
const getMode = () => mode;
|
|
253
|
+
function cancelAnimation() {
|
|
254
|
+
if (currentAnimRafId !== null) {
|
|
255
|
+
cancelAnimationFrame(currentAnimRafId);
|
|
256
|
+
currentAnimRafId = null;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
249
259
|
const handleWheel = setupWheelZoom(canvas, bounds, zoomFactor, isEnabled);
|
|
250
260
|
const panHandlers = setupMousePan(canvas, getMode, isEnabled);
|
|
251
261
|
const cleanupPinch = setupPinchZoom(canvas, bounds, isEnabled);
|
|
@@ -291,6 +301,7 @@ function enablePanAndZoom(canvas, options) {
|
|
|
291
301
|
);
|
|
292
302
|
},
|
|
293
303
|
panToObject(object, panOpts) {
|
|
304
|
+
cancelAnimation();
|
|
294
305
|
const zoom = canvas.getZoom();
|
|
295
306
|
const objectCenter = object.getCenterPoint();
|
|
296
307
|
const canvasCenterX = canvas.getWidth() / 2;
|
|
@@ -333,12 +344,37 @@ function enablePanAndZoom(canvas, options) {
|
|
|
333
344
|
currentY
|
|
334
345
|
]);
|
|
335
346
|
if (t < 1) {
|
|
336
|
-
requestAnimationFrame(step);
|
|
347
|
+
currentAnimRafId = requestAnimationFrame(step);
|
|
348
|
+
} else {
|
|
349
|
+
currentAnimRafId = null;
|
|
337
350
|
}
|
|
338
351
|
}
|
|
339
|
-
requestAnimationFrame(step);
|
|
352
|
+
currentAnimRafId = requestAnimationFrame(step);
|
|
353
|
+
},
|
|
354
|
+
zoomToFit(object, fitOpts) {
|
|
355
|
+
cancelAnimation();
|
|
356
|
+
const padding = fitOpts?.padding ?? 0.1;
|
|
357
|
+
const objWidth = (object.width ?? 0) * (object.scaleX ?? 1);
|
|
358
|
+
const objHeight = (object.height ?? 0) * (object.scaleY ?? 1);
|
|
359
|
+
if (!objWidth || !objHeight) return;
|
|
360
|
+
const canvasWidth = canvas.getWidth();
|
|
361
|
+
const canvasHeight = canvas.getHeight();
|
|
362
|
+
const availableWidth = canvasWidth * (1 - padding * 2);
|
|
363
|
+
const availableHeight = canvasHeight * (1 - padding * 2);
|
|
364
|
+
const zoom = Math.min(
|
|
365
|
+
Math.max(
|
|
366
|
+
Math.min(availableWidth / objWidth, availableHeight / objHeight),
|
|
367
|
+
bounds.minZoom
|
|
368
|
+
),
|
|
369
|
+
bounds.maxZoom
|
|
370
|
+
);
|
|
371
|
+
const objectCenter = object.getCenterPoint();
|
|
372
|
+
const offsetX = canvasWidth / 2 - objectCenter.x * zoom;
|
|
373
|
+
const offsetY = canvasHeight / 2 - objectCenter.y * zoom;
|
|
374
|
+
canvas.setViewportTransform([zoom, 0, 0, zoom, offsetX, offsetY]);
|
|
340
375
|
},
|
|
341
376
|
cleanup() {
|
|
377
|
+
cancelAnimation();
|
|
342
378
|
canvas.off("mouse:wheel", handleWheel);
|
|
343
379
|
canvas.off("mouse:down", panHandlers.handleMouseDown);
|
|
344
380
|
canvas.off("mouse:move", panHandlers.handleMouseMove);
|
|
@@ -351,6 +387,9 @@ function resetViewport(canvas) {
|
|
|
351
387
|
canvas.setViewportTransform([1, 0, 0, 1, 0, 0]);
|
|
352
388
|
}
|
|
353
389
|
|
|
390
|
+
// src/hooks/shared.ts
|
|
391
|
+
import { useMemo } from "react";
|
|
392
|
+
|
|
354
393
|
// src/background.ts
|
|
355
394
|
import { FabricImage, filters } from "fabric";
|
|
356
395
|
function getBackgroundImage(canvas) {
|
|
@@ -481,7 +520,8 @@ function resizeImageUrl(url, options) {
|
|
|
481
520
|
async function setBackgroundImage(canvas, url, options) {
|
|
482
521
|
const prevContrast = options?.preserveContrast ? getBackgroundContrast(canvas) : void 0;
|
|
483
522
|
let imageUrl = url;
|
|
484
|
-
|
|
523
|
+
const hasResizeOptions = options?.maxSize !== void 0 || options?.minSize !== void 0;
|
|
524
|
+
if (hasResizeOptions) {
|
|
485
525
|
const result = await resizeImageUrl(url, options);
|
|
486
526
|
imageUrl = result.url;
|
|
487
527
|
}
|
|
@@ -499,29 +539,35 @@ function syncZoom(canvasRef, setZoom) {
|
|
|
499
539
|
const canvas = canvasRef.current;
|
|
500
540
|
if (canvas) setZoom(canvas.getZoom());
|
|
501
541
|
}
|
|
502
|
-
function
|
|
503
|
-
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
542
|
+
function useViewportActions(canvasRef, viewportRef, setZoom) {
|
|
543
|
+
return useMemo(() => {
|
|
544
|
+
const resetViewport2 = () => {
|
|
545
|
+
const canvas = canvasRef.current;
|
|
546
|
+
if (!canvas) return;
|
|
547
|
+
if (canvas.backgroundImage) {
|
|
548
|
+
fitViewportToBackground(canvas);
|
|
549
|
+
} else {
|
|
550
|
+
resetViewport(canvas);
|
|
551
|
+
}
|
|
552
|
+
setZoom(canvas.getZoom());
|
|
553
|
+
};
|
|
554
|
+
const zoomIn = (step) => {
|
|
555
|
+
viewportRef.current?.zoomIn(step);
|
|
556
|
+
syncZoom(canvasRef, setZoom);
|
|
557
|
+
};
|
|
558
|
+
const zoomOut = (step) => {
|
|
559
|
+
viewportRef.current?.zoomOut(step);
|
|
560
|
+
syncZoom(canvasRef, setZoom);
|
|
561
|
+
};
|
|
562
|
+
const panToObject = (object, panOpts) => {
|
|
563
|
+
viewportRef.current?.panToObject(object, panOpts);
|
|
564
|
+
};
|
|
565
|
+
const zoomToFit = (object, fitOpts) => {
|
|
566
|
+
viewportRef.current?.zoomToFit(object, fitOpts);
|
|
567
|
+
syncZoom(canvasRef, setZoom);
|
|
568
|
+
};
|
|
569
|
+
return { resetViewport: resetViewport2, zoomIn, zoomOut, panToObject, zoomToFit };
|
|
570
|
+
}, []);
|
|
525
571
|
}
|
|
526
572
|
function resolveAlignmentEnabled(enableAlignment, alignmentProp) {
|
|
527
573
|
if (enableAlignment !== void 0) return enableAlignment;
|
|
@@ -1588,7 +1634,7 @@ function createPolygonAtPoint(canvas, point, options) {
|
|
|
1588
1634
|
canvas.requestRenderAll();
|
|
1589
1635
|
return polygon;
|
|
1590
1636
|
}
|
|
1591
|
-
function createPolygonFromDrag(canvas, start, end,
|
|
1637
|
+
function createPolygonFromDrag(canvas, start, end, options) {
|
|
1592
1638
|
const width = Math.abs(end.x - start.x);
|
|
1593
1639
|
const height = Math.abs(end.y - start.y);
|
|
1594
1640
|
const left = Math.min(start.x, end.x) + width / 2;
|
|
@@ -1600,16 +1646,16 @@ function createPolygonFromDrag(canvas, start, end, style) {
|
|
|
1600
1646
|
{ x: width, y: height },
|
|
1601
1647
|
{ x: 0, y: height }
|
|
1602
1648
|
],
|
|
1603
|
-
{ ...DEFAULT_SHAPE_STYLE, left, top, ...
|
|
1649
|
+
{ ...DEFAULT_SHAPE_STYLE, left, top, ...options }
|
|
1604
1650
|
);
|
|
1605
1651
|
canvas.add(polygon);
|
|
1606
1652
|
canvas.requestRenderAll();
|
|
1607
1653
|
return polygon;
|
|
1608
1654
|
}
|
|
1609
|
-
function createPolygonFromVertices(canvas, points,
|
|
1655
|
+
function createPolygonFromVertices(canvas, points, options) {
|
|
1610
1656
|
const polygon = new Polygon2(
|
|
1611
1657
|
points.map((p) => ({ x: p.x, y: p.y })),
|
|
1612
|
-
{ ...DEFAULT_SHAPE_STYLE, ...
|
|
1658
|
+
{ ...DEFAULT_SHAPE_STYLE, ...options }
|
|
1613
1659
|
);
|
|
1614
1660
|
canvas.add(polygon);
|
|
1615
1661
|
canvas.requestRenderAll();
|
|
@@ -1733,14 +1779,14 @@ function enableDrawToCreate(canvas, options) {
|
|
|
1733
1779
|
const finalize = () => {
|
|
1734
1780
|
removePreviewElements();
|
|
1735
1781
|
snapping.clearSnapResult();
|
|
1736
|
-
const
|
|
1782
|
+
const obj = options?.factory ? options.factory(canvas, [...points]) : createPolygonFromVertices(canvas, points, options?.style);
|
|
1737
1783
|
if (options?.data) {
|
|
1738
|
-
|
|
1784
|
+
obj.data = options.data;
|
|
1739
1785
|
}
|
|
1740
1786
|
canvas.selection = previousSelection;
|
|
1741
1787
|
canvas.requestRenderAll();
|
|
1742
1788
|
restoreViewport(options?.viewport);
|
|
1743
|
-
options?.onCreated?.(
|
|
1789
|
+
options?.onCreated?.(obj);
|
|
1744
1790
|
points.length = 0;
|
|
1745
1791
|
};
|
|
1746
1792
|
const handleMouseDown = (event) => {
|
|
@@ -2052,16 +2098,19 @@ function enableVertexEdit(canvas, polygon, options, onExit) {
|
|
|
2052
2098
|
});
|
|
2053
2099
|
canvas.discardActiveObject();
|
|
2054
2100
|
canvas.requestRenderAll();
|
|
2055
|
-
onExit?.();
|
|
2101
|
+
(options?.onExit ?? onExit)?.();
|
|
2056
2102
|
}
|
|
2057
2103
|
return cleanup;
|
|
2058
2104
|
}
|
|
2059
2105
|
|
|
2060
2106
|
// src/serialization.ts
|
|
2061
|
-
import {
|
|
2107
|
+
import {
|
|
2108
|
+
FabricImage as FabricImage2,
|
|
2109
|
+
Rect as Rect5
|
|
2110
|
+
} from "fabric";
|
|
2062
2111
|
var strokeBaseMap = /* @__PURE__ */ new WeakMap();
|
|
2063
2112
|
var borderRadiusBaseMap = /* @__PURE__ */ new WeakMap();
|
|
2064
|
-
var
|
|
2113
|
+
var DEFAULT_VIEW_BORDER_RADIUS = 4;
|
|
2065
2114
|
function enableScaledStrokes(canvas) {
|
|
2066
2115
|
function applyScaledStrokes() {
|
|
2067
2116
|
const zoom = canvas.getZoom();
|
|
@@ -2086,13 +2135,14 @@ function enableScaledStrokes(canvas) {
|
|
|
2086
2135
|
});
|
|
2087
2136
|
};
|
|
2088
2137
|
}
|
|
2089
|
-
function enableScaledBorderRadius(canvas) {
|
|
2138
|
+
function enableScaledBorderRadius(canvas, options) {
|
|
2139
|
+
const radius = options?.radius ?? DEFAULT_VIEW_BORDER_RADIUS;
|
|
2090
2140
|
function applyScaledBorderRadius() {
|
|
2091
2141
|
canvas.forEachObject((obj) => {
|
|
2092
2142
|
if (!(obj instanceof Rect5)) return;
|
|
2093
2143
|
if (!borderRadiusBaseMap.has(obj)) return;
|
|
2094
|
-
const rx =
|
|
2095
|
-
const ry =
|
|
2144
|
+
const rx = radius / (obj.scaleX ?? 1);
|
|
2145
|
+
const ry = radius / (obj.scaleY ?? 1);
|
|
2096
2146
|
obj.set({ rx, ry });
|
|
2097
2147
|
});
|
|
2098
2148
|
}
|
|
@@ -2155,6 +2205,20 @@ function serializeCanvas(canvas, options) {
|
|
|
2155
2205
|
async function loadCanvas(canvas, json, options) {
|
|
2156
2206
|
await canvas.loadFromJSON(json);
|
|
2157
2207
|
canvas.backgroundColor = "";
|
|
2208
|
+
delete canvas.backgroundFilters;
|
|
2209
|
+
const bg = canvas.backgroundImage;
|
|
2210
|
+
if (bg instanceof FabricImage2) {
|
|
2211
|
+
if (bg.originX !== "center" || bg.originY !== "center") {
|
|
2212
|
+
const center = bg.getCenterPoint();
|
|
2213
|
+
bg.set({
|
|
2214
|
+
originX: "center",
|
|
2215
|
+
originY: "center",
|
|
2216
|
+
left: center.x,
|
|
2217
|
+
top: center.y
|
|
2218
|
+
});
|
|
2219
|
+
bg.setCoords();
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2158
2222
|
if (options?.filter) {
|
|
2159
2223
|
const toRemove = [];
|
|
2160
2224
|
canvas.forEachObject((obj) => {
|
|
@@ -2174,14 +2238,19 @@ async function loadCanvas(canvas, json, options) {
|
|
|
2174
2238
|
obj.setCoords();
|
|
2175
2239
|
});
|
|
2176
2240
|
canvas.forEachObject((obj) => {
|
|
2241
|
+
const data = obj.data;
|
|
2242
|
+
if (data?.strokeWidthBase !== void 0) {
|
|
2243
|
+
delete data.strokeWidthBase;
|
|
2244
|
+
}
|
|
2177
2245
|
obj.set(DEFAULT_CONTROL_STYLE);
|
|
2178
2246
|
if (obj.shapeType === "circle" && obj instanceof Rect5) {
|
|
2179
2247
|
restoreCircleConstraints(obj);
|
|
2180
2248
|
}
|
|
2181
|
-
|
|
2249
|
+
const borderRadius = options?.borderRadius ?? DEFAULT_VIEW_BORDER_RADIUS;
|
|
2250
|
+
if (borderRadius !== false && obj instanceof Rect5 && obj.shapeType !== "circle" && obj.data?.type !== "DEVICE") {
|
|
2182
2251
|
borderRadiusBaseMap.set(obj, { rx: obj.rx ?? 0, ry: obj.ry ?? 0 });
|
|
2183
|
-
const rx =
|
|
2184
|
-
const ry =
|
|
2252
|
+
const rx = borderRadius / (obj.scaleX ?? 1);
|
|
2253
|
+
const ry = borderRadius / (obj.scaleY ?? 1);
|
|
2185
2254
|
obj.set({ rx, ry });
|
|
2186
2255
|
}
|
|
2187
2256
|
});
|
|
@@ -2189,6 +2258,87 @@ async function loadCanvas(canvas, json, options) {
|
|
|
2189
2258
|
return canvas.getObjects();
|
|
2190
2259
|
}
|
|
2191
2260
|
|
|
2261
|
+
// src/history.ts
|
|
2262
|
+
function createHistoryTracker(canvas, options) {
|
|
2263
|
+
const maxSize = options?.maxSize ?? 50;
|
|
2264
|
+
const debounceMs = options?.debounce ?? 300;
|
|
2265
|
+
const snapshots = [];
|
|
2266
|
+
let currentIndex = -1;
|
|
2267
|
+
let isUndoRedo = false;
|
|
2268
|
+
let debounceTimer = null;
|
|
2269
|
+
function captureSnapshot() {
|
|
2270
|
+
if (isUndoRedo) return;
|
|
2271
|
+
const snapshot = serializeCanvas(canvas);
|
|
2272
|
+
if (currentIndex < snapshots.length - 1) {
|
|
2273
|
+
snapshots.length = currentIndex + 1;
|
|
2274
|
+
}
|
|
2275
|
+
snapshots.push(snapshot);
|
|
2276
|
+
if (snapshots.length > maxSize) {
|
|
2277
|
+
snapshots.shift();
|
|
2278
|
+
}
|
|
2279
|
+
currentIndex = snapshots.length - 1;
|
|
2280
|
+
}
|
|
2281
|
+
function debouncedCapture() {
|
|
2282
|
+
if (debounceTimer !== null) {
|
|
2283
|
+
clearTimeout(debounceTimer);
|
|
2284
|
+
}
|
|
2285
|
+
debounceTimer = setTimeout(() => {
|
|
2286
|
+
debounceTimer = null;
|
|
2287
|
+
captureSnapshot();
|
|
2288
|
+
}, debounceMs);
|
|
2289
|
+
}
|
|
2290
|
+
const onChange = () => {
|
|
2291
|
+
if (!isUndoRedo) debouncedCapture();
|
|
2292
|
+
};
|
|
2293
|
+
canvas.on("object:added", onChange);
|
|
2294
|
+
canvas.on("object:modified", onChange);
|
|
2295
|
+
canvas.on("object:removed", onChange);
|
|
2296
|
+
async function loadSnapshot(index) {
|
|
2297
|
+
if (index < 0 || index >= snapshots.length) return;
|
|
2298
|
+
isUndoRedo = true;
|
|
2299
|
+
currentIndex = index;
|
|
2300
|
+
try {
|
|
2301
|
+
await loadCanvas(canvas, snapshots[index]);
|
|
2302
|
+
} finally {
|
|
2303
|
+
isUndoRedo = false;
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
return {
|
|
2307
|
+
async undo() {
|
|
2308
|
+
if (currentIndex <= 0) return;
|
|
2309
|
+
await loadSnapshot(currentIndex - 1);
|
|
2310
|
+
},
|
|
2311
|
+
async redo() {
|
|
2312
|
+
if (currentIndex >= snapshots.length - 1) return;
|
|
2313
|
+
await loadSnapshot(currentIndex + 1);
|
|
2314
|
+
},
|
|
2315
|
+
canUndo() {
|
|
2316
|
+
return currentIndex > 0;
|
|
2317
|
+
},
|
|
2318
|
+
canRedo() {
|
|
2319
|
+
return currentIndex < snapshots.length - 1;
|
|
2320
|
+
},
|
|
2321
|
+
pushSnapshot() {
|
|
2322
|
+
if (debounceTimer !== null) {
|
|
2323
|
+
clearTimeout(debounceTimer);
|
|
2324
|
+
debounceTimer = null;
|
|
2325
|
+
}
|
|
2326
|
+
captureSnapshot();
|
|
2327
|
+
},
|
|
2328
|
+
cleanup() {
|
|
2329
|
+
if (debounceTimer !== null) {
|
|
2330
|
+
clearTimeout(debounceTimer);
|
|
2331
|
+
debounceTimer = null;
|
|
2332
|
+
}
|
|
2333
|
+
canvas.off("object:added", onChange);
|
|
2334
|
+
canvas.off("object:modified", onChange);
|
|
2335
|
+
canvas.off("object:removed", onChange);
|
|
2336
|
+
snapshots.length = 0;
|
|
2337
|
+
currentIndex = -1;
|
|
2338
|
+
}
|
|
2339
|
+
};
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2192
2342
|
// src/hooks/useEditCanvas.ts
|
|
2193
2343
|
function useEditCanvas(options) {
|
|
2194
2344
|
const canvasRef = useRef2(null);
|
|
@@ -2198,11 +2348,19 @@ function useEditCanvas(options) {
|
|
|
2198
2348
|
const modeCleanupRef = useRef2(null);
|
|
2199
2349
|
const vertexEditCleanupRef = useRef2(null);
|
|
2200
2350
|
const keyboardCleanupRef = useRef2(null);
|
|
2351
|
+
const historyRef = useRef2(null);
|
|
2352
|
+
const optionsRef = useRef2(options);
|
|
2353
|
+
optionsRef.current = options;
|
|
2354
|
+
const savedSelectabilityRef = useRef2(
|
|
2355
|
+
/* @__PURE__ */ new WeakMap()
|
|
2356
|
+
);
|
|
2201
2357
|
const [zoom, setZoom] = useState(1);
|
|
2202
2358
|
const [selected, setSelected] = useState([]);
|
|
2203
2359
|
const [viewportMode, setViewportModeState] = useState("select");
|
|
2204
2360
|
const [isEditingVertices, setIsEditingVertices] = useState(false);
|
|
2205
2361
|
const [isDirty, setIsDirty] = useState(false);
|
|
2362
|
+
const [canUndo, setCanUndo] = useState(false);
|
|
2363
|
+
const [canRedo, setCanRedo] = useState(false);
|
|
2206
2364
|
const setMode = useCallback((setup) => {
|
|
2207
2365
|
vertexEditCleanupRef.current?.();
|
|
2208
2366
|
vertexEditCleanupRef.current = null;
|
|
@@ -2214,10 +2372,17 @@ function useEditCanvas(options) {
|
|
|
2214
2372
|
if (setup === null) {
|
|
2215
2373
|
canvas.selection = true;
|
|
2216
2374
|
canvas.forEachObject((obj) => {
|
|
2217
|
-
|
|
2218
|
-
obj.
|
|
2375
|
+
const saved = savedSelectabilityRef.current.get(obj);
|
|
2376
|
+
obj.selectable = saved?.selectable ?? true;
|
|
2377
|
+
obj.evented = saved?.evented ?? true;
|
|
2219
2378
|
});
|
|
2220
2379
|
} else {
|
|
2380
|
+
canvas.forEachObject((obj) => {
|
|
2381
|
+
savedSelectabilityRef.current.set(obj, {
|
|
2382
|
+
selectable: obj.selectable,
|
|
2383
|
+
evented: obj.evented
|
|
2384
|
+
});
|
|
2385
|
+
});
|
|
2221
2386
|
canvas.selection = false;
|
|
2222
2387
|
canvas.forEachObject((obj) => {
|
|
2223
2388
|
obj.selectable = false;
|
|
@@ -2232,34 +2397,38 @@ function useEditCanvas(options) {
|
|
|
2232
2397
|
const onReady = useCallback(
|
|
2233
2398
|
(canvas) => {
|
|
2234
2399
|
canvasRef.current = canvas;
|
|
2235
|
-
|
|
2400
|
+
const opts = optionsRef.current;
|
|
2401
|
+
if (opts?.scaledStrokes !== false) {
|
|
2236
2402
|
enableScaledStrokes(canvas);
|
|
2237
2403
|
}
|
|
2238
|
-
|
|
2239
|
-
|
|
2404
|
+
if (opts?.borderRadius !== false) {
|
|
2405
|
+
const borderRadiusOpts = typeof opts?.borderRadius === "number" ? { radius: opts.borderRadius } : void 0;
|
|
2406
|
+
enableScaledBorderRadius(canvas, borderRadiusOpts);
|
|
2407
|
+
}
|
|
2408
|
+
if (opts?.keyboardShortcuts !== false) {
|
|
2240
2409
|
keyboardCleanupRef.current = enableKeyboardShortcuts(canvas);
|
|
2241
2410
|
}
|
|
2242
|
-
setCanvasAlignmentEnabled(canvas,
|
|
2243
|
-
if (
|
|
2411
|
+
setCanvasAlignmentEnabled(canvas, opts?.enableAlignment);
|
|
2412
|
+
if (opts?.panAndZoom !== false) {
|
|
2244
2413
|
viewportRef.current = enablePanAndZoom(
|
|
2245
2414
|
canvas,
|
|
2246
|
-
typeof
|
|
2415
|
+
typeof opts?.panAndZoom === "object" ? opts.panAndZoom : void 0
|
|
2247
2416
|
);
|
|
2248
2417
|
}
|
|
2249
2418
|
const alignmentEnabled = resolveAlignmentEnabled(
|
|
2250
|
-
|
|
2251
|
-
|
|
2419
|
+
opts?.enableAlignment,
|
|
2420
|
+
opts?.alignment
|
|
2252
2421
|
);
|
|
2253
2422
|
if (alignmentEnabled) {
|
|
2254
2423
|
alignmentCleanupRef.current = enableObjectAlignment(
|
|
2255
2424
|
canvas,
|
|
2256
|
-
typeof
|
|
2425
|
+
typeof opts?.alignment === "object" ? opts.alignment : void 0
|
|
2257
2426
|
);
|
|
2258
2427
|
}
|
|
2259
|
-
if (
|
|
2428
|
+
if (opts?.rotationSnap !== false) {
|
|
2260
2429
|
rotationSnapCleanupRef.current = enableRotationSnap(
|
|
2261
2430
|
canvas,
|
|
2262
|
-
typeof
|
|
2431
|
+
typeof opts?.rotationSnap === "object" ? opts.rotationSnap : void 0
|
|
2263
2432
|
);
|
|
2264
2433
|
}
|
|
2265
2434
|
canvas.on("mouse:wheel", () => {
|
|
@@ -2274,41 +2443,60 @@ function useEditCanvas(options) {
|
|
|
2274
2443
|
canvas.on("selection:cleared", () => {
|
|
2275
2444
|
setSelected([]);
|
|
2276
2445
|
});
|
|
2277
|
-
if (
|
|
2446
|
+
if (opts?.trackChanges) {
|
|
2278
2447
|
canvas.on("object:added", () => setIsDirty(true));
|
|
2279
2448
|
canvas.on("object:removed", () => setIsDirty(true));
|
|
2280
2449
|
canvas.on("object:modified", () => setIsDirty(true));
|
|
2281
2450
|
}
|
|
2282
|
-
if (
|
|
2283
|
-
const
|
|
2451
|
+
if (opts?.history) {
|
|
2452
|
+
const syncHistoryState = () => {
|
|
2453
|
+
const h = historyRef.current;
|
|
2454
|
+
if (!h) return;
|
|
2455
|
+
setTimeout(() => {
|
|
2456
|
+
setCanUndo(h.canUndo());
|
|
2457
|
+
setCanRedo(h.canRedo());
|
|
2458
|
+
}, 350);
|
|
2459
|
+
};
|
|
2460
|
+
canvas.on("object:added", syncHistoryState);
|
|
2461
|
+
canvas.on("object:removed", syncHistoryState);
|
|
2462
|
+
canvas.on("object:modified", syncHistoryState);
|
|
2463
|
+
}
|
|
2464
|
+
if (opts?.vertexEdit !== false) {
|
|
2465
|
+
const vertexOpts = typeof opts?.vertexEdit === "object" ? opts.vertexEdit : void 0;
|
|
2284
2466
|
canvas.on("mouse:dblclick", (e) => {
|
|
2285
2467
|
if (e.target && e.target instanceof Polygon4) {
|
|
2286
2468
|
vertexEditCleanupRef.current?.();
|
|
2287
|
-
vertexEditCleanupRef.current = enableVertexEdit(
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
vertexOpts,
|
|
2291
|
-
() => {
|
|
2469
|
+
vertexEditCleanupRef.current = enableVertexEdit(canvas, e.target, {
|
|
2470
|
+
...vertexOpts,
|
|
2471
|
+
onExit: () => {
|
|
2292
2472
|
vertexEditCleanupRef.current = null;
|
|
2293
2473
|
setIsEditingVertices(false);
|
|
2294
2474
|
}
|
|
2295
|
-
);
|
|
2475
|
+
});
|
|
2296
2476
|
setIsEditingVertices(true);
|
|
2297
2477
|
}
|
|
2298
2478
|
});
|
|
2299
2479
|
}
|
|
2300
|
-
|
|
2301
|
-
|
|
2480
|
+
if (opts?.history) {
|
|
2481
|
+
const historyOpts = typeof opts.history === "object" ? opts.history : void 0;
|
|
2482
|
+
historyRef.current = createHistoryTracker(canvas, historyOpts);
|
|
2483
|
+
}
|
|
2484
|
+
const onReadyResult = opts?.onReady?.(canvas);
|
|
2485
|
+
if (opts?.autoFitToBackground !== false) {
|
|
2302
2486
|
Promise.resolve(onReadyResult).then(() => {
|
|
2303
2487
|
if (canvas.backgroundImage) {
|
|
2304
2488
|
fitViewportToBackground(canvas);
|
|
2305
2489
|
syncZoom(canvasRef, setZoom);
|
|
2306
2490
|
}
|
|
2491
|
+
historyRef.current?.pushSnapshot();
|
|
2492
|
+
});
|
|
2493
|
+
} else {
|
|
2494
|
+
Promise.resolve(onReadyResult).then(() => {
|
|
2495
|
+
historyRef.current?.pushSnapshot();
|
|
2307
2496
|
});
|
|
2308
2497
|
}
|
|
2309
2498
|
},
|
|
2310
2499
|
// onReady and panAndZoom are intentionally excluded — we only initialize once
|
|
2311
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2312
2500
|
[]
|
|
2313
2501
|
);
|
|
2314
2502
|
useEffect2(() => {
|
|
@@ -2333,24 +2521,20 @@ function useEditCanvas(options) {
|
|
|
2333
2521
|
viewportRef.current?.setMode(mode);
|
|
2334
2522
|
setViewportModeState(mode);
|
|
2335
2523
|
}, []);
|
|
2336
|
-
const { resetViewport: resetViewport2, zoomIn, zoomOut, panToObject } =
|
|
2337
|
-
canvasRef,
|
|
2338
|
-
viewportRef,
|
|
2339
|
-
setZoom
|
|
2340
|
-
);
|
|
2524
|
+
const { resetViewport: resetViewport2, zoomIn, zoomOut, panToObject, zoomToFit } = useViewportActions(canvasRef, viewportRef, setZoom);
|
|
2341
2525
|
const setBackground = useCallback(
|
|
2342
2526
|
async (url, bgOpts) => {
|
|
2343
2527
|
const canvas = canvasRef.current;
|
|
2344
2528
|
if (!canvas) throw new Error("Canvas not ready");
|
|
2345
|
-
const
|
|
2529
|
+
const opts = optionsRef.current;
|
|
2530
|
+
const resizeOpts = opts?.backgroundResize !== false ? typeof opts?.backgroundResize === "object" ? { ...opts.backgroundResize, ...bgOpts } : { ...bgOpts } : bgOpts?.preserveContrast ? { preserveContrast: true } : void 0;
|
|
2346
2531
|
const img = await setBackgroundImage(canvas, url, resizeOpts);
|
|
2347
|
-
if (
|
|
2532
|
+
if (opts?.autoFitToBackground !== false) {
|
|
2348
2533
|
fitViewportToBackground(canvas);
|
|
2349
2534
|
syncZoom(canvasRef, setZoom);
|
|
2350
2535
|
}
|
|
2351
2536
|
return img;
|
|
2352
2537
|
},
|
|
2353
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2354
2538
|
[]
|
|
2355
2539
|
);
|
|
2356
2540
|
return {
|
|
@@ -2375,7 +2559,9 @@ function useEditCanvas(options) {
|
|
|
2375
2559
|
/** Zoom out from the canvas center. Default step: 0.2. */
|
|
2376
2560
|
zoomOut,
|
|
2377
2561
|
/** Pan the viewport to center on a specific object. */
|
|
2378
|
-
panToObject
|
|
2562
|
+
panToObject,
|
|
2563
|
+
/** Zoom and pan to fit a specific object in the viewport. */
|
|
2564
|
+
zoomToFit
|
|
2379
2565
|
},
|
|
2380
2566
|
/** Whether vertex edit mode is currently active (reactive). */
|
|
2381
2567
|
isEditingVertices,
|
|
@@ -2407,7 +2593,27 @@ function useEditCanvas(options) {
|
|
|
2407
2593
|
/** Whether the canvas has been modified since the last `resetDirty()` call. Requires `trackChanges: true`. */
|
|
2408
2594
|
isDirty,
|
|
2409
2595
|
/** Reset the dirty flag (e.g., after a successful save). */
|
|
2410
|
-
resetDirty: useCallback(() => setIsDirty(false), [])
|
|
2596
|
+
resetDirty: useCallback(() => setIsDirty(false), []),
|
|
2597
|
+
/** Undo the last change. Requires `history: true`. */
|
|
2598
|
+
undo: useCallback(async () => {
|
|
2599
|
+
const h = historyRef.current;
|
|
2600
|
+
if (!h) return;
|
|
2601
|
+
await h.undo();
|
|
2602
|
+
setCanUndo(h.canUndo());
|
|
2603
|
+
setCanRedo(h.canRedo());
|
|
2604
|
+
}, []),
|
|
2605
|
+
/** Redo a previously undone change. Requires `history: true`. */
|
|
2606
|
+
redo: useCallback(async () => {
|
|
2607
|
+
const h = historyRef.current;
|
|
2608
|
+
if (!h) return;
|
|
2609
|
+
await h.redo();
|
|
2610
|
+
setCanUndo(h.canUndo());
|
|
2611
|
+
setCanRedo(h.canRedo());
|
|
2612
|
+
}, []),
|
|
2613
|
+
/** Whether an undo operation is available (reactive). Requires `history: true`. */
|
|
2614
|
+
canUndo,
|
|
2615
|
+
/** Whether a redo operation is available (reactive). Requires `history: true`. */
|
|
2616
|
+
canRedo
|
|
2411
2617
|
};
|
|
2412
2618
|
}
|
|
2413
2619
|
|
|
@@ -2422,16 +2628,22 @@ function lockCanvas(canvas) {
|
|
|
2422
2628
|
function useViewCanvas(options) {
|
|
2423
2629
|
const canvasRef = useRef3(null);
|
|
2424
2630
|
const viewportRef = useRef3(null);
|
|
2631
|
+
const optionsRef = useRef3(options);
|
|
2632
|
+
optionsRef.current = options;
|
|
2425
2633
|
const [zoom, setZoom] = useState2(1);
|
|
2426
2634
|
const onReady = useCallback2(
|
|
2427
2635
|
(canvas) => {
|
|
2428
2636
|
canvasRef.current = canvas;
|
|
2429
|
-
|
|
2637
|
+
const opts = optionsRef.current;
|
|
2638
|
+
if (opts?.scaledStrokes !== false) {
|
|
2430
2639
|
enableScaledStrokes(canvas);
|
|
2431
2640
|
}
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2641
|
+
if (opts?.borderRadius !== false) {
|
|
2642
|
+
const borderRadiusOpts = typeof opts?.borderRadius === "number" ? { radius: opts.borderRadius } : void 0;
|
|
2643
|
+
enableScaledBorderRadius(canvas, borderRadiusOpts);
|
|
2644
|
+
}
|
|
2645
|
+
if (opts?.panAndZoom !== false) {
|
|
2646
|
+
const panAndZoomOpts = typeof opts?.panAndZoom === "object" ? opts.panAndZoom : {};
|
|
2435
2647
|
viewportRef.current = enablePanAndZoom(canvas, {
|
|
2436
2648
|
...panAndZoomOpts,
|
|
2437
2649
|
initialMode: "pan"
|
|
@@ -2444,8 +2656,8 @@ function useViewCanvas(options) {
|
|
|
2444
2656
|
canvas.on("mouse:wheel", () => {
|
|
2445
2657
|
setZoom(canvas.getZoom());
|
|
2446
2658
|
});
|
|
2447
|
-
const onReadyResult =
|
|
2448
|
-
if (
|
|
2659
|
+
const onReadyResult = opts?.onReady?.(canvas);
|
|
2660
|
+
if (opts?.autoFitToBackground !== false) {
|
|
2449
2661
|
Promise.resolve(onReadyResult).then(() => {
|
|
2450
2662
|
if (canvas.backgroundImage) {
|
|
2451
2663
|
fitViewportToBackground(canvas);
|
|
@@ -2455,14 +2667,9 @@ function useViewCanvas(options) {
|
|
|
2455
2667
|
}
|
|
2456
2668
|
},
|
|
2457
2669
|
// onReady and panAndZoom are intentionally excluded — we only initialize once
|
|
2458
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2459
2670
|
[]
|
|
2460
2671
|
);
|
|
2461
|
-
const { resetViewport: resetViewport2, zoomIn, zoomOut, panToObject } =
|
|
2462
|
-
canvasRef,
|
|
2463
|
-
viewportRef,
|
|
2464
|
-
setZoom
|
|
2465
|
-
);
|
|
2672
|
+
const { resetViewport: resetViewport2, zoomIn, zoomOut, panToObject, zoomToFit } = useViewportActions(canvasRef, viewportRef, setZoom);
|
|
2466
2673
|
const findObject = (id) => {
|
|
2467
2674
|
const c = canvasRef.current;
|
|
2468
2675
|
if (!c) return void 0;
|
|
@@ -2526,7 +2733,9 @@ function useViewCanvas(options) {
|
|
|
2526
2733
|
/** Zoom out from the canvas center. Default step: 0.2. */
|
|
2527
2734
|
zoomOut,
|
|
2528
2735
|
/** Pan the viewport to center on a specific object. */
|
|
2529
|
-
panToObject
|
|
2736
|
+
panToObject,
|
|
2737
|
+
/** Zoom and pan to fit a specific object in the viewport. */
|
|
2738
|
+
zoomToFit
|
|
2530
2739
|
},
|
|
2531
2740
|
/** Update a single object's visual style by its `data.id`. */
|
|
2532
2741
|
setObjectStyle,
|
|
@@ -2699,36 +2908,41 @@ function useObjectOverlay(canvasRef, object, options) {
|
|
|
2699
2908
|
const screenCoords = util4.transformPoint(center, vt);
|
|
2700
2909
|
const screenWidth = actualWidth * zoom;
|
|
2701
2910
|
const screenHeight = actualHeight * zoom;
|
|
2702
|
-
el.style.left = `${screenCoords.x - screenWidth / 2}px`;
|
|
2703
|
-
el.style.top = `${screenCoords.y - screenHeight / 2}px`;
|
|
2704
|
-
el.style.width = `${screenWidth}px`;
|
|
2705
|
-
el.style.height = `${screenHeight}px`;
|
|
2706
2911
|
const angle = object.angle ?? 0;
|
|
2707
|
-
el.style.rotate = angle !== 0 ? `${angle}deg` : "";
|
|
2708
2912
|
const opts = optionsRef.current;
|
|
2709
|
-
if (opts?.autoScaleContent) {
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
el.style.
|
|
2713
|
-
|
|
2714
|
-
|
|
2913
|
+
if (opts?.autoScaleContent !== false) {
|
|
2914
|
+
el.style.left = `${screenCoords.x - actualWidth / 2}px`;
|
|
2915
|
+
el.style.top = `${screenCoords.y - actualHeight / 2}px`;
|
|
2916
|
+
el.style.width = `${actualWidth}px`;
|
|
2917
|
+
el.style.height = `${actualHeight}px`;
|
|
2918
|
+
el.style.transformOrigin = "center center";
|
|
2919
|
+
el.style.transform = angle !== 0 ? `scale(${zoom}) rotate(${angle}deg)` : `scale(${zoom})`;
|
|
2920
|
+
el.style.rotate = "";
|
|
2921
|
+
el.style.setProperty("--overlay-scale", String(zoom));
|
|
2922
|
+
if (opts?.textSelector) {
|
|
2923
|
+
const textMinScale = opts?.textMinScale ?? 0.5;
|
|
2715
2924
|
const textEls = el.querySelectorAll(opts.textSelector);
|
|
2716
|
-
const display =
|
|
2925
|
+
const display = zoom < textMinScale ? "none" : "";
|
|
2717
2926
|
textEls.forEach((t) => {
|
|
2718
2927
|
t.style.display = display;
|
|
2719
2928
|
});
|
|
2720
2929
|
}
|
|
2930
|
+
} else {
|
|
2931
|
+
el.style.left = `${screenCoords.x - screenWidth / 2}px`;
|
|
2932
|
+
el.style.top = `${screenCoords.y - screenHeight / 2}px`;
|
|
2933
|
+
el.style.width = `${screenWidth}px`;
|
|
2934
|
+
el.style.height = `${screenHeight}px`;
|
|
2935
|
+
el.style.transform = "";
|
|
2936
|
+
el.style.rotate = angle !== 0 ? `${angle}deg` : "";
|
|
2721
2937
|
}
|
|
2722
2938
|
}
|
|
2723
2939
|
update();
|
|
2724
2940
|
canvas.on("after:render", update);
|
|
2725
|
-
canvas.on("mouse:wheel", update);
|
|
2726
2941
|
object.on("moving", update);
|
|
2727
2942
|
object.on("scaling", update);
|
|
2728
2943
|
object.on("rotating", update);
|
|
2729
2944
|
return () => {
|
|
2730
2945
|
canvas.off("after:render", update);
|
|
2731
|
-
canvas.off("mouse:wheel", update);
|
|
2732
2946
|
object.off("moving", update);
|
|
2733
2947
|
object.off("scaling", update);
|
|
2734
2948
|
object.off("rotating", update);
|
|
@@ -2741,7 +2955,7 @@ function useObjectOverlay(canvasRef, object, options) {
|
|
|
2741
2955
|
import {
|
|
2742
2956
|
Canvas as Canvas2,
|
|
2743
2957
|
FabricObject as FabricObject5,
|
|
2744
|
-
FabricImage as
|
|
2958
|
+
FabricImage as FabricImage3,
|
|
2745
2959
|
Rect as Rect6,
|
|
2746
2960
|
Polygon as Polygon5,
|
|
2747
2961
|
Point as Point9,
|
|
@@ -2756,13 +2970,14 @@ export {
|
|
|
2756
2970
|
DEFAULT_GUIDELINE_SHAPE_STYLE,
|
|
2757
2971
|
DEFAULT_SHAPE_STYLE,
|
|
2758
2972
|
Canvas2 as FabricCanvas,
|
|
2759
|
-
|
|
2973
|
+
FabricImage3 as FabricImage,
|
|
2760
2974
|
FabricObject5 as FabricObject,
|
|
2761
2975
|
Point9 as Point,
|
|
2762
2976
|
Polygon5 as Polygon,
|
|
2763
2977
|
Rect6 as Rect,
|
|
2764
2978
|
createCircle,
|
|
2765
2979
|
createCircleAtPoint,
|
|
2980
|
+
createHistoryTracker,
|
|
2766
2981
|
createPolygon,
|
|
2767
2982
|
createPolygonAtPoint,
|
|
2768
2983
|
createPolygonFromDrag,
|