@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.
Files changed (127) hide show
  1. package/dist/components/node/custom-node-overlay-layer.d.ts +44 -0
  2. package/dist/components/node/custom-node-overlay-layer.d.ts.map +1 -0
  3. package/dist/components/node/custom-node-overlay-layer.js +353 -0
  4. package/dist/components/node/custom-node.d.ts +17 -0
  5. package/dist/components/node/custom-node.d.ts.map +1 -0
  6. package/dist/components/node/custom-node.js +63 -0
  7. package/dist/components/node/document-node.d.ts +14 -0
  8. package/dist/components/node/document-node.d.ts.map +1 -0
  9. package/dist/components/node/document-node.js +58 -0
  10. package/dist/components/toolbar/document-dropdown.d.ts +14 -0
  11. package/dist/components/toolbar/document-dropdown.d.ts.map +1 -0
  12. package/dist/components/toolbar/document-dropdown.js +66 -0
  13. package/dist/components/toolbar/options/arrow-options.d.ts +8 -0
  14. package/dist/components/toolbar/options/arrow-options.d.ts.map +1 -0
  15. package/dist/components/toolbar/options/arrow-options.js +109 -0
  16. package/dist/components/toolbar/options/erase-option.d.ts +2 -0
  17. package/dist/components/toolbar/options/erase-option.d.ts.map +1 -0
  18. package/dist/components/toolbar/options/erase-option.js +20 -0
  19. package/dist/components/toolbar/options/image-options.d.ts +2 -0
  20. package/dist/components/toolbar/options/image-options.d.ts.map +1 -0
  21. package/dist/components/toolbar/options/image-options.js +10 -0
  22. package/dist/components/toolbar/options/line-options.d.ts +2 -0
  23. package/dist/components/toolbar/options/line-options.d.ts.map +1 -0
  24. package/dist/components/toolbar/options/line-options.js +46 -0
  25. package/dist/components/toolbar/options/pen-option.d.ts +2 -0
  26. package/dist/components/toolbar/options/pen-option.d.ts.map +1 -0
  27. package/dist/components/toolbar/options/pen-option.js +53 -0
  28. package/dist/components/toolbar/options/shape-option.d.ts +6 -0
  29. package/dist/components/toolbar/options/shape-option.d.ts.map +1 -0
  30. package/dist/components/toolbar/options/shape-option.js +58 -0
  31. package/dist/components/toolbar/options/text-option.d.ts +2 -0
  32. package/dist/components/toolbar/options/text-option.d.ts.map +1 -0
  33. package/dist/components/toolbar/options/text-option.js +73 -0
  34. package/dist/components/toolbar/task-dropdown.d.ts +15 -0
  35. package/dist/components/toolbar/task-dropdown.d.ts.map +1 -0
  36. package/dist/components/toolbar/task-dropdown.js +85 -0
  37. package/dist/components/toolbar/toolbar-button.d.ts +12 -0
  38. package/dist/components/toolbar/toolbar-button.d.ts.map +1 -0
  39. package/dist/components/toolbar/toolbar-button.js +8 -0
  40. package/dist/components/toolbar/toolbar-seperator.d.ts +6 -0
  41. package/dist/components/toolbar/toolbar-seperator.d.ts.map +1 -0
  42. package/dist/components/toolbar/toolbar-seperator.js +5 -0
  43. package/dist/components/toolbar/tooloptions-panel.d.ts +8 -0
  44. package/dist/components/toolbar/tooloptions-panel.d.ts.map +1 -0
  45. package/dist/components/toolbar/tooloptions-panel.js +88 -0
  46. package/dist/components/toolbar/whiteboard-toolbar.d.ts +28 -0
  47. package/dist/components/toolbar/whiteboard-toolbar.d.ts.map +1 -0
  48. package/dist/components/toolbar/whiteboard-toolbar.js +160 -0
  49. package/dist/components/ui/dropdown-menu.d.ts +26 -0
  50. package/dist/components/ui/dropdown-menu.d.ts.map +1 -0
  51. package/dist/components/ui/dropdown-menu.js +51 -0
  52. package/dist/components/ui/label.d.ts +5 -0
  53. package/dist/components/ui/label.d.ts.map +1 -0
  54. package/dist/components/ui/label.js +8 -0
  55. package/dist/components/ui/slider.d.ts +5 -0
  56. package/dist/components/ui/slider.d.ts.map +1 -0
  57. package/dist/components/ui/slider.js +14 -0
  58. package/dist/components/whiteboard/whiteboard-test.d.ts +2 -0
  59. package/dist/components/whiteboard/whiteboard-test.d.ts.map +1 -0
  60. package/dist/components/whiteboard/whiteboard-test.js +207 -0
  61. package/dist/components/whiteboard/whiteboard.d.ts +1 -0
  62. package/dist/components/whiteboard/whiteboard.d.ts.map +1 -0
  63. package/dist/components/whiteboard/whiteboard.js +911 -0
  64. package/dist/components/zoomcontrol/zoom-control.d.ts +9 -0
  65. package/dist/components/zoomcontrol/zoom-control.d.ts.map +1 -0
  66. package/dist/components/zoomcontrol/zoom-control.js +7 -0
  67. package/dist/hooks/useCanvasInit.d.ts +15 -0
  68. package/dist/hooks/useCanvasInit.d.ts.map +1 -0
  69. package/dist/hooks/useCanvasInit.js +89 -0
  70. package/dist/hooks/useDrawing.d.ts +23 -0
  71. package/dist/hooks/useDrawing.d.ts.map +1 -0
  72. package/dist/hooks/useDrawing.js +142 -0
  73. package/dist/hooks/useEraser.d.ts +27 -0
  74. package/dist/hooks/useEraser.d.ts.map +1 -0
  75. package/dist/hooks/useEraser.js +143 -0
  76. package/dist/hooks/useLiveUpdate.d.ts +9 -0
  77. package/dist/hooks/useLiveUpdate.d.ts.map +1 -0
  78. package/dist/hooks/useLiveUpdate.js +63 -0
  79. package/dist/hooks/useMouseHandlers.d.ts +25 -0
  80. package/dist/hooks/useMouseHandlers.d.ts.map +1 -0
  81. package/dist/hooks/useMouseHandlers.js +44 -0
  82. package/dist/hooks/usePan.d.ts +17 -0
  83. package/dist/hooks/usePan.d.ts.map +1 -0
  84. package/dist/hooks/usePan.js +80 -0
  85. package/dist/hooks/usePersistance.d.ts +13 -0
  86. package/dist/hooks/usePersistance.d.ts.map +1 -0
  87. package/dist/hooks/usePersistance.js +79 -0
  88. package/dist/hooks/useSelection.d.ts +21 -0
  89. package/dist/hooks/useSelection.d.ts.map +1 -0
  90. package/dist/hooks/useSelection.js +142 -0
  91. package/dist/hooks/useTextStyle.d.ts +9 -0
  92. package/dist/hooks/useTextStyle.d.ts.map +1 -0
  93. package/dist/hooks/useTextStyle.js +32 -0
  94. package/dist/hooks/useToolManager.d.ts +15 -0
  95. package/dist/hooks/useToolManager.d.ts.map +1 -0
  96. package/dist/hooks/useToolManager.js +115 -0
  97. package/dist/hooks/useZoom.d.ts +25 -0
  98. package/dist/hooks/useZoom.d.ts.map +1 -0
  99. package/dist/hooks/useZoom.js +133 -0
  100. package/dist/index.d.ts +4 -0
  101. package/dist/index.d.ts.map +1 -0
  102. package/dist/index.js +30 -0
  103. package/dist/lib/eraser-brush.d.ts +1 -0
  104. package/dist/lib/eraser-brush.d.ts.map +1 -0
  105. package/dist/lib/eraser-brush.js +21 -0
  106. package/dist/lib/fabric-arrow.d.ts +16 -0
  107. package/dist/lib/fabric-arrow.d.ts.map +1 -0
  108. package/dist/lib/fabric-arrow.js +50 -0
  109. package/dist/lib/fabric-bidirectional-arrow.d.ts +20 -0
  110. package/dist/lib/fabric-bidirectional-arrow.d.ts.map +1 -0
  111. package/dist/lib/fabric-bidirectional-arrow.js +65 -0
  112. package/dist/lib/fabric-frame.d.ts +7 -0
  113. package/dist/lib/fabric-frame.d.ts.map +1 -0
  114. package/dist/lib/fabric-frame.js +25 -0
  115. package/dist/lib/fabric-utils.d.ts +30 -0
  116. package/dist/lib/fabric-utils.d.ts.map +1 -0
  117. package/dist/lib/fabric-utils.js +273 -0
  118. package/dist/lib/utils.d.ts +3 -0
  119. package/dist/lib/utils.d.ts.map +1 -0
  120. package/dist/lib/utils.js +5 -0
  121. package/dist/store/whiteboard-store.d.ts +99 -0
  122. package/dist/store/whiteboard-store.d.ts.map +1 -0
  123. package/dist/store/whiteboard-store.js +137 -0
  124. package/dist/types/canvas-node.d.ts +24 -0
  125. package/dist/types/canvas-node.d.ts.map +1 -0
  126. package/dist/types/canvas-node.js +1 -0
  127. 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
+ // }