@mhamz.01/easyflow-whiteboard 2.1.0 → 2.3.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 +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,2CAudzB"}
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,2CA2dzB"}
@@ -142,21 +142,26 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
142
142
  // ── Selection box detection ──────────────────────────────────────────────────
143
143
  useEffect(() => {
144
144
  if (!selectionBox)
145
- return; // Don't clear on null — let keyboard/click handlers do that
145
+ return;
146
+ // ── O(n) single pass — no sort, no join, no extra allocations ──
146
147
  const newSelected = new Set();
147
- localTasks.forEach((task) => {
148
+ for (const task of localTasks) {
148
149
  if (isItemInSelectionBox(task.x, task.y, 300, 140, selectionBox))
149
150
  newSelected.add(task.id);
150
- });
151
- localDocuments.forEach((doc) => {
152
- if (isItemInSelectionBox(doc.x, doc.y, 300, 160, selectionBox))
151
+ }
152
+ for (const doc of localDocuments) {
153
+ if (isItemInSelectionBox(doc.x, doc.y, 320, 160, selectionBox))
153
154
  newSelected.add(doc.id);
154
- });
155
- // Only update if something actually changed (avoids re-render churn)
155
+ }
156
+ // ── O(n) equality check: size first (fast path), then membership ──
156
157
  setSelectedIds((prev) => {
157
- const prevArr = Array.from(prev).sort().join(",");
158
- const nextArr = Array.from(newSelected).sort().join(",");
159
- return prevArr === nextArr ? prev : newSelected;
158
+ if (prev.size !== newSelected.size)
159
+ return newSelected;
160
+ for (const id of newSelected) {
161
+ if (!prev.has(id))
162
+ return newSelected; // found a difference, swap
163
+ }
164
+ return prev; // identical — return same reference, no re-render
160
165
  });
161
166
  }, [selectionBox, localTasks, localDocuments]);
162
167
  // ── Drag start (HTML Node side) ──────────────────────────────────────────────
@@ -16,6 +16,6 @@ interface UseSelectionProps {
16
16
  setSelectedCanvasObjects: (objects: FabricObject[]) => void;
17
17
  isDrawingRef: React.MutableRefObject<boolean>;
18
18
  }
19
- export declare const useSelection: ({ fabricCanvas, activeTool, canvasZoom, canvasViewport, setSelectionBox, setSelectedCanvasObjects, isDrawingRef, }: UseSelectionProps) => void;
19
+ export declare const useSelection: ({ fabricCanvas, activeTool, setSelectionBox, setSelectedCanvasObjects, isDrawingRef, }: UseSelectionProps) => void;
20
20
  export {};
21
21
  //# sourceMappingURL=useSelection.d.ts.map
@@ -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;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
+ {"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,wFAM1B,iBAAiB,SAsJnB,CAAC"}
@@ -5,17 +5,11 @@ import { Frame } from "../lib/fabric-frame";
5
5
  import { Arrow } from "../lib/fabric-arrow";
6
6
  import { BidirectionalArrow } from "../lib/fabric-bidirectional-arrow";
7
7
  import { useWhiteboardStore } from "../store/whiteboard-store";
