@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.
- 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 +3 -2
- package/dist/components/node/hooks/useNodeDrag.d.ts.map +1 -1
- package/dist/components/node/hooks/useNodeDrag.js +51 -26
- 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) {
|
|
@@ -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
|
-
* -
|
|
25
|
-
*
|
|
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
|
|
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
|
-
* -
|
|
21
|
-
*
|
|
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
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
};
|