@mhamz.01/easyflow-whiteboard 2.15.0 → 2.16.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;AAS9C,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,2CAsgBzB"}
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;AAoBD,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,2CAqZzB"}
@@ -1,108 +1,93 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useState, useEffect, useRef } from "react";
3
+ import { useState, useEffect, useRef, useCallback, memo } from "react";
4
4
  import TaskNode from "./custom-node";
5
5
  import DocumentNode from "./document-node";
6
+ // ── PERF: Memoized node wrappers ──────────────────────────────────────────────
7
+ // Prevents sibling nodes from re-rendering when only one node's position changes.
8
+ // Without memo, every setLocalTasks call during drag re-renders ALL nodes.
9
+ const MemoTaskNode = memo(TaskNode);
10
+ const MemoDocumentNode = memo(DocumentNode);
6
11
  // ─── Component ────────────────────────────────────────────────────────────────
7
12
  export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, onDocumentsUpdate, canvasZoom = 1, canvasViewport = { x: 0, y: 0 }, selectionBox = null, selectedCanvasObjects = [], fabricCanvas, }) {
8
13
  const [localTasks, setLocalTasks] = useState(tasks);
9
14
  const [localDocuments, setLocalDocuments] = useState(documents);
10
15
  const [selectedIds, setSelectedIds] = useState(new Set());
11
- const [dragging, setDragging] = useState(null);
16
+ // ─── Core refs ────────────────────────────────────────────────────────────
12
17
  const dragStateRef = useRef({
13
- isDragging: false,
14
- itemIds: [],
15
- startPositions: new Map(),
16
- canvasObjectsStartPos: new Map(),
17
- offsetX: 0,
18
- offsetY: 0,
18
+ isDragging: false, itemIds: [],
19
+ startPositions: new Map(), canvasObjectsStartPos: new Map(),
20
+ offsetX: 0, offsetY: 0,
19
21
  });
20
22
  const rafIdRef = useRef(null);
21
23
  const overlayRef = useRef(null);
22
- // ── Sync props local state ────────────────────────────────────────────────
24
+ // ── PERF: Mutex prevents object:moving and handleMove writing simultaneously ─
25
+ const isHtmlDraggingRef = useRef(false);
26
+ // ── PERF: Always-fresh refs — rebuilt synchronously every render ──────────
27
+ // Eliminates ALL stale closure problems without any useCallback deps.
28
+ // nodePositionsRef: ground truth for positions at drag start
29
+ const nodePositionsRef = useRef(new Map());
30
+ nodePositionsRef.current = new Map([
31
+ ...localTasks.map((t) => [t.id, { x: t.x, y: t.y }]),
32
+ ...localDocuments.map((d) => [d.id, { x: d.x, y: d.y }]),
33
+ ]);
34
+ // selectedIdsRef: used in stable effect closures
35
+ const selectedIdsRef = useRef(selectedIds);
36
+ selectedIdsRef.current = selectedIds;
37
+ // selectedCanvasObjectsRef: avoids stale prop in handleDragStart
38
+ const selectedCanvasObjectsRef = useRef(selectedCanvasObjects);
39
+ selectedCanvasObjectsRef.current = selectedCanvasObjects;
40
+ // Parent callbacks in refs — handleEnd never captures stale callbacks
41
+ const onTasksUpdateRef = useRef(onTasksUpdate);
42
+ const onDocumentsUpdateRef = useRef(onDocumentsUpdate);
43
+ onTasksUpdateRef.current = onTasksUpdate;
44
+ onDocumentsUpdateRef.current = onDocumentsUpdate;
45
+ // ─── Sync props → local state ─────────────────────────────────────────────
23
46
  useEffect(() => { setLocalTasks(tasks); }, [tasks]);
24
47
  useEffect(() => { setLocalDocuments(documents); }, [documents]);
25
- // ── Event Forwarding (Fixes Zooming on Nodes) ───────────────────────────────
26
- const handleOverlayWheel = (e) => {
27
- if (e.ctrlKey || e.metaKey || e.shiftKey) {
28
- const canvas = fabricCanvas?.current;
29
- if (!canvas)
30
- return;
31
- const nativeEvent = e.nativeEvent;
32
- // getScenePoint handles the transformation from screen to canvas space
33
- const scenePoint = canvas.getScenePoint(nativeEvent);
34
- // Viewport point is simply the mouse position relative to the canvas element
35
- const rect = canvas.getElement().getBoundingClientRect();
36
- const viewportPoint = {
37
- x: nativeEvent.clientX - rect.left,
38
- y: nativeEvent.clientY - rect.top,
39
- };
40
- // We cast to 'any' here because we are manually triggering an internal
41
- // event bus, and Fabric's internal types for .fire() can be overly strict.
42
- canvas.fire("mouse:wheel", {
43
- e: nativeEvent,
44
- scenePoint,
45
- viewportPoint,
46
- });
47
- e.preventDefault();
48
- e.stopPropagation();
49
- }
50
- };
48
+ // ─── Wheel forwarding ─────────────────────────────────────────────────────
51
49
  useEffect(() => {
52
50
  const overlayEl = overlayRef.current;
53
51
  const canvas = fabricCanvas?.current;
54
52
  if (!overlayEl || !canvas)
55
53
  return;
56
54
  const handleGlobalWheel = (e) => {
57
- // Check if the user is hovering over an element that has pointer-events: auto
58
- // (meaning they are hovering over a Task or Document)
59
- const target = e.target;
60
- const isOverNode = target !== overlayEl;
55
+ const isOverNode = e.target !== overlayEl;
61
56
  if ((e.ctrlKey || e.metaKey) && isOverNode) {
62
- // 1. Prevent Browser Zoom immediately
63
57
  e.preventDefault();
64
58
  e.stopPropagation();
65
- // 2. Calculate coordinates for Fabric
66
59
  const scenePoint = canvas.getScenePoint(e);
67
60
  const rect = canvas.getElement().getBoundingClientRect();
68
- const viewportPoint = {
69
- x: e.clientX - rect.left,
70
- y: e.clientY - rect.top,
71
- };
72
- // 3. Manually fire the event into Fabric
73
61
  canvas.fire("mouse:wheel", {
74
- e: e,
75
- scenePoint,
76
- viewportPoint,
62
+ e, scenePoint,
63
+ viewportPoint: { x: e.clientX - rect.left, y: e.clientY - rect.top },
77
64
  });
78
65
  }
79
66
  };
80
- // CRITICAL: { passive: false } allows us to cancel the browser's zoom
81
67
  overlayEl.addEventListener("wheel", handleGlobalWheel, { passive: false });
82
- return () => {
83
- overlayEl.removeEventListener("wheel", handleGlobalWheel);
84
- };
85
- }, [fabricCanvas, canvasZoom]); // Re-bind when zoom changes to keep closure fresh
86
- // ── Fabric → Overlay Sync (Fixes Dragging from Fabric area) ──────────────────
68
+ return () => overlayEl.removeEventListener("wheel", handleGlobalWheel);
69
+ }, [fabricCanvas]);
70
+ // ─── Fabric → Overlay sync ────────────────────────────────────────────────
87
71
  useEffect(() => {
88
72
  const canvas = fabricCanvas?.current;
89
73
  if (!canvas)
90
74
  return;
91
75
  const handleObjectMoving = (e) => {
76
+ // MUTEX: HTML drag and Fabric drag must never write positions simultaneously
77
+ if (isHtmlDraggingRef.current)
78
+ return;
92
79
  const target = e.transform?.target || e.target;
93
80
  if (!target)
94
81
  return;
95
- // 1. Calculate delta in raw Scene Coordinates
96
- // We do NOT divide by zoom here because target.left/top are world units.
97
82
  const deltaX = target.left - (target._prevLeft ?? target.left);
98
83
  const deltaY = target.top - (target._prevTop ?? target.top);
99
84
  target._prevLeft = target.left;
100
85
  target._prevTop = target.top;
101
86
  if (deltaX === 0 && deltaY === 0)
102
87
  return;
103
- // 2. Apply the raw delta to HTML items
104
- setLocalTasks((prev) => prev.map((t) => (selectedIds.has(t.id) ? { ...t, x: t.x + deltaX, y: t.y + deltaY } : t)));
105
- setLocalDocuments((prev) => prev.map((d) => (selectedIds.has(d.id) ? { ...d, x: d.x + deltaX, y: d.y + deltaY } : d)));
88
+ const sel = selectedIdsRef.current;
89
+ setLocalTasks((prev) => prev.map((t) => sel.has(t.id) ? { ...t, x: t.x + deltaX, y: t.y + deltaY } : t));
90
+ setLocalDocuments((prev) => prev.map((d) => sel.has(d.id) ? { ...d, x: d.x + deltaX, y: d.y + deltaY } : d));
106
91
  };
107
92
  const handleMouseDown = (e) => {
108
93
  const target = e.target;
@@ -117,138 +102,68 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
117
102
  canvas.off("object:moving", handleObjectMoving);
118
103
  canvas.off("mouse:down", handleMouseDown);
119
104
  };
120
- }, [canvasZoom, selectedIds, fabricCanvas]);
121
- // ── Helpers ─────────────────────────────────────────────────────────────────
122
- const getItemPosition = (id) => {
123
- const task = localTasks.find((t) => t.id === id);
124
- if (task)
125
- return { x: task.x, y: task.y };
126
- const doc = localDocuments.find((d) => d.id === id);
127
- if (doc)
128
- return { x: doc.x, y: doc.y };
129
- return undefined;
130
- };
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);
141
- };
142
- // ── Selection box detection ──────────────────────────────────────────────────
105
+ }, [fabricCanvas]);
106
+ // ─── Selection box ────────────────────────────────────────────────────────
143
107
  useEffect(() => {
144
108
  if (!selectionBox)
145
109
  return;
146
- // ── O(n) single pass — no sort, no join, no extra allocations ──
147
110
  const newSelected = new Set();
148
111
  for (const task of localTasks) {
149
- if (isItemInSelectionBox(task.x, task.y, 300, 140, selectionBox))
112
+ const x1 = task.x * canvasZoom + canvasViewport.x;
113
+ const y1 = task.y * canvasZoom + canvasViewport.y;
114
+ const x2 = x1 + 300 * canvasZoom;
115
+ const y2 = y1 + 140 * canvasZoom;
116
+ const bX1 = Math.min(selectionBox.x1, selectionBox.x2);
117
+ const bY1 = Math.min(selectionBox.y1, selectionBox.y2);
118
+ const bX2 = Math.max(selectionBox.x1, selectionBox.x2);
119
+ const bY2 = Math.max(selectionBox.y1, selectionBox.y2);
120
+ if (!(bX2 < x1 || bX1 > x2 || bY2 < y1 || bY1 > y2))
150
121
  newSelected.add(task.id);
151
122
  }
152
123
  for (const doc of localDocuments) {
153
- if (isItemInSelectionBox(doc.x, doc.y, 320, 160, selectionBox))
124
+ const x1 = doc.x * canvasZoom + canvasViewport.x;
125
+ const y1 = doc.y * canvasZoom + canvasViewport.y;
126
+ const x2 = x1 + 320 * canvasZoom;
127
+ const y2 = y1 + 160 * canvasZoom;
128
+ const bX1 = Math.min(selectionBox.x1, selectionBox.x2);
129
+ const bY1 = Math.min(selectionBox.y1, selectionBox.y2);
130
+ const bX2 = Math.max(selectionBox.x1, selectionBox.x2);
131
+ const bY2 = Math.max(selectionBox.y1, selectionBox.y2);
132
+ if (!(bX2 < x1 || bX1 > x2 || bY2 < y1 || bY1 > y2))
154
133
  newSelected.add(doc.id);
155
134
  }
156
- // ── O(n) equality check: size first (fast path), then membership ──
135
+ // O(n) equality same Set ref if unchanged, blocks unnecessary re-render
157
136
  setSelectedIds((prev) => {
158
137
  if (prev.size !== newSelected.size)
159
138
  return newSelected;
160
- for (const id of newSelected) {
139
+ for (const id of newSelected)
161
140
  if (!prev.has(id))
162
- return newSelected; // found a difference, swap
163
- }
164
- return prev; // identical — return same reference, no re-render
165
- });
166
- }, [selectionBox, localTasks, localDocuments]);
167
- // ── Drag start (HTML Node side) ──────────────────────────────────────────────
168
- // Helper to extract coordinates regardless of event type
169
- const getPointerEvent = (e) => {
170
- if ('touches' in e && e.touches.length > 0)
171
- return e.touches[0];
172
- return e;
173
- };
174
- const handleDragStart = (itemId, e) => {
175
- // 1. Safety check for the Fabric instance
176
- const canvas = fabricCanvas?.current;
177
- if (!canvas)
178
- return;
179
- // 2. Normalize the event (Touch vs Mouse)
180
- if (e.cancelable)
181
- e.preventDefault();
182
- const pointer = getPointerEvent(e);
183
- // 3. Determine which items are being dragged
184
- // selection update DOES NOT trigger before drag snapshot
185
- let itemsToDrag;
186
- if (selectedIds.has(itemId)) {
187
- itemsToDrag = Array.from(selectedIds);
188
- }
189
- else {
190
- itemsToDrag = [itemId];
191
- }
192
- // 4. Capture current World Transform (Zoom & Pan)
193
- // We read directly from the canvas to ensure zero-frame lag
194
- const vpt = canvas.viewportTransform || [1, 0, 0, 1, 0, 0];
195
- const liveZoom = vpt[0];
196
- const liveVpX = vpt[4];
197
- const liveVpY = vpt[5];
198
- // 5. Convert the Click Position from Screen Pixels to World Units
199
- const clickWorldX = (pointer.clientX - liveVpX) / liveZoom;
200
- const clickWorldY = (pointer.clientY - liveVpY) / liveZoom;
201
- // 6. Get the clicked item's current World Position
202
- const clickedPos = getItemPosition(itemId);
203
- if (!clickedPos)
204
- return;
205
- // 7. Calculate the Offset in WORLD UNITS
206
- // This is the distance from the mouse to the node's top-left in the infinite grid.
207
- // This value remains constant even if you zoom during the drag.
208
- const worldOffsetX = clickWorldX - clickedPos.x;
209
- const worldOffsetY = clickWorldY - clickedPos.y;
210
- // 8. Snapshot starting positions for all selected HTML nodes
211
- const startPositions = new Map();
212
- itemsToDrag.forEach((id) => {
213
- const pos = getItemPosition(id);
214
- if (pos)
215
- startPositions.set(id, pos);
216
- });
217
- // 9. Snapshot starting positions for all selected Fabric objects
218
- const canvasObjectsStartPos = new Map();
219
- selectedCanvasObjects.forEach((obj) => {
220
- canvasObjectsStartPos.set(obj, { left: obj.left || 0, top: obj.top || 0 });
141
+ return newSelected;
142
+ return prev;
221
143
  });
222
- // 10. Commit to the ref for the requestAnimationFrame loop
223
- dragStateRef.current = {
224
- isDragging: true,
225
- itemIds: itemsToDrag,
226
- startPositions,
227
- canvasObjectsStartPos,
228
- offsetX: worldOffsetX, // Now stored as World Units
229
- offsetY: worldOffsetY, // Now stored as World Units
230
- };
231
- if (!selectedIds.has(itemId)) {
232
- setSelectedIds(new Set([itemId]));
233
- }
234
- // 11. Trigger UI states
235
- setDragging({ itemIds: itemsToDrag });
236
- document.body.style.cursor = "grabbing";
237
- document.body.style.userSelect = "none";
238
- document.body.style.touchAction = "none";
239
- };
240
- // ── Drag move (HTML Node side) ───────────────────────────────────────────────
144
+ }, [selectionBox, localTasks, localDocuments, canvasZoom, canvasViewport]);
145
+ // ─── Global drag listeners — attached ONCE on mount ───────────────────────
146
+ //
147
+ // ROOT CAUSE OF THE POSITION JUMP — fixed here:
148
+ //
149
+ // Old pattern: useEffect(() => { mousemove }, [dragging, localTasks, localDocuments])
150
+ // Problem:
151
+ // 1. handleDragStart setDragging() React re-render
152
+ // 2. Re-render tears down mousemove listener, attaches new one
153
+ // 3. First real mousemove fires into old (dead) listener → ignored or wrong
154
+ // 4. New listener fires with potentially different startPositions → JUMP
155
+ //
156
+ // Fix: attach listeners ONCE. They read all mutable state via refs.
157
+ // Zero re-registration, zero double-fires, zero stale closure issues.
158
+ //
241
159
  useEffect(() => {
242
- if (!dragging)
243
- return;
244
- // Inside the useEffect that watches [dragging, localTasks, localDocuments, fabricCanvas]
245
160
  const handleMove = (e) => {
246
161
  if (!dragStateRef.current.isDragging)
247
162
  return;
248
163
  if (e.cancelable)
249
164
  e.preventDefault();
250
- const pointer = getPointerEvent(e);
251
- // 1. Throttle updates using requestAnimationFrame for 120Hz/144Hz screen support
165
+ const pointer = "touches" in e && e.touches.length > 0
166
+ ? e.touches[0] : e;
252
167
  if (rafIdRef.current !== null)
253
168
  cancelAnimationFrame(rafIdRef.current);
254
169
  rafIdRef.current = requestAnimationFrame(() => {
@@ -256,59 +171,50 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
256
171
  const canvas = fabricCanvas?.current;
257
172
  if (!canvas)
258
173
  return;
259
- // 2. Read the "Source of Truth" transform from the canvas
174
+ // Live VPT never from React state
260
175
  const vpt = canvas.viewportTransform;
261
- const liveZoom = vpt[0]; // Scale
262
- const liveVpX = vpt[4]; // Pan X
263
- const liveVpY = vpt[5]; // Pan Y
264
- // 3. Convert current Mouse Screen Position → World Position
265
- const currentWorldX = (pointer.clientX - liveVpX) / liveZoom;
266
- const currentWorldY = (pointer.clientY - liveVpY) / liveZoom;
267
- // 4. Calculate where the "Anchor" node should be in World Units
268
- // (Current Mouse World - Initial World Offset from Start)
269
- const newWorldX = currentWorldX - offsetX;
270
- const newWorldY = currentWorldY - offsetY;
271
- // 5. Calculate the Movement Delta in World Units
272
- // We compare where the first item started vs where it is now.
273
- const firstId = itemIds[0];
274
- const firstStart = startPositions.get(firstId);
176
+ const liveZoom = vpt[0];
177
+ const liveVpX = vpt[4];
178
+ const liveVpY = vpt[5];
179
+ // Screen → World, subtract world-space offset
180
+ const newWorldX = (pointer.clientX - liveVpX) / liveZoom - offsetX;
181
+ const newWorldY = (pointer.clientY - liveVpY) / liveZoom - offsetY;
182
+ const firstStart = startPositions.get(itemIds[0]);
275
183
  if (!firstStart)
276
184
  return;
277
185
  const deltaX = newWorldX - firstStart.x;
278
186
  const deltaY = newWorldY - firstStart.y;
279
- // 6. Update HTML Nodes (Batching these into one state update)
280
- setLocalTasks((prev) => prev.map((t) => itemIds.includes(t.id) ? {
281
- ...t,
282
- x: (startPositions.get(t.id)?.x ?? t.x) + deltaX,
283
- y: (startPositions.get(t.id)?.y ?? t.y) + deltaY,
284
- } : t));
285
- setLocalDocuments((prev) => prev.map((d) => itemIds.includes(d.id) ? {
286
- ...d,
287
- x: (startPositions.get(d.id)?.x ?? d.x) + deltaX,
288
- y: (startPositions.get(d.id)?.y ?? d.y) + deltaY,
289
- } : d));
290
- // 7. Sync Fabric Objects (Imperative update for performance)
187
+ // ── PERF: Only map over items being dragged ────────────────────────
188
+ setLocalTasks((prev) => prev.map((t) => itemIds.includes(t.id)
189
+ ? { ...t, x: (startPositions.get(t.id)?.x ?? t.x) + deltaX,
190
+ y: (startPositions.get(t.id)?.y ?? t.y) + deltaY }
191
+ : t));
192
+ setLocalDocuments((prev) => prev.map((d) => itemIds.includes(d.id)
193
+ ? { ...d, x: (startPositions.get(d.id)?.x ?? d.x) + deltaX,
194
+ y: (startPositions.get(d.id)?.y ?? d.y) + deltaY }
195
+ : d));
196
+ // Sync Fabric objects imperatively
291
197
  canvasObjectsStartPos.forEach((startPos, obj) => {
292
- obj.set({
293
- left: startPos.left + deltaX,
294
- top: startPos.top + deltaY,
295
- });
296
- obj.setCoords(); // Required for selection/intersection accuracy
198
+ obj.set({ left: startPos.left + deltaX, top: startPos.top + deltaY });
199
+ obj.setCoords();
297
200
  });
298
- // 8. Single render call for all Fabric changes
299
201
  canvas.requestRenderAll();
300
202
  });
301
203
  };
302
204
  const handleEnd = () => {
303
- if (rafIdRef.current !== null)
205
+ if (!dragStateRef.current.isDragging)
206
+ return;
207
+ if (rafIdRef.current !== null) {
304
208
  cancelAnimationFrame(rafIdRef.current);
209
+ rafIdRef.current = null;
210
+ }
305
211
  dragStateRef.current.isDragging = false;
306
- setDragging(null);
212
+ isHtmlDraggingRef.current = false;
307
213
  document.body.style.cursor = "";
308
214
  document.body.style.userSelect = "";
309
215
  document.body.style.touchAction = "";
310
- onTasksUpdate?.(localTasks);
311
- onDocumentsUpdate?.(localDocuments);
216
+ setLocalTasks((prev) => { onTasksUpdateRef.current?.(prev); return prev; });
217
+ setLocalDocuments((prev) => { onDocumentsUpdateRef.current?.(prev); return prev; });
312
218
  };
313
219
  window.addEventListener("mousemove", handleMove, { passive: false });
314
220
  window.addEventListener("mouseup", handleEnd);
@@ -322,9 +228,62 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
322
228
  window.removeEventListener("touchend", handleEnd);
323
229
  window.removeEventListener("touchcancel", handleEnd);
324
230
  };
325
- }, [dragging, localTasks, localDocuments, fabricCanvas]);
326
- // ── Selection, Status, Keyboard Logic ────────────────────────────────────────
327
- const handleSelect = (id, e) => {
231
+ // Empty deps — registered once, reads everything via refs
232
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
233
+ // ─── Drag start ───────────────────────────────────────────────────────────
234
+ const handleDragStart = useCallback((itemId, e) => {
235
+ const canvas = fabricCanvas?.current;
236
+ if (!canvas)
237
+ return;
238
+ if (e.cancelable)
239
+ e.preventDefault();
240
+ const pointer = "touches" in e && e.touches.length > 0
241
+ ? e.touches[0] : e;
242
+ // Read from ref — not state (avoids async lag from setSelectedIds)
243
+ const currentSelected = selectedIdsRef.current;
244
+ const itemsToDrag = currentSelected.has(itemId)
245
+ ? Array.from(currentSelected) : [itemId];
246
+ if (!currentSelected.has(itemId))
247
+ setSelectedIds(new Set([itemId]));
248
+ // Live VPT
249
+ const vpt = canvas.viewportTransform || [1, 0, 0, 1, 0, 0];
250
+ const liveZoom = vpt[0];
251
+ const liveVpX = vpt[4];
252
+ const liveVpY = vpt[5];
253
+ // Pointer → world
254
+ const pointerWorldX = (pointer.clientX - liveVpX) / liveZoom;
255
+ const pointerWorldY = (pointer.clientY - liveVpY) / liveZoom;
256
+ // nodePositionsRef is rebuilt synchronously every render — never stale
257
+ const clickedPos = nodePositionsRef.current.get(itemId);
258
+ if (!clickedPos)
259
+ return;
260
+ // Snapshot ALL start positions from ref synchronously
261
+ const startPositions = new Map();
262
+ for (const id of itemsToDrag) {
263
+ const pos = nodePositionsRef.current.get(id);
264
+ if (pos)
265
+ startPositions.set(id, { x: pos.x, y: pos.y });
266
+ }
267
+ const canvasObjectsStartPos = new Map();
268
+ for (const obj of selectedCanvasObjectsRef.current) {
269
+ canvasObjectsStartPos.set(obj, { left: obj.left || 0, top: obj.top || 0 });
270
+ }
271
+ dragStateRef.current = {
272
+ isDragging: true,
273
+ itemIds: itemsToDrag,
274
+ startPositions,
275
+ canvasObjectsStartPos,
276
+ // World-space offset: pointer distance from node top-left in world units
277
+ offsetX: pointerWorldX - clickedPos.x,
278
+ offsetY: pointerWorldY - clickedPos.y,
279
+ };
280
+ isHtmlDraggingRef.current = true;
281
+ document.body.style.cursor = "grabbing";
282
+ document.body.style.userSelect = "none";
283
+ document.body.style.touchAction = "none";
284
+ }, [fabricCanvas]);
285
+ // ─── Node interaction ─────────────────────────────────────────────────────
286
+ const handleSelect = useCallback((id, e) => {
328
287
  if (e?.shiftKey || e?.ctrlKey || e?.metaKey) {
329
288
  setSelectedIds((prev) => {
330
289
  const next = new Set(prev);
@@ -335,68 +294,48 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
335
294
  else {
336
295
  setSelectedIds(new Set([id]));
337
296
  }
338
- };
339
- const handleStatusChange = (taskId, newStatus) => {
340
- const updated = localTasks.map((t) => (t.id === taskId ? { ...t, status: newStatus } : t));
341
- setLocalTasks(updated);
342
- onTasksUpdate?.(updated);
343
- };
297
+ }, []);
298
+ const handleStatusChange = useCallback((taskId, newStatus) => {
299
+ setLocalTasks((prev) => {
300
+ const updated = prev.map((t) => t.id === taskId ? { ...t, status: newStatus } : t);
301
+ onTasksUpdateRef.current?.(updated);
302
+ return updated;
303
+ });
304
+ }, []);
305
+ // ─── Keyboard shortcuts ───────────────────────────────────────────────────
344
306
  useEffect(() => {
345
307
  const handleKeyDown = (e) => {
346
- // Don't trigger if typing in input
347
308
  if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement)
348
309
  return;
349
- // Select All
350
310
  if ((e.ctrlKey || e.metaKey) && e.key === "a") {
351
311
  e.preventDefault();
352
312
  setSelectedIds(new Set([...localTasks.map((t) => t.id), ...localDocuments.map((d) => d.id)]));
353
313
  }
354
- // Clear selection
355
- if (e.key === "Escape") {
314
+ if (e.key === "Escape")
356
315
  setSelectedIds(new Set());
357
- }
358
- // ← ADD THIS: Delete selected nodes
359
- if ((e.key === "Delete" || e.key === "Backspace") && selectedIds.size > 0) {
316
+ if ((e.key === "Delete" || e.key === "Backspace") && selectedIdsRef.current.size > 0) {
360
317
  e.preventDefault();
361
- const updatedTasks = localTasks.filter((t) => !selectedIds.has(t.id));
362
- const updatedDocs = localDocuments.filter((d) => !selectedIds.has(d.id));
363
- setLocalTasks(updatedTasks);
364
- setLocalDocuments(updatedDocs);
318
+ const ids = selectedIdsRef.current;
319
+ setLocalTasks((prev) => { const u = prev.filter((t) => !ids.has(t.id)); onTasksUpdateRef.current?.(u); return u; });
320
+ setLocalDocuments((prev) => { const u = prev.filter((d) => !ids.has(d.id)); onDocumentsUpdateRef.current?.(u); return u; });
365
321
  setSelectedIds(new Set());
366
- onTasksUpdate?.(updatedTasks);
367
- onDocumentsUpdate?.(updatedDocs);
368
322
  }
369
323
  };
370
324
  window.addEventListener("keydown", handleKeyDown);
371
325
  return () => window.removeEventListener("keydown", handleKeyDown);
372
- }, [localTasks, localDocuments, selectedIds, onTasksUpdate, onDocumentsUpdate]);
373
- // ── Render helper ────────────────────────────────────────────────────────────
374
- const renderItem = (id, x, y, children) => {
375
- const screenX = x * canvasZoom;
376
- const screenY = y * canvasZoom;
377
- // 1. Detect if the user is interacting with the canvas at all
378
- // 'dragging' is your existing state.
379
- // You might want to pass 'isZooming' or 'isPanning' from your main canvas component here.
380
- const isDragging = dragging?.itemIds.includes(id);
381
- return (_jsx("div", { className: "pointer-events-auto absolute", style: {
382
- left: 0,
383
- top: 0,
384
- // 2. Use translate3d for GPU performance
385
- transform: `translate3d(${screenX}px, ${screenY}px, 0) scale(${canvasZoom})`,
386
- transformOrigin: "top left",
387
- // 3. THE FIX: Remove transition entirely during any viewport change
388
- // Any 'ease' during zoom causes the "shaking" behavior.
389
- transition: "none",
390
- // 4. Optimization
391
- willChange: "transform",
392
- zIndex: isDragging ? 1000 : 1,
393
- }, children: children }, id));
394
- };
395
- return (_jsx("div", { ref: overlayRef, className: "absolute inset-0 pointer-events-none", style: { zIndex: 50 }, onWheel: handleOverlayWheel, onClick: (e) => {
396
- if (e.target === e.currentTarget)
397
- setSelectedIds(new Set());
398
- }, children: _jsxs("div", { className: "absolute top-0 left-0 pointer-events-none", style: {
326
+ }, [localTasks, localDocuments]);
327
+ // ─── Render ───────────────────────────────────────────────────────────────
328
+ const renderItem = (id, x, y, children) => (_jsx("div", { className: "pointer-events-auto absolute", style: {
329
+ left: 0, top: 0,
330
+ transform: `translate3d(${x * canvasZoom}px, ${y * canvasZoom}px, 0) scale(${canvasZoom})`,
331
+ transformOrigin: "top left",
332
+ transition: "none", // No CSS transitions causes shaking during zoom
333
+ willChange: "transform", // GPU layer hint
334
+ zIndex: dragStateRef.current.itemIds.includes(id) ? 1000 : 1,
335
+ }, children: children }, id));
336
+ return (_jsx("div", { ref: overlayRef, className: "absolute inset-0 pointer-events-none", style: { zIndex: 50 }, onClick: (e) => { if (e.target === e.currentTarget)
337
+ setSelectedIds(new Set()); }, children: _jsxs("div", { className: "absolute top-0 left-0 pointer-events-none", style: {
399
338
  transform: `translate(${canvasViewport.x}px, ${canvasViewport.y}px)`,
400
339
  transformOrigin: "top left",
401
- }, children: [localTasks.map((task) => renderItem(task.id, task.x, task.y, _jsx(TaskNode, { ...task, isSelected: selectedIds.has(task.id), onSelect: handleSelect, onDragStart: handleDragStart, onStatusChange: handleStatusChange, zoom: 1 }))), localDocuments.map((doc) => renderItem(doc.id, doc.x, doc.y, _jsx(DocumentNode, { ...doc, isSelected: selectedIds.has(doc.id), onSelect: handleSelect, onDragStart: handleDragStart })))] }) }));
340
+ }, children: [localTasks.map((task) => renderItem(task.id, task.x, task.y, _jsx(MemoTaskNode, { ...task, isSelected: selectedIds.has(task.id), onSelect: handleSelect, onDragStart: handleDragStart, onStatusChange: handleStatusChange, zoom: 1 }))), localDocuments.map((doc) => renderItem(doc.id, doc.x, doc.y, _jsx(MemoDocumentNode, { ...doc, isSelected: selectedIds.has(doc.id), onSelect: handleSelect, onDragStart: handleDragStart })))] }) }));
402
341
  }
