@mhamz.01/easyflow-whiteboard 2.170.0 → 2.172.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.
@@ -14,16 +14,27 @@ interface UseFabricSyncProps {
14
14
  setLocalDocuments: Dispatch<SetStateAction<Document[]>>;
15
15
  }
16
16
  /**
17
- * SRP: listens to Fabric canvas events and propagates changes to the HTML
18
- * overlay state. Handles four events:
17
+ * SRP: listens to Fabric canvas events and propagates changes to the HTML overlay.
19
18
  *
20
- * object:moving — mirrors Fabric object movement onto HTML node positions
21
- * (rAF-throttled, uses dragSelectedIdsRef snapshot)
22
- * mouse:down — clears HTML selection when clicking a Fabric object,
23
- * respects guard refs to avoid feedback loops
24
- * selection:created clears HTML selection when Fabric creates a selection,
25
- * skipped when the HTML layer initiated the selection
26
- * selection:updated — same as above for selection updates
19
+ * object:moving — mirrors Fabric drag onto HTML node positions (rAF-throttled).
20
+ * Uses an accumulated-delta ref so no frames are ever lost even
21
+ * when multiple mousemove events fire within one display frame.
22
+ * mouse:down — snapshots drag selection; clears HTML selection for Fabric drags.
23
+ * selection:created / selection:updated clear HTML selection when Fabric owns it.
24
+ *
25
+ * Three correctness invariants maintained here:
26
+ *
27
+ * 1. _prevLeft/Top is tracked on the ACTIVE OBJECT (which may be an activeSelection
28
+ * group), not on `e.target` (which may be a member of the group). Both
29
+ * `mouse:down` and `object:moving` must agree on which object they track.
30
+ *
31
+ * 2. Deltas are ACCUMULATED in a ref between RAF frames. When the RAF guard blocks
32
+ * a frame, the delta is NOT discarded — it is added to the pending accumulator
33
+ * and flushed in the next RAF callback. This prevents HTML nodes from drifting
34
+ * behind Fabric objects during fast drags.
35
+ *
36
+ * 3. Guard refs (isHtmlSelecting, isSelectionBoxActive, htmlNodesSelectedByBox)
37
+ * prevent feedback loops between the two selection systems.
27
38
  */
28
39
  export declare function useFabricSync({ fabricCanvas, canvasReady, dragSelectedIdsRef, selectedIdsRef, isHtmlSelectingRef, isSelectionBoxActiveRef, htmlNodesSelectedByBoxRef, setSelectedIds, setLocalTasks, setLocalDocuments, }: UseFabricSyncProps): void;
