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