@@ -1 +1 @@
1
- {"version":3,"file":"task-dropdown.d.ts","sourceRoot":"","sources":["../../../src/components/toolbar/task-dropdown.tsx"],"names":[],"mappings":"AAWA,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,GAAG,aAAa,GAAG,MAAM,CAAC;IACxC,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,UAAU,iBAAiB;IACzB,SAAS,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,IAAI,CAAC;CACzC;AA2DD,MAAM,CAAC,OAAO,UAAU,YAAY,CAAC,EAAE,SAAS,EAAE,EAAE,iBAAiB,2CAkGpE"}
1
+ {"version":3,"file":"task-dropdown.d.ts","sourceRoot":"","sources":["../../../src/components/toolbar/task-dropdown.tsx"],"names":[],"mappings":"AAUA,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,GAAG,aAAa,GAAG,MAAM,CAAC;IACxC,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,UAAU,iBAAiB;IACzB,SAAS,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,IAAI,CAAC;CACzC;AA2DD,MAAM,CAAC,OAAO,UAAU,YAAY,CAAC,EAAE,SAAS,EAAE,EAAE,iBAAiB,2CAkGpE"}
@@ -1,5 +1,5 @@
1
1
  import { RefObject } from "react";
2
- import * as fabric from "fabric";
2
+ import fabric from "fabric";
3
3
  interface ToolOptionsPanelProps {
4
4
  fabricCanvas: RefObject<fabric.Canvas | null>;
5
5
  }
