Skip to content

Commit 89b06f5

Browse files
committed
fix: fix bidirecional line start drag
1 parent cf68a5b commit 89b06f5

File tree

4 files changed

+153
-122
lines changed

4 files changed

+153
-122
lines changed

src/lib/shared/components/line/DraggableLine.tsx

+114-74
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
import React, { useCallback, useEffect, useMemo, useState } from 'react';
1+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
22
import { useSetObserver } from 'react-observing';
33
import { useFrame } from 'react-frame-component';
44

55
import { useBoardScrollContext, useBoardZoomContext, useDragLineContext, useToggleSelectedItem } from '../../context';
6-
import { getEdgeParams, getStraightPath } from '../../services';
7-
import { useLinePath } from './UseLinePath';
6+
import { getCtrlKeyBySystem, getCurvedPath, getEdgeParams, getStraightPath } from '../../services';
87
import { TId } from '../../types';
98

109

@@ -19,20 +18,23 @@ interface IDraggableLineProps {
1918
height1: number;
2019
height2: number;
2120
lineWidth: number;
21+
isCurved?: boolean;
2222
newConnection?: boolean;
2323
lineId: TId | undefined;
2424
onDragLineEnd?: () => void;
2525
onDragLineStart?: () => void;
2626
position1FromCenter?: boolean;
2727
disableStartDraggable?: boolean;
2828
}
29-
export const DraggableLine: React.FC<IDraggableLineProps> = ({ lineId, newConnection = false, position1FromCenter = false, disableStartDraggable = false, nodeId, lineWidth, onDragLineEnd, onDragLineStart, ...rest }) => {
29+
export const DraggableLine: React.FC<IDraggableLineProps> = ({ lineId, isCurved, newConnection = false, position1FromCenter = false, disableStartDraggable = false, nodeId, lineWidth, onDragLineEnd, onDragLineStart, ...rest }) => {
3030
const setDragLine = useSetObserver(useDragLineContext());
3131
const addSelectedItem = useToggleSelectedItem();
3232
const scrollObject = useBoardScrollContext();
3333
const zoomObject = useBoardZoomContext();
3434
const { window } = useFrame();
3535

36+
const isDragging = useRef(false);
37+
3638

3739
const [rawTop1, setRawTop1] = useState(rest.top1);
3840
const [rawTop2, setRawTop2] = useState(rest.top2);
@@ -49,52 +51,66 @@ export const DraggableLine: React.FC<IDraggableLineProps> = ({ lineId, newConnec
4951
}, [rest.top1, rest.top2, rest.left1, rest.left2]);
5052

5153

52-
const linePath = useLinePath({
53-
top1: rawTop1,
54-
top2: rawTop2,
55-
left1: rawLeft1,
56-
left2: rawLeft2,
57-
width1: rest.width1,
58-
width2: rest.width2,
59-
height1: rest.height1,
60-
height2: rest.height2,
61-
});
62-
54+
const intersectionPoints = useMemo(() => {
55+
const dragAroundSpace = 20;
6356

64-
const linePath2 = useMemo(() => {
65-
const { sx, sy, tx, ty } = getEdgeParams(
57+
return getEdgeParams(
6658
{
67-
y: rawTop1,
68-
x: rawLeft1,
69-
width: rest.width1 + 10,
70-
height: rest.height1 + 10,
59+
y: rawTop1 - (showDragLine === 'start' ? dragAroundSpace / 2 : 5),
60+
x: rawLeft1 - (showDragLine === 'start' ? dragAroundSpace / 2 : 5),
61+
width: showDragLine === 'start' ? dragAroundSpace : rest.width1 + 10,
62+
height: showDragLine === 'start' ? dragAroundSpace : rest.height1 + 10,
7163
},
7264
{
73-
y: rawTop2 - 5,
74-
x: rawLeft2 - 5,
75-
width: 10,
76-
height: 10,
65+
y: rawTop2 - (showDragLine === 'end' ? dragAroundSpace / 2 : 5),
66+
x: rawLeft2 - (showDragLine === 'end' ? dragAroundSpace / 2 : 5),
67+
width: showDragLine === 'end' ? dragAroundSpace : rest.width2 + 10,
68+
height: showDragLine === 'end' ? dragAroundSpace : rest.height2 + 10,
7769
}
7870
);
71+
}, [showDragLine, rawTop1, rawTop2, rawLeft1, rawLeft2, rest.width1, rest.height1, rest.width2, rest.height2]);
72+
7973

80-
const [edgePath] = getStraightPath({
81-
sourceX: sx,
82-
sourceY: sy,
83-
targetX: tx,
84-
targetY: ty,
74+
const [linePath] = useMemo(() => {
75+
const path = getStraightPath({
76+
sourceX: intersectionPoints.sourceX,
77+
sourceY: intersectionPoints.sourceY,
78+
targetX: intersectionPoints.targetX,
79+
targetY: intersectionPoints.targetY,
8580
});
8681

87-
return edgePath;
88-
}, [rawTop1, rawTop2, rawLeft1, rawLeft2, rest.width1, rest.width2, rest.height1, rest.height2]);
82+
return path;
83+
}, [intersectionPoints]);
84+
85+
const [curvedLinePath] = useMemo(() => {
86+
const [path] = getCurvedPath(
87+
{
88+
sourceX: intersectionPoints.sourceX,
89+
sourceY: intersectionPoints.sourceY,
90+
targetX: intersectionPoints.targetX,
91+
targetY: intersectionPoints.targetY,
92+
},
93+
{ offset: 35 }
94+
);
95+
96+
97+
return [path];
98+
}, [intersectionPoints]);
8999

90100

91101
const handleStartMouseDown = useCallback((e: React.MouseEvent) => {
92-
if (lineId) addSelectedItem([lineId], false);
93-
setShowDragLine('start');
94-
onDragLineStart?.();
102+
if (lineId) addSelectedItem([lineId], getCtrlKeyBySystem(e.nativeEvent));
95103
if (!window) return;
96104

97105
const handleMouseMove = (e: MouseEvent) => {
106+
if (!isDragging.current) {
107+
setShowDragLine('start');
108+
onDragLineStart?.();
109+
setDragLine({ type: 'start', nodeId, lineId });
110+
111+
isDragging.current = true;
112+
}
113+
98114
const newLeft = (e.pageX - scrollObject.left.value) / zoomObject.value;
99115
const newTop = (e.pageY - scrollObject.top.value) / zoomObject.value;
100116

@@ -103,6 +119,8 @@ export const DraggableLine: React.FC<IDraggableLineProps> = ({ lineId, newConnec
103119
}
104120

105121
const handleMouseUp = () => {
122+
isDragging.current = false;
123+
106124
setShowDragLine(undefined);
107125
setRawLeft1(rest.left1);
108126
setDragLine(undefined);
@@ -112,20 +130,24 @@ export const DraggableLine: React.FC<IDraggableLineProps> = ({ lineId, newConnec
112130
window.removeEventListener('mouseup', handleMouseUp)
113131
}
114132

115-
handleMouseMove(e.nativeEvent);
116-
117-
setDragLine({ type: 'start', nodeId, lineId });
118133
window.addEventListener('mousemove', handleMouseMove);
119134
window.addEventListener('mouseup', handleMouseUp);
120-
}, [setDragLine, onDragLineStart, onDragLineEnd, window, scrollObject, zoomObject, rawTop1, rawLeft1, linePath.y1, linePath.x1, rest.left1, rest.top1, nodeId, lineId]);
135+
}, [setDragLine, onDragLineStart, onDragLineEnd, window, scrollObject, zoomObject, rawTop1, rawLeft1, rest.left1, rest.top1, nodeId, lineId]);
121136

122137
const handleEndMouseDown = useCallback((e: React.MouseEvent) => {
123-
if (lineId) addSelectedItem([lineId], false);
124-
setShowDragLine('end');
125-
onDragLineStart?.();
138+
if (lineId) addSelectedItem([lineId], getCtrlKeyBySystem(e.nativeEvent));
126139
if (!window) return;
127140

141+
128142
const handleMouseMove = (e: MouseEvent) => {
143+
if (!isDragging.current) {
144+
setShowDragLine('end');
145+
onDragLineStart?.();
146+
setDragLine({ type: 'end', nodeId, lineId });
147+
148+
isDragging.current = true;
149+
}
150+
129151
const newLeft = (e.pageX - scrollObject.left.value) / zoomObject.value;
130152
const newTop = (e.pageY - scrollObject.top.value) / zoomObject.value;
131153

@@ -134,6 +156,8 @@ export const DraggableLine: React.FC<IDraggableLineProps> = ({ lineId, newConnec
134156
}
135157

136158
const handleMouseUp = () => {
159+
isDragging.current = false;
160+
137161
setShowDragLine(undefined);
138162
setRawLeft2(rest.left2);
139163
setDragLine(undefined);
@@ -143,20 +167,51 @@ export const DraggableLine: React.FC<IDraggableLineProps> = ({ lineId, newConnec
143167
window.removeEventListener('mouseup', handleMouseUp)
144168
}
145169

146-
handleMouseMove(e.nativeEvent);
147-
148-
setDragLine({ type: 'end', nodeId, lineId });
149170
window.addEventListener('mousemove', handleMouseMove)
150171
window.addEventListener('mouseup', handleMouseUp)
151-
}, [setDragLine, onDragLineStart, onDragLineEnd, window, scrollObject, zoomObject, linePath.y2, linePath.x2, rest.left2, rest.top2, nodeId, lineId]);
172+
}, [setDragLine, onDragLineStart, onDragLineEnd, window, scrollObject, zoomObject, rest.left2, rest.top2, nodeId, lineId]);
173+
174+
const handleMoveDown = useCallback((event: React.MouseEvent<SVGPathElement>) => {
175+
const pathLength = event.currentTarget.getTotalLength();
176+
177+
const clickX = event.clientX;
178+
const clickY = event.clientY;
179+
180+
let closestPointLength = 0;
181+
let closestDistance = Infinity;
182+
183+
const segmentCount = 100;
184+
for (let i = 0; i <= segmentCount; i++) {
185+
const pointLength = (i / segmentCount) * pathLength;
186+
const point = event.currentTarget.getPointAtLength(pointLength);
187+
188+
const distance = Math.sqrt(
189+
Math.pow(clickX - point.x, 2) + Math.pow(clickY - point.y, 2)
190+
);
191+
192+
if (distance < closestDistance) {
193+
closestDistance = distance;
194+
closestPointLength = pointLength;
195+
}
196+
}
197+
198+
const halfPathLength = pathLength / 2;
199+
const isCloserToStart = closestPointLength <= halfPathLength;
200+
201+
if (isCloserToStart) {
202+
handleStartMouseDown(event);
203+
} else {
204+
handleEndMouseDown(event);
205+
}
206+
}, [handleStartMouseDown, handleEndMouseDown]);
152207

153208

154209
return (
155210
<>
156211
{showDragLine && (
157212
<path
158213
fill="none"
159-
d={linePath2}
214+
d={linePath}
160215
stroke="#0f77bf"
161216
strokeLinecap="round"
162217
strokeWidth={lineWidth}
@@ -166,40 +221,25 @@ export const DraggableLine: React.FC<IDraggableLineProps> = ({ lineId, newConnec
166221

167222
{newConnection && (
168223
<rect
169-
x={rawLeft1 - 3}
224+
x={rawLeft1 - 6.5}
170225
fill='transparent'
171-
width={linePath.width1}
172-
height={(linePath.height1 / 2)}
226+
width={rest.width1 + 15}
227+
height={(rest.height1 / 2) + 15}
173228
onMouseDown={handleEndMouseDown}
174-
y={rawTop1 + (linePath.height1 / 2) + 2}
229+
y={rawTop1 + (rest.height1 / 2)}
175230
style={{ cursor: 'crosshair', pointerEvents: showDragLine ? 'none' : 'auto' }}
176231
/>
177232
)}
178233

179234
{(!showDragLine && !newConnection) && (
180-
<>
181-
{!disableStartDraggable && (
182-
<rect
183-
width={20}
184-
height={20}
185-
fill='transparent'
186-
y={linePath.y1 - 10}
187-
x={linePath.x1 - 10}
188-
onMouseDown={handleStartMouseDown}
189-
style={{ cursor: 'crosshair', pointerEvents: 'auto' }}
190-
/>
191-
)}
192-
193-
<rect
194-
width={20}
195-
height={20}
196-
fill='transparent'
197-
y={linePath.y2 - 10}
198-
x={linePath.x2 - 10}
199-
onMouseDown={handleEndMouseDown}
200-
style={{ cursor: 'crosshair', pointerEvents: 'auto' }}
201-
/>
202-
</>
235+
<path
236+
fill="none"
237+
strokeWidth={14}
238+
stroke="transparent"
239+
onMouseDown={handleMoveDown}
240+
d={isCurved ? curvedLinePath : linePath}
241+
style={{ cursor: 'crosshair', pointerEvents: 'auto' }}
242+
/>
203243
)}
204244
</>
205245
);

0 commit comments

Comments
 (0)