@mhamz.01/easyflow-whiteboard 2.38.0 → 2.40.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/hooks/usePan.d.ts.map +1 -1
- package/dist/hooks/usePan.js +35 -23
- package/dist/hooks/usePersistance.d.ts.map +1 -1
- package/dist/hooks/usePersistance.js +2 -0
- package/dist/store/whiteboard-store.d.ts +6 -22
- package/dist/store/whiteboard-store.d.ts.map +1 -1
- package/dist/store/whiteboard-store.js +50 -103
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usePan.d.ts","sourceRoot":"","sources":["../../src/hooks/usePan.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"usePan.d.ts","sourceRoot":"","sources":["../../src/hooks/usePan.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,SAAS,EAAE,MAAM,OAAO,CAAC;AACrD,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,UAAU,WAAW;IACnB,YAAY,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACrE,iBAAiB,EAAE,CAAC,QAAQ,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CACjE;AAED,eAAO,MAAM,MAAM,GAAI,8DAKpB,WAAW,SA8Gb,CAAC"}
|
package/dist/hooks/usePan.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
|
-
import { useEffect } from "react";
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
2
|
export const usePan = ({ fabricCanvas, activeTool, handleZoom, setCanvasViewport, }) => {
|
|
3
|
+
// Refs so the effect registers ONCE but always reads latest values.
|
|
4
|
+
// Avoids tearing down/reattaching mouse:down/move/up on every tool change.
|
|
5
|
+
const activeToolRef = useRef(activeTool);
|
|
6
|
+
const handleZoomRef = useRef(handleZoom);
|
|
7
|
+
const setCanvasViewportRef = useRef(setCanvasViewport);
|
|
8
|
+
// Assigned synchronously in render — always fresh before any event fires
|
|
9
|
+
activeToolRef.current = activeTool;
|
|
10
|
+
handleZoomRef.current = handleZoom;
|
|
11
|
+
setCanvasViewportRef.current = setCanvasViewport;
|
|
3
12
|
useEffect(() => {
|
|
4
13
|
const canvas = fabricCanvas.current;
|
|
5
14
|
if (!canvas)
|
|
@@ -11,16 +20,14 @@ export const usePan = ({ fabricCanvas, activeTool, handleZoom, setCanvasViewport
|
|
|
11
20
|
let lastY = 0;
|
|
12
21
|
let lastTouchDistance = 0;
|
|
13
22
|
const onDown = (opt) => {
|
|
14
|
-
if (
|
|
23
|
+
if (activeToolRef.current !== "pan")
|
|
15
24
|
return;
|
|
16
25
|
const e = opt.e;
|
|
17
|
-
// Pinch initialization
|
|
18
26
|
if (e.touches && e.touches.length === 2) {
|
|
19
27
|
isPanning = false;
|
|
20
28
|
lastTouchDistance = Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY);
|
|
21
29
|
return;
|
|
22
30
|
}
|
|
23
|
-
// Pan initialization
|
|
24
31
|
const pointer = e.touches ? e.touches[0] : e;
|
|
25
32
|
isPanning = true;
|
|
26
33
|
lastX = pointer.clientX;
|
|
@@ -28,45 +35,50 @@ export const usePan = ({ fabricCanvas, activeTool, handleZoom, setCanvasViewport
|
|
|
28
35
|
canvas.setCursor("grabbing");
|
|
29
36
|
};
|
|
30
37
|
const onMove = (opt) => {
|
|
31
|
-
if (
|
|
38
|
+
if (activeToolRef.current !== "pan")
|
|
32
39
|
return;
|
|
33
40
|
const e = opt.e;
|
|
34
|
-
// Handle pinch zoom (two fingers)
|
|
35
41
|
if (e.touches && e.touches.length === 2) {
|
|
36
42
|
const currentDistance = Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY);
|
|
37
43
|
if (lastTouchDistance > 0) {
|
|
38
44
|
const zoom = canvas.getZoom();
|
|
39
45
|
const delta = (currentDistance - lastTouchDistance) * 0.01;
|
|
40
|
-
const newZoom = zoom + delta;
|
|
41
46
|
const midX = (e.touches[0].clientX + e.touches[1].clientX) / 2;
|
|
42
47
|
const midY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
|
|
43
48
|
const rect = canvasEl.getBoundingClientRect();
|
|
44
|
-
|
|
49
|
+
handleZoomRef.current(zoom + delta, {
|
|
50
|
+
x: midX - rect.left,
|
|
51
|
+
y: midY - rect.top,
|
|
52
|
+
});
|
|
45
53
|
}
|
|
46
54
|
lastTouchDistance = currentDistance;
|
|
47
55
|
return;
|
|
48
56
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
if (!isPanning)
|
|
58
|
+
return; // early exit — avoids any work on non-pan moves
|
|
59
|
+
const pointer = e.touches ? e.touches[0] : e;
|
|
60
|
+
const vpt = canvas.viewportTransform;
|
|
61
|
+
if (!vpt)
|
|
62
|
+
return;
|
|
63
|
+
// VPT mutation is synchronous — NO rAF here intentionally.
|
|
64
|
+
// rAF delays the mutation by one frame which makes pan feel like 20fps.
|
|
65
|
+
vpt[4] += pointer.clientX - lastX;
|
|
66
|
+
vpt[5] += pointer.clientY - lastY;
|
|
67
|
+
canvas.requestRenderAll();
|
|
68
|
+
lastX = pointer.clientX;
|
|
69
|
+
lastY = pointer.clientY;
|
|
61
70
|
};
|
|
62
71
|
const onUp = () => {
|
|
72
|
+
if (!isPanning && lastTouchDistance === 0)
|
|
73
|
+
return; // nothing to clean up
|
|
63
74
|
const vpt = canvas.viewportTransform;
|
|
64
75
|
if (vpt) {
|
|
65
|
-
|
|
76
|
+
// React state update only on mouse:up — not per-frame during pan
|
|
77
|
+
setCanvasViewportRef.current({ x: vpt[4], y: vpt[5] });
|
|
66
78
|
}
|
|
67
79
|
isPanning = false;
|
|
68
80
|
lastTouchDistance = 0;
|
|
69
|
-
canvas.setCursor(
|
|
81
|
+
canvas.setCursor(activeToolRef.current === "pan" ? "grab" : "default");
|
|
70
82
|
};
|
|
71
83
|
canvas.on("mouse:down", onDown);
|
|
72
84
|
canvas.on("mouse:move", onMove);
|
|
@@ -76,5 +88,5 @@ export const usePan = ({ fabricCanvas, activeTool, handleZoom, setCanvasViewport
|
|
|
76
88
|
canvas.off("mouse:move", onMove);
|
|
77
89
|
canvas.off("mouse:up", onUp);
|
|
78
90
|
};
|
|
79
|
-
}, [
|
|
91
|
+
}, [fabricCanvas]); // registered once per canvas mount
|
|
80
92
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usePersistance.d.ts","sourceRoot":"","sources":["../../src/hooks/usePersistance.ts"],"names":[],"mappings":"AAAA,OAAO,KAAgC,MAAM,OAAO,CAAC;AACrD,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,UAAU,mBAAmB;IAC3B,YAAY,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC7C,KAAK,EAAE,GAAG,EAAE,CAAC;IACb,SAAS,EAAE,GAAG,EAAE,CAAC;IACjB,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,cAAc,EAAE,KAAK,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;IACzC,kBAAkB,EAAE,KAAK,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;CAC9C;AAKD,eAAO,MAAM,cAAc,GAAI,sFAO5B,mBAAmB,
|
|
1
|
+
{"version":3,"file":"usePersistance.d.ts","sourceRoot":"","sources":["../../src/hooks/usePersistance.ts"],"names":[],"mappings":"AAAA,OAAO,KAAgC,MAAM,OAAO,CAAC;AACrD,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,UAAU,mBAAmB;IAC3B,YAAY,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC7C,KAAK,EAAE,GAAG,EAAE,CAAC;IACb,SAAS,EAAE,GAAG,EAAE,CAAC;IACjB,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,cAAc,EAAE,KAAK,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;IACzC,kBAAkB,EAAE,KAAK,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;CAC9C;AAKD,eAAO,MAAM,cAAc,GAAI,sFAO5B,mBAAmB,SAkFrB,CAAC"}
|
|
@@ -12,6 +12,7 @@ export const usePersistence = ({ fabricCanvas, tasks, documents, pushHistory, is
|
|
|
12
12
|
const json = JSON.stringify(canvas.toJSON());
|
|
13
13
|
localStorage.setItem(CANVAS_KEY, json);
|
|
14
14
|
pushHistory(json);
|
|
15
|
+
console.log("💾 Canvas content saving to storage is: " + json);
|
|
15
16
|
console.log("💾 Canvas saved via requestIdleCallback");
|
|
16
17
|
}
|
|
17
18
|
catch (err) {
|
|
@@ -34,6 +35,7 @@ export const usePersistence = ({ fabricCanvas, tasks, documents, pushHistory, is
|
|
|
34
35
|
try {
|
|
35
36
|
const nodesData = { tasks, documents };
|
|
36
37
|
localStorage.setItem(NODES_KEY, JSON.stringify(nodesData));
|
|
38
|
+
console.log("💾 Nodes saving to storage is: " + JSON.stringify(nodesData));
|
|
37
39
|
console.log("💾 Custom nodes saved");
|
|
38
40
|
}
|
|
39
41
|
catch (err) {
|
|
@@ -62,38 +62,22 @@ interface WhiteboardState {
|
|
|
62
62
|
addCanvasObject: (obj: FabricObject) => void;
|
|
63
63
|
removeCanvasObject: (obj: FabricObject) => void;
|
|
64
64
|
clearCanvasObjects: () => void;
|
|
65
|
+
selectedObjects: FabricObject[];
|
|
66
|
+
setSelectedObjects: (objects: FabricObject[]) => void;
|
|
65
67
|
toolOptions: ToolOptions;
|
|
66
68
|
setToolOption: <T extends keyof ToolOptions>(tool: T, option: keyof ToolOptions[T], value: any) => void;
|
|
67
69
|
history: string[];
|
|
68
70
|
historyIndex: number;
|
|
69
71
|
canUndo: boolean;
|
|
70
72
|
canRedo: boolean;
|
|
71
|
-
pushHistory: (
|
|
73
|
+
pushHistory: (snapshot: string) => void;
|
|
72
74
|
undo: () => string | null;
|
|
73
75
|
redo: () => string | null;
|
|
74
|
-
setCanUndo: (
|
|
75
|
-
setCanRedo: (
|
|
76
|
+
setCanUndo: (v: boolean) => void;
|
|
77
|
+
setCanRedo: (v: boolean) => void;
|
|
76
78
|
zoom: number;
|
|
77
79
|
setZoom: (zoom: number) => void;
|
|
78
|
-
selectedObjects: FabricObject[];
|
|
79
|
-
setSelectedObjects: (objects: FabricObject[]) => void;
|
|
80
80
|
}
|
|
81
|
-
export declare const useWhiteboardStore: import("zustand").UseBoundStore<
|
|
82
|
-
setState(partial: WhiteboardState | Partial<WhiteboardState> | ((state: WhiteboardState) => WhiteboardState | Partial<WhiteboardState>), replace?: false | undefined, action?: (string | {
|
|
83
|
-
[x: string]: unknown;
|
|
84
|
-
[x: number]: unknown;
|
|
85
|
-
[x: symbol]: unknown;
|
|
86
|
-
type: string;
|
|
87
|
-
}) | undefined): void;
|
|
88
|
-
setState(state: WhiteboardState | ((state: WhiteboardState) => WhiteboardState), replace: true, action?: (string | {
|
|
89
|
-
[x: string]: unknown;
|
|
90
|
-
[x: number]: unknown;
|
|
91
|
-
[x: symbol]: unknown;
|
|
92
|
-
type: string;
|
|
93
|
-
}) | undefined): void;
|
|
94
|
-
devtools: {
|
|
95
|
-
cleanup: () => void;
|
|
96
|
-
};
|
|
97
|
-
}>;
|
|
81
|
+
export declare const useWhiteboardStore: import("zustand").UseBoundStore<import("zustand").StoreApi<WhiteboardState>>;
|
|
98
82
|
export {};
|
|
99
83
|
//# sourceMappingURL=whiteboard-store.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"whiteboard-store.d.ts","sourceRoot":"","sources":["../../src/store/whiteboard-store.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"whiteboard-store.d.ts","sourceRoot":"","sources":["../../src/store/whiteboard-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,137 +1,84 @@
|
|
|
1
1
|
import { create } from "zustand";
|
|
2
|
-
|
|
2
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
3
|
+
// Each history entry is a full JSON canvas snapshot (can be 100KB+).
|
|
4
|
+
// Without a cap: 200 actions × 100KB = 20MB of undo history in memory.
|
|
5
|
+
const MAX_HISTORY = 50;
|
|
3
6
|
const defaultToolOptions = {
|
|
4
|
-
pen: {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
},
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
strokeWidth: 2,
|
|
14
|
-
strokeDashArray: null,
|
|
15
|
-
},
|
|
16
|
-
circle: {
|
|
17
|
-
fillColor: "transparent",
|
|
18
|
-
strokeColor: "#ffffff",
|
|
19
|
-
strokeWidth: 2,
|
|
20
|
-
strokeDashArray: null,
|
|
21
|
-
},
|
|
22
|
-
frame: {
|
|
23
|
-
fillColor: "#FFFFFF",
|
|
24
|
-
strokeColor: "#E5E7EB",
|
|
25
|
-
strokeWidth: 1,
|
|
26
|
-
strokeDashArray: null,
|
|
27
|
-
},
|
|
28
|
-
text: {
|
|
29
|
-
fontSize: 24,
|
|
30
|
-
fontFamily: "cursive",
|
|
31
|
-
color: "#ffffff",
|
|
32
|
-
fontWeight: "400",
|
|
33
|
-
textAlign: "left",
|
|
34
|
-
},
|
|
35
|
-
image: {
|
|
36
|
-
opacity: 1,
|
|
37
|
-
filters: [],
|
|
38
|
-
},
|
|
39
|
-
eraser: {
|
|
40
|
-
size: 20,
|
|
41
|
-
},
|
|
42
|
-
line: {
|
|
43
|
-
strokeColor: "#ffffff",
|
|
44
|
-
strokeWidth: 2,
|
|
45
|
-
strokeDashArray: null,
|
|
46
|
-
}, // ← ADD THIS
|
|
47
|
-
arrow: {
|
|
48
|
-
strokeColor: "#ffffff",
|
|
49
|
-
strokeWidth: 2,
|
|
50
|
-
strokeDashArray: null,
|
|
51
|
-
}, // ← ADD THIS
|
|
7
|
+
pen: { color: "#ffffff", strokeWidth: 2, opacity: 1, strokeDashArray: null },
|
|
8
|
+
rectangle: { fillColor: "transparent", strokeColor: "#ffffff", strokeWidth: 2, strokeDashArray: null },
|
|
9
|
+
circle: { fillColor: "transparent", strokeColor: "#ffffff", strokeWidth: 2, strokeDashArray: null },
|
|
10
|
+
frame: { fillColor: "#FFFFFF", strokeColor: "#E5E7EB", strokeWidth: 1, strokeDashArray: null },
|
|
11
|
+
text: { fontSize: 24, fontFamily: "cursive", color: "#ffffff", fontWeight: "400", textAlign: "left" },
|
|
12
|
+
image: { opacity: 1, filters: [] },
|
|
13
|
+
eraser: { size: 20 },
|
|
14
|
+
line: { strokeColor: "#ffffff", strokeWidth: 2, strokeDashArray: null },
|
|
15
|
+
arrow: { strokeColor: "#ffffff", strokeWidth: 2, strokeDashArray: null },
|
|
52
16
|
};
|
|
53
|
-
|
|
54
|
-
|
|
17
|
+
// ─── Store ────────────────────────────────────────────────────────────────────
|
|
18
|
+
export const useWhiteboardStore = create()((set, get) => ({
|
|
19
|
+
// ── Tool ──────────────────────────────────────────────────────────────────
|
|
55
20
|
activeTool: "select",
|
|
56
21
|
setActiveTool: (tool) => set({ activeTool: tool }),
|
|
57
|
-
//
|
|
22
|
+
// ── Dropdown ──────────────────────────────────────────────────────────────
|
|
23
|
+
activeDropdown: null,
|
|
24
|
+
setActiveDropdown: (id) => set({ activeDropdown: id }),
|
|
25
|
+
// ── Selected object type ──────────────────────────────────────────────────
|
|
58
26
|
selectedObjectType: null,
|
|
59
27
|
setSelectedObjectType: (type) => set({ selectedObjectType: type }),
|
|
60
|
-
// Canvas objects
|
|
28
|
+
// ── Canvas objects ────────────────────────────────────────────────────────
|
|
29
|
+
// NOTE: Fabric instances have circular refs and DOM refs — they cannot be
|
|
30
|
+
// serialized. If you add devtools back, exclude this slice or it will throw.
|
|
31
|
+
// Using concat() instead of spread avoids the intermediate spread allocation.
|
|
61
32
|
canvasObjects: [],
|
|
62
|
-
addCanvasObject: (obj) => set((
|
|
63
|
-
|
|
64
|
-
})),
|
|
65
|
-
removeCanvasObject: (obj) => set((state) => ({
|
|
66
|
-
canvasObjects: state.canvasObjects.filter((o) => o !== obj),
|
|
67
|
-
})),
|
|
33
|
+
addCanvasObject: (obj) => set((s) => ({ canvasObjects: s.canvasObjects.concat(obj) })),
|
|
34
|
+
removeCanvasObject: (obj) => set((s) => ({ canvasObjects: s.canvasObjects.filter((o) => o !== obj) })),
|
|
68
35
|
clearCanvasObjects: () => set({ canvasObjects: [] }),
|
|
69
|
-
//
|
|
36
|
+
// ── Selected objects ──────────────────────────────────────────────────────
|
|
37
|
+
selectedObjects: [],
|
|
38
|
+
setSelectedObjects: (objects) => set({ selectedObjects: objects }),
|
|
39
|
+
// ── Tool options ──────────────────────────────────────────────────────────
|
|
70
40
|
toolOptions: defaultToolOptions,
|
|
71
|
-
setToolOption: (tool, option, value) => set((
|
|
41
|
+
setToolOption: (tool, option, value) => set((s) => ({
|
|
72
42
|
toolOptions: {
|
|
73
|
-
...
|
|
74
|
-
[tool]: {
|
|
75
|
-
...state.toolOptions[tool],
|
|
76
|
-
[option]: value,
|
|
77
|
-
},
|
|
43
|
+
...s.toolOptions,
|
|
44
|
+
[tool]: { ...s.toolOptions[tool], [option]: value },
|
|
78
45
|
},
|
|
79
46
|
})),
|
|
80
|
-
// History
|
|
47
|
+
// ── History ───────────────────────────────────────────────────────────────
|
|
81
48
|
history: [],
|
|
82
49
|
historyIndex: -1,
|
|
83
50
|
canUndo: false,
|
|
84
51
|
canRedo: false,
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
// Can undo if there is at least 1 previous state (index > 0)
|
|
94
|
-
canUndo: newHistory.length > 1,
|
|
95
|
-
canRedo: false,
|
|
96
|
-
});
|
|
97
|
-
},
|
|
98
|
-
// Sets the selected dropdown (task/document) - used to auto-switch back to select tool when closing
|
|
99
|
-
activeDropdown: null,
|
|
100
|
-
setActiveDropdown: (id) => set({
|
|
101
|
-
activeDropdown: id,
|
|
52
|
+
// Uses set() updater (single atomic write) instead of get() + set() which
|
|
53
|
+
// could interleave in React concurrent mode.
|
|
54
|
+
pushHistory: (snapshot) => set((s) => {
|
|
55
|
+
const trimmed = s.history.slice(0, s.historyIndex + 1);
|
|
56
|
+
trimmed.push(snapshot);
|
|
57
|
+
const capped = trimmed.length > MAX_HISTORY ? trimmed.slice(-MAX_HISTORY) : trimmed;
|
|
58
|
+
const newIndex = capped.length - 1;
|
|
59
|
+
return { history: capped, historyIndex: newIndex, canUndo: newIndex > 0, canRedo: false };
|
|
102
60
|
}),
|
|
103
61
|
undo: () => {
|
|
104
62
|
const { history, historyIndex } = get();
|
|
105
|
-
// Must have a previous state to go back to
|
|
106
63
|
if (historyIndex > 0) {
|
|
107
|
-
const
|
|
108
|
-
set({
|
|
109
|
-
|
|
110
|
-
canUndo: newIndex > 0,
|
|
111
|
-
canRedo: true,
|
|
112
|
-
});
|
|
113
|
-
return history[newIndex];
|
|
64
|
+
const i = historyIndex - 1;
|
|
65
|
+
set({ historyIndex: i, canUndo: i > 0, canRedo: true });
|
|
66
|
+
return history[i];
|
|
114
67
|
}
|
|
115
68
|
return null;
|
|
116
69
|
},
|
|
117
70
|
redo: () => {
|
|
118
71
|
const { history, historyIndex } = get();
|
|
119
72
|
if (historyIndex < history.length - 1) {
|
|
120
|
-
const
|
|
121
|
-
set({
|
|
122
|
-
|
|
123
|
-
canUndo: true,
|
|
124
|
-
canRedo: newIndex < history.length - 1,
|
|
125
|
-
});
|
|
126
|
-
return history[newIndex];
|
|
73
|
+
const i = historyIndex + 1;
|
|
74
|
+
set({ historyIndex: i, canUndo: true, canRedo: i < history.length - 1 });
|
|
75
|
+
return history[i];
|
|
127
76
|
}
|
|
128
77
|
return null;
|
|
129
78
|
},
|
|
130
79
|
setCanUndo: (v) => set({ canUndo: v }),
|
|
131
80
|
setCanRedo: (v) => set({ canRedo: v }),
|
|
132
|
-
// Zoom
|
|
81
|
+
// ── Zoom ──────────────────────────────────────────────────────────────────
|
|
133
82
|
zoom: 1,
|
|
134
83
|
setZoom: (zoom) => set({ zoom }),
|
|
135
|
-
|
|
136
|
-
setSelectedObjects: (objects) => set({ selectedObjects: objects }),
|
|
137
|
-
}), { name: "fabric-whiteboard-store" }));
|
|
84
|
+
}));
|