@mhamz.01/easyflow-whiteboard 1.27.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.
- package/dist/components/node/custom-node-overlay-layer.d.ts.map +1 -1
- package/dist/components/node/custom-node-overlay-layer.js +31 -33
- package/dist/components/whiteboard/whiteboard.js +7 -5
- package/dist/hooks/useSelection.d.ts.map +1 -1
- package/dist/hooks/useSelection.js +53 -35
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"custom-node-overlay-layer.d.ts","sourceRoot":"","sources":["../../../src/components/node/custom-node-overlay-layer.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAM9C,MAAM,WAAW,IAAI;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,GAAG,aAAa,GAAG,MAAM,CAAC;IACxC,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IACrC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAED,UAAU,uBAAuB;IAC/B,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,IAAI,CAAC;IACxC,iBAAiB,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,EAAE,KAAK,IAAI,CAAC;IACpD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1C,YAAY,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACzE,qBAAqB,CAAC,EAAE,YAAY,EAAE,CAAC;IACvC,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC/C;AAaD,MAAM,CAAC,OAAO,UAAU,kBAAkB,CAAC,EACzC,KAAK,EACL,SAAS,EACT,aAAa,EACb,iBAAiB,EACjB,UAAc,EACd,cAA+B,EAC/B,YAAmB,EACnB,qBAA0B,EAC1B,YAAY,GACb,EAAE,uBAAuB,
|
|
1
|
+
{"version":3,"file":"custom-node-overlay-layer.d.ts","sourceRoot":"","sources":["../../../src/components/node/custom-node-overlay-layer.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAM9C,MAAM,WAAW,IAAI;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,GAAG,aAAa,GAAG,MAAM,CAAC;IACxC,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IACrC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAED,UAAU,uBAAuB;IAC/B,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,IAAI,CAAC;IACxC,iBAAiB,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,EAAE,KAAK,IAAI,CAAC;IACpD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1C,YAAY,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACzE,qBAAqB,CAAC,EAAE,YAAY,EAAE,CAAC;IACvC,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC/C;AAaD,MAAM,CAAC,OAAO,UAAU,kBAAkB,CAAC,EACzC,KAAK,EACL,SAAS,EACT,aAAa,EACb,iBAAiB,EACjB,UAAc,EACd,cAA+B,EAC/B,YAAmB,EACnB,qBAA0B,EAC1B,YAAY,GACb,EAAE,uBAAuB,2CAudzB"}
|
|
@@ -128,38 +128,37 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
|
|
|
128
128
|
return { x: doc.x, y: doc.y };
|
|
129
129
|
return undefined;
|
|
130
130
|
};
|
|
131
|
-
const isItemInSelectionBox = (
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
131
|
+
const isItemInSelectionBox = (x, y, width, height, box) => {
|
|
132
|
+
const itemX1 = x * canvasZoom + canvasViewport.x;
|
|
133
|
+
const itemY1 = y * canvasZoom + canvasViewport.y;
|
|
134
|
+
const itemX2 = itemX1 + width * canvasZoom;
|
|
135
|
+
const itemY2 = itemY1 + height * canvasZoom;
|
|
136
|
+
const boxX1 = Math.min(box.x1, box.x2);
|
|
137
|
+
const boxY1 = Math.min(box.y1, box.y2);
|
|
138
|
+
const boxX2 = Math.max(box.x1, box.x2);
|
|
139
|
+
const boxY2 = Math.max(box.y1, box.y2);
|
|
140
|
+
return !(boxX2 < itemX1 || boxX1 > itemX2 || boxY2 < itemY1 || boxY1 > itemY2);
|
|
140
141
|
};
|
|
141
142
|
// ── Selection box detection ──────────────────────────────────────────────────
|
|
142
143
|
useEffect(() => {
|
|
143
144
|
if (!selectionBox)
|
|
144
|
-
return;
|
|
145
|
+
return; // Don't clear on null — let keyboard/click handlers do that
|
|
145
146
|
const newSelected = new Set();
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const TASK_HEIGHT = 140;
|
|
150
|
-
const DOC_HEIGHT = 160;
|
|
151
|
-
localTasks.forEach((t) => {
|
|
152
|
-
if (isItemInSelectionBox(t.x, t.y, NODE_WIDTH, TASK_HEIGHT, selectionBox)) {
|
|
153
|
-
newSelected.add(t.id);
|
|
154
|
-
}
|
|
147
|
+
localTasks.forEach((task) => {
|
|
148
|
+
if (isItemInSelectionBox(task.x, task.y, 300, 140, selectionBox))
|
|
149
|
+
newSelected.add(task.id);
|
|
155
150
|
});
|
|
156
|
-
localDocuments.forEach((
|
|
157
|
-
if (isItemInSelectionBox(
|
|
158
|
-
newSelected.add(
|
|
159
|
-
|
|
151
|
+
localDocuments.forEach((doc) => {
|
|
152
|
+
if (isItemInSelectionBox(doc.x, doc.y, 300, 160, selectionBox))
|
|
153
|
+
newSelected.add(doc.id);
|
|
154
|
+
});
|
|
155
|
+
// Only update if something actually changed (avoids re-render churn)
|
|
156
|
+
setSelectedIds((prev) => {
|
|
157
|
+
const prevArr = Array.from(prev).sort().join(",");
|
|
158
|
+
const nextArr = Array.from(newSelected).sort().join(",");
|
|
159
|
+
return prevArr === nextArr ? prev : newSelected;
|
|
160
160
|
});
|
|
161
|
-
|
|
162
|
-
}, [selectionBox]);
|
|
161
|
+
}, [selectionBox, localTasks, localDocuments]);
|
|
163
162
|
// ── Drag start (HTML Node side) ──────────────────────────────────────────────
|
|
164
163
|
// Helper to extract coordinates regardless of event type
|
|
165
164
|
const getPointerEvent = (e) => {
|
|
@@ -329,25 +328,24 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
|
|
|
329
328
|
}, [localTasks, localDocuments, selectedIds, onTasksUpdate, onDocumentsUpdate]);
|
|
330
329
|
// ── Render helper ────────────────────────────────────────────────────────────
|
|
331
330
|
const renderItem = (id, x, y, children) => {
|
|
332
|
-
// We apply the viewport offset to the container,
|
|
333
|
-
// so here we only care about the scaled local position.
|
|
334
331
|
const screenX = x * canvasZoom;
|
|
335
332
|
const screenY = y * canvasZoom;
|
|
333
|
+
// 1. Detect if the user is interacting with the canvas at all
|
|
334
|
+
// 'dragging' is your existing state.
|
|
335
|
+
// You might want to pass 'isZooming' or 'isPanning' from your main canvas component here.
|
|
336
336
|
const isDragging = dragging?.itemIds.includes(id);
|
|
337
337
|
return (_jsx("div", { className: "pointer-events-auto absolute", style: {
|
|
338
338
|
left: 0,
|
|
339
339
|
top: 0,
|
|
340
|
-
//
|
|
340
|
+
// 2. Use translate3d for GPU performance
|
|
341
341
|
transform: `translate3d(${screenX}px, ${screenY}px, 0) scale(${canvasZoom})`,
|
|
342
342
|
transformOrigin: "top left",
|
|
343
|
-
//
|
|
344
|
-
//
|
|
343
|
+
// 3. THE FIX: Remove transition entirely during any viewport change
|
|
344
|
+
// Any 'ease' during zoom causes the "shaking" behavior.
|
|
345
345
|
transition: "none",
|
|
346
|
+
// 4. Optimization
|
|
346
347
|
willChange: "transform",
|
|
347
348
|
zIndex: isDragging ? 1000 : 1,
|
|
348
|
-
// Added: Prevent browser from trying to optimize text rendering during move
|
|
349
|
-
// which causes the "shaking" text bug.
|
|
350
|
-
backfaceVisibility: "hidden",
|
|
351
349
|
}, children: children }, id));
|
|
352
350
|
};
|
|
353
351
|
return (_jsx("div", { ref: overlayRef, className: "absolute inset-0 pointer-events-none", style: { zIndex: 50 }, onWheel: handleOverlayWheel, onClick: (e) => {
|
|
@@ -11,16 +11,16 @@
|
|
|
11
11
|
// FabricImage,
|
|
12
12
|
// classRegistry
|
|
13
13
|
// } from "fabric";
|
|
14
|
-
// import { useWhiteboardStore } from "
|
|
14
|
+
// import { useWhiteboardStore } from "../../store/whiteboard-store";
|
|
15
15
|
// import WhiteboardToolbar from "../toolbar/whiteboard-toolbar";
|
|
16
16
|
// import ToolOptionsPanel from "../toolbar/tooloptions-panel";
|
|
17
17
|
// import CanvasOverlayLayer from "../node/custom-node-overlay-layer";
|
|
18
18
|
// import type { Document } from "../node/custom-node-overlay-layer";
|
|
19
19
|
// import ZoomControls from "../zoomcontrol/zoom-control";
|
|
20
|
-
// import { initializeFabricCanvas, updateDrawingObject, addText, calculateDashArray,addWelcomeContent } from "
|
|
21
|
-
// import { Frame } from "
|
|
22
|
-
// import { Arrow } from "
|
|
23
|
-
// import { BidirectionalArrow } from "
|
|
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
24
|
// import * as fabric from "fabric";
|
|
25
25
|
// interface Task {
|
|
26
26
|
// id: string;
|
|
@@ -896,6 +896,7 @@
|
|
|
896
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
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
898
|
// return (
|
|
899
|
+
// <div className="easyflow-whiteboard">
|
|
899
900
|
// <div ref={containerRef} className="relative w-full h-screen overflow-hidden bg-[#0b0b0b]">
|
|
900
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 }} />
|
|
901
902
|
// <canvas ref={canvasRef} className="absolute inset-0" style={{ zIndex: 1 }} />
|
|
@@ -907,5 +908,6 @@
|
|
|
907
908
|
// <div className="pointer-events-auto"><ZoomControls zoom={canvasZoom} onZoomIn={handleZoomIn} onZoomOut={handleZoomOut} onResetZoom={handleResetZoom} /></div>
|
|
908
909
|
// </div>
|
|
909
910
|
// </div>
|
|
911
|
+
// </div>
|
|
910
912
|
// );
|
|
911
913
|
// }
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSelection.d.ts","sourceRoot":"","sources":["../../src/hooks/useSelection.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAQ,YAAY,EAAe,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"useSelection.d.ts","sourceRoot":"","sources":["../../src/hooks/useSelection.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAQ,YAAY,EAAe,MAAM,QAAQ,CAAC;AAMjE,UAAU,iBAAiB;IACzB,YAAY,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC7C,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,eAAe,EAAE,CAAC,GAAG,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,KAAK,IAAI,CAAC;IAC1F,wBAAwB,EAAE,CAAC,OAAO,EAAE,YAAY,EAAE,KAAK,IAAI,CAAC;IAC5D,YAAY,EAAE,KAAK,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC;CAC/C;AAED,eAAO,MAAM,YAAY,GAAI,oHAQ1B,iBAAiB,SAgLnB,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// hooks/useSelection.ts
|
|
2
|
-
import { useEffect } from "react";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
3
|
import { Rect, FabricImage } from "fabric";
|
|
4
4
|
import { Frame } from "../lib/fabric-frame";
|
|
5
5
|
import { Arrow } from "../lib/fabric-arrow";
|
|
@@ -8,6 +8,15 @@ import { useWhiteboardStore } from "../store/whiteboard-store";
|
|
|
8
8
|
export const useSelection = ({ fabricCanvas, activeTool, canvasZoom, canvasViewport, setSelectionBox, setSelectedCanvasObjects, isDrawingRef, }) => {
|
|
9
9
|
const setSelectedObjectType = useWhiteboardStore((state) => state.setSelectedObjectType);
|
|
10
10
|
const setActiveTool = useWhiteboardStore((state) => state.setActiveTool);
|
|
11
|
+
// ── KEY FIX 1: Store zoom/viewport in refs so the effect never re-registers ──
|
|
12
|
+
// This prevents listener teardown/re-attach during pan/zoom gestures
|
|
13
|
+
const zoomRef = useRef(canvasZoom);
|
|
14
|
+
const viewportRef = useRef(canvasViewport);
|
|
15
|
+
const activeToolRef = useRef(activeTool);
|
|
16
|
+
// Keep refs in sync with latest props on every render
|
|
17
|
+
useEffect(() => { zoomRef.current = canvasZoom; }, [canvasZoom]);
|
|
18
|
+
useEffect(() => { viewportRef.current = canvasViewport; }, [canvasViewport]);
|
|
19
|
+
useEffect(() => { activeToolRef.current = activeTool; }, [activeTool]);
|
|
11
20
|
useEffect(() => {
|
|
12
21
|
const canvas = fabricCanvas.current;
|
|
13
22
|
if (!canvas)
|
|
@@ -17,9 +26,11 @@ export const useSelection = ({ fabricCanvas, activeTool, canvasZoom, canvasViewp
|
|
|
17
26
|
let selRect = null;
|
|
18
27
|
let rafId = null;
|
|
19
28
|
const onDown = (e) => {
|
|
20
|
-
|
|
29
|
+
// ── KEY FIX 2: Use ref instead of closure-captured activeTool ──
|
|
30
|
+
if (activeToolRef.current !== "select" || e.target)
|
|
21
31
|
return;
|
|
22
32
|
isSelecting = true;
|
|
33
|
+
// getScenePoint returns world coordinates (zoom + pan already factored in by Fabric)
|
|
23
34
|
const p = canvas.getScenePoint(e.e);
|
|
24
35
|
selStart = { x: p.x, y: p.y };
|
|
25
36
|
selRect = new Rect({
|
|
@@ -27,17 +38,18 @@ export const useSelection = ({ fabricCanvas, activeTool, canvasZoom, canvasViewp
|
|
|
27
38
|
top: p.y,
|
|
28
39
|
width: 0,
|
|
29
40
|
height: 0,
|
|
30
|
-
fill: "
|
|
31
|
-
stroke: "
|
|
32
|
-
strokeWidth:
|
|
41
|
+
fill: "rgba(2, 154, 255, 0.08)",
|
|
42
|
+
stroke: "#029AFF",
|
|
43
|
+
strokeWidth: 1 / zoomRef.current, // stays 1px visually at any zoom
|
|
33
44
|
selectable: false,
|
|
34
45
|
evented: false,
|
|
35
|
-
|
|
46
|
+
// ── KEY FIX 3: Make it visible so user sees the selection rect ──
|
|
47
|
+
// was `visible: false` before — invisible but still blocking
|
|
48
|
+
excludeFromExport: true,
|
|
36
49
|
});
|
|
37
50
|
canvas.add(selRect);
|
|
38
51
|
canvas.renderAll();
|
|
39
52
|
};
|
|
40
|
-
// hooks/useSelection.ts - UPDATED
|
|
41
53
|
const onMove = (e) => {
|
|
42
54
|
if (!isSelecting || !selRect)
|
|
43
55
|
return;
|
|
@@ -47,34 +59,46 @@ export const useSelection = ({ fabricCanvas, activeTool, canvasZoom, canvasViewp
|
|
|
47
59
|
const p = canvas.getScenePoint(e.e);
|
|
48
60
|
const w = p.x - selStart.x;
|
|
49
61
|
const h = p.y - selStart.y;
|
|
50
|
-
const x1 = Math.min(selStart.x, p.x);
|
|
51
|
-
const y1 = Math.min(selStart.y, p.y);
|
|
52
|
-
const x2 = Math.max(selStart.x, p.x);
|
|
53
|
-
const y2 = Math.max(selStart.y, p.y);
|
|
54
62
|
selRect.set({
|
|
55
|
-
left:
|
|
56
|
-
top:
|
|
57
|
-
width:
|
|
58
|
-
height:
|
|
63
|
+
left: w < 0 ? p.x : selStart.x,
|
|
64
|
+
top: h < 0 ? p.y : selStart.y,
|
|
65
|
+
width: Math.abs(w),
|
|
66
|
+
height: Math.abs(h),
|
|
67
|
+
strokeWidth: 1 / zoomRef.current, // keep stroke sharp at any zoom
|
|
59
68
|
});
|
|
60
69
|
selRect.setCoords();
|
|
61
70
|
canvas.renderAll();
|
|
62
|
-
//
|
|
63
|
-
|
|
71
|
+
// ── KEY FIX 4: Read from refs — always fresh, no stale closure ──
|
|
72
|
+
const zoom = zoomRef.current;
|
|
73
|
+
const vp = viewportRef.current;
|
|
74
|
+
// World → Screen conversion:
|
|
75
|
+
// screenX = worldX * zoom + panX (matches exactly how nodes are rendered)
|
|
76
|
+
setSelectionBox({
|
|
77
|
+
x1: Math.min(selStart.x, p.x) * zoom + vp.x,
|
|
78
|
+
y1: Math.min(selStart.y, p.y) * zoom + vp.y,
|
|
79
|
+
x2: Math.max(selStart.x, p.x) * zoom + vp.x,
|
|
80
|
+
y2: Math.max(selStart.y, p.y) * zoom + vp.y,
|
|
81
|
+
});
|
|
64
82
|
});
|
|
65
83
|
};
|
|
66
84
|
const onUp = () => {
|
|
67
|
-
if (!isSelecting
|
|
85
|
+
if (!isSelecting)
|
|
68
86
|
return;
|
|
69
87
|
if (rafId !== null) {
|
|
70
88
|
cancelAnimationFrame(rafId);
|
|
71
89
|
rafId = null;
|
|
72
90
|
}
|
|
73
|
-
|
|
74
|
-
|
|
91
|
+
if (selRect) {
|
|
92
|
+
canvas.remove(selRect);
|
|
93
|
+
canvas.renderAll();
|
|
94
|
+
selRect = null;
|
|
95
|
+
}
|
|
75
96
|
isSelecting = false;
|
|
76
|
-
|
|
77
|
-
|
|
97
|
+
// ── KEY FIX 5: Delay clear long enough for overlay useEffect to fire ──
|
|
98
|
+
// onDeselected (selection:cleared) was calling setSelectionBox(null) immediately,
|
|
99
|
+
// racing with the overlay's useEffect. 150ms ensures the React render cycle
|
|
100
|
+
// processes the final box position before it's cleared.
|
|
101
|
+
setTimeout(() => setSelectionBox(null), 150);
|
|
78
102
|
};
|
|
79
103
|
const onSelected = () => {
|
|
80
104
|
const sel = canvas.getActiveObject();
|
|
@@ -105,9 +129,11 @@ export const useSelection = ({ fabricCanvas, activeTool, canvasZoom, canvasViewp
|
|
|
105
129
|
};
|
|
106
130
|
const onDeselected = () => {
|
|
107
131
|
setSelectedObjectType(null);
|
|
108
|
-
|
|
132
|
+
// ── KEY FIX 6: Do NOT clear selectionBox here ──
|
|
133
|
+
// This was racing with onUp's setTimeout and clearing before overlay processed it.
|
|
134
|
+
// onUp owns the selectionBox lifecycle. Only clear canvas objects here.
|
|
109
135
|
setSelectedCanvasObjects([]);
|
|
110
|
-
if (!isDrawingRef.current &&
|
|
136
|
+
if (!isDrawingRef.current && activeToolRef.current !== "select" && activeToolRef.current !== "pan") {
|
|
111
137
|
setActiveTool("select");
|
|
112
138
|
}
|
|
113
139
|
};
|
|
@@ -129,15 +155,7 @@ export const useSelection = ({ fabricCanvas, activeTool, canvasZoom, canvasViewp
|
|
|
129
155
|
if (selRect)
|
|
130
156
|
canvas.remove(selRect);
|
|
131
157
|
};
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
canvasViewport,
|
|
136
|
-
fabricCanvas,
|
|
137
|
-
setSelectionBox,
|
|
138
|
-
setSelectedCanvasObjects,
|
|
139
|
-
setSelectedObjectType,
|
|
140
|
-
setActiveTool,
|
|
141
|
-
isDrawingRef
|
|
142
|
-
]);
|
|
158
|
+
// ── KEY FIX 7: Remove canvasZoom/canvasViewport/activeTool from deps ──
|
|
159
|
+
// They are now read via refs — effect only registers once per canvas mount.
|
|
160
|
+
}, [fabricCanvas, setSelectionBox, setSelectedCanvasObjects, setSelectedObjectType, setActiveTool, isDrawingRef]);
|
|
143
161
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mhamz.01/easyflow-whiteboard",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "A feature-rich whiteboard component built with Fabric.js and React",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@tailwindcss/cli": "^4.2.1",
|
|
45
45
|
"@types/fabric": "^5.3.11",
|
|
46
|
-
"@types/node": "^20.
|
|
46
|
+
"@types/node": "^20.19.37",
|
|
47
47
|
"@types/react": "^19.0.0",
|
|
48
48
|
"@types/react-dom": "^19.0.0",
|
|
49
49
|
"typescript": "^5.0.0"
|