feat(canvas): implement dropped connection resolution and enhance connection handling

This commit is contained in:
2026-04-04 09:56:01 +02:00
parent 12202ad337
commit 90d6fe55b1
18 changed files with 1288 additions and 165 deletions

View File

@@ -4,6 +4,7 @@ import { readCanvasOps } from "@/lib/canvas-local-persistence";
import type { Id } from "@/convex/_generated/dataModel";
import type { CanvasNodeDeleteBlockReason } from "@/lib/toast";
import { getSourceImage } from "@/lib/image-pipeline/contracts";
import { NODE_HANDLE_MAP } from "@/lib/canvas-utils";
export const OPTIMISTIC_NODE_PREFIX = "optimistic_";
export const OPTIMISTIC_EDGE_PREFIX = "optimistic_edge_";
@@ -67,6 +68,110 @@ export function getConnectEndClientPoint(
return null;
}
export type DroppedConnectionTarget = {
sourceNodeId: string;
targetNodeId: string;
sourceHandle?: string;
targetHandle?: string;
};
function getNodeElementAtClientPoint(point: { x: number; y: number }): HTMLElement | null {
if (typeof document === "undefined") {
return null;
}
const hit = document.elementsFromPoint(point.x, point.y).find((element) => {
if (!(element instanceof HTMLElement)) return false;
return (
element.classList.contains("react-flow__node") &&
typeof element.dataset.id === "string" &&
element.dataset.id.length > 0
);
});
return hit instanceof HTMLElement ? hit : null;
}
function getCompareBodyDropTargetHandle(args: {
point: { x: number; y: number };
nodeElement: HTMLElement;
targetNodeId: string;
edges: RFEdge[];
}): string | undefined {
const { point, nodeElement, targetNodeId, edges } = args;
const rect = nodeElement.getBoundingClientRect();
const midY = rect.top + rect.height / 2;
const incomingEdges = edges.filter(
(edge) => edge.target === targetNodeId && edge.className !== "temp",
);
const leftTaken = incomingEdges.some((edge) => edge.targetHandle === "left");
const rightTaken = incomingEdges.some((edge) => edge.targetHandle === "right");
if (!leftTaken && !rightTaken) {
return point.y < midY ? "left" : "right";
}
if (!leftTaken) {
return "left";
}
if (!rightTaken) {
return "right";
}
return point.y < midY ? "left" : "right";
}
export function resolveDroppedConnectionTarget(args: {
point: { x: number; y: number };
fromNodeId: string;
fromHandleId?: string;
fromHandleType: "source" | "target";
nodes: RFNode[];
edges: RFEdge[];
}): DroppedConnectionTarget | null {
const nodeElement = getNodeElementAtClientPoint(args.point);
if (!nodeElement) {
return null;
}
const targetNodeId = nodeElement.dataset.id;
if (!targetNodeId) {
return null;
}
const targetNode = args.nodes.find((node) => node.id === targetNodeId);
if (!targetNode) {
return null;
}
const handles = NODE_HANDLE_MAP[targetNode.type ?? ""];
if (args.fromHandleType === "source") {
return {
sourceNodeId: args.fromNodeId,
targetNodeId,
sourceHandle: args.fromHandleId,
targetHandle:
targetNode.type === "compare"
? getCompareBodyDropTargetHandle({
point: args.point,
nodeElement,
targetNodeId,
edges: args.edges,
})
: handles?.target,
};
}
return {
sourceNodeId: targetNodeId,
targetNodeId: args.fromNodeId,
sourceHandle: handles?.source,
targetHandle: args.fromHandleId,
};
}
/** Kanten-Split nach Drag: wartet auf echte Node-ID, wenn der Knoten noch optimistisch ist. */
export type PendingEdgeSplit = {
intersectedEdgeId: Id<"edges">;