@mhamz.01/easyflow-whiteboard 1.0.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/components/node/custom-node-overlay-layer.d.ts +44 -0
- package/dist/components/node/custom-node-overlay-layer.d.ts.map +1 -0
- package/dist/components/node/custom-node-overlay-layer.js +353 -0
- package/dist/components/node/custom-node.d.ts +17 -0
- package/dist/components/node/custom-node.d.ts.map +1 -0
- package/dist/components/node/custom-node.js +63 -0
- package/dist/components/node/document-node.d.ts +14 -0
- package/dist/components/node/document-node.d.ts.map +1 -0
- package/dist/components/node/document-node.js +58 -0
- package/dist/components/toolbar/document-dropdown.d.ts +14 -0
- package/dist/components/toolbar/document-dropdown.d.ts.map +1 -0
- package/dist/components/toolbar/document-dropdown.js +66 -0
- package/dist/components/toolbar/options/arrow-options.d.ts +8 -0
- package/dist/components/toolbar/options/arrow-options.d.ts.map +1 -0
- package/dist/components/toolbar/options/arrow-options.js +109 -0
- package/dist/components/toolbar/options/erase-option.d.ts +2 -0
- package/dist/components/toolbar/options/erase-option.d.ts.map +1 -0
- package/dist/components/toolbar/options/erase-option.js +20 -0
- package/dist/components/toolbar/options/image-options.d.ts +2 -0
- package/dist/components/toolbar/options/image-options.d.ts.map +1 -0
- package/dist/components/toolbar/options/image-options.js +10 -0
- package/dist/components/toolbar/options/line-options.d.ts +2 -0
- package/dist/components/toolbar/options/line-options.d.ts.map +1 -0
- package/dist/components/toolbar/options/line-options.js +46 -0
- package/dist/components/toolbar/options/pen-option.d.ts +2 -0
- package/dist/components/toolbar/options/pen-option.d.ts.map +1 -0
- package/dist/components/toolbar/options/pen-option.js +53 -0
- package/dist/components/toolbar/options/shape-option.d.ts +6 -0
- package/dist/components/toolbar/options/shape-option.d.ts.map +1 -0
- package/dist/components/toolbar/options/shape-option.js +58 -0
- package/dist/components/toolbar/options/text-option.d.ts +2 -0
- package/dist/components/toolbar/options/text-option.d.ts.map +1 -0
- package/dist/components/toolbar/options/text-option.js +73 -0
- package/dist/components/toolbar/task-dropdown.d.ts +15 -0
- package/dist/components/toolbar/task-dropdown.d.ts.map +1 -0
- package/dist/components/toolbar/task-dropdown.js +85 -0
- package/dist/components/toolbar/toolbar-button.d.ts +12 -0
- package/dist/components/toolbar/toolbar-button.d.ts.map +1 -0
- package/dist/components/toolbar/toolbar-button.js +8 -0
- package/dist/components/toolbar/toolbar-seperator.d.ts +6 -0
- package/dist/components/toolbar/toolbar-seperator.d.ts.map +1 -0
- package/dist/components/toolbar/toolbar-seperator.js +5 -0
- package/dist/components/toolbar/tooloptions-panel.d.ts +8 -0
- package/dist/components/toolbar/tooloptions-panel.d.ts.map +1 -0
- package/dist/components/toolbar/tooloptions-panel.js +88 -0
- package/dist/components/toolbar/whiteboard-toolbar.d.ts +28 -0
- package/dist/components/toolbar/whiteboard-toolbar.d.ts.map +1 -0
- package/dist/components/toolbar/whiteboard-toolbar.js +160 -0
- package/dist/components/ui/dropdown-menu.d.ts +26 -0
- package/dist/components/ui/dropdown-menu.d.ts.map +1 -0
- package/dist/components/ui/dropdown-menu.js +51 -0
- package/dist/components/ui/label.d.ts +5 -0
- package/dist/components/ui/label.d.ts.map +1 -0
- package/dist/components/ui/label.js +8 -0
- package/dist/components/ui/slider.d.ts +5 -0
- package/dist/components/ui/slider.d.ts.map +1 -0
- package/dist/components/ui/slider.js +14 -0
- package/dist/components/whiteboard/whiteboard-test.d.ts +2 -0
- package/dist/components/whiteboard/whiteboard-test.d.ts.map +1 -0
- package/dist/components/whiteboard/whiteboard-test.js +207 -0
- package/dist/components/whiteboard/whiteboard.d.ts +1 -0
- package/dist/components/whiteboard/whiteboard.d.ts.map +1 -0
- package/dist/components/whiteboard/whiteboard.js +911 -0
- package/dist/components/zoomcontrol/zoom-control.d.ts +9 -0
- package/dist/components/zoomcontrol/zoom-control.d.ts.map +1 -0
- package/dist/components/zoomcontrol/zoom-control.js +7 -0
- package/dist/hooks/useCanvasInit.d.ts +15 -0
- package/dist/hooks/useCanvasInit.d.ts.map +1 -0
- package/dist/hooks/useCanvasInit.js +89 -0
- package/dist/hooks/useDrawing.d.ts +23 -0
- package/dist/hooks/useDrawing.d.ts.map +1 -0
- package/dist/hooks/useDrawing.js +142 -0
- package/dist/hooks/useEraser.d.ts +27 -0
- package/dist/hooks/useEraser.d.ts.map +1 -0
- package/dist/hooks/useEraser.js +143 -0
- package/dist/hooks/useLiveUpdate.d.ts +9 -0
- package/dist/hooks/useLiveUpdate.d.ts.map +1 -0
- package/dist/hooks/useLiveUpdate.js +63 -0
- package/dist/hooks/useMouseHandlers.d.ts +25 -0
- package/dist/hooks/useMouseHandlers.d.ts.map +1 -0
- package/dist/hooks/useMouseHandlers.js +44 -0
- package/dist/hooks/usePan.d.ts +17 -0
- package/dist/hooks/usePan.d.ts.map +1 -0
- package/dist/hooks/usePan.js +80 -0
- package/dist/hooks/usePersistance.d.ts +13 -0
- package/dist/hooks/usePersistance.d.ts.map +1 -0
- package/dist/hooks/usePersistance.js +79 -0
- package/dist/hooks/useSelection.d.ts +21 -0
- package/dist/hooks/useSelection.d.ts.map +1 -0
- package/dist/hooks/useSelection.js +142 -0
- package/dist/hooks/useTextStyle.d.ts +9 -0
- package/dist/hooks/useTextStyle.d.ts.map +1 -0
- package/dist/hooks/useTextStyle.js +32 -0
- package/dist/hooks/useToolManager.d.ts +15 -0
- package/dist/hooks/useToolManager.d.ts.map +1 -0
- package/dist/hooks/useToolManager.js +115 -0
- package/dist/hooks/useZoom.d.ts +25 -0
- package/dist/hooks/useZoom.d.ts.map +1 -0
- package/dist/hooks/useZoom.js +133 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/lib/eraser-brush.d.ts +1 -0
- package/dist/lib/eraser-brush.d.ts.map +1 -0
- package/dist/lib/eraser-brush.js +21 -0
- package/dist/lib/fabric-arrow.d.ts +16 -0
- package/dist/lib/fabric-arrow.d.ts.map +1 -0
- package/dist/lib/fabric-arrow.js +50 -0
- package/dist/lib/fabric-bidirectional-arrow.d.ts +20 -0
- package/dist/lib/fabric-bidirectional-arrow.d.ts.map +1 -0
- package/dist/lib/fabric-bidirectional-arrow.js +65 -0
- package/dist/lib/fabric-frame.d.ts +7 -0
- package/dist/lib/fabric-frame.d.ts.map +1 -0
- package/dist/lib/fabric-frame.js +25 -0
- package/dist/lib/fabric-utils.d.ts +30 -0
- package/dist/lib/fabric-utils.d.ts.map +1 -0
- package/dist/lib/fabric-utils.js +273 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +5 -0
- package/dist/store/whiteboard-store.d.ts +99 -0
- package/dist/store/whiteboard-store.d.ts.map +1 -0
- package/dist/store/whiteboard-store.js +137 -0
- package/dist/types/canvas-node.d.ts +24 -0
- package/dist/types/canvas-node.d.ts.map +1 -0
- package/dist/types/canvas-node.js +1 -0
- package/package.json +34 -0
|
@@ -0,0 +1,911 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// "use client";
|
|
3
|
+
// import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
|
+
// import {
|
|
5
|
+
// Canvas,
|
|
6
|
+
// FabricObject,
|
|
7
|
+
// Rect,
|
|
8
|
+
// Circle,
|
|
9
|
+
// Line,
|
|
10
|
+
// TPointerEventInfo,
|
|
11
|
+
// FabricImage,
|
|
12
|
+
// classRegistry
|
|
13
|
+
// } from "fabric";
|
|
14
|
+
// import { useWhiteboardStore } from "@/app/store/whiteboard-store";
|
|
15
|
+
// import WhiteboardToolbar from "../toolbar/whiteboard-toolbar";
|
|
16
|
+
// import ToolOptionsPanel from "../toolbar/tooloptions-panel";
|
|
17
|
+
// import CanvasOverlayLayer from "../node/custom-node-overlay-layer";
|
|
18
|
+
// import type { Document } from "../node/custom-node-overlay-layer";
|
|
19
|
+
// import ZoomControls from "../zoomcontrol/zoom-control";
|
|
20
|
+
// import { initializeFabricCanvas, updateDrawingObject, addText, calculateDashArray,addWelcomeContent } from "@/lib/fabric-utils";
|
|
21
|
+
// import { Frame } from "@/lib/fabric-frame";
|
|
22
|
+
// import { Arrow } from "@/lib/fabric-arrow";
|
|
23
|
+
// import { BidirectionalArrow } from "@/lib/fabric-bidirectional-arrow";
|
|
24
|
+
// import * as fabric from "fabric";
|
|
25
|
+
// interface Task {
|
|
26
|
+
// id: string;
|
|
27
|
+
// title: string;
|
|
28
|
+
// status: "todo" | "in-progress" | "done";
|
|
29
|
+
// x: number;
|
|
30
|
+
// y: number;
|
|
31
|
+
// project?: string;
|
|
32
|
+
// assignee?: string;
|
|
33
|
+
// priority?: "low" | "medium" | "high";
|
|
34
|
+
// dueDate?: string;
|
|
35
|
+
// }
|
|
36
|
+
// classRegistry.setClass(Frame, 'frame');
|
|
37
|
+
// export default function FabricWhiteboard() {
|
|
38
|
+
// const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
39
|
+
// const fabricCanvasRef = useRef<Canvas | null>(null);
|
|
40
|
+
// const containerRef = useRef<HTMLDivElement>(null);
|
|
41
|
+
// const isDrawingRef = useRef(false);
|
|
42
|
+
// const startPointRef = useRef<{ x: number; y: number } | null>(null);
|
|
43
|
+
// const currentShapeRef = useRef<FabricObject | null>(null);
|
|
44
|
+
// const selectedObjectTypeRef = useRef<string | null>(null);
|
|
45
|
+
// const suppressHistoryRef = useRef(false);
|
|
46
|
+
// const isRestoringRef = useRef(false);
|
|
47
|
+
// // ── ERASER REFS ──────────────────────────────────────────────────────────────
|
|
48
|
+
// const eraserTraceRef = useRef<Circle | null>(null);
|
|
49
|
+
// const eraserTargetsRef = useRef<Set<FabricObject>>(new Set());
|
|
50
|
+
// const eraserActiveRef = useRef(false);
|
|
51
|
+
// const eraserPathRef = useRef<fabric.Path | null>(null); // ← NEW: Smooth path trail
|
|
52
|
+
// const eraserPathPointsRef = useRef<any[]>([]);
|
|
53
|
+
// const activeTool = useWhiteboardStore((state) => state.activeTool);
|
|
54
|
+
// const toolOptions = useWhiteboardStore((state) => state.toolOptions);
|
|
55
|
+
// const addCanvasObject = useWhiteboardStore((state) => state.addCanvasObject);
|
|
56
|
+
// const setSelectedObjectType = useWhiteboardStore((state) => state.setSelectedObjectType);
|
|
57
|
+
// const pushHistory = useWhiteboardStore((state) => state.pushHistory);
|
|
58
|
+
// const [tasks, setTasks] = useState<Task[]>([]);
|
|
59
|
+
// const [documents, setDocuments] = useState<Document[]>([]);
|
|
60
|
+
// const [canvasZoom, setCanvasZoom] = useState(1);
|
|
61
|
+
// const [canvasViewport, setCanvasViewport] = useState({ x: 0, y: 0 });
|
|
62
|
+
// const [selectionBox, setSelectionBox] = useState<{
|
|
63
|
+
// x1: number; y1: number; x2: number; y2: number;
|
|
64
|
+
// } | null>(null);
|
|
65
|
+
// const [selectedCanvasObjects, setSelectedCanvasObjects] = useState<FabricObject[]>([]);
|
|
66
|
+
// const MIN_ZOOM = 0.1;
|
|
67
|
+
// const MAX_ZOOM = 5;
|
|
68
|
+
// const ZOOM_STEP = 0.1;
|
|
69
|
+
// useEffect(() => {
|
|
70
|
+
// if (!canvasRef.current) return;
|
|
71
|
+
// const canvas = new Canvas(canvasRef.current, {
|
|
72
|
+
// width: window.innerWidth,
|
|
73
|
+
// height: window.innerHeight,
|
|
74
|
+
// backgroundColor: "transparent",
|
|
75
|
+
// selection: true,
|
|
76
|
+
// allowTouchScrolling: false,
|
|
77
|
+
// stopContextMenu: true,
|
|
78
|
+
// });
|
|
79
|
+
// fabricCanvasRef.current = canvas;
|
|
80
|
+
// initializeFabricCanvas(canvas);
|
|
81
|
+
// const canvasElement = canvas.getElement();
|
|
82
|
+
// canvasElement.style.touchAction = "none";
|
|
83
|
+
// // Storage keys
|
|
84
|
+
// const CANVAS_KEY = "easyflow_whiteboard_canvas";
|
|
85
|
+
// const NODES_KEY = "easyflow_whiteboard_nodes";
|
|
86
|
+
// // ═══════════════════════════════════════════════════════════════════════
|
|
87
|
+
// // LOAD SAVED DATA
|
|
88
|
+
// // ═══════════════════════════════════════════════════════════════════════
|
|
89
|
+
// const savedCanvas = localStorage.getItem(CANVAS_KEY);
|
|
90
|
+
// const savedNodes = localStorage.getItem(NODES_KEY);
|
|
91
|
+
// if (savedCanvas || savedNodes) {
|
|
92
|
+
// isRestoringRef.current = true;
|
|
93
|
+
// // Load Fabric canvas
|
|
94
|
+
// if (savedCanvas) {
|
|
95
|
+
// canvas
|
|
96
|
+
// .loadFromJSON(JSON.parse(savedCanvas))
|
|
97
|
+
// .then(() => {
|
|
98
|
+
// canvas.renderAll();
|
|
99
|
+
// pushHistory(JSON.stringify(canvas.toJSON()));
|
|
100
|
+
// })
|
|
101
|
+
// .catch((err) => {
|
|
102
|
+
// console.error("Canvas load failed:", err);
|
|
103
|
+
// addWelcomeContent(canvas, "/images/easyflow-logo.png");
|
|
104
|
+
// });
|
|
105
|
+
// } else {
|
|
106
|
+
// addWelcomeContent(canvas, "/images/easyflow-logo.png");
|
|
107
|
+
// }
|
|
108
|
+
// // Load custom nodes (tasks & documents)
|
|
109
|
+
// if (savedNodes) {
|
|
110
|
+
// try {
|
|
111
|
+
// const { tasks: savedTasks, documents: savedDocs } = JSON.parse(savedNodes);
|
|
112
|
+
// if (savedTasks) setTasks(savedTasks);
|
|
113
|
+
// if (savedDocs) setDocuments(savedDocs);
|
|
114
|
+
// console.log("✅ Custom nodes loaded:", { tasks: savedTasks?.length, docs: savedDocs?.length });
|
|
115
|
+
// } catch (err) {
|
|
116
|
+
// console.error("Nodes load failed:", err);
|
|
117
|
+
// }
|
|
118
|
+
// }
|
|
119
|
+
// isRestoringRef.current = false;
|
|
120
|
+
// } else {
|
|
121
|
+
// // No saved data - show welcome
|
|
122
|
+
// addWelcomeContent(canvas, "/images/easyflow-logo.png");
|
|
123
|
+
// pushHistory(JSON.stringify(canvas.toJSON()));
|
|
124
|
+
// }
|
|
125
|
+
// // Touch prevention
|
|
126
|
+
// const preventDefaults = (e: TouchEvent) => {
|
|
127
|
+
// if (e.touches.length === 1 && activeTool !== "pan") return;
|
|
128
|
+
// e.preventDefault();
|
|
129
|
+
// };
|
|
130
|
+
// const handleResize = () => {
|
|
131
|
+
// canvas.setDimensions({
|
|
132
|
+
// width: window.innerWidth,
|
|
133
|
+
// height: window.innerHeight,
|
|
134
|
+
// });
|
|
135
|
+
// canvas.renderAll();
|
|
136
|
+
// };
|
|
137
|
+
// window.addEventListener("resize", handleResize);
|
|
138
|
+
// canvasElement.addEventListener("touchstart", preventDefaults, { passive: false });
|
|
139
|
+
// canvasElement.addEventListener("touchmove", preventDefaults, { passive: false });
|
|
140
|
+
// return () => {
|
|
141
|
+
// canvasElement.removeEventListener("touchstart", preventDefaults);
|
|
142
|
+
// canvasElement.removeEventListener("touchmove", preventDefaults);
|
|
143
|
+
// window.removeEventListener("resize", handleResize);
|
|
144
|
+
// canvas.dispose();
|
|
145
|
+
// };
|
|
146
|
+
// }, []);
|
|
147
|
+
// // ─────────────────────────────────────────────────────────────────────────
|
|
148
|
+
// // STEP 2: Separate Saving Logic for Canvas and Nodes
|
|
149
|
+
// // ─────────────────────────────────────────────────────────────────────────
|
|
150
|
+
// // Save Fabric Canvas
|
|
151
|
+
// useEffect(() => {
|
|
152
|
+
// const canvas = fabricCanvasRef.current;
|
|
153
|
+
// if (!canvas) return;
|
|
154
|
+
// let saveTimeout: NodeJS.Timeout;
|
|
155
|
+
// const saveCanvas = () => {
|
|
156
|
+
// if (isRestoringRef.current || suppressHistoryRef.current) return;
|
|
157
|
+
// clearTimeout(saveTimeout);
|
|
158
|
+
// saveTimeout = setTimeout(() => {
|
|
159
|
+
// const performSave = () => {
|
|
160
|
+
// try {
|
|
161
|
+
// const json = JSON.stringify(canvas.toJSON());
|
|
162
|
+
// localStorage.setItem("easyflow_whiteboard_canvas", json);
|
|
163
|
+
// // Also update history
|
|
164
|
+
// pushHistory(json);
|
|
165
|
+
// console.log("💾 Canvas saved");
|
|
166
|
+
// } catch (err) {
|
|
167
|
+
// console.error("Canvas save failed:", err);
|
|
168
|
+
// }
|
|
169
|
+
// };
|
|
170
|
+
// if (typeof window.requestIdleCallback === "function") {
|
|
171
|
+
// window.requestIdleCallback(performSave);
|
|
172
|
+
// } else {
|
|
173
|
+
// performSave();
|
|
174
|
+
// }
|
|
175
|
+
// }, 2000);
|
|
176
|
+
// };
|
|
177
|
+
// const onObjectChanged = (opt?: any) => {
|
|
178
|
+
// if (opt?.target?.excludeFromExport) return;
|
|
179
|
+
// saveCanvas();
|
|
180
|
+
// };
|
|
181
|
+
// canvas.on("object:modified", onObjectChanged);
|
|
182
|
+
// canvas.on("object:added", onObjectChanged);
|
|
183
|
+
// canvas.on("object:removed", onObjectChanged);
|
|
184
|
+
// canvas.on("path:created", onObjectChanged);
|
|
185
|
+
// return () => {
|
|
186
|
+
// clearTimeout(saveTimeout);
|
|
187
|
+
// canvas.off("object:modified", onObjectChanged);
|
|
188
|
+
// canvas.off("object:added", onObjectChanged);
|
|
189
|
+
// canvas.off("object:removed", onObjectChanged);
|
|
190
|
+
// canvas.off("path:created", onObjectChanged);
|
|
191
|
+
// };
|
|
192
|
+
// }, []);
|
|
193
|
+
// // Save Custom Nodes (Tasks & Documents)
|
|
194
|
+
// useEffect(() => {
|
|
195
|
+
// if (isRestoringRef.current) return;
|
|
196
|
+
// let saveTimeout: NodeJS.Timeout;
|
|
197
|
+
// const saveNodes = () => {
|
|
198
|
+
// clearTimeout(saveTimeout);
|
|
199
|
+
// saveTimeout = setTimeout(() => {
|
|
200
|
+
// const performSave = () => {
|
|
201
|
+
// try {
|
|
202
|
+
// const nodesData = {
|
|
203
|
+
// tasks,
|
|
204
|
+
// documents,
|
|
205
|
+
// };
|
|
206
|
+
// localStorage.setItem(
|
|
207
|
+
// "easyflow_whiteboard_nodes",
|
|
208
|
+
// JSON.stringify(nodesData)
|
|
209
|
+
// );
|
|
210
|
+
// console.log("💾 Custom nodes saved:", {
|
|
211
|
+
// tasks: tasks.length,
|
|
212
|
+
// documents: documents.length,
|
|
213
|
+
// });
|
|
214
|
+
// } catch (err) {
|
|
215
|
+
// console.error("Nodes save failed:", err);
|
|
216
|
+
// }
|
|
217
|
+
// };
|
|
218
|
+
// if (typeof window.requestIdleCallback === "function") {
|
|
219
|
+
// window.requestIdleCallback(performSave);
|
|
220
|
+
// } else {
|
|
221
|
+
// performSave();
|
|
222
|
+
// }
|
|
223
|
+
// }, 2000);
|
|
224
|
+
// };
|
|
225
|
+
// saveNodes();
|
|
226
|
+
// return () => clearTimeout(saveTimeout);
|
|
227
|
+
// }, [tasks, documents]);
|
|
228
|
+
// // ── Tool cursor/mode management ─────────────────────────────────────────────
|
|
229
|
+
// useEffect(() => {
|
|
230
|
+
// const canvas = fabricCanvasRef.current;
|
|
231
|
+
// if (!canvas) return;
|
|
232
|
+
// // Clean up eraser trace when switching tools
|
|
233
|
+
// if (activeTool !== "eraser" && eraserTraceRef.current) {
|
|
234
|
+
// canvas.remove(eraserTraceRef.current);
|
|
235
|
+
// eraserTraceRef.current = null;
|
|
236
|
+
// eraserTargetsRef.current.clear();
|
|
237
|
+
// // Clean up trail path
|
|
238
|
+
// if (eraserPathRef.current) {
|
|
239
|
+
// canvas.remove(eraserPathRef.current);
|
|
240
|
+
// eraserPathRef.current = null;
|
|
241
|
+
// eraserPathPointsRef.current = [];
|
|
242
|
+
// }
|
|
243
|
+
// }
|
|
244
|
+
// switch (activeTool) {
|
|
245
|
+
// case "select":
|
|
246
|
+
// canvas.isDrawingMode = false;
|
|
247
|
+
// canvas.selection = true;
|
|
248
|
+
// canvas.defaultCursor = "default";
|
|
249
|
+
// canvas.hoverCursor = "move";
|
|
250
|
+
// canvas.forEachObject((obj) => { obj.selectable = true; obj.evented = true; obj.hoverCursor = "move"; });
|
|
251
|
+
// break;
|
|
252
|
+
// case "pan":
|
|
253
|
+
// canvas.isDrawingMode = false;
|
|
254
|
+
// canvas.selection = false;
|
|
255
|
+
// canvas.defaultCursor = "grab";
|
|
256
|
+
// canvas.hoverCursor = "grab";
|
|
257
|
+
// canvas.forEachObject((obj) => { obj.selectable = false; obj.evented = false; });
|
|
258
|
+
// break;
|
|
259
|
+
// case "pen":
|
|
260
|
+
// canvas.isDrawingMode = true;
|
|
261
|
+
// canvas.selection = false;
|
|
262
|
+
// canvas.defaultCursor = "crosshair";
|
|
263
|
+
// canvas.hoverCursor = "crosshair";
|
|
264
|
+
// if (canvas.freeDrawingBrush) {
|
|
265
|
+
// canvas.freeDrawingBrush.color = toolOptions.pen.color;
|
|
266
|
+
// canvas.freeDrawingBrush.width = toolOptions.pen.strokeWidth;
|
|
267
|
+
// // ← ADD THIS: Apply dash pattern
|
|
268
|
+
// if (toolOptions.pen.strokeDashArray) {
|
|
269
|
+
// canvas.freeDrawingBrush.strokeDashArray = toolOptions.pen.strokeDashArray;
|
|
270
|
+
// const dynamicDashArray = calculateDashArray(
|
|
271
|
+
// toolOptions.pen.strokeDashArray,
|
|
272
|
+
// toolOptions.pen.strokeWidth
|
|
273
|
+
// );
|
|
274
|
+
// canvas.freeDrawingBrush.strokeDashArray = dynamicDashArray || null;
|
|
275
|
+
// } else {
|
|
276
|
+
// canvas.freeDrawingBrush.strokeDashArray = null; // Solid line
|
|
277
|
+
// }
|
|
278
|
+
// }
|
|
279
|
+
// canvas.forEachObject((obj) => {
|
|
280
|
+
// obj.selectable = false;
|
|
281
|
+
// obj.evented = false;
|
|
282
|
+
// });
|
|
283
|
+
// break;
|
|
284
|
+
// case "eraser":
|
|
285
|
+
// canvas.isDrawingMode = false;
|
|
286
|
+
// canvas.selection = false;
|
|
287
|
+
// canvas.defaultCursor = "none"; // Hide default cursor
|
|
288
|
+
// canvas.hoverCursor = "none";
|
|
289
|
+
// if (eraserTraceRef.current) {
|
|
290
|
+
// eraserTraceRef.current.set({ visible: false });
|
|
291
|
+
// }
|
|
292
|
+
// canvas.forEachObject((obj) => {
|
|
293
|
+
// obj.selectable = false;
|
|
294
|
+
// obj.evented = true; // Keep events for hover detection
|
|
295
|
+
// });
|
|
296
|
+
// break;
|
|
297
|
+
// case "text":
|
|
298
|
+
// canvas.isDrawingMode = false;
|
|
299
|
+
// canvas.selection = false;
|
|
300
|
+
// canvas.defaultCursor = "text";
|
|
301
|
+
// canvas.hoverCursor = "text";
|
|
302
|
+
// canvas.forEachObject((obj) => { obj.selectable = false; obj.evented = false; });
|
|
303
|
+
// break;
|
|
304
|
+
// case "rectangle":
|
|
305
|
+
// case "circle":
|
|
306
|
+
// case "line":
|
|
307
|
+
// case "frame":
|
|
308
|
+
// case "arrow":
|
|
309
|
+
// canvas.isDrawingMode = false;
|
|
310
|
+
// canvas.selection = false;
|
|
311
|
+
// canvas.defaultCursor = "crosshair";
|
|
312
|
+
// canvas.hoverCursor = "crosshair";
|
|
313
|
+
// canvas.forEachObject((obj) => { obj.selectable = false; obj.evented = false; });
|
|
314
|
+
// break;
|
|
315
|
+
// default:
|
|
316
|
+
// canvas.isDrawingMode = false;
|
|
317
|
+
// canvas.selection = true;
|
|
318
|
+
// canvas.defaultCursor = "default";
|
|
319
|
+
// canvas.hoverCursor = "move";
|
|
320
|
+
// }
|
|
321
|
+
// canvas.renderAll();
|
|
322
|
+
// }, [activeTool, toolOptions]);
|
|
323
|
+
// const handleMouseOver = () => {
|
|
324
|
+
// if (activeTool === "eraser" && eraserTraceRef.current) {
|
|
325
|
+
// eraserTraceRef.current.set({ visible: true });
|
|
326
|
+
// fabricCanvasRef.current?.renderAll();
|
|
327
|
+
// }
|
|
328
|
+
// };
|
|
329
|
+
// const handleMouseOut = (opt: TPointerEventInfo) => {
|
|
330
|
+
// // opt.e is the native event. If it's null or we are leaving the canvas boundaries:
|
|
331
|
+
// if (activeTool === "eraser" && eraserTraceRef.current) {
|
|
332
|
+
// eraserTraceRef.current.set({ visible: false });
|
|
333
|
+
// fabricCanvasRef.current?.renderAll();
|
|
334
|
+
// }
|
|
335
|
+
// };
|
|
336
|
+
// // ── Mouse down ───────────────────────────────────────────────────────────────
|
|
337
|
+
// const handleMouseDown = (opt: TPointerEventInfo) => {
|
|
338
|
+
// const canvas = fabricCanvasRef.current;
|
|
339
|
+
// if (!canvas) return;
|
|
340
|
+
// const pointer = canvas.getScenePoint(opt.e as MouseEvent);
|
|
341
|
+
// // ── ERASER: Activate eraser mode ────────────────────────────────────────────
|
|
342
|
+
// if (activeTool === "eraser") {
|
|
343
|
+
// eraserActiveRef.current = true;
|
|
344
|
+
// suppressHistoryRef.current = true;
|
|
345
|
+
// return;
|
|
346
|
+
// }
|
|
347
|
+
// if (opt.target || canvas.getActiveObject()) return;
|
|
348
|
+
// if (activeTool === "text") {
|
|
349
|
+
// addText(canvas, {
|
|
350
|
+
// x: pointer.x,
|
|
351
|
+
// y: pointer.y,
|
|
352
|
+
// fontSize: toolOptions.text.fontSize,
|
|
353
|
+
// fontFamily: toolOptions.text.fontFamily,
|
|
354
|
+
// fontWeight: toolOptions.text.fontWeight, // ← ADD THIS
|
|
355
|
+
// color: toolOptions.text.color,
|
|
356
|
+
// textAlign: toolOptions.text.textAlign, // ← ADD THIS
|
|
357
|
+
// });
|
|
358
|
+
// pushHistory(JSON.stringify(canvas.toJSON()));
|
|
359
|
+
// useWhiteboardStore.getState().setActiveTool("select");
|
|
360
|
+
// return;
|
|
361
|
+
// }
|
|
362
|
+
// if (!["rectangle", "circle", "frame", "line", "arrow"].includes(activeTool)) return;
|
|
363
|
+
// suppressHistoryRef.current = true;
|
|
364
|
+
// isDrawingRef.current = true;
|
|
365
|
+
// startPointRef.current = { x: pointer.x, y: pointer.y };
|
|
366
|
+
// let shape: FabricObject | null = null;
|
|
367
|
+
// switch (activeTool) {
|
|
368
|
+
// case "rectangle":
|
|
369
|
+
// shape = new Rect({
|
|
370
|
+
// left: pointer.x,
|
|
371
|
+
// top: pointer.y,
|
|
372
|
+
// width: 1,
|
|
373
|
+
// height: 1,
|
|
374
|
+
// fill: toolOptions.rectangle.fillColor === "transparent"
|
|
375
|
+
// ? "transparent"
|
|
376
|
+
// : toolOptions.rectangle.fillColor,
|
|
377
|
+
// stroke: toolOptions.rectangle.strokeColor,
|
|
378
|
+
// strokeWidth: toolOptions.rectangle.strokeWidth,
|
|
379
|
+
// // ↓ THE FIX: Apply strokeDashArray
|
|
380
|
+
// strokeDashArray: calculateDashArray(
|
|
381
|
+
// toolOptions.rectangle.strokeDashArray,
|
|
382
|
+
// toolOptions.rectangle.strokeWidth
|
|
383
|
+
// ),
|
|
384
|
+
// rx: 5,
|
|
385
|
+
// ry: 5,
|
|
386
|
+
// strokeLineCap: "round",
|
|
387
|
+
// strokeLineJoin: "round",
|
|
388
|
+
// selectable: false,
|
|
389
|
+
// });
|
|
390
|
+
// break;
|
|
391
|
+
// case "circle":
|
|
392
|
+
// shape = new Circle({
|
|
393
|
+
// left: pointer.x,
|
|
394
|
+
// top: pointer.y,
|
|
395
|
+
// radius: 1,
|
|
396
|
+
// fill: toolOptions.circle.fillColor === "transparent"
|
|
397
|
+
// ? "transparent"
|
|
398
|
+
// : toolOptions.circle.fillColor,
|
|
399
|
+
// stroke: toolOptions.circle.strokeColor,
|
|
400
|
+
// strokeWidth: toolOptions.circle.strokeWidth,
|
|
401
|
+
// // ↓ THE FIX: Apply strokeDashArray
|
|
402
|
+
// strokeDashArray: toolOptions.circle.strokeDashArray
|
|
403
|
+
// ? [...toolOptions.circle.strokeDashArray]
|
|
404
|
+
// : undefined,
|
|
405
|
+
// selectable: false,
|
|
406
|
+
// });
|
|
407
|
+
// break;
|
|
408
|
+
// case "frame":
|
|
409
|
+
// shape = new Frame({
|
|
410
|
+
// left: pointer.x,
|
|
411
|
+
// top: pointer.y,
|
|
412
|
+
// width: 1,
|
|
413
|
+
// height: 1,
|
|
414
|
+
// // ↓ IMPORTANT: Use frame options, not rectangle
|
|
415
|
+
// stroke: toolOptions.frame.strokeColor,
|
|
416
|
+
// strokeWidth: toolOptions.frame.strokeWidth,
|
|
417
|
+
// strokeDashArray: toolOptions.frame.strokeDashArray
|
|
418
|
+
// ? [...toolOptions.frame.strokeDashArray]
|
|
419
|
+
// : undefined,
|
|
420
|
+
// fill: toolOptions.frame.fillColor,
|
|
421
|
+
// selectable: false,
|
|
422
|
+
// });
|
|
423
|
+
// break;
|
|
424
|
+
// case "line":
|
|
425
|
+
// shape = new Line([pointer.x, pointer.y, pointer.x, pointer.y], {
|
|
426
|
+
// stroke: toolOptions.line.strokeColor, // ← Use line options
|
|
427
|
+
// strokeWidth: toolOptions.line.strokeWidth, // ← Use line options
|
|
428
|
+
// strokeDashArray: toolOptions.line.strokeDashArray || undefined, // ← ADD THIS
|
|
429
|
+
// selectable: false,
|
|
430
|
+
// });
|
|
431
|
+
// break;
|
|
432
|
+
// case "arrow":
|
|
433
|
+
// shape = new Arrow([pointer.x, pointer.y, pointer.x, pointer.y], {
|
|
434
|
+
// stroke: toolOptions.arrow.strokeColor,
|
|
435
|
+
// strokeWidth: toolOptions.arrow.strokeWidth,
|
|
436
|
+
// strokeDashArray: toolOptions.arrow.strokeDashArray || undefined,
|
|
437
|
+
// selectable: false,
|
|
438
|
+
// });
|
|
439
|
+
// break;
|
|
440
|
+
// }
|
|
441
|
+
// if (shape) {
|
|
442
|
+
// canvas.add(shape);
|
|
443
|
+
// currentShapeRef.current = shape;
|
|
444
|
+
// canvas.renderAll();
|
|
445
|
+
// }
|
|
446
|
+
// };
|
|
447
|
+
// // ── Mouse move ───────────────────────────────────────────────────────────────
|
|
448
|
+
// const handleMouseMove = (opt: TPointerEventInfo) => {
|
|
449
|
+
// const canvas = fabricCanvasRef.current;
|
|
450
|
+
// if (!canvas) return;
|
|
451
|
+
// const pointer = canvas.getScenePoint(opt.e as MouseEvent);
|
|
452
|
+
// // ── ERASER: Update trace circle and highlight objects ──────────────────────
|
|
453
|
+
// if (activeTool === "eraser") {
|
|
454
|
+
// // Create or update trace circle
|
|
455
|
+
// if (!eraserTraceRef.current) {
|
|
456
|
+
// eraserTraceRef.current = new Circle({
|
|
457
|
+
// radius: toolOptions.eraser.size /1.5,
|
|
458
|
+
// fill: "rgba(195, 195, 195, 0.2)",
|
|
459
|
+
// stroke: "#e2e2e0",
|
|
460
|
+
// strokeWidth: 2,
|
|
461
|
+
// selectable: false,
|
|
462
|
+
// evented: false,
|
|
463
|
+
// originX: "center",
|
|
464
|
+
// originY: "center",
|
|
465
|
+
// excludeFromExport: true,
|
|
466
|
+
// });
|
|
467
|
+
// canvas.add(eraserTraceRef.current);
|
|
468
|
+
// }
|
|
469
|
+
// if (!eraserTraceRef.current.visible) {
|
|
470
|
+
// eraserTraceRef.current.set({ visible: true });
|
|
471
|
+
// }
|
|
472
|
+
// // Move trace circle to mouse position
|
|
473
|
+
// eraserTraceRef.current.set({ left: pointer.x, top: pointer.y });
|
|
474
|
+
// eraserTraceRef.current.setCoords();
|
|
475
|
+
// canvas.bringObjectToFront(eraserTraceRef.current);
|
|
476
|
+
// // ── SMOOTH CONTINUOUS TRAIL (FIXED) ──────────────────────────────────────
|
|
477
|
+
// if (eraserActiveRef.current) {
|
|
478
|
+
// suppressHistoryRef.current = true; // Ensure history is locked during movement
|
|
479
|
+
// if (eraserPathPointsRef.current.length === 0) {
|
|
480
|
+
// // Start new path - use SVG path string format
|
|
481
|
+
// eraserPathPointsRef.current = [`M ${pointer.x} ${pointer.y}`];
|
|
482
|
+
// const pathString = eraserPathPointsRef.current.join(' ');
|
|
483
|
+
// const path = new fabric.Path(pathString, {
|
|
484
|
+
// stroke: "rgba(220, 220, 220, 0.7)", // ← Bright white/gray for dark bg
|
|
485
|
+
// strokeWidth: toolOptions.eraser.size,
|
|
486
|
+
// fill: null, // ← Must be null, not empty string
|
|
487
|
+
// selectable: false,
|
|
488
|
+
// evented: false,
|
|
489
|
+
// strokeLineCap: "round",
|
|
490
|
+
// strokeLineJoin: "round",
|
|
491
|
+
// opacity: 1,
|
|
492
|
+
// excludeFromExport: true,
|
|
493
|
+
// });
|
|
494
|
+
// canvas.add(path);
|
|
495
|
+
// eraserPathRef.current = path;
|
|
496
|
+
// } else {
|
|
497
|
+
// // Continue path by adding line segments
|
|
498
|
+
// eraserPathPointsRef.current.push(`L ${pointer.x} ${pointer.y}`);
|
|
499
|
+
// const pathString = eraserPathPointsRef.current.join(' ');
|
|
500
|
+
// // Recreate path with updated points (more reliable than updating)
|
|
501
|
+
// if (eraserPathRef.current) {
|
|
502
|
+
// canvas.remove(eraserPathRef.current);
|
|
503
|
+
// }
|
|
504
|
+
// const path = new fabric.Path(pathString, {
|
|
505
|
+
// stroke: "rgba(53, 53, 53, 0.7)",
|
|
506
|
+
// strokeWidth: toolOptions.eraser.size,
|
|
507
|
+
// fill: null,
|
|
508
|
+
// selectable: false,
|
|
509
|
+
// evented: false,
|
|
510
|
+
// strokeLineCap: "round",
|
|
511
|
+
// strokeLineJoin: "round",
|
|
512
|
+
// opacity: 1,
|
|
513
|
+
// excludeFromExport: true,
|
|
514
|
+
// });
|
|
515
|
+
// canvas.add(path);
|
|
516
|
+
// eraserPathRef.current = path;
|
|
517
|
+
// }
|
|
518
|
+
// // Keep trace circle on top
|
|
519
|
+
// canvas.bringObjectToFront(eraserTraceRef.current!);
|
|
520
|
+
// }
|
|
521
|
+
// // Find objects under eraser
|
|
522
|
+
// const previousTargets = new Set(eraserTargetsRef.current);
|
|
523
|
+
// eraserTargetsRef.current.clear();
|
|
524
|
+
// canvas.forEachObject((obj) => {
|
|
525
|
+
// if (obj === eraserTraceRef.current) return;
|
|
526
|
+
// if (obj === eraserPathRef.current) return;
|
|
527
|
+
// if (obj.intersectsWithObject(eraserTraceRef.current!)) {
|
|
528
|
+
// eraserTargetsRef.current.add(obj);
|
|
529
|
+
// if (eraserActiveRef.current && obj.opacity !== 0.3) {
|
|
530
|
+
// obj.set({ opacity: 0.3 });
|
|
531
|
+
// // obj.dirty = true;
|
|
532
|
+
// }
|
|
533
|
+
// } else if (previousTargets.has(obj)) {
|
|
534
|
+
// obj.set({ opacity: 1 });
|
|
535
|
+
// // obj.dirty = true;
|
|
536
|
+
// }
|
|
537
|
+
// });
|
|
538
|
+
// canvas.renderAll();
|
|
539
|
+
// return;
|
|
540
|
+
// }
|
|
541
|
+
// // Normal drawing logic
|
|
542
|
+
// if (!isDrawingRef.current || !currentShapeRef.current || !startPointRef.current) return;
|
|
543
|
+
// updateDrawingObject(currentShapeRef.current, activeTool, startPointRef.current, pointer);
|
|
544
|
+
// canvas.renderAll();
|
|
545
|
+
// };
|
|
546
|
+
// // ── Mouse up ─────────────────────────────────────────────────────────────────
|
|
547
|
+
// const handleMouseUp = () => {
|
|
548
|
+
// const canvas = fabricCanvasRef.current;
|
|
549
|
+
// if (!canvas) return;
|
|
550
|
+
// // ── ERASER: Delete highlighted objects ─────────────────────────────────────
|
|
551
|
+
// if (activeTool === "eraser" && eraserActiveRef.current) {
|
|
552
|
+
// eraserActiveRef.current = false;
|
|
553
|
+
// // 1. Clean up visuals (opacity and path)
|
|
554
|
+
// canvas.forEachObject((obj) => {
|
|
555
|
+
// if (obj.opacity === 0.3) obj.set({ opacity: 1 });
|
|
556
|
+
// });
|
|
557
|
+
// if (eraserPathRef.current) {
|
|
558
|
+
// canvas.remove(eraserPathRef.current);
|
|
559
|
+
// eraserPathRef.current = null;
|
|
560
|
+
// eraserPathPointsRef.current = [];
|
|
561
|
+
// }
|
|
562
|
+
// // 2. Perform the actual deletion
|
|
563
|
+
// eraserTargetsRef.current.forEach((obj) => {
|
|
564
|
+
// canvas.remove(obj);
|
|
565
|
+
// });
|
|
566
|
+
// eraserTargetsRef.current.clear();
|
|
567
|
+
// canvas.renderAll();
|
|
568
|
+
// // 3. IMMEDIATE SAVE (don't wait for debounce)
|
|
569
|
+
// try {
|
|
570
|
+
// const canvasJson = JSON.stringify(canvas.toJSON());
|
|
571
|
+
// localStorage.setItem("easyflow_whiteboard_canvas", canvasJson);
|
|
572
|
+
// localStorage.setItem("easyflow_whiteboard_nodes", JSON.stringify({ tasks, documents }));
|
|
573
|
+
// console.log("💾 Eraser: Saved immediately");
|
|
574
|
+
// } catch (err) {
|
|
575
|
+
// console.error("Save failed:", err);
|
|
576
|
+
// }
|
|
577
|
+
// // 4. Also fire event for regular save flow
|
|
578
|
+
// canvas.fire('object:removed');
|
|
579
|
+
// // 5. Update history
|
|
580
|
+
// suppressHistoryRef.current = false;
|
|
581
|
+
// pushHistory(JSON.stringify(canvas.toJSON()));
|
|
582
|
+
// return;
|
|
583
|
+
// }
|
|
584
|
+
// // Normal shape drawing
|
|
585
|
+
// if (!isDrawingRef.current || !currentShapeRef.current) return;
|
|
586
|
+
// const shape = currentShapeRef.current;
|
|
587
|
+
// shape.set({ selectable: true, evented: true });
|
|
588
|
+
// if (shape instanceof Frame && canvas) canvas.sendObjectToBack(shape);
|
|
589
|
+
// addCanvasObject(shape);
|
|
590
|
+
// suppressHistoryRef.current = false;
|
|
591
|
+
// pushHistory(JSON.stringify(canvas.toJSON()));
|
|
592
|
+
// isDrawingRef.current = false;
|
|
593
|
+
// startPointRef.current = null;
|
|
594
|
+
// currentShapeRef.current = null;
|
|
595
|
+
// useWhiteboardStore.getState().setActiveTool("select");
|
|
596
|
+
// canvas.discardActiveObject();
|
|
597
|
+
// canvas.renderAll();
|
|
598
|
+
// };
|
|
599
|
+
// useEffect(() => {
|
|
600
|
+
// const canvas = fabricCanvasRef.current;
|
|
601
|
+
// if (!canvas) return;
|
|
602
|
+
// canvas.on("mouse:over", handleMouseOver);
|
|
603
|
+
// canvas.on("mouse:out", handleMouseOut);
|
|
604
|
+
// canvas.on("mouse:down", handleMouseDown);
|
|
605
|
+
// canvas.on("mouse:move", handleMouseMove);
|
|
606
|
+
// canvas.on("mouse:up", handleMouseUp);
|
|
607
|
+
// return () => {
|
|
608
|
+
// canvas.off("mouse:over", handleMouseOver);
|
|
609
|
+
// canvas.off("mouse:out", handleMouseOut);
|
|
610
|
+
// canvas.off("mouse:down", handleMouseDown);
|
|
611
|
+
// canvas.off("mouse:move", handleMouseMove);
|
|
612
|
+
// canvas.off("mouse:up", handleMouseUp);
|
|
613
|
+
// };
|
|
614
|
+
// }, [activeTool, toolOptions]);
|
|
615
|
+
// // ── Update text style on selection change ─────────────────────────────────────
|
|
616
|
+
// useEffect(() => {
|
|
617
|
+
// const canvas = fabricCanvasRef.current;
|
|
618
|
+
// if (!canvas) return;
|
|
619
|
+
// const activeObject = canvas.getActiveObject();
|
|
620
|
+
// if (!activeObject) return;
|
|
621
|
+
// // Only update text objects (IText or Text type)
|
|
622
|
+
// const updateTextStyle = (obj: FabricObject) => {
|
|
623
|
+
// if (obj.type !== "i-text" && obj.type !== "text") return;
|
|
624
|
+
// const options = toolOptions.text;
|
|
625
|
+
// obj.set({
|
|
626
|
+
// fontSize: options.fontSize,
|
|
627
|
+
// fontFamily: options.fontFamily,
|
|
628
|
+
// fontWeight: options.fontWeight,
|
|
629
|
+
// fill: options.color,
|
|
630
|
+
// textAlign: options.textAlign,
|
|
631
|
+
// });
|
|
632
|
+
// obj.dirty = true;
|
|
633
|
+
// };
|
|
634
|
+
// // Handle single or multiple selection
|
|
635
|
+
// if (activeObject.type === "activeSelection") {
|
|
636
|
+
// const objects = (activeObject as any).getObjects();
|
|
637
|
+
// objects.forEach((obj: FabricObject) => updateTextStyle(obj));
|
|
638
|
+
// } else {
|
|
639
|
+
// updateTextStyle(activeObject);
|
|
640
|
+
// }
|
|
641
|
+
// canvas.requestRenderAll();
|
|
642
|
+
// }, [toolOptions.text]);
|
|
643
|
+
// // ── Zoom ─────────────────────────────────────────────────────────────────────
|
|
644
|
+
// const handleZoom = useCallback((newZoom: number, point?: { x: number; y: number }) => {
|
|
645
|
+
// const canvas = fabricCanvasRef.current;
|
|
646
|
+
// if (!canvas) return;
|
|
647
|
+
// // 1. Clamp the zoom level immediately
|
|
648
|
+
// const clampedZoom = Math.min(Math.max(newZoom, MIN_ZOOM), MAX_ZOOM);
|
|
649
|
+
// // 2. Determine the pivot point (default to center)
|
|
650
|
+
// const pivot = point ? new fabric.Point(point.x, point.y) : new fabric.Point(canvas.getWidth() / 2, canvas.getHeight() / 2);
|
|
651
|
+
// // 3. Use Fabric's optimized built-in method
|
|
652
|
+
// // This handles the matrix math (vpt[4], vpt[5]) much faster than manual assignment
|
|
653
|
+
// canvas.zoomToPoint(pivot, clampedZoom);
|
|
654
|
+
// // 4. Update the Canvas state
|
|
655
|
+
// const vpt = canvas.viewportTransform;
|
|
656
|
+
// if (vpt) {
|
|
657
|
+
// // We update the local state to keep the UI in sync
|
|
658
|
+
// // Use requestAnimationFrame if you find this still lags,
|
|
659
|
+
// // but usually, Fabric's internal update is enough for the "feel"
|
|
660
|
+
// setCanvasZoom(clampedZoom);
|
|
661
|
+
// setCanvasViewport({ x: vpt[4], y: vpt[5] });
|
|
662
|
+
// }
|
|
663
|
+
// // 5. Use requestRenderAll for better performance than renderAll
|
|
664
|
+
// canvas.requestRenderAll();
|
|
665
|
+
// }, [MIN_ZOOM, MAX_ZOOM, setCanvasZoom, setCanvasViewport]);
|
|
666
|
+
// const handleZoomIn = () => handleZoom(canvasZoom + ZOOM_STEP);
|
|
667
|
+
// const handleZoomOut = () => handleZoom(canvasZoom - ZOOM_STEP);
|
|
668
|
+
// const handleResetZoom = () => { const canvas = fabricCanvasRef.current; if (!canvas) return; canvas.setZoom(1); const vpt = canvas.viewportTransform; if (vpt) { vpt[4] = 0; vpt[5] = 0; setCanvasViewport({ x: 0, y: 0 }); } setCanvasZoom(1); canvas.renderAll(); };
|
|
669
|
+
// useEffect(() => {
|
|
670
|
+
// const canvas = fabricCanvasRef.current;
|
|
671
|
+
// if (!canvas) return;
|
|
672
|
+
// const canvasEl = canvas.getElement();
|
|
673
|
+
// canvasEl.style.touchAction = 'none';
|
|
674
|
+
// let isPanning = false;
|
|
675
|
+
// let lastX = 0;
|
|
676
|
+
// let lastY = 0;
|
|
677
|
+
// let lastTouchDistance = 0;
|
|
678
|
+
// const onDown = (opt: any) => {
|
|
679
|
+
// if (activeTool !== "pan") return;
|
|
680
|
+
// const e = opt.e;
|
|
681
|
+
// // --- PINCH INITIALIZATION ---
|
|
682
|
+
// if (e.touches && e.touches.length === 2) {
|
|
683
|
+
// isPanning = false; // Kill panning immediately
|
|
684
|
+
// lastTouchDistance = Math.hypot(
|
|
685
|
+
// e.touches[0].clientX - e.touches[1].clientX,
|
|
686
|
+
// e.touches[0].clientY - e.touches[1].clientY
|
|
687
|
+
// );
|
|
688
|
+
// return;
|
|
689
|
+
// }
|
|
690
|
+
// // --- PAN INITIALIZATION ---
|
|
691
|
+
// const pointer = e.touches ? e.touches[0] : e;
|
|
692
|
+
// isPanning = true;
|
|
693
|
+
// lastX = pointer.clientX;
|
|
694
|
+
// lastY = pointer.clientY;
|
|
695
|
+
// canvas.setCursor("grabbing");
|
|
696
|
+
// };
|
|
697
|
+
// const onMove = (opt: any) => {
|
|
698
|
+
// if (activeTool !== "pan") return;
|
|
699
|
+
// const e = opt.e;
|
|
700
|
+
// // 1. HANDLE PINCH ZOOM (Two Fingers)
|
|
701
|
+
// if (e.touches && e.touches.length === 2) {
|
|
702
|
+
// const currentDistance = Math.hypot(
|
|
703
|
+
// e.touches[0].clientX - e.touches[1].clientX,
|
|
704
|
+
// e.touches[0].clientY - e.touches[1].clientY
|
|
705
|
+
// );
|
|
706
|
+
// if (lastTouchDistance > 0) {
|
|
707
|
+
// const zoom = canvas.getZoom();
|
|
708
|
+
// // Constant 0.01 multiplier for controlled, buttery zoom
|
|
709
|
+
// const delta = (currentDistance - lastTouchDistance) * 0.01;
|
|
710
|
+
// const newZoom = zoom + delta;
|
|
711
|
+
// // Midpoint between fingers for the zoom anchor
|
|
712
|
+
// const midX = (e.touches[0].clientX + e.touches[1].clientX) / 2;
|
|
713
|
+
// const midY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
|
|
714
|
+
// const rect = canvasEl.getBoundingClientRect();
|
|
715
|
+
// handleZoom(newZoom, { x: midX - rect.left, y: midY - rect.top });
|
|
716
|
+
// }
|
|
717
|
+
// lastTouchDistance = currentDistance;
|
|
718
|
+
// return; // Exit so we don't trigger panning math
|
|
719
|
+
// }
|
|
720
|
+
// // 2. HANDLE PANNING (One Finger or Mouse)
|
|
721
|
+
// if (isPanning) {
|
|
722
|
+
// const pointer = e.touches ? e.touches[0] : e;
|
|
723
|
+
// const vpt = canvas.viewportTransform;
|
|
724
|
+
// if (vpt) {
|
|
725
|
+
// vpt[4] += pointer.clientX - lastX;
|
|
726
|
+
// vpt[5] += pointer.clientY - lastY;
|
|
727
|
+
// canvas.requestRenderAll();
|
|
728
|
+
// }
|
|
729
|
+
// lastX = pointer.clientX;
|
|
730
|
+
// lastY = pointer.clientY;
|
|
731
|
+
// }
|
|
732
|
+
// };
|
|
733
|
+
// const onUp = () => {
|
|
734
|
+
// // Only sync the store at the very end of the interaction
|
|
735
|
+
// const vpt = canvas.viewportTransform;
|
|
736
|
+
// if (vpt) {
|
|
737
|
+
// setCanvasViewport({ x: vpt[4], y: vpt[5] });
|
|
738
|
+
// }
|
|
739
|
+
// isPanning = false;
|
|
740
|
+
// lastTouchDistance = 0;
|
|
741
|
+
// canvas.setCursor(activeTool === "pan" ? "grab" : "default");
|
|
742
|
+
// };
|
|
743
|
+
// canvas.on("mouse:down", onDown);
|
|
744
|
+
// canvas.on("mouse:move", onMove);
|
|
745
|
+
// canvas.on("mouse:up", onUp);
|
|
746
|
+
// return () => {
|
|
747
|
+
// canvas.off("mouse:down", onDown);
|
|
748
|
+
// canvas.off("mouse:move", onMove);
|
|
749
|
+
// canvas.off("mouse:up", onUp);
|
|
750
|
+
// };
|
|
751
|
+
// }, [activeTool, handleZoom, setCanvasViewport]);
|
|
752
|
+
// // ── Wheel Handler (Pan on scroll, Zoom on Ctrl+Scroll) ───────────────────────
|
|
753
|
+
// useEffect(() => {
|
|
754
|
+
// const canvas = fabricCanvasRef.current;
|
|
755
|
+
// if (!canvas) return;
|
|
756
|
+
// const onWheel = (opt: any) => {
|
|
757
|
+
// const e = opt.e as WheelEvent;
|
|
758
|
+
// e.preventDefault();
|
|
759
|
+
// e.stopPropagation();
|
|
760
|
+
// const vpt = canvas.viewportTransform;
|
|
761
|
+
// if (!vpt) return;
|
|
762
|
+
// if (e.ctrlKey || e.metaKey) {
|
|
763
|
+
// // Zoom logic
|
|
764
|
+
// const rect = canvas.getElement().getBoundingClientRect();
|
|
765
|
+
// handleZoom(
|
|
766
|
+
// e.deltaY < 0 ? canvasZoom * 1.1 : canvasZoom / 1.1,
|
|
767
|
+
// { x: e.clientX - rect.left, y: e.clientY - rect.top }
|
|
768
|
+
// );
|
|
769
|
+
// } else {
|
|
770
|
+
// // ── PAN VIA WHEEL (Your scroll logic) ─────────────────────────────────
|
|
771
|
+
// const delta = e.deltaY;
|
|
772
|
+
// const shiftDelta = e.deltaX || e.deltaY;
|
|
773
|
+
// // Vertical scroll (normal wheel)
|
|
774
|
+
// vpt[5] -= delta;
|
|
775
|
+
// // Horizontal scroll (shift + wheel)
|
|
776
|
+
// if (e.shiftKey) vpt[4] -= shiftDelta;
|
|
777
|
+
// setCanvasViewport({ x: vpt[4], y: vpt[5] });
|
|
778
|
+
// // ── CRITICAL FIX: Sync selection coordinates ──────────────────────────
|
|
779
|
+
// const activeObj = canvas.getActiveObject();
|
|
780
|
+
// if (activeObj) {
|
|
781
|
+
// activeObj.setCoords();
|
|
782
|
+
// // For multi-selection, update all objects
|
|
783
|
+
// if (activeObj.type === 'activeSelection') {
|
|
784
|
+
// const objects = (activeObj as any).getObjects();
|
|
785
|
+
// objects.forEach((obj: FabricObject) => obj.setCoords());
|
|
786
|
+
// (activeObj as any).setCoords();
|
|
787
|
+
// }
|
|
788
|
+
// }
|
|
789
|
+
// canvas.calcOffset();
|
|
790
|
+
// canvas.requestRenderAll();
|
|
791
|
+
// }
|
|
792
|
+
// };
|
|
793
|
+
// canvas.on("mouse:wheel", onWheel);
|
|
794
|
+
// return () => canvas.off("mouse:wheel", onWheel);
|
|
795
|
+
// }, [canvasZoom, canvasViewport]); // Keep deps
|
|
796
|
+
// useEffect(() => { const prevent = (e: WheelEvent) => { if (e.ctrlKey || e.metaKey) e.preventDefault(); }; window.addEventListener("wheel", prevent, { passive: false }); return () => window.removeEventListener("wheel", prevent); }, []);
|
|
797
|
+
// useEffect(() => { const canvas = fabricCanvasRef.current; if (!canvas) return; const sync = () => { const vpt = canvas.viewportTransform; if (!vpt) return; const z = canvas.getZoom(); setCanvasViewport((p) => (p.x !== vpt[4] || p.y !== vpt[5] ? { x: vpt[4], y: vpt[5] } : p)); setCanvasZoom((p) => (p !== z ? z : p)); }; canvas.on("after:render", sync); canvas.on("object:moving", sync); canvas.on("mouse:up", sync); return () => { canvas.off("after:render", sync); canvas.off("object:moving", sync); canvas.off("mouse:up", sync); }; }, []);
|
|
798
|
+
// useEffect(() => { const onKey = (e: KeyboardEvent) => { if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return; if (!(e.ctrlKey || e.metaKey)) return; if (e.key === "=" || e.key === "+") { e.preventDefault(); handleZoomIn(); } else if (e.key === "-" || e.key === "_") { e.preventDefault(); handleZoomOut(); } else if (e.key === "0") { e.preventDefault(); handleResetZoom(); } }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [canvasZoom]);
|
|
799
|
+
// // ═══════════════════════════════════════════════════════════════════════════
|
|
800
|
+
// // ADD THIS TO YOUR fabric-whiteboard.tsx
|
|
801
|
+
// // Place it after all other useEffects, before the return statement
|
|
802
|
+
// // ═══════════════════════════════════════════════════════════════════════════
|
|
803
|
+
// // ── Live Update Selected Shapes When Tool Options Change ────────────────────
|
|
804
|
+
// useEffect(() => {
|
|
805
|
+
// const canvas = fabricCanvasRef.current;
|
|
806
|
+
// if (!canvas) return;
|
|
807
|
+
// const activeObject = canvas.getActiveObject();
|
|
808
|
+
// if (!activeObject) return;
|
|
809
|
+
// const updateObjectStyle = (obj: FabricObject) => {
|
|
810
|
+
// let options: any = null;
|
|
811
|
+
// // 1. Map all types to options, including "path" (Pen)
|
|
812
|
+
// if (obj.type === "rect") options = toolOptions.rectangle;
|
|
813
|
+
// else if (obj.type === "circle") options = toolOptions.circle;
|
|
814
|
+
// else if (obj instanceof Frame) options = toolOptions.frame;
|
|
815
|
+
// else if (obj.type === "line") options = toolOptions.line;
|
|
816
|
+
// else if (obj.type === "path") options = toolOptions.pen; // ← ADDED: Pen mapping
|
|
817
|
+
// else if (obj instanceof FabricImage || obj.type === "image") options = toolOptions.image;
|
|
818
|
+
// else if (obj.type === "arrow" || obj.type === "bidirectional-arrow") options = toolOptions.arrow;
|
|
819
|
+
// if (!options) return;
|
|
820
|
+
// const update: any = {};
|
|
821
|
+
// // Opacity applies to EVERYTHING (Images, Paths, Shapes)
|
|
822
|
+
// if (typeof options.opacity === 'number') {
|
|
823
|
+
// update.opacity = options.opacity;
|
|
824
|
+
// }
|
|
825
|
+
// // Styles for Vector objects (Shapes + Pen Paths)
|
|
826
|
+
// if (obj.type !== "image" && !(obj instanceof FabricImage)) {
|
|
827
|
+
// // Paths (Pen) usually only care about stroke, not fill
|
|
828
|
+
// if (obj.type === "path") {
|
|
829
|
+
// update.stroke = options.color || options.strokeColor; // Pen uses .color usually
|
|
830
|
+
// } else {
|
|
831
|
+
// update.fill = options.fillColor === "transparent" ? "transparent" : options.fillColor;
|
|
832
|
+
// update.stroke = options.strokeColor;
|
|
833
|
+
// }
|
|
834
|
+
// update.strokeWidth = options.strokeWidth;
|
|
835
|
+
// // Dynamic dash calculation based on the current stroke width
|
|
836
|
+
// update.strokeDashArray = calculateDashArray(
|
|
837
|
+
// options.strokeDashArray,
|
|
838
|
+
// options.strokeWidth
|
|
839
|
+
// );
|
|
840
|
+
// }
|
|
841
|
+
// // Single batch update for performance
|
|
842
|
+
// obj.set(update);
|
|
843
|
+
// obj.setCoords();
|
|
844
|
+
// obj.dirty = true;
|
|
845
|
+
// };
|
|
846
|
+
// // 4. Performance: Use a single render call
|
|
847
|
+
// if (activeObject.type === "activeSelection") {
|
|
848
|
+
// (activeObject as any).getObjects().forEach((obj: FabricObject) => updateObjectStyle(obj));
|
|
849
|
+
// } else {
|
|
850
|
+
// updateObjectStyle(activeObject);
|
|
851
|
+
// }
|
|
852
|
+
// canvas.requestRenderAll();
|
|
853
|
+
// }, [toolOptions]);
|
|
854
|
+
// // ── Selection box ────────────────────────────────────────────────────────────
|
|
855
|
+
// useEffect(() => {
|
|
856
|
+
// const canvas = fabricCanvasRef.current;
|
|
857
|
+
// if (!canvas) return;
|
|
858
|
+
// let isSelecting = false, selStart = { x: 0, y: 0 }, selRect: Rect | null = null, rafId: number | null = null;
|
|
859
|
+
// const onDown = (e: any) => { if (activeTool !== "select" || e.target) return; isSelecting = true; const p = canvas.getScenePoint(e.e); selStart = { x: p.x, y: p.y }; selRect = new Rect({ left: p.x, top: p.y, width: 0, height: 0, fill: "transparent", stroke: "transparent", strokeWidth: 0, selectable: false, evented: false, visible: false }); canvas.add(selRect); canvas.renderAll(); };
|
|
860
|
+
// const onMove = (e: any) => { if (!isSelecting || !selRect) return; if (rafId !== null) cancelAnimationFrame(rafId); rafId = requestAnimationFrame(() => { const p = canvas.getScenePoint(e.e); const w = p.x - selStart.x, h = p.y - selStart.y; selRect!.set({ left: w < 0 ? p.x : selStart.x, top: h < 0 ? p.y : selStart.y, width: Math.abs(w), height: Math.abs(h) }); selRect!.setCoords(); canvas.renderAll(); setSelectionBox({ x1: Math.min(selStart.x, p.x) * canvasZoom + canvasViewport.x, y1: Math.min(selStart.y, p.y) * canvasZoom + canvasViewport.y, x2: Math.max(selStart.x, p.x) * canvasZoom + canvasViewport.x, y2: Math.max(selStart.y, p.y) * canvasZoom + canvasViewport.y }); }); };
|
|
861
|
+
// const onUp = () => { if (!isSelecting || !selRect) return; if (rafId !== null) { cancelAnimationFrame(rafId); rafId = null; } canvas.remove(selRect); canvas.renderAll(); isSelecting = false; selRect = null; setTimeout(() => setSelectionBox(null), 100); };
|
|
862
|
+
// const onSelected = () => {
|
|
863
|
+
// const sel = canvas.getActiveObject();
|
|
864
|
+
// if (!sel || isDrawingRef.current) return;
|
|
865
|
+
// setSelectedCanvasObjects(
|
|
866
|
+
// sel.type === "activeSelection" ? (sel as any).getObjects() : [sel]
|
|
867
|
+
// );
|
|
868
|
+
// const typeMap: Record<string, string> = {
|
|
869
|
+
// rect: "rectangle",
|
|
870
|
+
// circle: "circle",
|
|
871
|
+
// line: "line",
|
|
872
|
+
// arrow: "arrow",
|
|
873
|
+
// "bidirectional-arrow": "arrow", // ← ADD THIS - map to "arrow" to show arrow options
|
|
874
|
+
// "i-text": "text",
|
|
875
|
+
// text: "text",
|
|
876
|
+
// path: "pen",
|
|
877
|
+
// image: "image",
|
|
878
|
+
// };
|
|
879
|
+
// const t = sel instanceof Frame
|
|
880
|
+
// ? "frame"
|
|
881
|
+
// : sel instanceof FabricImage
|
|
882
|
+
// ? "image"
|
|
883
|
+
// : sel instanceof Arrow
|
|
884
|
+
// ? "arrow"
|
|
885
|
+
// : sel instanceof BidirectionalArrow // ← ADD THIS
|
|
886
|
+
// ? "arrow"
|
|
887
|
+
// : typeMap[sel.type] ?? null;
|
|
888
|
+
// selectedObjectTypeRef.current = t;
|
|
889
|
+
// setSelectedObjectType(t);
|
|
890
|
+
// };
|
|
891
|
+
// const onDeselected = () => { selectedObjectTypeRef.current = null; setSelectedObjectType(null); setSelectionBox(null); setSelectedCanvasObjects([]); if (!isDrawingRef.current && activeTool !== "select" && activeTool !== "pan") useWhiteboardStore.getState().setActiveTool("select"); };
|
|
892
|
+
// canvas.on("selection:created", onSelected); canvas.on("selection:updated", onSelected); canvas.on("selection:cleared", onDeselected); canvas.on("mouse:down", onDown); canvas.on("mouse:move", onMove); canvas.on("mouse:up", onUp);
|
|
893
|
+
// return () => { canvas.off("selection:created", onSelected); canvas.off("selection:updated", onSelected); canvas.off("selection:cleared", onDeselected); canvas.off("mouse:down", onDown); canvas.off("mouse:move", onMove); canvas.off("mouse:up", onUp); if (rafId !== null) cancelAnimationFrame(rafId); if (selRect) canvas.remove(selRect); };
|
|
894
|
+
// }, [activeTool, canvasZoom, canvasViewport]);
|
|
895
|
+
// // ── Dropdown handlers ────────────────────────────────────────────────────────
|
|
896
|
+
// const handleAddTaskFromDropdown = (taskTemplate: { id: string; title: string; status: "todo" | "in-progress" | "done"; assignee?: string; project?: string; priority?: "low" | "medium" | "high"; dueDate?: string; }) => { const canvas = fabricCanvasRef.current; if (!canvas) return; const vpt = canvas.viewportTransform; if (!vpt) return; const cx = (canvas.getWidth() / 2 - vpt[4]) / canvasZoom; const cy = (canvas.getHeight() / 2 - vpt[5]) / canvasZoom; setTasks((prev) => [...prev, { ...taskTemplate, id: `${taskTemplate.id}-${Date.now()}`, x: cx - 150, y: cy - 60 }]); };
|
|
897
|
+
// const handleAddDocumentFromDropdown = (docTemplate: Omit<Document, "x" | "y">) => { const canvas = fabricCanvasRef.current; if (!canvas) return; const vpt = canvas.viewportTransform; if (!vpt) return; const cx = (canvas.getWidth() / 2 - vpt[4]) / canvasZoom; const cy = (canvas.getHeight() / 2 - vpt[5]) / canvasZoom; setDocuments((prev) => [...prev, { ...docTemplate, id: `${docTemplate.id}-${Date.now()}`, x: cx - 150, y: cy - 80 }]); };
|
|
898
|
+
// return (
|
|
899
|
+
// <div ref={containerRef} className="relative w-full h-screen overflow-hidden bg-[#0b0b0b]">
|
|
900
|
+
// <div className="absolute inset-0 pointer-events-none" style={{ backgroundImage: `radial-gradient(circle, rgba(255,255,255,0.2) 1.2px, transparent 1.2px)`, backgroundSize: "40px 40px", zIndex: 0 }} />
|
|
901
|
+
// <canvas ref={canvasRef} className="absolute inset-0" style={{ zIndex: 1 }} />
|
|
902
|
+
// <CanvasOverlayLayer tasks={tasks} documents={documents} onTasksUpdate={setTasks} onDocumentsUpdate={setDocuments} canvasZoom={canvasZoom} canvasViewport={canvasViewport} selectionBox={selectionBox} selectedCanvasObjects={selectedCanvasObjects} fabricCanvas={fabricCanvasRef }
|
|
903
|
+
// />
|
|
904
|
+
// <div className="absolute inset-0 pointer-events-none" style={{ zIndex: 100 }} >
|
|
905
|
+
// <div className="pointer-events-auto"><WhiteboardToolbar fabricCanvas={fabricCanvasRef} isRestoringRef={isRestoringRef} onAddTask={handleAddTaskFromDropdown} onAddDocument={handleAddDocumentFromDropdown} /></div>
|
|
906
|
+
// <div className="pointer-events-auto"><ToolOptionsPanel fabricCanvas={fabricCanvasRef} /></div>
|
|
907
|
+
// <div className="pointer-events-auto"><ZoomControls zoom={canvasZoom} onZoomIn={handleZoomIn} onZoomOut={handleZoomOut} onResetZoom={handleResetZoom} /></div>
|
|
908
|
+
// </div>
|
|
909
|
+
// </div>
|
|
910
|
+
// );
|
|
911
|
+
// }
|