Files
git.stella-ops.org/src/__Libraries/StellaOps.ElkSharp/ElkEdgePostProcessor.FaceConflictRepair.SharedLaneSlotRepair.cs
master d04483560b Complete ElkSharp document rendering cleanup and source decomposition
- 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>
2026-04-01 14:16:10 +03:00

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