29
40
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"useFabricSync.d.ts","sourceRoot":"","sources":["../../../../src/components/node/hooks/useFabricSync.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,gBAAgB,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC;AACnF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAE7D,UAAU,kBAAkB;IAC1B,YAAY,CAAC,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACxC,WAAW,EAAE,OAAO,CAAC;IACrB,kBAAkB,EAAE,gBAAgB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;IAClD,cAAc,EAAE,gBAAgB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;IAC9C,kBAAkB,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAC;IAC9C,uBAAuB,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAC;IACnD,yBAAyB,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAC;IACrD,cAAc,EAAE,QAAQ,CAAC,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IACtD,aAAa,EAAE,QAAQ,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IAChD,iBAAiB,EAAE,QAAQ,CAAC,cAAc,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;CACzD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,EAC5B,YAAY,EACZ,WAAW,EACX,kBAAkB,EAClB,cAAc,EACd,kBAAkB,EAClB,uBAAuB,EACvB,yBAAyB,EACzB,cAAc,EACd,aAAa,EACb,iBAAiB,GAClB,EAAE,kBAAkB,QAmFpB"}
1
+ {"version":3,"file":"useFabricSync.d.ts","sourceRoot":"","sources":["../../../../src/components/node/hooks/useFabricSync.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,gBAAgB,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC;AACnF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAE7D,UAAU,kBAAkB;IAC1B,YAAY,CAAC,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACxC,WAAW,EAAE,OAAO,CAAC;IACrB,kBAAkB,EAAE,gBAAgB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;IAClD,cAAc,EAAE,gBAAgB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;IAC9C,kBAAkB,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAC;IAC9C,uBAAuB,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAC;IACnD,yBAAyB,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAC;IACrD,cAAc,EAAE,QAAQ,CAAC,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IACtD,aAAa,EAAE,QAAQ,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IAChD,iBAAiB,EAAE,QAAQ,CAAC,cAAc,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;CACzD;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,aAAa,CAAC,EAC5B,YAAY,EACZ,WAAW,EACX,kBAAkB,EAClB,cAAc,EACd,kBAAkB,EAClB,uBAAuB,EACvB,yBAAyB,EACzB,cAAc,EACd,aAAa,EACb,iBAAiB,GAClB,EAAE,kBAAkB,QAoHpB"}
@@ -1,23 +1,38 @@
1
1
  import { useEffect, useRef } from "react";
2
2
  /**
3
- * SRP: listens to Fabric canvas events and propagates changes to the HTML
4
- * overlay state. Handles four events:
3
+ * SRP: listens to Fabric canvas events and propagates changes to the HTML overlay.
5
4
  *
6
- * object:moving — mirrors Fabric object movement onto HTML node positions
7
- * (rAF-throttled, uses dragSelectedIdsRef snapshot)
8
- * mouse:down — clears HTML selection when clicking a Fabric object,
9
- * respects guard refs to avoid feedback loops
10
- * selection:created clears HTML selection when Fabric creates a selection,
11
- * skipped when the HTML layer initiated the selection
12
- * selection:updated — same as above for selection updates
5
+ * object:moving — mirrors Fabric drag onto HTML node positions (rAF-throttled).
6
+ * Uses an accumulated-delta ref so no frames are ever lost even
7
+ * when multiple mousemove events fire within one display frame.
8
+ * mouse:down — snapshots drag selection; clears HTML selection for Fabric drags.
9
+ * selection:created / selection:updated clear HTML selection when Fabric owns it.
10
+ *
11
+ * Three correctness invariants maintained here:
12
+ *
13
+ * 1. _prevLeft/Top is tracked on the ACTIVE OBJECT (which may be an activeSelection
14
+ * group), not on `e.target` (which may be a member of the group). Both
15
+ * `mouse:down` and `object:moving` must agree on which object they track.
16
+ *
17
+ * 2. Deltas are ACCUMULATED in a ref between RAF frames. When the RAF guard blocks
18
+ * a frame, the delta is NOT discarded — it is added to the pending accumulator
19
+ * and flushed in the next RAF callback. This prevents HTML nodes from drifting
20
+ * behind Fabric objects during fast drags.
21
+ *
22
+ * 3. Guard refs (isHtmlSelecting, isSelectionBoxActive, htmlNodesSelectedByBox)
23
+ * prevent feedback loops between the two selection systems.
13
24
  */
14
25
  export function useFabricSync({ fabricCanvas, canvasReady, dragSelectedIdsRef, selectedIdsRef, isHtmlSelectingRef, isSelectionBoxActiveRef, htmlNodesSelectedByBoxRef, setSelectedIds, setLocalTasks, setLocalDocuments, }) {
15
26
  const fabricMoveRafRef = useRef(null);
27
+ // Accumulates ALL deltas between RAF frames so none are ever dropped.
28
+ const pendingDelta = useRef({ x: 0, y: 0 });
16
29
  useEffect(() => {
17
30
  const canvas = fabricCanvas?.current;
18
31
  if (!canvas)
19
32
  return;
20
33
  const handleObjectMoving = (e) => {
34
+ // e.transform.target is the object actually being dragged by Fabric's
35
+ // interaction system. For an activeSelection it is the GROUP, not a member.
21
36
  const target = e.transform?.target || e.target;
22
37
  if (!target)
23
38
  return;
@@ -27,21 +42,46 @@ export function useFabricSync({ fabricCanvas, canvasReady, dragSelectedIdsRef, s
27
42
  target._prevTop = target.top;
28
43
  if (deltaX === 0 && deltaY === 0)
29
44
  return;
30
- const sel = dragSelectedIdsRef.current;
45
+ // Always accumulate — even when the RAF guard is active.
46
+ // This is the key fix for Bug 3: no delta is ever lost.
47
+ pendingDelta.current.x += deltaX;
48
+ pendingDelta.current.y += deltaY;
31
49
  if (fabricMoveRafRef.current !== null)
32
50
  return;
33
51
  fabricMoveRafRef.current = requestAnimationFrame(() => {
34
52
  fabricMoveRafRef.current = null;
35
- setLocalTasks((prev) => prev.map((t) => (sel.has(t.id) ? { ...t, x: t.x + deltaX, y: t.y + deltaY } : t)));
36
- setLocalDocuments((prev) => prev.map((d) => (sel.has(d.id) ? { ...d, x: d.x + deltaX, y: d.y + deltaY } : d)));
53
+ // Drain the accumulator atomically.
54
+ const dx = pendingDelta.current.x;
55
+ const dy = pendingDelta.current.y;
56
+ pendingDelta.current.x = 0;
57
+ pendingDelta.current.y = 0;
58
+ const sel = dragSelectedIdsRef.current;
59
+ setLocalTasks((prev) => prev.map((t) => (sel.has(t.id) ? { ...t, x: t.x + dx, y: t.y + dy } : t)));
60
+ setLocalDocuments((prev) => prev.map((d) => (sel.has(d.id) ? { ...d, x: d.x + dx, y: d.y + dy } : d)));
37
61
  });
38
62
  };
39
63
  const handleMouseDown = (e) => {
40
- const target = e.target;
41
- if (target) {
42
- target._prevLeft = target.left;
43
- target._prevTop = target.top;
64
+ // Bug 2 fix: track _prevLeft on the ACTIVE OBJECT, not e.target.
65
+ //
66
+ // When multiple objects are selected (activeSelection), Fabric fires
67
+ // object:moving with target = the activeSelection GROUP. But e.target in
68
+ // mouse:down can be an individual member object. If we set _prevLeft on
69
+ // the member and then object:moving reads it from the group, _prevLeft is
70
+ // undefined → first-frame delta is always 0 → HTML nodes don't move on
71
+ // frame 1 → visible drift at the start of every Fabric drag.
72
+ //
73
+ // canvas.getActiveObject() returns the activeSelection when multiple
74
+ // objects are selected, which is exactly what object:moving will give us.
75
+ const activeObj = canvas.getActiveObject();
76
+ const trackTarget = activeObj ?? e.target;
77
+ if (trackTarget) {
78
+ trackTarget._prevLeft = trackTarget.left;
79
+ trackTarget._prevTop = trackTarget.top;
44
80
  }
81
+ // Reset the delta accumulator on every new drag.
82
+ pendingDelta.current.x = 0;
83
+ pendingDelta.current.y = 0;
84
+ const target = e.target;
45
85
  const activeObjects = canvas.getActiveObjects();
46
86
  const isClickingIntoActiveSelection = target && activeObjects.length > 1 && activeObjects.includes(target);
47
87
  if (isClickingIntoActiveSelection) {
@@ -21,8 +21,9 @@ interface UseNodeDragProps {
21
21
  * during a drag stays correct without extra state dependencies.
22
22
  * - startPositions + anchor-offset approach prevents the "jump on drag start"
23
23
  * bug that occurs when using raw mouse coords.
24
- * - activeSelection group positions are resolved via calcTransformMatrix so
25
- * multi-selected Fabric objects stay in sync when dragged from HTML nodes.
24
+ * - Fabric object positions are read via queueMicrotask (after discardActiveObject)
25
+ * so they are always in ground-truth world coords, immune to Fabric v6's
26
+ * matrix-decomposition vs matrix-multiplication divergence.
26
27
  */
27
28
  export declare function useNodeDrag({ selectedIdsRef, dragSelectedIdsRef, localTasksRef, localDocumentsRef, fabricCanvas, setLocalTasks, setLocalDocuments, onTasksUpdate, onDocumentsUpdate, }: UseNodeDragProps): {
28
29
  dragging: {
@@ -1 +1 @@
1
- {"version":3,"file":"useNodeDrag.d.ts","sourceRoot":"","sources":["../../../../src/components/node/hooks/useNodeDrag.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,SAAS,EACT,gBAAgB,EAChB,QAAQ,EACR,cAAc,EACd,UAAU,IAAI,eAAe,EAC7B,UAAU,IAAI,eAAe,EAC9B,MAAM,OAAO,CAAC;AAEf,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAA4B,MAAM,wBAAwB,CAAC;AAEvF,UAAU,gBAAgB;IACxB,cAAc,EAAE,gBAAgB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;IAC9C,kBAAkB,EAAE,gBAAgB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;IAClD,aAAa,EAAE,gBAAgB,CAAC,IAAI,EAAE,CAAC,CAAC;IACxC,iBAAiB,EAAE,gBAAgB,CAAC,QAAQ,EAAE,CAAC,CAAC;IAChD,YAAY,CAAC,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACxC,aAAa,EAAE,QAAQ,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IAChD,iBAAiB,EAAE,QAAQ,CAAC,cAAc,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;IACxD,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,IAAI,CAAC;IACxC,iBAAiB,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,EAAE,KAAK,IAAI,CAAC;CACrD;AAcD;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,EAC1B,cAAc,EACd,kBAAkB,EAClB,aAAa,EACb,iBAAiB,EACjB,YAAY,EACZ,aAAa,EACb,iBAAiB,EACjB,aAAa,EACb,iBAAiB,GAClB,EAAE,gBAAgB;;iBACmC,MAAM,EAAE;;0BAcrD,MAAM,KAAG;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS;8BAWzC,MAAM,KAAK,eAAe,GAAG,eAAe;EA2JxD"}
1
+ {"version":3,"file":"useNodeDrag.d.ts","sourceRoot":"","sources":["../../../../src/components/node/hooks/useNodeDrag.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,SAAS,EACT,gBAAgB,EAChB,QAAQ,EACR,cAAc,EACd,UAAU,IAAI,eAAe,EAC7B,UAAU,IAAI,eAAe,EAC9B,MAAM,OAAO,CAAC;AAEf,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAA4B,MAAM,wBAAwB,CAAC;AAEvF,UAAU,gBAAgB;IACxB,cAAc,EAAE,gBAAgB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;IAC9C,kBAAkB,EAAE,gBAAgB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;IAClD,aAAa,EAAE,gBAAgB,CAAC,IAAI,EAAE,CAAC,CAAC;IACxC,iBAAiB,EAAE,gBAAgB,CAAC,QAAQ,EAAE,CAAC,CAAC;IAChD,YAAY,CAAC,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACxC,aAAa,EAAE,QAAQ,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IAChD,iBAAiB,EAAE,QAAQ,CAAC,cAAc,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;IACxD,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,IAAI,CAAC;IACxC,iBAAiB,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,EAAE,KAAK,IAAI,CAAC;CACrD;AAcD;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CAAC,EAC1B,cAAc,EACd,kBAAkB,EAClB,aAAa,EACb,iBAAiB,EACjB,YAAY,EACZ,aAAa,EACb,iBAAiB,EACjB,aAAa,EACb,iBAAiB,GAClB,EAAE,gBAAgB;;iBACmC,MAAM,EAAE;;0BAcrD,MAAM,KAAG;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS;8BAWzC,MAAM,KAAK,eAAe,GAAG,eAAe;EAgLxD"}
@@ -17,8 +17,9 @@ function readPointer(e) {
17
17
  * during a drag stays correct without extra state dependencies.
18
18
  * - startPositions + anchor-offset approach prevents the "jump on drag start"
19
19
  * bug that occurs when using raw mouse coords.
20
- * - activeSelection group positions are resolved via calcTransformMatrix so
21
- * multi-selected Fabric objects stay in sync when dragged from HTML nodes.
20
+ * - Fabric object positions are read via queueMicrotask (after discardActiveObject)
21
+ * so they are always in ground-truth world coords, immune to Fabric v6's
22
+ * matrix-decomposition vs matrix-multiplication divergence.
22
23
  */
23
24
  export function useNodeDrag({ selectedIdsRef, dragSelectedIdsRef, localTasksRef, localDocumentsRef, fabricCanvas, setLocalTasks, setLocalDocuments, onTasksUpdate, onDocumentsUpdate, }) {
24
25
  const [dragging, setDragging] = useState(null);
@@ -47,8 +48,11 @@ export function useNodeDrag({ selectedIdsRef, dragSelectedIdsRef, localTasksRef,
47
48
  if (e.cancelable)
48
49
  e.preventDefault();
49
50
  const pointer = readPointer(e);
51
+ // The clicked item MUST be first so handleMove's anchor calculation is correct.
52
+ // offsetX/Y is relative to itemId's position, and handleMove reads firstStart
53
+ // from itemIds[0] — they must be the same node.
50
54
  const itemsToDrag = selectedIdsRef.current.has(itemId)
51
- ? Array.from(selectedIdsRef.current)
55
+ ? [itemId, ...Array.from(selectedIdsRef.current).filter((id) => id !== itemId)]
52
56
  : [itemId];
53
57
  const vpt = canvas.viewportTransform || [1, 0, 0, 1, 0, 0];
54
58
  const liveZoom = vpt[0];
@@ -69,21 +73,34 @@ export function useNodeDrag({ selectedIdsRef, dragSelectedIdsRef, localTasksRef,
69
73
  if (pos)
70
74
  startPositions.set(id, pos);
71
75
  });
72
- // Resolve Fabric object start positions. For activeSelection groups,
73
- // calcTransformMatrix gives the true world position regardless of the
74
- // group's internal coordinate space.
76
+ // Capture Fabric object references NOW (before handleSelect fires
77
+ // discardActiveObject), but read their actual left/top positions via a
78
+ // queueMicrotask that runs AFTER discardActiveObject() has restored every
79
+ // object to true world coordinates, yet BEFORE any requestAnimationFrame
80
+ // callback (where handleMove runs).
81
+ //
82
+ // Why not calcTransformMatrix() here?
83
+ // In Fabric v6 the matrix-multiplication path (calcTransformMatrix) and
84
+ // the matrix-decomposition path (ActiveSelection.destroy) are independent
85
+ // algorithms. They can diverge when objects carry scale, strokeUniform, or
86
+ // accumulated float rounding — causing a visible position jump on frame 1.
87
+ // Reading obj.left/top after discard is the ground-truth value: no
88
+ // prediction, no mismatch.
75
89
  const liveActiveObjects = canvas.getActiveObjects();
76
- const activeSelection = canvas.getActiveObject();
77
90
  const canvasObjectsStartPos = new Map();
78
- liveActiveObjects.forEach((obj) => {
79
- if (activeSelection?.type === "activeSelection") {
80
- const matrix = obj.calcTransformMatrix();
81
- canvasObjectsStartPos.set(obj, { left: matrix[4], top: matrix[5] });
82
- }
83
- else {
84
- canvasObjectsStartPos.set(obj, { left: obj.left || 0, top: obj.top || 0 });
85
- }
86
- });
91
+ const objectsToSync = [...liveActiveObjects];
92
+ if (objectsToSync.length > 0) {
93
+ queueMicrotask(() => {
94
+ if (!dragStateRef.current.isDragging)
95
+ return;
96
+ objectsToSync.forEach((obj) => {
97
+ dragStateRef.current.canvasObjectsStartPos.set(obj, {
98
+ left: obj.left || 0,
99
+ top: obj.top || 0,
100
+ });
101
+ });
102
+ });
103
+ }
87
104
  dragStateRef.current = {
88
105
  isDragging: true,
89
106
  itemIds: itemsToDrag,
@@ -118,25 +135,33 @@ export function useNodeDrag({ selectedIdsRef, dragSelectedIdsRef, localTasksRef,
118
135
  const vpt = canvas.viewportTransform;
119
136
  const currentWorldX = (pointer.clientX - vpt[4]) / vpt[0];
120
137
  const currentWorldY = (pointer.clientY - vpt[5]) / vpt[0];
138
+ // itemIds[0] is always the clicked node (guaranteed by handleDragStart).
139
+ // offsetX/Y is relative to that same node, so the anchor is consistent.
121
140
  const firstStart = startPositions.get(itemIds[0]);
122
141
  if (!firstStart)
123
142
  return;
124
143
  const deltaX = currentWorldX - offsetX - firstStart.x;
125
144
  const deltaY = currentWorldY - offsetY - firstStart.y;
126
- setLocalTasks((prev) => prev.map((t) => itemIds.includes(t.id)
127
- ? { ...t, x: (startPositions.get(t.id)?.x ?? t.x) + deltaX, y: (startPositions.get(t.id)?.y ?? t.y) + deltaY }
128
- : t));
129
- setLocalDocuments((prev) => prev.map((d) => itemIds.includes(d.id)
130
- ? { ...d, x: (startPositions.get(d.id)?.x ?? d.x) + deltaX, y: (startPositions.get(d.id)?.y ?? d.y) + deltaY }
131
- : d));
145
+ // O(1) Set lookup critical for 100+ node selections.
146
+ const itemIdSet = new Set(itemIds);
147
+ setLocalTasks((prev) => prev.map((t) => {
148
+ const sp = startPositions.get(t.id);
149
+ return sp && itemIdSet.has(t.id)
150
+ ? { ...t, x: sp.x + deltaX, y: sp.y + deltaY }
151
+ : t;
152
+ }));
153
+ setLocalDocuments((prev) => prev.map((d) => {
154
+ const sp = startPositions.get(d.id);
155
+ return sp && itemIdSet.has(d.id)
156
+ ? { ...d, x: sp.x + deltaX, y: sp.y + deltaY }
157
+ : d;
158
+ }));
159
+ // Move Fabric objects in sync. setCoords() updates hit-testing after
160
+ // the imperative position change.
132
161
  canvasObjectsStartPos.forEach((startPos, obj) => {
133
162
  obj.set({ left: startPos.left + deltaX, top: startPos.top + deltaY });
134
163
  obj.setCoords();
135
164
  });
136
- const activeObj = canvas.getActiveObject();
137
- if (activeObj?.type === "activeSelection") {
138
- activeObj.setCoords();
139
- }
140
165
  canvas.requestRenderAll();
141
166
  });
142
167
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mhamz.01/easyflow-whiteboard",
3
- "version": "2.170.0",
3
+ "version": "2.172.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",