Files
git.stella-ops.org/src/__Libraries/StellaOps.ElkSharp/ElkEdgePostProcessor.FaceConflictRepair.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

386 lines
19 KiB
C#

namespace StellaOps.ElkSharp;
internal static partial class ElkEdgePostProcessor
{
internal static ElkRoutedEdge[] RepairBoundaryAnglesAndTargetApproaches(
ElkRoutedEdge[] edges,
ElkPositionedNode[] nodes,
double minLineClearance,
IReadOnlyCollection<string>? restrictedEdgeIds = null)
{
if (edges.Length == 0 || nodes.Length == 0)
{
return edges;
}
var restrictedSet = restrictedEdgeIds is null
? null
: restrictedEdgeIds.ToHashSet(StringComparer.Ordinal);
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
var graphMinY = nodes.Min(node => node.Y);
var graphMaxY = nodes.Max(node => node.Y + node.Height);
var targetSlots = ResolveTargetApproachSlots(edges, nodesById, graphMinY, graphMaxY, minLineClearance, restrictedSet);
var result = new ElkRoutedEdge[edges.Length];
for (var i = 0; i < edges.Length; i++)
{
var edge = edges[i];
if (restrictedSet is not null && !restrictedSet.Contains(edge.Id))
{
result[i] = edge;
continue;
}
var path = ExtractFullPath(edge);
if (path.Count < 2)
{
result[i] = edge;
continue;
}
var normalized = path
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
.ToList();
if (nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out var sourceNode)
&& ElkShapeBoundaries.IsGatewayShape(sourceNode))
{
var preserveSourceExit = ShouldPreserveSourceExitGeometry(edge, graphMinY, graphMaxY);
if (!preserveSourceExit)
{
var gatewaySourceNormalized = NormalizeGatewayExitPath(normalized, sourceNode, nodes, edge.SourceNodeId, edge.TargetNodeId);
if (PathStartsAtDecisionVertex(gatewaySourceNormalized, sourceNode))
{
gatewaySourceNormalized = ForceDecisionSourceExitOffVertex(
gatewaySourceNormalized,
sourceNode,
nodes,
edge.SourceNodeId,
edge.TargetNodeId);
}
if (PathChanged(normalized, gatewaySourceNormalized)
&& HasAcceptableGatewayBoundaryPath(gatewaySourceNormalized, nodes, edge.SourceNodeId, edge.TargetNodeId, sourceNode, fromStart: true))
{
normalized = gatewaySourceNormalized;
}
}
}
else if (nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out sourceNode)
&& !HasValidBoundaryAngle(normalized[0], normalized[1], sourceNode))
{
var sourceSide = ResolvePreferredRectSourceExitSide(normalized, sourceNode);
var sourcePath = normalized
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
.ToList();
sourcePath[0] = BuildRectBoundaryPointForSide(sourceNode, sourceSide, sourcePath[1]);
var sourceNormalized = NormalizeExitPath(sourcePath, sourceNode, sourceSide);
if (HasClearBoundarySegments(sourceNormalized, nodes, edge.SourceNodeId, edge.TargetNodeId, true, 3))
{
normalized = sourceNormalized;
}
}
if (nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out var targetNode))
{
var assignedEndpoint = targetSlots.TryGetValue(edge.Id, out var slot)
? slot
: normalized[^1];
if (ElkShapeBoundaries.IsGatewayShape(targetNode))
{
List<ElkPoint>? preferredGatewayTargetNormalized = null;
if (nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out var gatewaySourceNode)
&& TryBuildPreferredGatewayTargetEntryPath(
normalized,
gatewaySourceNode,
targetNode,
nodes,
edge.SourceNodeId,
edge.TargetNodeId,
out var preferredGatewayTargetRepair))
{
preferredGatewayTargetNormalized = preferredGatewayTargetRepair;
}
var gatewayTargetNormalized = NormalizeGatewayEntryPath(normalized, targetNode, assignedEndpoint);
if (gatewayTargetNormalized.Count >= 2
&& !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, gatewayTargetNormalized[^1], gatewayTargetNormalized[^2]))
{
var projectedBoundary = ElkShapeBoundaries.ProjectOntoShapeBoundary(targetNode, gatewayTargetNormalized[^2]);
projectedBoundary = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(targetNode, projectedBoundary, gatewayTargetNormalized[^2]);
var projectedGatewayTargetNormalized = NormalizeGatewayEntryPath(normalized, targetNode, projectedBoundary);
if (PathChanged(gatewayTargetNormalized, projectedGatewayTargetNormalized)
&& HasAcceptableGatewayBoundaryPath(projectedGatewayTargetNormalized, nodes, edge.SourceNodeId, edge.TargetNodeId, targetNode, fromStart: false))
{
gatewayTargetNormalized = projectedGatewayTargetNormalized;
}
}
if (preferredGatewayTargetNormalized is not null
&& (gatewayTargetNormalized.Count < 2
|| NeedsGatewayTargetBoundaryRepair(gatewayTargetNormalized, targetNode)
|| !string.Equals(
ElkEdgeRoutingGeometry.ResolveBoundarySide(gatewayTargetNormalized[^1], targetNode),
ElkEdgeRoutingGeometry.ResolveBoundarySide(preferredGatewayTargetNormalized[^1], targetNode),
StringComparison.Ordinal)
|| ComputePathLength(preferredGatewayTargetNormalized) + 4d < ComputePathLength(gatewayTargetNormalized)
|| HasTargetApproachBacktracking(gatewayTargetNormalized, targetNode)))
{
gatewayTargetNormalized = preferredGatewayTargetNormalized;
}
if (PathChanged(normalized, gatewayTargetNormalized)
&& HasAcceptableGatewayBoundaryPath(gatewayTargetNormalized, nodes, edge.SourceNodeId, edge.TargetNodeId, targetNode, fromStart: false))
{
normalized = gatewayTargetNormalized;
}
}
else
{
if (nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out var shortcutSourceNode)
&& TryApplyPreferredBoundaryShortcut(
normalized,
shortcutSourceNode,
targetNode,
nodes,
edge.SourceNodeId,
edge.TargetNodeId,
requireUnderNodeImprovement: false,
minLineClearance,
out var preferredShortcut))
{
normalized = preferredShortcut;
}
var targetSide = ElkEdgeRoutingGeometry.ResolveBoundarySide(assignedEndpoint, targetNode);
if (IsOnWrongSideOfTarget(normalized[^2], targetNode, targetSide, 0.5d)
&& TryResolveNonGatewayBacktrackingEndpoint(normalized, targetNode, out var correctedSide, out var correctedBoundary))
{
targetSide = correctedSide;
assignedEndpoint = correctedBoundary;
}
if (HasTargetApproachBacktracking(normalized, targetNode)
&& TryResolveNonGatewayBacktrackingEndpoint(normalized, targetNode, out var preferredSide, out var preferredBoundary))
{
targetSide = preferredSide;
assignedEndpoint = preferredBoundary;
}
if (!HasValidBoundaryAngle(normalized[^1], normalized[^2], targetNode))
{
var alignedAssignedSideEntry = NormalizeEntryPath(normalized, targetNode, targetSide, assignedEndpoint);
if (HasClearBoundarySegments(alignedAssignedSideEntry, nodes, edge.SourceNodeId, edge.TargetNodeId, false, 3)
&& HasValidBoundaryAngle(alignedAssignedSideEntry[^1], alignedAssignedSideEntry[^2], targetNode))
{
normalized = alignedAssignedSideEntry;
}
else
{
var preferredEntrySide = ResolvePreferredRectTargetEntrySide(normalized, targetNode);
if (!string.Equals(preferredEntrySide, targetSide, StringComparison.Ordinal))
{
targetSide = preferredEntrySide;
assignedEndpoint = BuildRectBoundaryPointForSide(targetNode, targetSide, normalized[^2]);
}
}
}
if (!ElkEdgeRoutingGeometry.PointsEqual(assignedEndpoint, normalized[^1])
|| !HasValidBoundaryAngle(normalized[^1], normalized[^2], targetNode))
{
var targetNormalized = NormalizeEntryPath(normalized, targetNode, targetSide, assignedEndpoint);
if (HasClearBoundarySegments(targetNormalized, nodes, edge.SourceNodeId, edge.TargetNodeId, false, 3))
{
normalized = targetNormalized;
}
}
var shortenedApproach = TrimTargetApproachBacktracking(normalized, targetNode, targetSide, assignedEndpoint);
if (PathChanged(normalized, shortenedApproach)
&& HasClearBoundarySegments(shortenedApproach, nodes, edge.SourceNodeId, edge.TargetNodeId, false, 3)
&& HasValidBoundaryAngle(shortenedApproach[^1], shortenedApproach[^2], targetNode))
{
normalized = shortenedApproach;
}
if (HasTargetApproachBacktracking(normalized, targetNode)
&& TryNormalizeNonGatewayBacktrackingEntry(normalized, targetNode, out var backtrackingRepair)
&& HasClearBoundarySegments(backtrackingRepair, nodes, edge.SourceNodeId, edge.TargetNodeId, false, 3)
&& HasValidBoundaryAngle(backtrackingRepair[^1], backtrackingRepair[^2], targetNode))
{
normalized = backtrackingRepair;
}
}
}
if (normalized.Count == path.Count
&& normalized.Zip(path, ElkEdgeRoutingGeometry.PointsEqual).All(equal => equal))
{
result[i] = edge;
continue;
}
result[i] = BuildSingleSectionEdge(edge, normalized);
}
return result;
}
internal static ElkRoutedEdge[] ElevateUnderNodeViolations(
ElkRoutedEdge[] edges,
ElkPositionedNode[] nodes,
double minLineClearance,
IReadOnlyCollection<string>? restrictedEdgeIds = null)
{
if (edges.Length == 0 || nodes.Length == 0)
{
return edges;
}
var restrictedSet = restrictedEdgeIds is null
? null
: restrictedEdgeIds.ToHashSet(StringComparer.Ordinal);
var result = edges.ToArray();
for (var i = 0; i < result.Length; i++)
{
var edge = result[i];
if (restrictedSet is not null && !restrictedSet.Contains(edge.Id))
{
continue;
}
var path = ExtractFullPath(edge);
if (path.Count < 2)
{
continue;
}
if (TryResolveUnderNodeWithPreferredShortcut(
edge,
path,
nodes,
minLineClearance,
out var directRepair))
{
var currentLocalHardPressure = ComputeUnderNodeRepairLocalHardPressure(edge, nodes);
var repairedEdge = BuildSingleSectionEdge(edge, directRepair);
repairedEdge = ResolveUnderNodePeerTargetConflicts(
repairedEdge,
result,
i,
nodes,
minLineClearance);
var repairedPath = ExtractFullPath(repairedEdge);
var repairedUnderNodeSegments = CountUnderNodeSegments(repairedPath, nodes, edge.SourceNodeId, edge.TargetNodeId, minLineClearance);
var currentUnderNodeSegments = CountUnderNodeSegments(path, nodes, edge.SourceNodeId, edge.TargetNodeId, minLineClearance);
var repairedCrossesNode = HasNodeObstacleCrossing(repairedPath, nodes, edge.SourceNodeId, edge.TargetNodeId);
var repairedLocalHardPressure = ComputeUnderNodeRepairLocalHardPressure(repairedEdge, nodes);
WriteUnderNodeDebug(
edge.Id,
$"accept-check raw current={currentUnderNodeSegments} repaired={repairedUnderNodeSegments} crossing={repairedCrossesNode} local={repairedLocalHardPressure}/{currentLocalHardPressure} repaired={FormatPath(repairedPath)}");
if (repairedUnderNodeSegments < currentUnderNodeSegments
&& !repairedCrossesNode
&& repairedLocalHardPressure <= currentLocalHardPressure)
{
WriteUnderNodeDebug(edge.Id, "accept-check raw accepted");
result[i] = repairedUnderNodeSegments == 0
? ProtectUnderNodeGeometry(repairedEdge)
: repairedEdge;
continue;
}
repairedEdge = RepairBoundaryAnglesAndTargetApproaches(
[repairedEdge],
nodes,
minLineClearance)[0];
repairedEdge = FinalizeGatewayBoundaryGeometry([repairedEdge], nodes)[0];
repairedEdge = NormalizeBoundaryAngles([repairedEdge], nodes)[0];
repairedEdge = NormalizeSourceExitAngles([repairedEdge], nodes)[0];
repairedEdge = ResolveUnderNodePeerTargetConflicts(
repairedEdge,
result,
i,
nodes,
minLineClearance);
repairedPath = ExtractFullPath(repairedEdge);
repairedUnderNodeSegments = CountUnderNodeSegments(repairedPath, nodes, edge.SourceNodeId, edge.TargetNodeId, minLineClearance);
repairedCrossesNode = HasNodeObstacleCrossing(repairedPath, nodes, edge.SourceNodeId, edge.TargetNodeId);
repairedLocalHardPressure = ComputeUnderNodeRepairLocalHardPressure(repairedEdge, nodes);
WriteUnderNodeDebug(
edge.Id,
$"accept-check normalized current={currentUnderNodeSegments} repaired={repairedUnderNodeSegments} crossing={repairedCrossesNode} local={repairedLocalHardPressure}/{currentLocalHardPressure} repaired={FormatPath(repairedPath)}");
if (repairedUnderNodeSegments < currentUnderNodeSegments
&& !repairedCrossesNode
&& repairedLocalHardPressure <= currentLocalHardPressure)
{
WriteUnderNodeDebug(edge.Id, "accept-check normalized accepted");
result[i] = repairedUnderNodeSegments == 0
? ProtectUnderNodeGeometry(repairedEdge)
: repairedEdge;
}
continue;
}
var lifted = TryLiftUnderNodeSegments(
path,
nodes,
edge.SourceNodeId,
edge.TargetNodeId,
minLineClearance);
if (!PathChanged(path, lifted))
{
continue;
}
var liftedEdge = BuildSingleSectionEdge(edge, lifted);
liftedEdge = NormalizeBoundaryAngles([liftedEdge], nodes)[0];
liftedEdge = NormalizeSourceExitAngles([liftedEdge], nodes)[0];
var liftedPath = ExtractFullPath(liftedEdge);
if (CountUnderNodeSegments(liftedPath, nodes, edge.SourceNodeId, edge.TargetNodeId, minLineClearance)
< CountUnderNodeSegments(path, nodes, edge.SourceNodeId, edge.TargetNodeId, minLineClearance)
&& !HasNodeObstacleCrossing(liftedPath, nodes, edge.SourceNodeId, edge.TargetNodeId))
{
result[i] = CountUnderNodeSegments(liftedPath, nodes, edge.SourceNodeId, edge.TargetNodeId, minLineClearance) == 0
? ProtectUnderNodeGeometry(liftedEdge)
: liftedEdge;
}
}
return result;
}
internal static ElkRoutedEdge[] PolishTargetPeerConflicts(
ElkRoutedEdge[] edges,
ElkPositionedNode[] nodes,
double minLineClearance,
IReadOnlyCollection<string>? restrictedEdgeIds = null)
{
if (edges.Length < 2 || nodes.Length == 0)
{
return edges;
}
var restrictedSet = restrictedEdgeIds is null
? null
: restrictedEdgeIds.ToHashSet(StringComparer.Ordinal);
var result = edges.ToArray();
for (var i = 0; i < result.Length; i++)
{
if (restrictedSet is not null && !restrictedSet.Contains(result[i].Id))
{
continue;
}
result[i] = ResolveUnderNodePeerTargetConflicts(
result[i],
result,
i,
nodes,
minLineClearance);
}
return result;
}
}