Files
git.stella-ops.org/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterAnchors.cs
2026-03-23 13:23:19 +02:00

261 lines
10 KiB
C#

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);
}
}