8
- export const useSelection = ({ fabricCanvas, activeTool, canvasZoom, canvasViewport, setSelectionBox, setSelectedCanvasObjects, isDrawingRef, }) => {
9
- const setSelectedObjectType = useWhiteboardStore((state) => state.setSelectedObjectType);
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);
8
+ export const useSelection = ({ fabricCanvas, activeTool, setSelectionBox, setSelectedCanvasObjects, isDrawingRef, }) => {
9
+ const setSelectedObjectType = useWhiteboardStore((s) => s.setSelectedObjectType);
10
+ const setActiveTool = useWhiteboardStore((s) => s.setActiveTool);
11
+ // activeTool ref so effect never re-registers on tool change
15
12
  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
13
  useEffect(() => { activeToolRef.current = activeTool; }, [activeTool]);
20
14
  useEffect(() => {
21
15
  const canvas = fabricCanvas.current;
@@ -26,11 +20,9 @@ export const useSelection = ({ fabricCanvas, activeTool, canvasZoom, canvasViewp
26
20
  let selRect = null;
27
21
  let rafId = null;
28
22
  const onDown = (e) => {
29
- // ── KEY FIX 2: Use ref instead of closure-captured activeTool ──
30
23
  if (activeToolRef.current !== "select" || e.target)
31
24
  return;
32
25
  isSelecting = true;
33
- // getScenePoint returns world coordinates (zoom + pan already factored in by Fabric)
34
26
  const p = canvas.getScenePoint(e.e);
35
27
  selStart = { x: p.x, y: p.y };
36
28
  selRect = new Rect({
@@ -38,13 +30,12 @@ export const useSelection = ({ fabricCanvas, activeTool, canvasZoom, canvasViewp
38
30
  top: p.y,
39
31
  width: 0,
40
32
  height: 0,
41
- fill: "rgba(2, 154, 255, 0.08)",
42
- stroke: "#029AFF",
43
- strokeWidth: 1 / zoomRef.current, // stays 1px visually at any zoom
33
+ fill: "transparent",
34
+ stroke: "transparent",
35
+ strokeWidth: 0,
44
36
  selectable: false,
45
37
  evented: false,
46
- // ── KEY FIX 3: Make it visible so user sees the selection rect ──
47
- // was `visible: false` before — invisible but still blocking
38
+ visible: false, // hidden, pure coordinate tracker
48
39
  excludeFromExport: true,
49
40
  });
50
41
  canvas.add(selRect);
@@ -64,20 +55,20 @@ export const useSelection = ({ fabricCanvas, activeTool, canvasZoom, canvasViewp
64
55
  top: h < 0 ? p.y : selStart.y,
65
56
  width: Math.abs(w),
66
57
  height: Math.abs(h),
67
- strokeWidth: 1 / zoomRef.current, // keep stroke sharp at any zoom
58
+ // strokeWidth line removed
68
59
  });
69
60
  selRect.setCoords();
70
61
  canvas.renderAll();
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)
62
+ // ── Read directly from Fabric VPT — always frame-perfect, zero React lag ──
63
+ const vpt = canvas.viewportTransform;
64
+ const zoom = vpt[0]; // scaleX
65
+ const vpX = vpt[4]; // panX
66
+ const vpY = vpt[5]; // panY
76
67
  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,
68
+ x1: Math.min(selStart.x, p.x) * zoom + vpX,
69
+ y1: Math.min(selStart.y, p.y) * zoom + vpY,
70
+ x2: Math.max(selStart.x, p.x) * zoom + vpX,
71
+ y2: Math.max(selStart.y, p.y) * zoom + vpY,
81
72
  });
82
73
  });
83
74
  };
@@ -94,10 +85,6 @@ export const useSelection = ({ fabricCanvas, activeTool, canvasZoom, canvasViewp
94
85
  selRect = null;
95
86
  }
96
87
  isSelecting = false;
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
88
  setTimeout(() => setSelectionBox(null), 150);
102
89
  };
103
90
  const onSelected = () => {
@@ -106,34 +93,24 @@ export const useSelection = ({ fabricCanvas, activeTool, canvasZoom, canvasViewp
106
93
  return;
107
94
  setSelectedCanvasObjects(sel.type === "activeSelection" ? sel.getObjects() : [sel]);
108
95
  const typeMap = {
109
- rect: "rectangle",
110
- circle: "circle",
111
- line: "line",
112
- arrow: "arrow",
113
- "bidirectional-arrow": "arrow",
114
- "i-text": "text",
115
- text: "text",
116
- path: "pen",
117
- image: "image",
96
+ rect: "rectangle", circle: "circle", line: "line",
97
+ arrow: "arrow", "bidirectional-arrow": "arrow",
98
+ "i-text": "text", text: "text", path: "pen", image: "image",
118
99
  };
119
- const t = sel instanceof Frame
120
- ? "frame"
121
- : sel instanceof FabricImage
122
- ? "image"
123
- : sel instanceof Arrow
124
- ? "arrow"
125
- : sel instanceof BidirectionalArrow
126
- ? "arrow"
100
+ const t = sel instanceof Frame ? "frame"
101
+ : sel instanceof FabricImage ? "image"
102
+ : sel instanceof Arrow ? "arrow"
103
+ : sel instanceof BidirectionalArrow ? "arrow"
127
104
  : typeMap[sel.type] ?? null;
128
105
  setSelectedObjectType(t);
129
106
  };
130
107
  const onDeselected = () => {
131
108
  setSelectedObjectType(null);
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
+ // ── Do NOT touch selectionBox here — onUp owns its lifecycle ──
135
110
  setSelectedCanvasObjects([]);
136
- if (!isDrawingRef.current && activeToolRef.current !== "select" && activeToolRef.current !== "pan") {
111
+ if (!isDrawingRef.current &&
112
+ activeToolRef.current !== "select" &&
113
+ activeToolRef.current !== "pan") {
137
114
  setActiveTool("select");
138
115
  }
139
116
  };
@@ -152,10 +129,10 @@ export const useSelection = ({ fabricCanvas, activeTool, canvasZoom, canvasViewp
152
129
  canvas.off("mouse:up", onUp);
153
130
  if (rafId !== null)
154
131
  cancelAnimationFrame(rafId);
155
- if (selRect)
132
+ if (selRect) {
156
133
  canvas.remove(selRect);
134
+ }
157
135
  };
158
- // ── KEY FIX 7: Remove canvasZoom/canvasViewport/activeTool from deps ──
159
- // They are now read via refs — effect only registers once per canvas mount.
136
+ // ── Stable deps only effect registers once per canvas mount ──
160
137
  }, [fabricCanvas, setSelectionBox, setSelectedCanvasObjects, setSelectedObjectType, setActiveTool, isDrawingRef]);
161
138
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mhamz.01/easyflow-whiteboard",
3
- "version": "2.1.0",
3
+ "version": "2.3.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",