@@ -1 +1 @@
1
- {"version":3,"file":"tooloptions-panel.d.ts","sourceRoot":"","sources":["../../../src/components/toolbar/tooloptions-panel.tsx"],"names":[],"mappings":"AAGA,OAAc,EAAE,SAAS,EAAgC,MAAM,OAAO,CAAC;AACvE,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AAYjC,UAAU,qBAAqB;IAC7B,YAAY,EAAE,SAAS,CAAC,MAAM,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC/C;AAED,MAAM,CAAC,OAAO,UAAU,gBAAgB,CAAC,EACvC,YAAY,GACb,EAAE,qBAAqB,2CAuMvB"}
1
+ {"version":3,"file":"tooloptions-panel.d.ts","sourceRoot":"","sources":["../../../src/components/toolbar/tooloptions-panel.tsx"],"names":[],"mappings":"AAGA,OAAc,EAAE,SAAS,EAAgC,MAAM,OAAO,CAAC;AAUvE,OAAO,MAA2C,MAAM,QAAQ,CAAC;AAGjE,UAAU,qBAAqB;IAC7B,YAAY,EAAE,SAAS,CAAC,MAAM,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC/C;AAED,MAAM,CAAC,OAAO,UAAU,gBAAgB,CAAC,EACvC,YAAY,GACb,EAAE,qBAAqB,2CAuMvB"}
@@ -1 +1 @@
1
- {"version":3,"file":"whiteboard-toolbar.d.ts","sourceRoot":"","sources":["../../../src/components/toolbar/whiteboard-toolbar.tsx"],"names":[],"mappings":"AAEA,OAAa,EAAqB,gBAAgB,EAAY,MAAM,OAAO,CAAC;AAI5E,OAAO,EAAE,MAAM,EAAQ,MAAM,QAAQ,CAAC;AAOtC,UAAU,YAAY;IACpB,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,aAAa,GAAG,MAAM,CAAC;IACnE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC7F;AACD,UAAU,gBAAgB;IACxB,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAC3C,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC5D;AAkBD,UAAU,sBAAsB;IAC9B,YAAY,EAAE,gBAAgB,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC9C,cAAc,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAC;IAC1C,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,IAAI,CAAC;IACzC,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,IAAI,CAAC;CACjD;AAED,MAAM,CAAC,OAAO,UAAU,iBAAiB,CAAC,EAAE,YAAY,EAAE,cAAc,EAAE,SAAS,EAAE,aAAa,EAAE,EAAE,sBAAsB,2CAiM3H"}
1
+ {"version":3,"file":"whiteboard-toolbar.d.ts","sourceRoot":"","sources":["../../../src/components/toolbar/whiteboard-toolbar.tsx"],"names":[],"mappings":"AAEA,OAAa,EAAqB,gBAAgB,EAAY,MAAM,OAAO,CAAC;AAK5E,OAAO,EAAE,MAAM,EAAQ,MAAM,QAAQ,CAAC;AAOtC,UAAU,YAAY;IACpB,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,aAAa,GAAG,MAAM,CAAC;IACnE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC7F;AACD,UAAU,gBAAgB;IACxB,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAC3C,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC5D;AAkBD,UAAU,sBAAsB;IAC9B,YAAY,EAAE,gBAAgB,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC9C,cAAc,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAC;IAC1C,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,IAAI,CAAC;IACzC,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,IAAI,CAAC;CACjD;AAED,MAAM,CAAC,OAAO,UAAU,iBAAiB,CAAC,EAAE,YAAY,EAAE,cAAc,EAAE,SAAS,EAAE,aAAa,EAAE,EAAE,sBAAsB,2CAiM3H"}
@@ -1 +1 @@
1
- {"version":3,"file":"useCanvasInit.d.ts","sourceRoot":"","sources":["../../src/hooks/useCanvasInit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,SAAS,EAAE,MAAM,OAAO,CAAC;AAC7C,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,UAAU,kBAAkB;IAC1B,SAAS,EAAE,SAAS,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAC;IAC/C,eAAe,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC1C,UAAU,EAAE,MAAM,CAAC;IACnB,kBAAkB,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC;IACvC,cAAc,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC;IACnC,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAErC,QAAQ,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,KAAK,IAAI,CAAC;IACjC,YAAY,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAC;CACrC;AAKD,eAAO,MAAM,aAAa,GAAI,sHAS3B,kBAAkB,SAyFpB,CAAC"}
1
+ {"version":3,"file":"useCanvasInit.d.ts","sourceRoot":"","sources":["../../src/hooks/useCanvasInit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,SAAS,EAAE,MAAM,OAAO,CAAC;AAC7C,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,UAAU,kBAAkB;IAC1B,SAAS,EAAE,SAAS,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAC;IAC/C,eAAe,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC1C,UAAU,EAAE,MAAM,CAAC;IACnB,kBAAkB,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC;IACvC,cAAc,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC;IACnC,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAErC,QAAQ,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,KAAK,IAAI,CAAC;IACjC,YAAY,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAC;CACrC;AAKD,eAAO,MAAM,aAAa,GAAI,sHAS3B,kBAAkB,SA0FpB,CAAC"}
@@ -15,6 +15,7 @@ export const useCanvasInit = ({ canvasRef, fabricCanvasRef, activeTool, suppress
15
15
  selection: true,
16
16
  allowTouchScrolling: false,
17
17
  stopContextMenu: true,
18
+ objectCaching: true
18
19
  });
19
20
  fabricCanvasRef.current = canvas;
20
21
  initializeFabricCanvas(canvas);
@@ -1,6 +1,5 @@
1
1
  import { RefObject } from "react";
2
- import { Canvas, Circle, FabricObject } from "fabric";
3
- import * as fabric from "fabric";
2
+ import fabric, { Canvas, Circle, FabricObject } from "fabric";
4
3
  interface UseEraserProps {
5
4
  fabricCanvas: RefObject<Canvas | null>;
6
5
  activeTool: string;
@@ -1 +1 @@
1
- {"version":3,"file":"useEraser.d.ts","sourceRoot":"","sources":["../../src/hooks/useEraser.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAClC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AACtD,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AAEjC,UAAU,cAAc;IACtB,YAAY,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,GAAG,CAAC;IACjB,cAAc,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACzC,gBAAgB,EAAE,SAAS,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC;IAC/C,eAAe,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC;IACpC,aAAa,EAAE,SAAS,CAAC,MAAM,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;IAC7C,mBAAmB,EAAE,SAAS,CAAC,MAAM,EAAE,CAAC,CAAC;IACzC,kBAAkB,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC;IACvC,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC;AAED,eAAO,MAAM,SAAS,GAAI,oKAWvB,cAAc;;;;gCAsBoB;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE;;CA0I5D,CAAC"}
1
+ {"version":3,"file":"useEraser.d.ts","sourceRoot":"","sources":["../../src/hooks/useEraser.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAClC,OAAO,MAAM,EAAC,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAE7D,UAAU,cAAc;IACtB,YAAY,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,GAAG,CAAC;IACjB,cAAc,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACzC,gBAAgB,EAAE,SAAS,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC;IAC/C,eAAe,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC;IACpC,aAAa,EAAE,SAAS,CAAC,MAAM,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;IAC7C,mBAAmB,EAAE,SAAS,CAAC,MAAM,EAAE,CAAC,CAAC;IACzC,kBAAkB,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC;IACvC,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC;AAED,eAAO,MAAM,SAAS,GAAI,oKAWvB,cAAc;;;;gCAsBoB;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE;;CA0I5D,CAAC"}
@@ -1,5 +1,4 @@
1
- import { Circle } from "fabric";
2
- import * as fabric from "fabric";
1
+ import fabric, { Circle } from "fabric";
3
2
  export const useEraser = ({ fabricCanvas, activeTool, toolOptions, eraserTraceRef, eraserTargetsRef, eraserActiveRef, eraserPathRef, eraserPathPointsRef, suppressHistoryRef, pushHistory, }) => {
4
3
  const handleMouseOver = () => {
5
4
  if (activeTool === "eraser" && eraserTraceRef.current) {
@@ -1,6 +1,5 @@
1
1
  import { RefObject } from "react";
2
- import { Canvas, Circle } from "fabric";
3
- import * as fabric from "fabric";
2
+ import fabric, { Canvas, Circle } from "fabric";
4
3
  interface UseToolManagerProps {
5
4
  fabricCanvas: RefObject<Canvas | null>;
6
5
  activeTool: string;
@@ -1 +1 @@
1
- {"version":3,"file":"useToolManager.d.ts","sourceRoot":"","sources":["../../src/hooks/useToolManager.ts"],"names":[],"mappings":"AACA,OAAO,EAAa,SAAS,EAAE,MAAM,OAAO,CAAC;AAC7C,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAExC,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AAEjC,UAAU,mBAAmB;IAC3B,YAAY,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,GAAG,CAAC;IACjB,cAAc,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACzC,aAAa,EAAE,SAAS,CAAC,MAAM,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;IAC7C,mBAAmB,EAAE,SAAS,CAAC,MAAM,EAAE,CAAC,CAAC;IACzC,gBAAgB,EAAE,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;CACvC;AAED,eAAO,MAAM,cAAc,GAAI,kHAQ5B,mBAAmB,SA8HrB,CAAC"}
1
+ {"version":3,"file":"useToolManager.d.ts","sourceRoot":"","sources":["../../src/hooks/useToolManager.ts"],"names":[],"mappings":"AACA,OAAO,EAAa,SAAS,EAAE,MAAM,OAAO,CAAC;AAC7C,OAAO,MAAM,EAAC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAI/C,UAAU,mBAAmB;IAC3B,YAAY,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,GAAG,CAAC;IACjB,cAAc,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACzC,aAAa,EAAE,SAAS,CAAC,MAAM,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;IAC7C,mBAAmB,EAAE,SAAS,CAAC,MAAM,EAAE,CAAC,CAAC;IACzC,gBAAgB,EAAE,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;CACvC;AAED,eAAO,MAAM,cAAc,GAAI,kHAQ5B,mBAAmB,SA8HrB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAkCA,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AACxF,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAE9D,YAAY,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AACxF,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAE9D,YAAY,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC"}
package/dist/index.js CHANGED
@@ -1,30 +1,2 @@
1
- // export {default as EasyflowWhiteboard} from "./components/whiteboard/whiteboard-test";
2
- // export * from "./hooks/useCanvasInit";
3
- // export * from "./hooks/useDrawing";
4
- // export * from "./hooks/useEraser";
5
- // export * from "./hooks/usePan";
6
- // export * from "./hooks/useZoom";
7
- // export * from "./hooks/useSelection";
8
- // export * from "./hooks/useLiveUpdate";
9
- // export * from "./hooks/useMouseHandlers";
10
- // export * from "./hooks/usePersistance";
11
- // export * from "./hooks/useToolManager";
12
- // export * from "./hooks/useTextStyle";
13
- // export * from "./lib/fabric-frame";
14
- // export * from "./lib/fabric-arrow";
15
- // export * from "./lib/fabric-bidirectional-arrow";
16
- // export * from "./lib/fabric-utils";
17
- // export * from "./lib/utils";
18
- // export * from "./store/whiteboard-store";
19
- // export * from "./components/toolbar/whiteboard-toolbar";
20
- // export * from "./components/toolbar/tooloptions-panel";
21
- // export * from "./components/node/custom-node-overlay-layer";
22
- // export * from "./components/zoomcontrol/zoom-control";
23
- // export * from "./components/whiteboard/whiteboard-test";
24
- // export * from "./components/node/document-node";
25
- // export * from "./components/node/custom-node";
26
- // export * from "./components/toolbar/document-dropdown";
27
- // export * from "./components/toolbar/task-dropdown";
28
- // export * from "./components/toolbar/document-dropdown";
29
1
  export { default as EasyflowWhiteboard } from "./components/whiteboard/whiteboard-test";
30
2
  export { useWhiteboardStore } from './store/whiteboard-store';
@@ -1 +1 @@
1
- {"version":3,"file":"fabric-utils.d.ts","sourceRoot":"","sources":["../../src/lib/fabric-utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,YAAY,EAAmE,WAAW,EAAc,MAAM,QAAQ,CAAC;AAKxI,eAAO,MAAM,sBAAsB,GAAI,QAAQ,MAAM,WAkBpD,CAAC;AAQF,eAAO,MAAM,mBAAmB,GAC9B,OAAO,YAAY,EACnB,UAAU,MAAM,EAChB,YAAY;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,EACpC,cAAc;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,SAwEvC,CAAC;AAKF,eAAO,MAAM,qBAAqB,GAAI,QAAQ,MAAM,SASnD,CAAC;AAGF,eAAO,MAAM,kBAAkB,GAAI,QAAQ,MAAM,SAEhD,CAAC;AAGF,eAAO,MAAM,oBAAoB,GAC/B,QAAQ,MAAM,EACd,MAAM,MAAM,EACZ,WAAW,MAAM,IAAI,SAMtB,CAAC;AAGF,eAAO,MAAM,mBAAmB,GAC9B,QAAQ,MAAM,EACd,SAAQ,KAAK,GAAG,MAAc,WAO/B,CAAC;AAUF,eAAO,MAAM,kBAAkB,GAC7B,iBAAiB,MAAM,EAAE,GAAG,IAAI,EAChC,aAAa,MAAM,KAClB,MAAM,EAAE,GAAG,SAKb,CAAC;AAKF,eAAO,MAAM,OAAO,GAClB,QAAQ,MAAM,EACd,SAAS;IACP,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,SAmBF,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAAU,QAAQ,MAAM,EAAE,KAAK,MAAM,iJAuCjE,CAAC;AAGF,eAAO,MAAM,WAAW,GAAI,QAAQ,MAAM,SAIzC,CAAC;AAGF,eAAO,MAAM,UAAU,GAAI,QAAQ,MAAM,EAAE,WAAW,MAAM,SAG3D,CAAC;AAGF,eAAO,MAAM,kBAAkB,GAAI,QAAQ,MAAM,SAoBhD,CAAC;AAIF,eAAO,MAAM,iBAAiB,GAAU,QAAQ,MAAM,EAAE,SAAS,MAAM,kBAoFtE,CAAC"}
1
+ {"version":3,"file":"fabric-utils.d.ts","sourceRoot":"","sources":["../../src/lib/fabric-utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,YAAY,EAAwE,WAAW,EAAc,MAAM,QAAQ,CAAC;AAK7I,eAAO,MAAM,sBAAsB,GAAI,QAAQ,MAAM,WAkBpD,CAAC;AAQF,eAAO,MAAM,mBAAmB,GAC9B,OAAO,YAAY,EACnB,UAAU,MAAM,EAChB,YAAY;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,EACpC,cAAc;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,SAwEvC,CAAC;AAKF,eAAO,MAAM,qBAAqB,GAAI,QAAQ,MAAM,SASnD,CAAC;AAGF,eAAO,MAAM,kBAAkB,GAAI,QAAQ,MAAM,SAEhD,CAAC;AAGF,eAAO,MAAM,oBAAoB,GAC/B,QAAQ,MAAM,EACd,MAAM,MAAM,EACZ,WAAW,MAAM,IAAI,SAMtB,CAAC;AAGF,eAAO,MAAM,mBAAmB,GAC9B,QAAQ,MAAM,EACd,SAAQ,KAAK,GAAG,MAAc,WAO/B,CAAC;AAUF,eAAO,MAAM,kBAAkB,GAC7B,iBAAiB,MAAM,EAAE,GAAG,IAAI,EAChC,aAAa,MAAM,KAClB,MAAM,EAAE,GAAG,SAKb,CAAC;AAKF,eAAO,MAAM,OAAO,GAClB,QAAQ,MAAM,EACd,SAAS;IACP,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,SAmBF,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAAU,QAAQ,MAAM,EAAE,KAAK,MAAM,iJAuCjE,CAAC;AAGF,eAAO,MAAM,WAAW,GAAI,QAAQ,MAAM,SAIzC,CAAC;AAGF,eAAO,MAAM,UAAU,GAAI,QAAQ,MAAM,EAAE,WAAW,MAAM,SAG3D,CAAC;AAGF,eAAO,MAAM,kBAAkB,GAAI,QAAQ,MAAM,SAoBhD,CAAC;AAIF,eAAO,MAAM,iBAAiB,GAAU,QAAQ,MAAM,EAAE,SAAS,MAAM,kBAoFtE,CAAC"}
@@ -1,5 +1,4 @@
1
- import { FabricObject, Line, Group, IText, PencilBrush, FabricImage, util } from "fabric";
2
- import { Text } from "fabric";
1
+ import { FabricObject, Text, Line, Group, IText, PencilBrush, FabricImage, util } from "fabric";
3
2
  // Initialize canvas with default settings
4
3
  export const initializeFabricCanvas = (canvas) => {
5
4
  // Set default object properties
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mhamz.01/easyflow-whiteboard",
3
- "version": "2.15.0",
3
+ "version": "2.16.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",