@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.
- package/dist/components/node/hooks/useFabricSync.d.ts +20 -9
- package/dist/components/node/hooks/useFabricSync.d.ts.map +1 -1
- package/dist/components/node/hooks/useFabricSync.js +56 -16
- package/dist/components/node/hooks/useNodeDrag.d.ts.map +1 -1
- package/dist/components/node/hooks/useNodeDrag.js +22 -11
- package/package.json +1 -1
|
@@ -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
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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
|
|
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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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;
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
};
|