feat(canvas): snap connection preview to magnet targets

This commit is contained in:
2026-04-11 09:04:59 +02:00
parent db71b2485a
commit baeb709acd
3 changed files with 273 additions and 3 deletions

View File

@@ -7,9 +7,38 @@ import {
getSmoothStepPath,
getStraightPath,
type ConnectionLineComponentProps,
useReactFlow,
} from "@xyflow/react";
import { useEffect, useMemo } from "react";
import {
HANDLE_SNAP_RADIUS_PX,
resolveCanvasMagnetTarget,
} from "@/components/canvas/canvas-connection-magnetism";
import { useCanvasConnectionMagnetism } from "@/components/canvas/canvas-connection-magnetism-context";
import { connectionLineAccentRgb } from "@/lib/canvas-utils";
function hasSameMagnetTarget(
a: Parameters<ReturnType<typeof useCanvasConnectionMagnetism>["setActiveTarget"]>[0],
b: Parameters<ReturnType<typeof useCanvasConnectionMagnetism>["setActiveTarget"]>[0],
): boolean {
if (a === b) {
return true;
}
if (!a || !b) {
return false;
}
return (
a.nodeId === b.nodeId &&
a.handleId === b.handleId &&
a.handleType === b.handleType &&
a.centerX === b.centerX &&
a.centerY === b.centerY &&
a.distancePx === b.distancePx
);
}
export default function CustomConnectionLine({
connectionLineType,
fromNode,
@@ -22,12 +51,50 @@ export default function CustomConnectionLine({
toPosition,
connectionStatus,
}: ConnectionLineComponentProps) {
const { getNodes, getEdges } = useReactFlow();
const { activeTarget, setActiveTarget } = useCanvasConnectionMagnetism();
const fromHandleType =
fromHandle?.type === "source" || fromHandle?.type === "target"
? fromHandle.type
: null;
const resolvedMagnetTarget = useMemo(() => {
if (!fromHandleType || !fromNode?.id) {
return null;
}
return resolveCanvasMagnetTarget({
point: { x: toX, y: toY },
fromNodeId: fromNode.id,
fromHandleId: fromHandle?.id ?? undefined,
fromHandleType,
nodes: getNodes(),
edges: getEdges(),
});
}, [fromHandle?.id, fromHandleType, fromNode?.id, getEdges, getNodes, toX, toY]);
useEffect(() => {
if (hasSameMagnetTarget(activeTarget, resolvedMagnetTarget)) {
return;
}
setActiveTarget(resolvedMagnetTarget);
}, [activeTarget, resolvedMagnetTarget, setActiveTarget]);
const magnetTarget = activeTarget ?? resolvedMagnetTarget;
const snappedTarget =
magnetTarget && magnetTarget.distancePx <= HANDLE_SNAP_RADIUS_PX
? magnetTarget
: null;
const targetX = snappedTarget?.centerX ?? toX;
const targetY = snappedTarget?.centerY ?? toY;
const pathParams = {
sourceX: fromX,
sourceY: fromY,
sourcePosition: fromPosition,
targetX: toX,
targetY: toY,
targetX,
targetY,
targetPosition: toPosition,
};
@@ -54,6 +121,10 @@ export default function CustomConnectionLine({
const [r, g, b] = connectionLineAccentRgb(fromNode.type, fromHandle.id);
const opacity = connectionStatus === "invalid" ? 0.45 : 1;
const strokeWidth = snappedTarget ? 3.25 : 2.5;
const filter = snappedTarget
? `drop-shadow(0 0 3px rgba(${r}, ${g}, ${b}, 0.7)) drop-shadow(0 0 8px rgba(${r}, ${g}, ${b}, 0.48))`
: undefined;
return (
<path
@@ -62,9 +133,10 @@ export default function CustomConnectionLine({
className="ls-connection-line-marching"
style={{
stroke: `rgb(${r}, ${g}, ${b})`,
strokeWidth: 2.5,
strokeWidth,
strokeLinecap: "round",
strokeDasharray: "10 8",
filter,
opacity,
}}
/>