namespace StellaOps.ElkSharp; internal static class ElkEdgeRouterAnchors { internal static ElkPoint ResolveAnchorPoint( ElkPositionedNode node, ElkPositionedNode otherNode, string? portId, ElkLayoutDirection direction, string? forcedSide = null) { if (!string.IsNullOrWhiteSpace(portId)) { var port = node.Ports.FirstOrDefault(x => string.Equals(x.Id, portId, StringComparison.Ordinal)); if (port is not null) { return new ElkPoint { X = port.X + (port.Width / 2d), Y = port.Y + (port.Height / 2d), }; } } var nodeCenterX = node.X + (node.Width / 2d); var nodeCenterY = node.Y + (node.Height / 2d); var otherCenterX = otherNode.X + (otherNode.Width / 2d); var otherCenterY = otherNode.Y + (otherNode.Height / 2d); if (Math.Abs(otherCenterX - nodeCenterX) < 0.001d && Math.Abs(otherCenterY - nodeCenterY) < 0.001d) { return new ElkPoint { X = nodeCenterX, Y = nodeCenterY, }; } return ResolvePreferredAnchorPoint(node, otherCenterX, otherCenterY, forcedSide, direction); } internal static (ElkPoint SourcePoint, ElkPoint TargetPoint) ResolveStraightChainAnchors( ElkPositionedNode sourceNode, ElkPositionedNode targetNode, ElkPoint sourcePoint, ElkPoint targetPoint, string sourceSide, string targetSide, EdgeChannel channel, ElkLayoutDirection direction) { if (direction == ElkLayoutDirection.LeftToRight) { if (channel.ForwardCount != 1 || channel.TargetIncomingCount != 1 || targetPoint.X < sourcePoint.X) { return (sourcePoint, targetPoint); } var sharedY = sourceNode.Y + (sourceNode.Height / 2d); return ( ResolvePreferredAnchorPoint(sourceNode, targetNode.X, sharedY, sourceSide, direction), ResolvePreferredAnchorPoint(targetNode, sourceNode.X + sourceNode.Width, sharedY, targetSide, direction)); } if (channel.ForwardCount != 1 || channel.TargetIncomingCount != 1 || targetPoint.Y < sourcePoint.Y) { return (sourcePoint, targetPoint); } var sharedX = sourceNode.X + (sourceNode.Width / 2d); return ( ResolvePreferredAnchorPoint(sourceNode, sharedX, targetNode.Y, sourceSide, direction), ResolvePreferredAnchorPoint(targetNode, sharedX, sourceNode.Y + sourceNode.Height, targetSide, direction)); } internal static (string SourceSide, string TargetSide) ResolveRouteSides( ElkPositionedNode sourceNode, ElkPositionedNode targetNode, ElkLayoutDirection direction) { var sourceCenterX = sourceNode.X + (sourceNode.Width / 2d); var sourceCenterY = sourceNode.Y + (sourceNode.Height / 2d); var targetCenterX = targetNode.X + (targetNode.Width / 2d); var targetCenterY = targetNode.Y + (targetNode.Height / 2d); var deltaX = targetCenterX - sourceCenterX; var deltaY = targetCenterY - sourceCenterY; if (direction == ElkLayoutDirection.LeftToRight) { if (Math.Abs(deltaX) >= 24d || Math.Abs(deltaX) >= Math.Abs(deltaY) * 0.35d) { return deltaX >= 0d ? ("EAST", "WEST") : ("NORTH", "NORTH"); } return deltaY >= 0d ? ("SOUTH", "NORTH") : ("NORTH", "SOUTH"); } if (Math.Abs(deltaY) >= 24d || Math.Abs(deltaY) >= Math.Abs(deltaX) * 0.35d) { return deltaY >= 0d ? ("SOUTH", "NORTH") : ("NORTH", "NORTH"); } return deltaX >= 0d ? ("EAST", "WEST") : ("WEST", "EAST"); } internal static ElkPoint ResolvePreferredAnchorPoint( ElkPositionedNode node, double targetX, double targetY, string? forcedSide, ElkLayoutDirection direction) { var nodeCenterX = node.X + (node.Width / 2d); var nodeCenterY = node.Y + (node.Height / 2d); var deltaX = targetX - nodeCenterX; var deltaY = targetY - nodeCenterY; var insetX = Math.Min(18d, node.Width / 4d); var insetY = Math.Min(18d, node.Height / 4d); var preferredSide = forcedSide; if (string.IsNullOrWhiteSpace(preferredSide)) { preferredSide = direction == ElkLayoutDirection.LeftToRight ? (Math.Abs(deltaX) >= Math.Abs(deltaY) * 0.35d ? (deltaX >= 0d ? "EAST" : "WEST") : (deltaY >= 0d ? "SOUTH" : "NORTH")) : (Math.Abs(deltaY) >= Math.Abs(deltaX) * 0.35d ? (deltaY >= 0d ? "SOUTH" : "NORTH") : (deltaX >= 0d ? "EAST" : "WEST")); } var preferredTargetX = preferredSide switch { "EAST" => node.X + node.Width + 256d, "WEST" => node.X - 256d, _ => ElkLayoutHelpers.Clamp(targetX, node.X + insetX, node.X + node.Width - insetX), }; var preferredTargetY = preferredSide switch { "SOUTH" => node.Y + node.Height + 256d, "NORTH" => node.Y - 256d, _ => ElkLayoutHelpers.Clamp(targetY, node.Y + insetY, node.Y + node.Height - insetY), }; var adjustedDeltaX = preferredTargetX - nodeCenterX; var adjustedDeltaY = preferredTargetY - nodeCenterY; var candidate = new ElkPoint { X = preferredSide switch { "EAST" => node.X + node.Width, "WEST" => node.X, _ => ElkLayoutHelpers.Clamp(preferredTargetX, node.X + insetX, node.X + node.Width - insetX), }, Y = preferredSide switch { "SOUTH" => node.Y + node.Height, "NORTH" => node.Y, _ => ElkLayoutHelpers.Clamp(preferredTargetY, node.Y + insetY, node.Y + node.Height - insetY), }, }; return ElkShapeBoundaries.ResolveGatewayBoundaryPoint(node, candidate, adjustedDeltaX, adjustedDeltaY); } internal static ElkPoint ComputeSmartAnchor( ElkPositionedNode node, ElkPoint? approachPoint, bool isSource, double spreadY, int groupSize, ElkLayoutDirection direction) { if (direction != ElkLayoutDirection.LeftToRight || approachPoint is null) { var fallback = isSource ? new ElkPoint { X = node.X + node.Width, Y = ElkLayoutHelpers.Clamp(spreadY, node.Y + 6d, node.Y + node.Height - 6d) } : new ElkPoint { X = node.X, Y = ElkLayoutHelpers.Clamp(spreadY, node.Y + 6d, node.Y + node.Height - 6d) }; return ElkShapeBoundaries.ResolveGatewayBoundaryPoint( node, fallback, fallback.X - (node.X + (node.Width / 2d)), fallback.Y - (node.Y + (node.Height / 2d))); } var nodeCenterX = node.X + (node.Width / 2d); var nodeCenterY = node.Y + (node.Height / 2d); var deltaX = approachPoint.X - nodeCenterX; var deltaY = approachPoint.Y - nodeCenterY; if (isSource) { if (Math.Abs(deltaY) > Math.Abs(deltaX) * 1.5d && deltaY < 0d) { var topCandidate = new ElkPoint { X = ElkLayoutHelpers.Clamp(approachPoint.X, node.X + 8d, node.X + node.Width - 8d), Y = node.Y, }; return ElkShapeBoundaries.ResolveGatewayBoundaryPoint(node, topCandidate, topCandidate.X - nodeCenterX, topCandidate.Y - nodeCenterY); } if (Math.Abs(deltaY) > Math.Abs(deltaX) * 1.5d && deltaY > 0d) { var bottomCandidate = new ElkPoint { X = ElkLayoutHelpers.Clamp(approachPoint.X, node.X + 8d, node.X + node.Width - 8d), Y = node.Y + node.Height, }; return ElkShapeBoundaries.ResolveGatewayBoundaryPoint(node, bottomCandidate, bottomCandidate.X - nodeCenterX, bottomCandidate.Y - nodeCenterY); } var eastCandidate = new ElkPoint { X = node.X + node.Width, Y = groupSize > 1 ? ElkLayoutHelpers.Clamp(spreadY, node.Y + 6d, node.Y + node.Height - 6d) : ElkLayoutHelpers.Clamp(approachPoint.Y, node.Y + 6d, node.Y + node.Height - 6d), }; return ElkShapeBoundaries.ResolveGatewayBoundaryPoint(node, eastCandidate, eastCandidate.X - nodeCenterX, eastCandidate.Y - nodeCenterY); } if (Math.Abs(deltaY) > Math.Abs(deltaX) * 0.8d && deltaY < 0d) { var topCandidate = new ElkPoint { X = ElkLayoutHelpers.Clamp(approachPoint.X, node.X + 8d, node.X + node.Width - 8d), Y = node.Y, }; return ElkShapeBoundaries.ResolveGatewayBoundaryPoint(node, topCandidate, topCandidate.X - nodeCenterX, topCandidate.Y - nodeCenterY); } if (Math.Abs(deltaY) > Math.Abs(deltaX) * 0.8d && deltaY > 0d) { var bottomCandidate = new ElkPoint { X = ElkLayoutHelpers.Clamp(approachPoint.X, node.X + 8d, node.X + node.Width - 8d), Y = node.Y + node.Height, }; return ElkShapeBoundaries.ResolveGatewayBoundaryPoint(node, bottomCandidate, bottomCandidate.X - nodeCenterX, bottomCandidate.Y - nodeCenterY); } var westCandidate = new ElkPoint { X = node.X, Y = groupSize > 1 ? ElkLayoutHelpers.Clamp(spreadY, node.Y + 6d, node.Y + node.Height - 6d) : ElkLayoutHelpers.Clamp(approachPoint.Y, node.Y + 6d, node.Y + node.Height - 6d), }; return ElkShapeBoundaries.ResolveGatewayBoundaryPoint(node, westCandidate, westCandidate.X - nodeCenterX, westCandidate.Y - nodeCenterY); } }