@mhamz.01/easyflow-whiteboard 2.170.0 → 2.171.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) {
@@ -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;;;;;;;;;;;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;EAkKxD"}
@@ -47,8 +47,11 @@ export function useNodeDrag({ selectedIdsRef, dragSelectedIdsRef, localTasksRef,
47
47
  if (e.cancelable)
48
48
  e.preventDefault();
49
49
  const pointer = readPointer(e);
50
+ // The clicked item MUST be first so handleMove's anchor calculation is correct.
51
+ // offsetX/Y is relative to itemId's position, and handleMove reads firstStart
52
+ // from itemIds[0] — they must be the same node.
50
53
  const itemsToDrag = selectedIdsRef.current.has(itemId)
51
- ? Array.from(selectedIdsRef.current)
54
+ ? [itemId, ...Array.from(selectedIdsRef.current).filter((id) => id !== itemId)]
52
55
  : [itemId];
53
56
  const vpt = canvas.viewportTransform || [1, 0, 0, 1, 0, 0];
54
57
  const liveZoom = vpt[0];
@@ -118,25 +121,33 @@ export function useNodeDrag({ selectedIdsRef, dragSelectedIdsRef, localTasksRef,
118
121
  const vpt = canvas.viewportTransform;
119
122
  const currentWorldX = (pointer.clientX - vpt[4]) / vpt[0];
120
123
  const currentWorldY = (pointer.clientY - vpt[5]) / vpt[0];
124
+ // itemIds[0] is always the clicked node (guaranteed by handleDragStart).
125
+ // offsetX/Y is relative to that same node, so the anchor is consistent.
121
126
  const firstStart = startPositions.get(itemIds[0]);
122
127
  if (!firstStart)
123
128
  return;
124
129
  const deltaX = currentWorldX - offsetX - firstStart.x;
125
130
  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));
131
+ // O(1) Set lookup critical for 100+ node selections.
132
+ const itemIdSet = new Set(itemIds);
133
+ setLocalTasks((prev) => prev.map((t) => {
134
+ const sp = startPositions.get(t.id);
135
+ return sp && itemIdSet.has(t.id)
136
+ ? { ...t, x: sp.x + deltaX, y: sp.y + deltaY }
137
+ : t;
138
+ }));
139
+ setLocalDocuments((prev) => prev.map((d) => {
140
+ const sp = startPositions.get(d.id);
141
+ return sp && itemIdSet.has(d.id)
142
+ ? { ...d, x: sp.x + deltaX, y: sp.y + deltaY }
143
+ : d;
144
+ }));
145
+ // Move Fabric objects in sync. setCoords() updates hit-testing after
146
+ // the imperative position change.
132
147
  canvasObjectsStartPos.forEach((startPos, obj) => {
133
148
  obj.set({ left: startPos.left + deltaX, top: startPos.top + deltaY });
134
149
  obj.setCoords();
135
150
  });
136
- const activeObj = canvas.getActiveObject();
137
- if (activeObj?.type === "activeSelection") {
138
- activeObj.setCoords();
139
- }
140
151
  canvas.requestRenderAll();
141
152
  });
142
153
  };
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.171.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",