- Fix target-join (edge/4+edge/17): gateway face overflow redirect to left tip - Fix under-node (edge/14,15,20): push-first corridor reroute instead of top corridor - Fix boundary-slots (4->0): snap after gateway polish reordering - Fix gateway corner diagonals (2->0): post-pipeline straightening pass - Fix gateway interior adjacent: polygon-aware IsInsideNodeShapeInterior - Fix gateway source face mismatch (2->0): per-edge redirect with lenient validation - Fix gateway source scoring (5->0): per-edge scoring candidate application - Fix edge-node crossing (1->0): push horizontal segment above blocking node - Decompose 7 oversized files (~20K lines) into 55+ partials under 400 lines each - Archive sprints 004 (document cleanup), 005 (decomposition), 007 (render speed) All 44+ document-processing artifact assertions pass. Hybrid deterministic mode documented as recommended path for LeftToRight layouts. Tests verified: StraightExit 2/2, BoundarySlotOffenders 2/2, HybridDeterministicMode 3/3, DocumentProcessingWorkflow artifact 1/1. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
323 lines
13 KiB
C#
323 lines
13 KiB
C#
namespace StellaOps.ElkSharp;
|
|
|
|
internal static partial class ElkEdgePostProcessor
|
|
{
|
|
private static bool TryResolveSharedLaneByPairedNodeHandoffSlotRepair(
|
|
ElkRoutedEdge[] currentEdges,
|
|
int leftIndex,
|
|
ElkRoutedEdge leftEdge,
|
|
int rightIndex,
|
|
ElkRoutedEdge rightEdge,
|
|
ElkPositionedNode[] nodes,
|
|
double minLineClearance,
|
|
double graphMinY,
|
|
double graphMaxY,
|
|
out ElkRoutedEdge repairedLeftEdge,
|
|
out ElkRoutedEdge repairedRightEdge)
|
|
{
|
|
repairedLeftEdge = leftEdge;
|
|
repairedRightEdge = rightEdge;
|
|
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
|
|
if (!TryResolveSharedLaneNodeHandoffContext(leftEdge, rightEdge, nodesById, graphMinY, graphMaxY, out var leftContext)
|
|
|| !TryResolveSharedLaneNodeHandoffContext(rightEdge, leftEdge, nodesById, graphMinY, graphMaxY, out var rightContext)
|
|
|| !string.Equals(leftContext.SharedNode.Id, rightContext.SharedNode.Id, StringComparison.Ordinal)
|
|
|| !string.Equals(leftContext.Side, rightContext.Side, StringComparison.Ordinal)
|
|
|| leftContext.IsOutgoing == rightContext.IsOutgoing)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var baselineConflicts = ElkEdgeRoutingScoring.DetectSharedLaneConflicts(currentEdges, nodes);
|
|
var baselineConflictCount = baselineConflicts.Count;
|
|
var baselineLeftConflictCount = baselineConflicts.Count(conflict =>
|
|
string.Equals(conflict.LeftEdgeId, leftEdge.Id, StringComparison.Ordinal)
|
|
|| string.Equals(conflict.RightEdgeId, leftEdge.Id, StringComparison.Ordinal));
|
|
var baselineRightConflictCount = baselineConflicts.Count(conflict =>
|
|
string.Equals(conflict.LeftEdgeId, rightEdge.Id, StringComparison.Ordinal)
|
|
|| string.Equals(conflict.RightEdgeId, rightEdge.Id, StringComparison.Ordinal));
|
|
var baselineCombinedPathLength = ComputePathLength(leftContext.Path) + ComputePathLength(rightContext.Path);
|
|
|
|
var peerCoordinates = CollectSharedLaneNodeFaceBoundaryCoordinates(
|
|
currentEdges,
|
|
leftContext.SharedNode,
|
|
leftContext.Side,
|
|
graphMinY,
|
|
graphMaxY,
|
|
leftEdge.Id);
|
|
var leftRepairCoordinates = EnumerateSharedLaneBoundaryRepairCoordinates(
|
|
leftContext.SharedNode,
|
|
leftContext.Side,
|
|
leftContext.CurrentBoundaryCoordinate,
|
|
peerCoordinates)
|
|
.ToArray();
|
|
var rightRepairCoordinates = EnumerateSharedLaneBoundaryRepairCoordinates(
|
|
rightContext.SharedNode,
|
|
rightContext.Side,
|
|
rightContext.CurrentBoundaryCoordinate,
|
|
peerCoordinates)
|
|
.ToArray();
|
|
|
|
ElkRoutedEdge? bestLeft = null;
|
|
ElkRoutedEdge? bestRight = null;
|
|
var bestConflictCount = baselineConflictCount;
|
|
var bestLeftConflictCount = baselineLeftConflictCount;
|
|
var bestRightConflictCount = baselineRightConflictCount;
|
|
var bestCombinedPathLength = baselineCombinedPathLength;
|
|
|
|
foreach (var leftCoordinate in leftRepairCoordinates)
|
|
{
|
|
var leftCandidatePath = leftContext.IsOutgoing
|
|
? BuildMixedSourceFaceCandidate(leftContext.Path, leftContext.SharedNode, leftContext.Side, leftCoordinate, leftContext.AxisValue)
|
|
: BuildMixedTargetFaceCandidate(leftContext.Path, leftContext.SharedNode, leftContext.Side, leftCoordinate, leftContext.AxisValue);
|
|
if (!IsValidSharedLaneBoundaryRepairCandidate(
|
|
leftEdge,
|
|
leftContext.Path,
|
|
leftCandidatePath,
|
|
leftContext.SharedNode,
|
|
leftContext.IsOutgoing,
|
|
nodes,
|
|
graphMinY,
|
|
graphMaxY))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (var rightCoordinate in rightRepairCoordinates)
|
|
{
|
|
var rightCandidatePath = rightContext.IsOutgoing
|
|
? BuildMixedSourceFaceCandidate(rightContext.Path, rightContext.SharedNode, rightContext.Side, rightCoordinate, rightContext.AxisValue)
|
|
: BuildMixedTargetFaceCandidate(rightContext.Path, rightContext.SharedNode, rightContext.Side, rightCoordinate, rightContext.AxisValue);
|
|
if (!IsValidSharedLaneBoundaryRepairCandidate(
|
|
rightEdge,
|
|
rightContext.Path,
|
|
rightCandidatePath,
|
|
rightContext.SharedNode,
|
|
rightContext.IsOutgoing,
|
|
nodes,
|
|
graphMinY,
|
|
graphMaxY))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var candidateLeft = BuildSingleSectionEdge(leftEdge, leftCandidatePath);
|
|
var candidateRight = BuildSingleSectionEdge(rightEdge, rightCandidatePath);
|
|
if (ElkEdgeRoutingScoring.DetectSharedLaneConflicts([candidateLeft, candidateRight], nodes).Count > 0
|
|
|| ComputeUnderNodeRepairLocalHardPressure(candidateLeft, nodes) > ComputeUnderNodeRepairLocalHardPressure(leftEdge, nodes)
|
|
|| ComputeUnderNodeRepairLocalHardPressure(candidateRight, nodes) > ComputeUnderNodeRepairLocalHardPressure(rightEdge, nodes))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var candidateEdges = currentEdges.ToArray();
|
|
candidateEdges[leftIndex] = candidateLeft;
|
|
candidateEdges[rightIndex] = candidateRight;
|
|
var candidateConflicts = ElkEdgeRoutingScoring.DetectSharedLaneConflicts(candidateEdges, nodes);
|
|
var candidateConflictCount = candidateConflicts.Count;
|
|
var candidateLeftConflictCount = candidateConflicts.Count(conflict =>
|
|
string.Equals(conflict.LeftEdgeId, leftEdge.Id, StringComparison.Ordinal)
|
|
|| string.Equals(conflict.RightEdgeId, leftEdge.Id, StringComparison.Ordinal));
|
|
var candidateRightConflictCount = candidateConflicts.Count(conflict =>
|
|
string.Equals(conflict.LeftEdgeId, rightEdge.Id, StringComparison.Ordinal)
|
|
|| string.Equals(conflict.RightEdgeId, rightEdge.Id, StringComparison.Ordinal));
|
|
if (candidateConflictCount > bestConflictCount
|
|
|| candidateLeftConflictCount > bestLeftConflictCount
|
|
|| candidateRightConflictCount > bestRightConflictCount)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var candidateCombinedPathLength = ComputePathLength(leftCandidatePath) + ComputePathLength(rightCandidatePath);
|
|
var isBetter =
|
|
candidateConflictCount < bestConflictCount
|
|
|| candidateLeftConflictCount < bestLeftConflictCount
|
|
|| candidateRightConflictCount < bestRightConflictCount
|
|
|| candidateCombinedPathLength + 0.5d < bestCombinedPathLength;
|
|
if (!isBetter)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
bestLeft = candidateLeft;
|
|
bestRight = candidateRight;
|
|
bestConflictCount = candidateConflictCount;
|
|
bestLeftConflictCount = candidateLeftConflictCount;
|
|
bestRightConflictCount = candidateRightConflictCount;
|
|
bestCombinedPathLength = candidateCombinedPathLength;
|
|
}
|
|
}
|
|
|
|
if (bestLeft is null || bestRight is null || bestConflictCount >= baselineConflictCount)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
repairedLeftEdge = bestLeft;
|
|
repairedRightEdge = bestRight;
|
|
return true;
|
|
}
|
|
|
|
private static bool TryResolveSharedLaneByDirectSourceSlotRepair(
|
|
ElkRoutedEdge[] currentEdges,
|
|
int repairIndex,
|
|
ElkRoutedEdge edge,
|
|
ElkRoutedEdge otherEdge,
|
|
ElkPositionedNode[] nodes,
|
|
double minLineClearance,
|
|
double graphMinY,
|
|
double graphMaxY,
|
|
out ElkRoutedEdge repairedEdge)
|
|
{
|
|
repairedEdge = edge;
|
|
if (string.IsNullOrWhiteSpace(edge.SourceNodeId)
|
|
|| !string.Equals(edge.SourceNodeId, otherEdge.SourceNodeId, StringComparison.Ordinal))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
|
|
if (!nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out var sourceNode))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var path = ExtractFullPath(edge);
|
|
var otherPath = ExtractFullPath(otherEdge);
|
|
if (path.Count < 2
|
|
|| otherPath.Count < 2
|
|
|| !ShouldSpreadSourceDeparture(edge, graphMinY, graphMaxY)
|
|
|| !ShouldSpreadSourceDeparture(otherEdge, graphMinY, graphMaxY))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var side = ResolveSourceDepartureSide(path, sourceNode);
|
|
var otherSide = ResolveSourceDepartureSide(otherPath, sourceNode);
|
|
if (!string.Equals(side, otherSide, StringComparison.Ordinal))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var axisValue = TryExtractSourceDepartureRun(path, side, out _, out var runEndIndex)
|
|
? side is "left" or "right"
|
|
? path[runEndIndex].X
|
|
: path[runEndIndex].Y
|
|
: ResolveDefaultSourceDepartureAxis(sourceNode, side);
|
|
var currentBoundaryCoordinate = side is "left" or "right" ? path[0].Y : path[0].X;
|
|
var peerCoordinates = CollectSharedLaneSourceBoundaryCoordinates(
|
|
currentEdges,
|
|
sourceNode,
|
|
side,
|
|
graphMinY,
|
|
graphMaxY,
|
|
edge.Id);
|
|
|
|
foreach (var desiredCoordinate in EnumerateSharedLaneBoundaryRepairCoordinates(
|
|
sourceNode,
|
|
side,
|
|
currentBoundaryCoordinate,
|
|
peerCoordinates))
|
|
{
|
|
var candidatePath = BuildMixedSourceFaceCandidate(path, sourceNode, side, desiredCoordinate, axisValue);
|
|
if (!IsValidSharedLaneBoundaryRepairCandidate(
|
|
edge,
|
|
path,
|
|
candidatePath,
|
|
sourceNode,
|
|
isOutgoing: true,
|
|
nodes,
|
|
graphMinY,
|
|
graphMaxY))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var candidateEdges = currentEdges.ToArray();
|
|
candidateEdges[repairIndex] = BuildSingleSectionEdge(edge, candidatePath);
|
|
if (TryAcceptFocusedSharedLanePairRepair(
|
|
currentEdges,
|
|
candidateEdges,
|
|
repairIndex,
|
|
edge,
|
|
otherEdge,
|
|
nodes,
|
|
graphMinY,
|
|
graphMaxY,
|
|
out repairedEdge))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static bool TryResolveSharedLaneByDirectNodeHandoffSlotRepair(
|
|
ElkRoutedEdge[] currentEdges,
|
|
int repairIndex,
|
|
ElkRoutedEdge edge,
|
|
ElkRoutedEdge otherEdge,
|
|
ElkPositionedNode[] nodes,
|
|
double minLineClearance,
|
|
double graphMinY,
|
|
double graphMaxY,
|
|
out ElkRoutedEdge repairedEdge)
|
|
{
|
|
repairedEdge = edge;
|
|
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
|
|
if (!TryResolveSharedLaneNodeHandoffContext(edge, otherEdge, nodesById, graphMinY, graphMaxY, out var context))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var peerCoordinates = CollectSharedLaneNodeFaceBoundaryCoordinates(
|
|
currentEdges,
|
|
context.SharedNode,
|
|
context.Side,
|
|
graphMinY,
|
|
graphMaxY,
|
|
edge.Id);
|
|
|
|
foreach (var desiredCoordinate in EnumerateSharedLaneBoundaryRepairCoordinates(
|
|
context.SharedNode,
|
|
context.Side,
|
|
context.CurrentBoundaryCoordinate,
|
|
peerCoordinates))
|
|
{
|
|
var candidatePath = context.IsOutgoing
|
|
? BuildMixedSourceFaceCandidate(context.Path, context.SharedNode, context.Side, desiredCoordinate, context.AxisValue)
|
|
: BuildMixedTargetFaceCandidate(context.Path, context.SharedNode, context.Side, desiredCoordinate, context.AxisValue);
|
|
if (!IsValidSharedLaneBoundaryRepairCandidate(
|
|
edge,
|
|
context.Path,
|
|
candidatePath,
|
|
context.SharedNode,
|
|
context.IsOutgoing,
|
|
nodes,
|
|
graphMinY,
|
|
graphMaxY))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var candidateEdges = currentEdges.ToArray();
|
|
candidateEdges[repairIndex] = BuildSingleSectionEdge(edge, candidatePath);
|
|
if (TryAcceptFocusedSharedLanePairRepair(
|
|
currentEdges,
|
|
candidateEdges,
|
|
repairIndex,
|
|
edge,
|
|
otherEdge,
|
|
nodes,
|
|
graphMinY,
|
|
graphMaxY,
|
|
out repairedEdge))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|