{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "edge-geometry", "title": "Edge Geometry", "description": "Pure geometry helpers that pick the best connection sides between two node rectangles.", "dependencies": ["@xyflow/react"], "files": [ { "path": "src/registry/thornberry/lib/edge-geometry.ts", "content": "import { Position } from \"@xyflow/react\";\n\n/** Smooth step path offset for routed edges */\nexport const EDGE_OFFSET = 20;\n\n/** Penalty applied when a side pair aligns with the node layout direction */\nconst DIRECTION_BONUS = 50;\n\n/** A node rectangle expressed as top-left corner plus size */\nexport interface NodeRect {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\ninterface Point {\n x: number;\n y: number;\n}\n\n/** Center point of each side of a node rectangle */\nfunction getNodeSideCenters(rect: NodeRect) {\n return {\n top: { x: rect.x + rect.width / 2, y: rect.y },\n bottom: { x: rect.x + rect.width / 2, y: rect.y + rect.height },\n left: { x: rect.x, y: rect.y + rect.height / 2 },\n right: { x: rect.x + rect.width, y: rect.y + rect.height / 2 },\n };\n}\n\n/** Euclidean distance between two points */\nfunction distance(p1: Point, p2: Point): number {\n return Math.sqrt((p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2);\n}\n\n/** A connection point pair with full geometry */\ninterface ConnectionPoints {\n sourceX: number;\n sourceY: number;\n targetX: number;\n targetY: number;\n sourcePosition: Position;\n targetPosition: Position;\n}\n\n/**\n * Determine the best connection point coordinates between two nodes.\n *\n * Scores the four cardinal side pairs (Bottom->Top, Top->Bottom,\n * Right->Left, Left->Right) by distance, with a bonus when the pair\n * aligns with the relative layout direction of the two nodes, and\n * returns the lowest-scoring pair's coordinates.\n */\nexport function getBestConnectionPoints(\n sourceRect: NodeRect,\n targetRect: NodeRect,\n): ConnectionPoints {\n const sourceSides = getNodeSideCenters(sourceRect);\n const targetSides = getNodeSideCenters(targetRect);\n\n type PositionPair = {\n sourcePos: Position;\n targetPos: Position;\n source: Point;\n target: Point;\n };\n // a fixed 4-tuple so positionPairs[0] is provably defined without a non-null assertion\n const positionPairs: [\n PositionPair,\n PositionPair,\n PositionPair,\n PositionPair,\n ] = [\n {\n sourcePos: Position.Bottom,\n targetPos: Position.Top,\n source: sourceSides.bottom,\n target: targetSides.top,\n },\n {\n sourcePos: Position.Top,\n targetPos: Position.Bottom,\n source: sourceSides.top,\n target: targetSides.bottom,\n },\n {\n sourcePos: Position.Right,\n targetPos: Position.Left,\n source: sourceSides.right,\n target: targetSides.left,\n },\n {\n sourcePos: Position.Left,\n targetPos: Position.Right,\n source: sourceSides.left,\n target: targetSides.right,\n },\n ];\n\n let bestPair = positionPairs[0];\n let bestScore = Number.POSITIVE_INFINITY;\n\n for (const pair of positionPairs) {\n const dist = distance(pair.source, pair.target);\n let penalty = 0;\n\n if (\n pair.sourcePos === Position.Bottom &&\n pair.targetPos === Position.Top &&\n targetRect.y > sourceRect.y + sourceRect.height\n ) {\n penalty -= DIRECTION_BONUS;\n }\n if (\n pair.sourcePos === Position.Top &&\n pair.targetPos === Position.Bottom &&\n targetRect.y + targetRect.height < sourceRect.y\n ) {\n penalty -= DIRECTION_BONUS;\n }\n if (\n pair.sourcePos === Position.Right &&\n pair.targetPos === Position.Left &&\n targetRect.x > sourceRect.x + sourceRect.width\n ) {\n penalty -= DIRECTION_BONUS;\n }\n if (\n pair.sourcePos === Position.Left &&\n pair.targetPos === Position.Right &&\n targetRect.x + targetRect.width < sourceRect.x\n ) {\n penalty -= DIRECTION_BONUS;\n }\n\n const score = dist + penalty;\n if (score < bestScore) {\n bestScore = score;\n bestPair = pair;\n }\n }\n\n return {\n sourceX: bestPair.source.x,\n sourceY: bestPair.source.y,\n targetX: bestPair.target.x,\n targetY: bestPair.target.y,\n sourcePosition: bestPair.sourcePos,\n targetPosition: bestPair.targetPos,\n };\n}\n\n/**\n * Choose which sides of the source and target nodes an edge should\n * connect to, based purely on their geometry.\n */\nexport function chooseConnectionSides(\n source: NodeRect,\n target: NodeRect,\n): { sourcePosition: Position; targetPosition: Position } {\n const { sourcePosition, targetPosition } = getBestConnectionPoints(\n source,\n target,\n );\n return { sourcePosition, targetPosition };\n}\n", "type": "registry:lib" } ], "type": "registry:lib" }