Загрузка данных


    public sealed class BundleRouter
    {
        private const float TurnEpsilon = 0.0005f;

        private readonly struct LaneSortHint
        {
            public LaneSortHint(int side, float turnDistance, float barycenter)
            {
                Side = side;
                TurnDistance = turnDistance;
                Barycenter = barycenter;
            }

            public int Side { get; }
            public float TurnDistance { get; }
            public float Barycenter { get; }
        }

        private readonly struct FlowHintEntry
        {
            public FlowHintEntry(FlowRequest flow, LaneSortHint hint)
            {
                Flow = flow;
                Hint = hint;
            }

            public FlowRequest Flow { get; }
            public LaneSortHint Hint { get; }
        }

        private sealed class FlowRouteLookup
        {
            public GlobalRoute Route;
            public Dictionary<int, int> EdgeToIndex;
        }

        private readonly struct LaneSmoothEntry
        {
            public LaneSmoothEntry(FlowRequest flow, float preferredOrder, int currentOrder)
            {
                Flow = flow;
                PreferredOrder = preferredOrder;
                CurrentOrder = currentOrder;
            }

            public FlowRequest Flow { get; }
            public float PreferredOrder { get; }
            public int CurrentOrder { get; }
        }

        public BundleRoutingResult Build(CorridorGraph graph, IReadOnlyList<GlobalRoute> globalRoutes,
            RoutingConfig config)
        {
            BundleRoutingResult result = new BundleRoutingResult();
            if (graph == null || globalRoutes == null || globalRoutes.Count == 0)
            {
                return result;
            }

            float lanePitch = config != null ? config.lanePitch : 0.25f;
            result.UsedTurnAwareLaneOrdering = config != null && config.useTurnAwareLaneOrdering;
            result.RequestedLaneAlignmentPasses = config != null ? Mathf.Max(0, config.laneAlignmentPasses) : 2;
            Dictionary<FlowRequest, FlowRouteLookup> flowLookup = BuildFlowRouteLookup(globalRoutes);
            Dictionary<int, List<FlowRequest>> edgeToFlows = BuildEdgeToFlowMap(globalRoutes);
            foreach (KeyValuePair<int, List<FlowRequest>> pair in edgeToFlows)
            {
                int edgeId = pair.Key;
                List<FlowRequest> flows = pair.Value;
                if (flows.Count == 0)
                {
                    continue;
                }

                CorridorEdge edge = graph.Edges[edgeId];
                if (result.UsedTurnAwareLaneOrdering)
                {
                    SortFlowsForEdge(flows, edgeId, edge, graph, flowLookup);
                }
                else
                {
                    flows.Sort((left, right) =>
                    {
                        float leftKey = ComputeBarycenterKey(left, edge);
                        float rightKey = ComputeBarycenterKey(right, edge);
                        int compare = leftKey.CompareTo(rightKey);
                        if (compare != 0)
                        {
                            return compare;
                        }

                        string leftId = left != null && !string.IsNullOrWhiteSpace(left.Id) ? left.Id : "flow";
                        string rightId = right != null && !string.IsNullOrWhiteSpace(right.Id) ? right.Id : "flow";
                        return string.CompareOrdinal(leftId, rightId);
                    });
                }

                TrunkSegment trunk = new TrunkSegment { EdgeId = edgeId };
                AssignLaneOffsets(trunk, flows, lanePitch);
                result.TrunksByEdge[edgeId] = trunk;
            }

            // One lightweight pass aligns lane order across neighboring edges and removes obvious branch crossings.
            int alignmentPasses = result.RequestedLaneAlignmentPasses;
            for (int pass = 0; pass < alignmentPasses; pass++)
            {
                int changesBefore = result.LaneAlignmentChangeCount;
                AlignNeighboringEdges(globalRoutes, result, lanePitch, forward: true);
                AlignNeighboringEdges(globalRoutes, result, lanePitch, forward: false);
                SmoothEdgeOrders(result, flowLookup, lanePitch);
                result.AppliedLaneAlignmentPasses++;
                if (result.LaneAlignmentChangeCount == changesBefore)
                {
                    break;
                }
            }

            return result;
        }

        private static Dictionary<FlowRequest, FlowRouteLookup> BuildFlowRouteLookup(IReadOnlyList<GlobalRoute> routes)
        {
            Dictionary<FlowRequest, FlowRouteLookup>
                lookup = new Dictionary<FlowRequest, FlowRouteLookup>(routes.Count);
            for (int i = 0; i < routes.Count; i++)
            {
                GlobalRoute route = routes[i];
                if (route?.Flow == null)
                {
                    continue;
                }

                Dictionary<int, int> edgeToIndex = new Dictionary<int, int>(route.EdgeIds.Count);
                for (int edgeIndex = 0; edgeIndex < route.EdgeIds.Count; edgeIndex++)
                {
                    int edgeId = route.EdgeIds[edgeIndex];
                    if (!edgeToIndex.ContainsKey(edgeId))
                    {
                        edgeToIndex[edgeId] = edgeIndex;
                    }
                }

                lookup[route.Flow] = new FlowRouteLookup
                {
                    Route = route,
                    EdgeToIndex = edgeToIndex
                };
            }

            return lookup;
        }

        private static Dictionary<int, List<FlowRequest>> BuildEdgeToFlowMap(IReadOnlyList<GlobalRoute> routes)
        {
            Dictionary<int, List<FlowRequest>> map = new Dictionary<int, List<FlowRequest>>();
            for (int i = 0; i < routes.Count; i++)
            {
                GlobalRoute route = routes[i];
                for (int edgeIndex = 0; edgeIndex < route.EdgeIds.Count; edgeIndex++)
                {
                    int edgeId = route.EdgeIds[edgeIndex];
                    if (!map.TryGetValue(edgeId, out List<FlowRequest> flows))
                    {
                        flows = new List<FlowRequest>();
                        map[edgeId] = flows;
                    }

                    if (!flows.Contains(route.Flow))
                    {
                        flows.Add(route.Flow);
                    }
                }
            }

            return map;
        }

        private static void SortFlowsForEdge(
            List<FlowRequest> flows,
            int edgeId,
            CorridorEdge edge,
            CorridorGraph graph,
            Dictionary<FlowRequest, FlowRouteLookup> flowLookup)
        {
            List<FlowHintEntry> entries = new List<FlowHintEntry>(flows.Count);
            for (int i = 0; i < flows.Count; i++)
            {
                FlowRequest flow = flows[i];
                LaneSortHint hint = BuildLaneSortHint(flow, edgeId, edge, graph, flowLookup);
                entries.Add(new FlowHintEntry(flow, hint));
            }

            entries.Sort(CompareFlowHintEntries);
            for (int i = 0; i < entries.Count; i++)
            {
                flows[i] = entries[i].Flow;
            }
        }

        private static LaneSortHint BuildLaneSortHint(
            FlowRequest flow,
            int edgeId,
            CorridorEdge edge,
            CorridorGraph graph,
            Dictionary<FlowRequest, FlowRouteLookup> flowLookup)
        {
            float barycenter = ComputeBarycenterKey(flow, edge);
            if (flow == null ||
                !flowLookup.TryGetValue(flow, out FlowRouteLookup lookup) ||
                lookup?.Route == null ||
                lookup.EdgeToIndex == null ||
                !lookup.EdgeToIndex.TryGetValue(edgeId, out int edgeIndex))
            {
                return new LaneSortHint(0, float.PositiveInfinity, barycenter);
            }

            int side = FindFirstSideTurn(lookup.Route, edgeIndex, edge.Axis, graph, out float turnDistance);
            return new LaneSortHint(side, turnDistance, barycenter);
        }

        private static int CompareFlowHintEntries(FlowHintEntry left, FlowHintEntry right)
        {
            int compare = left.Hint.Side.CompareTo(right.Hint.Side);
            if (compare != 0)
            {
                return compare;
            }

            if (left.Hint.Side < 0)
            {
                compare = left.Hint.TurnDistance.CompareTo(right.Hint.TurnDistance);
            }
            else if (left.Hint.Side > 0)
            {
                compare = right.Hint.TurnDistance.CompareTo(left.Hint.TurnDistance);
            }
            else
            {
                compare = left.Hint.Barycenter.CompareTo(right.Hint.Barycenter);
            }

            if (compare != 0)
            {
                return compare;
            }

            compare = left.Hint.Barycenter.CompareTo(right.Hint.Barycenter);
            if (compare != 0)
            {
                return compare;
            }

            string leftId = left.Flow != null && !string.IsNullOrWhiteSpace(left.Flow.Id) ? left.Flow.Id : "flow";
            string rightId = right.Flow != null && !string.IsNullOrWhiteSpace(right.Flow.Id) ? right.Flow.Id : "flow";
            return string.CompareOrdinal(leftId, rightId);
        }

        private static int FindFirstSideTurn(
            GlobalRoute route,
            int edgeIndex,
            EdgeAxis baseAxis,
            CorridorGraph graph,
            out float distanceToTurn)
        {
            distanceToTurn = float.PositiveInfinity;
            if (route == null || graph == null || edgeIndex < 0 || edgeIndex >= route.EdgeIds.Count ||
                route.NodeIds.Count <= edgeIndex + 1)
            {
                return 0;
            }

            float traveled = 0f;
            for (int i = edgeIndex; i < route.EdgeIds.Count; i++)
            {
                int currentEdgeId = route.EdgeIds[i];
                CorridorEdge currentEdge = graph.Edges[currentEdgeId];
                if (currentEdge.Axis != baseAxis)
                {
                    if (route.NodeIds.Count <= i + 1)
                    {
                        return 0;
                    }

                    Vector3 currentDirection = graph.Nodes[route.NodeIds[i + 1]] - graph.Nodes[route.NodeIds[i]];
                    float lateral = GetLateralComponent(currentDirection, baseAxis);
                    if (Mathf.Abs(lateral) <= TurnEpsilon)
                    {
                        return 0;
                    }

                    distanceToTurn = traveled;
                    return lateral > 0f ? 1 : -1;
                }

                traveled += currentEdge.Length;
            }

            return 0;
        }

        private static float GetLateralComponent(Vector3 direction, EdgeAxis axis)
        {
            return axis switch
            {
                EdgeAxis.X => direction.z,
                EdgeAxis.Y => direction.x,
                _ => direction.x
            };
        }

        private static float ComputeBarycenterKey(FlowRequest flow, CorridorEdge edge)
        {
            if (flow == null || !flow.TryResolve(out SlotPose source, out SlotPose target))
            {
                return 0f;
            }

            float x = (source.Position.x + target.Position.x) * 0.5f;
            float y = (source.Position.y + target.Position.y) * 0.5f;
            float z = (source.Position.z + target.Position.z) * 0.5f;

            return edge.Axis switch
            {
                EdgeAxis.X => y * 10f + z,
                EdgeAxis.Y => x * 10f + z,
                _ => x * 10f + y
            };
        }

        private static void AlignNeighboringEdges(
            IReadOnlyList<GlobalRoute> globalRoutes,
            BundleRoutingResult result,
            float lanePitch,
            bool forward)
        {
            for (int i = 0; i < globalRoutes.Count; i++)
            {
                GlobalRoute route = globalRoutes[i];
                if (route == null || route.EdgeIds.Count < 2)
                {
                    continue;
                }

                if (forward)
                {
                    for (int edgeIndex = 1; edgeIndex < route.EdgeIds.Count; edgeIndex++)
                    {
                        int previousEdge = route.EdgeIds[edgeIndex - 1];
                        int currentEdge = route.EdgeIds[edgeIndex];
                        if (!result.TrunksByEdge.TryGetValue(previousEdge, out TrunkSegment previousTrunk) ||
                            !result.TrunksByEdge.TryGetValue(currentEdge, out TrunkSegment currentTrunk))
                        {
                            continue;
                        }

                        if (AlignOrders(previousTrunk, currentTrunk, lanePitch))
                        {
                            result.LaneAlignmentChangeCount++;
                        }
                    }
                }
                else
                {
                    for (int edgeIndex = route.EdgeIds.Count - 2; edgeIndex >= 0; edgeIndex--)
                    {
                        int currentEdge = route.EdgeIds[edgeIndex];
                        int nextEdge = route.EdgeIds[edgeIndex + 1];
                        if (!result.TrunksByEdge.TryGetValue(nextEdge, out TrunkSegment nextTrunk) ||
                            !result.TrunksByEdge.TryGetValue(currentEdge, out TrunkSegment currentTrunk))
                        {
                            continue;
                        }

                        if (AlignOrders(nextTrunk, currentTrunk, lanePitch))
                        {
                            result.LaneAlignmentChangeCount++;
                        }
                    }
                }
            }
        }

        private static void SmoothEdgeOrders(
            BundleRoutingResult result,
            Dictionary<FlowRequest, FlowRouteLookup> flowLookup,
            float lanePitch)
        {
            foreach (KeyValuePair<int, TrunkSegment> pair in result.TrunksByEdge)
            {
                int edgeId = pair.Key;
                TrunkSegment trunk = pair.Value;
                if (trunk == null || trunk.Lanes.Count <= 1)
                {
                    continue;
                }

                List<LaneSmoothEntry> entries = new List<LaneSmoothEntry>(trunk.Lanes.Count);
                for (int i = 0; i < trunk.Lanes.Count; i++)
                {
                    TrunkLane lane = trunk.Lanes[i];
                    float preferred = ComputePreferredOrder(edgeId, lane.Flow, lane.Order, result, flowLookup);
                    entries.Add(new LaneSmoothEntry(lane.Flow, preferred, lane.Order));
                }

                entries.Sort((left, right) =>
                {
                    int compare = left.PreferredOrder.CompareTo(right.PreferredOrder);
                    if (compare != 0)
                    {
                        return compare;
                    }

                    compare = left.CurrentOrder.CompareTo(right.CurrentOrder);
                    if (compare != 0)
                    {
                        return compare;
                    }

                    string leftId = left.Flow != null && !string.IsNullOrWhiteSpace(left.Flow.Id)
                        ? left.Flow.Id
                        : "flow";
                    string rightId = right.Flow != null && !string.IsNullOrWhiteSpace(right.Flow.Id)
                        ? right.Flow.Id
                        : "flow";
                    return string.CompareOrdinal(leftId, rightId);
                });

                bool changed = false;
                for (int i = 0; i < entries.Count; i++)
                {
                    if (trunk.Lanes[i].Flow != entries[i].Flow)
                    {
                        changed = true;
                        break;
                    }
                }

                if (!changed)
                {
                    continue;
                }

                result.LaneAlignmentChangeCount++;
                trunk.Lanes.Clear();
                int laneCount = entries.Count;
                for (int i = 0; i < entries.Count; i++)
                {
                    trunk.Lanes.Add(new TrunkLane
                    {
                        Flow = entries[i].Flow,
                        Order = i,
                        Offset = (i - (laneCount - 1) * 0.5f) * lanePitch
                    });
                }
            }
        }

        private static float ComputePreferredOrder(
            int edgeId,
            FlowRequest flow,
            int currentOrder,
            BundleRoutingResult result,
            Dictionary<FlowRequest, FlowRouteLookup> flowLookup)
        {
            if (flow == null ||
                !flowLookup.TryGetValue(flow, out FlowRouteLookup lookup) ||
                lookup?.Route == null ||
                lookup.EdgeToIndex == null ||
                !lookup.EdgeToIndex.TryGetValue(edgeId, out int edgeIndex))
            {
                return currentOrder;
            }

            float sum = currentOrder;
            int weight = 1;

            if (edgeIndex > 0)
            {
                int previousEdge = lookup.Route.EdgeIds[edgeIndex - 1];
                if (result.TrunksByEdge.TryGetValue(previousEdge, out TrunkSegment previousTrunk) &&
                    TryGetLaneOrder(previousTrunk, flow, out int previousOrder))
                {
                    sum += previousOrder;
                    weight++;
                }
            }

            if (edgeIndex + 1 < lookup.Route.EdgeIds.Count)
            {
                int nextEdge = lookup.Route.EdgeIds[edgeIndex + 1];
                if (result.TrunksByEdge.TryGetValue(nextEdge, out TrunkSegment nextTrunk) &&
                    TryGetLaneOrder(nextTrunk, flow, out int nextOrder))
                {
                    sum += nextOrder;
                    weight++;
                }
            }

            return sum / Mathf.Max(1, weight);
        }

        private static bool TryGetLaneOrder(TrunkSegment trunk, FlowRequest flow, out int order)
        {
            order = 0;
            if (trunk == null || flow == null)
            {
                return false;
            }

            for (int i = 0; i < trunk.Lanes.Count; i++)
            {
                if (trunk.Lanes[i].Flow == flow)
                {
                    order = trunk.Lanes[i].Order;
                    return true;
                }
            }

            return false;
        }

        private static void AssignLaneOffsets(TrunkSegment trunk, IReadOnlyList<FlowRequest> flows, float lanePitch)
        {
            trunk.Lanes.Clear();
            int n = flows.Count;
            for (int i = 0; i < flows.Count; i++)
            {
                float offset = (i - (n - 1) * 0.5f) * lanePitch;
                trunk.Lanes.Add(new TrunkLane
                {
                    Flow = flows[i],
                    Order = i,
                    Offset = offset
                });
            }
        }

        private static bool AlignOrders(TrunkSegment reference, TrunkSegment target, float lanePitch)
        {
            Dictionary<FlowRequest, int> referenceOrder = new Dictionary<FlowRequest, int>();
            for (int i = 0; i < reference.Lanes.Count; i++)
            {
                referenceOrder[reference.Lanes[i].Flow] = reference.Lanes[i].Order;
            }

            List<TrunkLane> shared = new List<TrunkLane>();
            for (int i = 0; i < target.Lanes.Count; i++)
            {
                if (referenceOrder.ContainsKey(target.Lanes[i].Flow))
                {
                    shared.Add(target.Lanes[i]);
                }
            }

            if (shared.Count <= 1)
            {
                return false;
            }

            shared.Sort((left, right) =>
            {
                int leftOrder = referenceOrder[left.Flow];
                int rightOrder = referenceOrder[right.Flow];
                return leftOrder.CompareTo(rightOrder);
            });

            bool changed = false;
            int sharedCursor = 0;
            for (int i = 0; i < target.Lanes.Count; i++)
            {
                if (!referenceOrder.ContainsKey(target.Lanes[i].Flow))
                {
                    continue;
                }

                TrunkLane nextLane = shared[sharedCursor++];
                if (target.Lanes[i].Flow != nextLane.Flow)
                {
                    changed = true;
                }

                target.Lanes[i] = nextLane;
            }

            if (!changed)
            {
                return false;
            }

            int laneCount = target.Lanes.Count;
            for (int i = 0; i < laneCount; i++)
            {
                TrunkLane lane = target.Lanes[i];
                lane.Order = i;
                lane.Offset = (i - (laneCount - 1) * 0.5f) * lanePitch;
                target.Lanes[i] = lane;
            }

            return true;
        }
    }

    public sealed class CorridorGraphBuilder
    {
        private const float Epsilon = 0.0001f;

        public CorridorGraph Build(
            IReadOnlyList<ModuleNode> modules,
            IReadOnlyList<FlowRequest> flowRequests,
            RoutingConfig config,
            out List<Bounds> inflatedObstacles)
        {
            inflatedObstacles = BuildInflatedObstacles(modules, config.clearance);
            List<Vector3> slotAnchors = CollectSlotAnchors(flowRequests);
            Bounds workArea = ComputeWorkArea(modules, slotAnchors, config.workAreaPadding);

            SortedSet<float> xs = new SortedSet<float>();
            SortedSet<float> ys = new SortedSet<float>();
            SortedSet<float> zs = new SortedSet<float>();
            CollectGuideCoordinates(workArea, inflatedObstacles, slotAnchors, xs, ys, zs);
            List<float> xGuides = ReduceGuideCoordinates(xs, config.maxGuideCoordsPerAxis, config.lanePitch * 0.5f);
            List<float> yGuides = ReduceGuideCoordinates(ys, config.maxGuideCoordsPerAxis, config.lanePitch * 0.5f);
            List<float> zGuides = ReduceGuideCoordinates(zs, config.maxGuideCoordsPerAxis, config.lanePitch * 0.5f);
            long buildBudgetTicks = (long)(Mathf.Max(0.1f, config.maxGraphBuildTimeSeconds) * Stopwatch.Frequency);
            long buildDeadline = Stopwatch.GetTimestamp() + buildBudgetTicks;

            CorridorGraph graph = new CorridorGraph { WorkArea = workArea };
            BuildNodes(graph, xGuides, yGuides, zGuides, inflatedObstacles, config, buildDeadline);
            BuildEdges(graph, xGuides, yGuides, zGuides, inflatedObstacles, config, buildDeadline);
            return graph;
        }

        private static List<Bounds> BuildInflatedObstacles(IReadOnlyList<ModuleNode> modules, float clearance)
        {
            List<Bounds> obstacles = new List<Bounds>(modules.Count);
            for (int i = 0; i < modules.Count; i++)
            {
                if (modules[i] == null)
                {
                    continue;
                }

                obstacles.Add(modules[i].GetInflatedBounds(clearance));
            }

            return obstacles;
        }

        private static List<Vector3> CollectSlotAnchors(IReadOnlyList<FlowRequest> flowRequests)
        {
            List<Vector3> anchors = new List<Vector3>(Mathf.Max(4, flowRequests.Count * 2));
            for (int i = 0; i < flowRequests.Count; i++)
            {
                if (flowRequests[i] != null && flowRequests[i].TryResolve(out SlotPose src, out SlotPose dst))
                {
                    anchors.Add(src.Position);
                    anchors.Add(dst.Position);
                }
            }

            return anchors;
        }

        private static Bounds ComputeWorkArea(IReadOnlyList<ModuleNode> modules, IReadOnlyList<Vector3> anchors, float padding)
        {
            bool hasValue = false;
            Vector3 min = Vector3.zero;
            Vector3 max = Vector3.zero;

            for (int i = 0; i < modules.Count; i++)
            {
                ModuleNode module = modules[i];
                if (module == null)
                {
                    continue;
                }

                IncludeBounds(ref hasValue, ref min, ref max, module.GetWorldBounds());
            }

            for (int i = 0; i < anchors.Count; i++)
            {
                IncludePoint(ref hasValue, ref min, ref max, anchors[i]);
            }

            if (!hasValue)
            {
                return new Bounds(Vector3.zero, Vector3.one * 20f);
            }

            Vector3 size = max - min;
            Vector3 center = (min + max) * 0.5f;
            Bounds bounds = new Bounds(center, size);
            bounds.Expand(Mathf.Max(0f, padding) * 2f);
            return bounds;
        }

        private static void IncludeBounds(ref bool hasValue, ref Vector3 min, ref Vector3 max, Bounds bounds)
        {
            IncludePoint(ref hasValue, ref min, ref max, bounds.min);
            IncludePoint(ref hasValue, ref min, ref max, bounds.max);
        }

        private static void IncludePoint(ref bool hasValue, ref Vector3 min, ref Vector3 max, Vector3 point)
        {
            if (!hasValue)
            {
                hasValue = true;
                min = point;
                max = point;
                return;
            }

            min = Vector3.Min(min, point);
            max = Vector3.Max(max, point);
        }

        private static void CollectGuideCoordinates(
            Bounds workArea,
            IReadOnlyList<Bounds> inflatedObstacles,
            IReadOnlyList<Vector3> slotAnchors,
            SortedSet<float> xs,
            SortedSet<float> ys,
            SortedSet<float> zs)
        {
            xs.Add(Quantize(workArea.min.x));
            xs.Add(Quantize(workArea.max.x));
            ys.Add(Quantize(workArea.min.y));
            ys.Add(Quantize(workArea.max.y));
            zs.Add(Quantize(workArea.min.z));
            zs.Add(Quantize(workArea.max.z));

            for (int i = 0; i < inflatedObstacles.Count; i++)
            {
                Bounds obstacle = inflatedObstacles[i];
                xs.Add(Quantize(obstacle.min.x));
                xs.Add(Quantize(obstacle.max.x));
                ys.Add(Quantize(obstacle.min.y));
                ys.Add(Quantize(obstacle.max.y));
                zs.Add(Quantize(obstacle.min.z));
                zs.Add(Quantize(obstacle.max.z));
            }

            for (int i = 0; i < slotAnchors.Count; i++)
            {
                Vector3 anchor = slotAnchors[i];
                xs.Add(Quantize(anchor.x));
                ys.Add(Quantize(anchor.y));
                zs.Add(Quantize(anchor.z));
            }
        }

        private static void BuildNodes(
            CorridorGraph graph,
            IReadOnlyList<float> xs,
            IReadOnlyList<float> ys,
            IReadOnlyList<float> zs,
            IReadOnlyList<Bounds> obstacles,
            RoutingConfig config,
            long buildDeadlineTicks)
        {
            for (int xIndex = 0; xIndex < xs.Count; xIndex++)
            {
                if (Stopwatch.GetTimestamp() >= buildDeadlineTicks)
                {
                    graph.TruncatedByLimits = true;
                    return;
                }

                float x = xs[xIndex];
                for (int yIndex = 0; yIndex < ys.Count; yIndex++)
                {
                    float y = ys[yIndex];
                    for (int zIndex = 0; zIndex < zs.Count; zIndex++)
                    {
                        float z = zs[zIndex];
                        Vector3 point = new Vector3(x, y, z);
                        if (IsInsideAnyObstacle(point, obstacles))
                        {
                            continue;
                        }

                        graph.AddNode(point);
                        if (graph.Nodes.Count >= config.maxGraphNodes)
                        {
                            graph.TruncatedByLimits = true;
                            return;
                        }
                    }
                }
            }
        }

        private static void BuildEdges(
            CorridorGraph graph,
            IReadOnlyList<float> xs,
            IReadOnlyList<float> ys,
            IReadOnlyList<float> zs,
            IReadOnlyList<Bounds> obstacles,
            RoutingConfig config,
            long buildDeadlineTicks)
        {
            for (int yIndex = 0; yIndex < ys.Count; yIndex++)
            {
                if (Stopwatch.GetTimestamp() >= buildDeadlineTicks)
                {
                    graph.TruncatedByLimits = true;
                    return;
                }

                float y = ys[yIndex];
                for (int zIndex = 0; zIndex < zs.Count; zIndex++)
                {
                    float z = zs[zIndex];
                    int prevNode = -1;
                    Vector3 prevPoint = default;
                    for (int xIndex = 0; xIndex < xs.Count; xIndex++)
                    {
                        float x = xs[xIndex];
                        Vector3 point = new Vector3(x, y, z);
                        if (!graph.TryGetNode(point, out int nodeId))
                        {
                            continue;
                        }

                        if (prevNode >= 0 && !SegmentIntersectsAnyObstacle(prevPoint, point, obstacles))
                        {
                            AddEdgeWithCost(graph, prevNode, nodeId, prevPoint, point, obstacles, config);
                        }

                        prevNode = nodeId;
                        prevPoint = point;
                        if (graph.Edges.Count >= config.maxGraphEdges)
                        {
                            graph.TruncatedByLimits = true;
                            return;
                        }
                    }
                }
            }

            for (int xIndex = 0; xIndex < xs.Count; xIndex++)
            {
                if (Stopwatch.GetTimestamp() >= buildDeadlineTicks)
                {
                    graph.TruncatedByLimits = true;
                    return;
                }

                float x = xs[xIndex];
                for (int zIndex = 0; zIndex < zs.Count; zIndex++)
                {
                    float z = zs[zIndex];
                    int prevNode = -1;
                    Vector3 prevPoint = default;
                    for (int yIndex = 0; yIndex < ys.Count; yIndex++)
                    {
                        float y = ys[yIndex];
                        Vector3 point = new Vector3(x, y, z);
                        if (!graph.TryGetNode(point, out int nodeId))
                        {
                            continue;
                        }

                        if (prevNode >= 0 && !SegmentIntersectsAnyObstacle(prevPoint, point, obstacles))
                        {
                            AddEdgeWithCost(graph, prevNode, nodeId, prevPoint, point, obstacles, config);
                        }

                        prevNode = nodeId;
                        prevPoint = point;
                        if (graph.Edges.Count >= config.maxGraphEdges)
                        {
                            graph.TruncatedByLimits = true;
                            return;
                        }
                    }
                }
            }

            for (int xIndex = 0; xIndex < xs.Count; xIndex++)
            {
                if (Stopwatch.GetTimestamp() >= buildDeadlineTicks)
                {
                    graph.TruncatedByLimits = true;
                    return;
                }

                float x = xs[xIndex];
                for (int yIndex = 0; yIndex < ys.Count; yIndex++)
                {
                    float y = ys[yIndex];
                    int prevNode = -1;
                    Vector3 prevPoint = default;
                    for (int zIndex = 0; zIndex < zs.Count; zIndex++)
                    {
                        float z = zs[zIndex];
                        Vector3 point = new Vector3(x, y, z);
                        if (!graph.TryGetNode(point, out int nodeId))
                        {
                            continue;
                        }

                        if (prevNode >= 0 && !SegmentIntersectsAnyObstacle(prevPoint, point, obstacles))
                        {
                            AddEdgeWithCost(graph, prevNode, nodeId, prevPoint, point, obstacles, config);
                        }

                        prevNode = nodeId;
                        prevPoint = point;
                        if (graph.Edges.Count >= config.maxGraphEdges)
                        {
                            graph.TruncatedByLimits = true;
                            return;
                        }
                    }
                }
            }
        }

        private static void AddEdgeWithCost(
            CorridorGraph graph,
            int nodeA,
            int nodeB,
            Vector3 pointA,
            Vector3 pointB,
            IReadOnlyList<Bounds> obstacles,
            RoutingConfig config)
        {
            float length = Vector3.Distance(pointA, pointB);
            if (length < Epsilon)
            {
                return;
            }

            Vector3 midpoint = (pointA + pointB) * 0.5f;
            float nearestDistance = NearestDistanceToObstacles(midpoint, obstacles);
            float proximityPenalty = Mathf.Max(0f, config.proximityPenaltyDistance - nearestDistance);

            float corridorWidth = Mathf.Max(config.lanePitch, nearestDistance * 2f);
            int capacity = Mathf.Max(1, Mathf.FloorToInt(corridorWidth / Mathf.Max(0.01f, config.lanePitch)));
            float baseCost = length + proximityPenalty;

            graph.AddEdge(nodeA, nodeB, baseCost, capacity, proximityPenalty);
        }

        private static bool IsInsideAnyObstacle(Vector3 point, IReadOnlyList<Bounds> obstacles)
        {
            for (int i = 0; i < obstacles.Count; i++)
            {
                Bounds obstacle = obstacles[i];
                if (point.x > obstacle.min.x + Epsilon &&
                    point.x < obstacle.max.x - Epsilon &&
                    point.y > obstacle.min.y + Epsilon &&
                    point.y < obstacle.max.y - Epsilon &&
                    point.z > obstacle.min.z + Epsilon &&
                    point.z < obstacle.max.z - Epsilon)
                {
                    return true;
                }
            }

            return false;
        }

        private static bool SegmentIntersectsAnyObstacle(Vector3 a, Vector3 b, IReadOnlyList<Bounds> obstacles)
        {
            for (int i = 0; i < obstacles.Count; i++)
            {
                if (SegmentIntersectsBoundsInterior(a, b, obstacles[i]))
                {
                    return true;
                }
            }

            return false;
        }

        private static bool SegmentIntersectsBoundsInterior(Vector3 a, Vector3 b, Bounds bounds)
        {
            Vector3 innerMin = bounds.min + Vector3.one * Epsilon;
            Vector3 innerMax = bounds.max - Vector3.one * Epsilon;
            if (innerMin.x >= innerMax.x || innerMin.y >= innerMax.y || innerMin.z >= innerMax.z)
            {
                return false;
            }

            float dx = Mathf.Abs(a.x - b.x);
            float dy = Mathf.Abs(a.y - b.y);
            float dz = Mathf.Abs(a.z - b.z);

            if (dx > dy && dx > dz)
            {
                float y = (a.y + b.y) * 0.5f;
                float z = (a.z + b.z) * 0.5f;
                if (y <= innerMin.y || y >= innerMax.y || z <= innerMin.z || z >= innerMax.z)
                {
                    return false;
                }

                float minX = Mathf.Min(a.x, b.x);
                float maxX = Mathf.Max(a.x, b.x);
                return maxX > innerMin.x && minX < innerMax.x;
            }

            if (dy > dx && dy > dz)
            {
                float x = (a.x + b.x) * 0.5f;
                float z = (a.z + b.z) * 0.5f;
                if (x <= innerMin.x || x >= innerMax.x || z <= innerMin.z || z >= innerMax.z)
                {
                    return false;
                }

                float minY = Mathf.Min(a.y, b.y);
                float maxY = Mathf.Max(a.y, b.y);
                return maxY > innerMin.y && minY < innerMax.y;
            }

            if (dz > dx && dz > dy)
            {
                float x = (a.x + b.x) * 0.5f;
                float y = (a.y + b.y) * 0.5f;
                if (x <= innerMin.x || x >= innerMax.x || y <= innerMin.y || y >= innerMax.y)
                {
                    return false;
                }

                float minZ = Mathf.Min(a.z, b.z);
                float maxZ = Mathf.Max(a.z, b.z);
                return maxZ > innerMin.z && minZ < innerMax.z;
            }

            return SegmentIntersectsBoundsGeneric(a, b, innerMin, innerMax);
        }

        private static bool SegmentIntersectsBoundsGeneric(Vector3 a, Vector3 b, Vector3 min, Vector3 max)
        {
            Vector3 d = b - a;
            float tMin = 0f;
            float tMax = 1f;

            if (!ClipAxis(min.x, max.x, a.x, d.x, ref tMin, ref tMax) ||
                !ClipAxis(min.y, max.y, a.y, d.y, ref tMin, ref tMax) ||
                !ClipAxis(min.z, max.z, a.z, d.z, ref tMin, ref tMax))
            {
                return false;
            }

            return tMax > tMin;
        }

        private static bool ClipAxis(float min, float max, float origin, float direction, ref float tMin, ref float tMax)
        {
            if (Mathf.Abs(direction) < Epsilon)
            {
                return origin >= min && origin <= max;
            }

            float inv = 1f / direction;
            float t1 = (min - origin) * inv;
            float t2 = (max - origin) * inv;
            if (t1 > t2)
            {
                float tmp = t1;
                t1 = t2;
                t2 = tmp;
            }

            tMin = Mathf.Max(tMin, t1);
            tMax = Mathf.Min(tMax, t2);
            return tMax >= tMin;
        }

        private static float NearestDistanceToObstacles(Vector3 point, IReadOnlyList<Bounds> obstacles)
        {
            if (obstacles.Count == 0)
            {
                return 999f;
            }

            float minDist = float.PositiveInfinity;
            for (int i = 0; i < obstacles.Count; i++)
            {
                Bounds obstacle = obstacles[i];
                float dx = Mathf.Max(obstacle.min.x - point.x, 0f, point.x - obstacle.max.x);
                float dy = Mathf.Max(obstacle.min.y - point.y, 0f, point.y - obstacle.max.y);
                float dz = Mathf.Max(obstacle.min.z - point.z, 0f, point.z - obstacle.max.z);
                float dist = Mathf.Sqrt(dx * dx + dy * dy + dz * dz);
                minDist = Mathf.Min(minDist, dist);
            }

            return minDist;
        }

        private static float Quantize(float value)
        {
            return Mathf.Round(value * 1000f) / 1000f;
        }

        private static List<float> ReduceGuideCoordinates(SortedSet<float> source, int maxCount, float minSpacing)
        {
            List<float> merged = new List<float>(source.Count);
            float safeSpacing = Mathf.Max(0.001f, minSpacing);
            bool hasLast = false;
            float last = 0f;
            foreach (float value in source)
            {
                if (!hasLast || Mathf.Abs(value - last) >= safeSpacing)
                {
                    merged.Add(value);
                    hasLast = true;
                    last = value;
                }
            }

            if (merged.Count <= maxCount || maxCount < 3)
            {
                return merged;
            }

            List<float> reduced = new List<float>(maxCount) { merged[0] };
            HashSet<int> used = new HashSet<int> { 0, merged.Count - 1 };
            float step = (merged.Count - 1f) / (maxCount - 1f);
            for (int i = 1; i < maxCount - 1; i++)
            {
                int index = Mathf.RoundToInt(i * step);
                index = Mathf.Clamp(index, 1, merged.Count - 2);
                while (used.Contains(index) && index < merged.Count - 2)
                {
                    index++;
                }

                if (used.Contains(index))
                {
                    continue;
                }

                used.Add(index);
                reduced.Add(merged[index]);
            }

            reduced.Add(merged[merged.Count - 1]);
            reduced.Sort();
            return reduced;
        }
    }

    public sealed class DetailedGeometryBuilder
    {
        private const float Epsilon = 0.0001f;

        private readonly struct SegmentLineKey
        {
            public SegmentLineKey(Axis axis, int a, int b)
            {
                Axis = axis;
                A = a;
                B = b;
            }

            public Axis Axis { get; }
            public int A { get; }
            public int B { get; }
        }

        private readonly struct SegmentRef
        {
            public SegmentRef(int pathIndex, int segmentIndex, Axis axis, float min, float max)
            {
                PathIndex = pathIndex;
                SegmentIndex = segmentIndex;
                Axis = axis;
                Min = min;
                Max = max;
            }

            public int PathIndex { get; }
            public int SegmentIndex { get; }
            public Axis Axis { get; }
            public float Min { get; }
            public float Max { get; }
        }

        private readonly struct SegmentId
        {
            public SegmentId(int pathIndex, int segmentIndex)
            {
                PathIndex = pathIndex;
                SegmentIndex = segmentIndex;
            }

            public int PathIndex { get; }
            public int SegmentIndex { get; }
        }

        public List<DetailedPath> Build(
            CorridorGraph graph,
            IReadOnlyList<GlobalRoute> routes,
            BundleRoutingResult bundleResult,
            IReadOnlyList<ModuleNode> modules,
            RoutingConfig config)
        {
            if (graph == null || routes == null)
            {
                return new List<DetailedPath>();
            }

            List<DetailedPath> output = new List<DetailedPath>(routes.Count);
            for (int i = 0; i < routes.Count; i++)
            {
                GlobalRoute route = routes[i];
                if (route.Flow == null || !route.Flow.TryResolve(out SlotPose source, out SlotPose target))
                {
                    continue;
                }

                DetailedPath detailedPath = BuildSinglePath(graph, route, source, target, bundleResult, config);
                output.Add(detailedPath);
            }

            SeparateOverlappingSegments(output, config);
            for (int i = 0; i < output.Count; i++)
            {
                NormalizeEndpointApproaches(output[i], config);
                FinalizeDetailedPath(output[i], config);
                AvoidModuleIntersections(output[i], modules, config);
            }

            return output;
        }

        public DetailedPath BuildPreviewPath(
            CorridorGraph graph,
            GlobalRoute route,
            BundleRoutingResult bundleResult,
            IReadOnlyList<ModuleNode> modules,
            RoutingConfig config)
        {
            if (graph == null ||
                route == null ||
                route.Flow == null ||
                !route.Flow.TryResolve(out SlotPose source, out SlotPose target))
            {
                return null;
            }

            DetailedPath detailedPath = BuildSinglePath(graph, route, source, target, bundleResult, config);
            NormalizeEndpointApproaches(detailedPath, config);
            FinalizeDetailedPath(detailedPath, config);
            AvoidModuleIntersections(detailedPath, modules, config);
            return detailedPath;
        }

        private static DetailedPath BuildSinglePath(
            CorridorGraph graph,
            GlobalRoute route,
            SlotPose source,
            SlotPose target,
            BundleRoutingResult bundleResult,
            RoutingConfig config)
        {
            DetailedPath detailed = new DetailedPath
            {
                Flow = route.Flow,
                UsesCrossingFallback = route.UsesCrossingFallback
            };

            List<Vector3> corridor = BuildCorridorPolyline(graph, route, bundleResult, config);
            Vector3 sourceFanout = source.Position + source.Normal * config.fanoutLength;
            Vector3 targetFanout = target.Position + target.Normal * config.fanoutLength;
            SanitizeEndpointCorridor(
                corridor,
                source,
                route.Flow.Source.Module,
                config,
                fromStart: true);
            SanitizeEndpointCorridor(
                corridor,
                target,
                route.Flow.Target.Module,
                config,
                fromStart: false);

            AppendUnique(detailed.Points, source.Position);
            AppendUnique(detailed.Points, sourceFanout);

            if (corridor.Count > 0)
            {
                AppendEndpointAwareConnection(
                    detailed.Points,
                    sourceFanout,
                    corridor[0],
                    source.Normal,
                    true,
                    config);

                for (int i = 1; i < corridor.Count; i++)
                {
                    AppendAxisAlignedConnection(
                        detailed.Points,
                        detailed.Points[detailed.Points.Count - 1],
                        corridor[i],
                        config.minSegmentLen);
                }

                AppendEndpointAwareConnection(
                    detailed.Points,
                    detailed.Points[detailed.Points.Count - 1],
                    targetFanout,
                    target.Normal,
                    false,
                    config);
            }
            else
            {
                AppendEndpointAwareConnection(
                    detailed.Points,
                    sourceFanout,
                    targetFanout,
                    source.Normal,
                    true,
                    config);
                AppendEndpointAwareConnection(
                    detailed.Points,
                    detailed.Points[detailed.Points.Count - 1],
                    targetFanout,
                    target.Normal,
                    false,
                    config);
            }

            AppendUnique(detailed.Points, targetFanout);
            AppendUnique(detailed.Points, target.Position);

            PreCleanDetailedPoints(detailed.Points, config);
            return detailed;
        }

        private static List<Vector3> BuildCorridorPolyline(
            CorridorGraph graph,
            GlobalRoute route,
            BundleRoutingResult bundleResult,
            RoutingConfig config)
        {
            List<Vector3> points = new List<Vector3>();
            if (route.EdgeIds.Count == 0 || route.NodeIds.Count <= 1)
            {
                return points;
            }

            for (int edgeIndex = 0; edgeIndex < route.EdgeIds.Count; edgeIndex++)
            {
                int edgeId = route.EdgeIds[edgeIndex];
                CorridorEdge edge = graph.Edges[edgeId];
                int fromNode = route.NodeIds[edgeIndex];
                int toNode = route.NodeIds[edgeIndex + 1];
                Vector3 start = graph.Nodes[fromNode];
                Vector3 end = graph.Nodes[toNode];

                float offset = bundleResult != null ? bundleResult.GetOffset(route.Flow, edgeId) : 0f;
                Vector3 shift = GetLaneShift(edge.Axis, offset);
                Vector3 shiftedStart = start + shift;
                Vector3 shiftedEnd = end + shift;

                if (points.Count == 0)
                {
                    AppendUnique(points, shiftedStart);
                }
                else
                {
                    AppendAxisAlignedConnection(points, points[points.Count - 1], shiftedStart, config.minSegmentLen);
                }

                AppendAxisAlignedConnection(points, points[points.Count - 1], shiftedEnd, config.minSegmentLen);
            }

            SnapNearlyAxisAligned(points);
            SimplifyCollinear(points);
            CleanupLocalDetours(points, config.minSegmentLen, config.lanePitch);
            SnapNearlyAxisAligned(points);
            SimplifyCollinear(points);
            return points;
        }

        private static void PreCleanDetailedPoints(List<Vector3> points, RoutingConfig config)
        {
            SnapNearlyAxisAligned(points);
            SimplifyCollinear(points);
            CleanupLocalDetours(points, config.minSegmentLen, config.lanePitch);
            SnapNearlyAxisAligned(points);
            SimplifyCollinear(points);
        }

        private static void NormalizeEndpointApproaches(DetailedPath path, RoutingConfig config)
        {
            if (path == null ||
                path.Flow == null ||
                path.Points.Count < 4 ||
                !path.Flow.TryResolve(out SlotPose source, out SlotPose target))
            {
                return;
            }

            Vector3 sourceFanout = source.Position + source.Normal * config.fanoutLength;
            Vector3 targetFanout = target.Position + target.Normal * config.fanoutLength;
            float endpointGuard = Mathf.Max(config.minSegmentLen, config.lanePitch);
            int sourceAnchorIndex = FindEndpointAnchorIndex(
                path.Points,
                source,
                config.fanoutLength,
                endpointGuard,
                fromStart: true);
            int targetAnchorIndex = FindEndpointAnchorIndex(
                path.Points,
                target,
                config.fanoutLength,
                endpointGuard,
                fromStart: false);

            List<Vector3> original = new List<Vector3>(path.Points);
            List<Vector3> rebuilt = new List<Vector3>(original.Count + 6);
            AppendUnique(rebuilt, source.Position);
            AppendUnique(rebuilt, sourceFanout);

            if (sourceAnchorIndex < targetAnchorIndex)
            {
                AppendEndpointAwareConnection(
                    rebuilt,
                    rebuilt[rebuilt.Count - 1],
                    original[sourceAnchorIndex],
                    source.Normal,
                    true,
                    config);
                for (int i = sourceAnchorIndex + 1; i <= targetAnchorIndex; i++)
                {
                    AppendAxisAlignedConnection(
                        rebuilt,
                        rebuilt[rebuilt.Count - 1],
                        original[i],
                        config.minSegmentLen);
                }
            }
            else
            {
                AppendEndpointAwareConnection(
                    rebuilt,
                    rebuilt[rebuilt.Count - 1],
                    targetFanout,
                    source.Normal,
                    true,
                    config);
            }

            AppendEndpointAwareConnection(
                rebuilt,
                rebuilt[rebuilt.Count - 1],
                targetFanout,
                target.Normal,
                false,
                config);
            AppendUnique(rebuilt, target.Position);

            path.Points.Clear();
            path.Points.AddRange(rebuilt);
            SnapNearlyAxisAligned(path.Points);
            SimplifyCollinear(path.Points);
        }

        private static int FindEndpointAnchorIndex(
            List<Vector3> points,
            SlotPose endpoint,
            float fanoutDistance,
            float guardDistance,
            bool fromStart)
        {
            Vector3 normal = endpoint.Normal.normalized;
            float minOutwardDistance = Mathf.Max(0f, fanoutDistance) + Mathf.Max(0f, guardDistance);
            if (fromStart)
            {
                int fallback = Mathf.Min(points.Count - 1, 2);
                for (int i = 1; i < points.Count - 1; i++)
                {
                    float outwardDistance = Vector3.Dot(points[i] - endpoint.Position, normal);
                    if (outwardDistance >= minOutwardDistance - Epsilon)
                    {
                        return i;
                    }
                }

                return fallback;
            }

            int reverseFallback = Mathf.Max(0, points.Count - 3);
            for (int i = points.Count - 2; i > 0; i--)
            {
                float outwardDistance = Vector3.Dot(points[i] - endpoint.Position, normal);
                if (outwardDistance >= minOutwardDistance - Epsilon)
                {
                    return i;
                }
            }

            return reverseFallback;
        }

        private static void FinalizeDetailedPath(DetailedPath path, RoutingConfig config)
        {
            if (path == null)
            {
                return;
            }

            path.CornerMarkers.Clear();
            SnapNearlyAxisAligned(path.Points);
            SimplifyCollinear(path.Points);
            CleanupLocalDetours(path.Points, config.minSegmentLen, config.lanePitch);
            CleanupLaneTransitionDoglegs(path.Points, config.minSegmentLen, config.lanePitch);
            CleanupSelfLoops(path.Points);
            SnapNearlyAxisAligned(path.Points);
            SimplifyCollinear(path.Points);
            ApplyCornerRounding(path.Points, path.CornerMarkers, config.cornerRadius, config.minSegmentLen);
            CleanupOppositeAxisSpikes(path.Points, Mathf.Max(config.minSegmentLen, config.lanePitch) * 0.35f);
            CleanupPostRoundingHooks(path.Points, config.minSegmentLen, config.lanePitch);
            CleanupSelfLoops(path.Points);
            SnapNearlyAxisAligned(path.Points);
            SimplifyCollinear(path.Points);
        }

        private static void AvoidModuleIntersections(
            DetailedPath path,
            IReadOnlyList<ModuleNode> modules,
            RoutingConfig config)
        {
            if (path == null || path.Points.Count < 2 || modules == null || modules.Count == 0)
            {
                return;
            }

            List<Bounds> moduleBounds = BuildModuleBounds(modules);
            if (moduleBounds.Count == 0)
            {
                return;
            }

            float lanePitch = config != null ? config.lanePitch : 0.25f;
            float minSegmentLength = config != null ? config.minSegmentLen : 0.05f;
            float padding = Mathf.Max(0.01f, lanePitch * 0.75f);
            float planarTolerance = config != null ? Mathf.Max(0.001f, config.sameHeightTolerance) : 0.001f;
            bool keepPlanar = IsPlanarPath(path.Points, planarTolerance);
            int safety = 0;
            bool changed;
            do
            {
                changed = false;
                for (int segmentIndex = 1; segmentIndex < path.Points.Count; segmentIndex++)
                {
                    Vector3 from = path.Points[segmentIndex - 1];
                    Vector3 to = path.Points[segmentIndex];
                    if (!TryFindFirstModuleIntersection(from, to, moduleBounds, out Bounds blockingBounds))
                    {
                        continue;
                    }

                    if (!TryBuildModuleDetour(
                            from,
                            to,
                            blockingBounds,
                            moduleBounds,
                            padding,
                            minSegmentLength,
                            keepPlanar,
                            out List<Vector3> replacement))
                    {
                        continue;
                    }

                    path.Points.RemoveAt(segmentIndex);
                    path.Points.InsertRange(segmentIndex, replacement);
                    SnapNearlyAxisAligned(path.Points);
                    SimplifyCollinear(path.Points);
                    changed = true;
                    break;
                }
            }
            while (changed && ++safety < 128);

            SnapNearlyAxisAligned(path.Points);
            SimplifyCollinear(path.Points);
        }

        private static List<Bounds> BuildModuleBounds(IReadOnlyList<ModuleNode> modules)
        {
            List<Bounds> bounds = new List<Bounds>(modules.Count);
            for (int i = 0; i < modules.Count; i++)
            {
                ModuleNode module = modules[i];
                if (module == null)
                {
                    continue;
                }

                Bounds moduleBounds = module.GetWorldBounds();
                if (moduleBounds.size.sqrMagnitude <= Epsilon)
                {
                    continue;
                }

                bounds.Add(moduleBounds);
            }

            return bounds;
        }

        private static bool IsPlanarPath(List<Vector3> points, float tolerance)
        {
            if (points.Count <= 1)
            {
                return true;
            }

            float y = points[0].y;
            for (int i = 1; i < points.Count; i++)
            {
                if (Mathf.Abs(points[i].y - y) > tolerance)
                {
                    return false;
                }
            }

            return true;
        }

        private static bool TryFindFirstModuleIntersection(
            Vector3 from,
            Vector3 to,
            IReadOnlyList<Bounds> moduleBounds,
            out Bounds blockingBounds)
        {
            for (int i = 0; i < moduleBounds.Count; i++)
            {
                Bounds bounds = moduleBounds[i];
                if (SegmentIntersectsBoundsInterior(from, to, bounds))
                {
                    blockingBounds = bounds;
                    return true;
                }
            }

            blockingBounds = default;
            return false;
        }

        private static bool TryBuildModuleDetour(
            Vector3 from,
            Vector3 to,
            Bounds blockingBounds,
            IReadOnlyList<Bounds> moduleBounds,
            float padding,
            float minSegmentLength,
            bool keepPlanar,
            out List<Vector3> replacement)
        {
            replacement = null;

            TryGetAxisDirection(to - from, out Axis segmentAxis, out _, out _);
            bool isAxisAlignedSegment = IsAxisAlignedSegment(from, to);
            Axis[] detourAxes = keepPlanar
                ? new[] { Axis.X, Axis.Z }
                : new[] { Axis.X, Axis.Y, Axis.Z };
            List<float>[] detourCoordinates = BuildDetourCoordinates(
                detourAxes,
                blockingBounds,
                moduleBounds,
                padding);

            int bestCollisions = int.MaxValue;
            float bestLength = float.PositiveInfinity;
            List<Vector3> bestCandidate = null;
            for (int i = 0; i < detourAxes.Length; i++)
            {
                Axis detourAxis = detourAxes[i];
                if (isAxisAlignedSegment && detourAxis == segmentAxis)
                {
                    continue;
                }

                List<float> coordinates = detourCoordinates[i];
                for (int coordinateIndex = 0; coordinateIndex < coordinates.Count; coordinateIndex++)
                {
                    EvaluateModuleDetourCandidate(
                        from,
                        to,
                        detourAxis,
                        coordinates[coordinateIndex],
                        moduleBounds,
                        minSegmentLength,
                        ref bestCollisions,
                        ref bestLength,
                        ref bestCandidate);
                }
            }

            for (int firstAxisIndex = 0; firstAxisIndex < detourAxes.Length; firstAxisIndex++)
            {
                Axis firstAxis = detourAxes[firstAxisIndex];
                for (int secondAxisIndex = 0; secondAxisIndex < detourAxes.Length; secondAxisIndex++)
                {
                    if (secondAxisIndex == firstAxisIndex)
                    {
                        continue;
                    }

                    Axis secondAxis = detourAxes[secondAxisIndex];
                    List<float> firstCoordinates = detourCoordinates[firstAxisIndex];
                    List<float> secondCoordinates = detourCoordinates[secondAxisIndex];
                    for (int firstCoordinateIndex = 0; firstCoordinateIndex < firstCoordinates.Count; firstCoordinateIndex++)
                    {
                        for (int secondCoordinateIndex = 0; secondCoordinateIndex < secondCoordinates.Count; secondCoordinateIndex++)
                        {
                            EvaluateCornerModuleDetourCandidate(
                                from,
                                to,
                                firstAxis,
                                firstCoordinates[firstCoordinateIndex],
                                secondAxis,
                                secondCoordinates[secondCoordinateIndex],
                                moduleBounds,
                                minSegmentLength,
                                ref bestCollisions,
                                ref bestLength,
                                ref bestCandidate);
                        }
                    }
                }
            }

            if (bestCandidate == null || bestCollisions > 0)
            {
                return false;
            }

            replacement = bestCandidate;
            return true;
        }

        private static List<float>[] BuildDetourCoordinates(
            Axis[] detourAxes,
            Bounds blockingBounds,
            IReadOnlyList<Bounds> moduleBounds,
            float padding)
        {
            List<float>[] coordinates = new List<float>[detourAxes.Length];
            for (int axisIndex = 0; axisIndex < detourAxes.Length; axisIndex++)
            {
                Axis axis = detourAxes[axisIndex];
                coordinates[axisIndex] = new List<float>();
                Bounds expandedBlockingBounds = blockingBounds;
                expandedBlockingBounds.Expand(padding * 2f);
                AddDetourCoordinate(coordinates[axisIndex], GetAxis(expandedBlockingBounds.min, axis));
                AddDetourCoordinate(coordinates[axisIndex], GetAxis(expandedBlockingBounds.max, axis));

                for (int moduleIndex = 0; moduleIndex < moduleBounds.Count; moduleIndex++)
                {
                    Bounds expandedBounds = moduleBounds[moduleIndex];
                    expandedBounds.Expand(padding * 2f);
                    AddDetourCoordinate(coordinates[axisIndex], GetAxis(expandedBounds.min, axis));
                    AddDetourCoordinate(coordinates[axisIndex], GetAxis(expandedBounds.max, axis));
                }
            }

            return coordinates;
        }

        private static void AddDetourCoordinate(List<float> coordinates, float value)
        {
            const float duplicateTolerance = 0.0005f;
            for (int i = 0; i < coordinates.Count; i++)
            {
                if (Mathf.Abs(coordinates[i] - value) <= duplicateTolerance)
                {
                    return;
                }
            }

            coordinates.Add(value);
        }

        private static void EvaluateModuleDetourCandidate(
            Vector3 from,
            Vector3 to,
            Axis detourAxis,
            float detourCoordinate,
            IReadOnlyList<Bounds> moduleBounds,
            float minSegmentLength,
            ref int bestCollisions,
            ref float bestLength,
            ref List<Vector3> bestCandidate)
        {
            Vector3 first = SetAxis(from, detourAxis, detourCoordinate);
            Vector3 second = SetAxis(to, detourAxis, detourCoordinate);
            List<Vector3> candidate = new List<Vector3> { from };
            AppendAxisAlignedConnection(candidate, candidate[candidate.Count - 1], first, minSegmentLength);
            AppendAxisAlignedConnection(candidate, candidate[candidate.Count - 1], second, minSegmentLength);
            AppendAxisAlignedConnection(candidate, candidate[candidate.Count - 1], to, minSegmentLength);

            int collisionCount = CountModuleIntersections(candidate, moduleBounds, 0);
            if (collisionCount > bestCollisions)
            {
                return;
            }

            float length = ComputePolylineLength(candidate);
            if (collisionCount == bestCollisions && length >= bestLength)
            {
                return;
            }

            bestCollisions = collisionCount;
            bestLength = length;
            bestCandidate = new List<Vector3>(candidate.Count - 1);
            for (int i = 1; i < candidate.Count; i++)
            {
                bestCandidate.Add(candidate[i]);
            }
        }

        private static void EvaluateCornerModuleDetourCandidate(
            Vector3 from,
            Vector3 to,
            Axis firstAxis,
            float firstCoordinate,
            Axis secondAxis,
            float secondCoordinate,
            IReadOnlyList<Bounds> moduleBounds,
            float minSegmentLength,
            ref int bestCollisions,
            ref float bestLength,
            ref List<Vector3> bestCandidate)
        {
            Vector3 first = SetAxis(from, firstAxis, firstCoordinate);
            Vector3 corner = SetAxis(first, secondAxis, secondCoordinate);
            Vector3 second = SetAxis(to, secondAxis, secondCoordinate);
            second = SetAxis(second, firstAxis, firstCoordinate);
            Vector3 third = SetAxis(to, firstAxis, firstCoordinate);

            List<Vector3> candidate = new List<Vector3> { from };
            AppendAxisAlignedConnection(candidate, candidate[candidate.Count - 1], first, minSegmentLength);
            AppendAxisAlignedConnection(candidate, candidate[candidate.Count - 1], corner, minSegmentLength);
            AppendAxisAlignedConnection(candidate, candidate[candidate.Count - 1], second, minSegmentLength);
            AppendAxisAlignedConnection(candidate, candidate[candidate.Count - 1], third, minSegmentLength);
            AppendAxisAlignedConnection(candidate, candidate[candidate.Count - 1], to, minSegmentLength);

            int collisionCount = CountModuleIntersections(candidate, moduleBounds, 0);
            if (collisionCount > bestCollisions)
            {
                return;
            }

            float length = ComputePolylineLength(candidate);
            if (collisionCount == bestCollisions && length >= bestLength)
            {
                return;
            }

            bestCollisions = collisionCount;
            bestLength = length;
            bestCandidate = new List<Vector3>(candidate.Count - 1);
            for (int i = 1; i < candidate.Count; i++)
            {
                bestCandidate.Add(candidate[i]);
            }
        }

        private static int CountModuleIntersections(
            List<Vector3> points,
            IReadOnlyList<Bounds> moduleBounds,
            int maxCount = int.MaxValue)
        {
            int count = 0;
            for (int segmentIndex = 1; segmentIndex < points.Count; segmentIndex++)
            {
                Vector3 from = points[segmentIndex - 1];
                Vector3 to = points[segmentIndex];
                for (int moduleIndex = 0; moduleIndex < moduleBounds.Count; moduleIndex++)
                {
                    if (SegmentIntersectsBoundsInterior(from, to, moduleBounds[moduleIndex]))
                    {
                        count++;
                        if (count > maxCount)
                        {
                            return count;
                        }

                        break;
                    }
                }
            }

            return count;
        }

        private static float ComputePolylineLength(List<Vector3> points)
        {
            float length = 0f;
            for (int i = 1; i < points.Count; i++)
            {
                length += Vector3.Distance(points[i - 1], points[i]);
            }

            return length;
        }

        private static bool SegmentIntersectsBoundsInterior(Vector3 from, Vector3 to, Bounds bounds)
        {
            Vector3 innerMin = bounds.min + Vector3.one * Epsilon;
            Vector3 innerMax = bounds.max - Vector3.one * Epsilon;
            if (innerMin.x >= innerMax.x || innerMin.y >= innerMax.y || innerMin.z >= innerMax.z)
            {
                return false;
            }

            Vector3 delta = to - from;
            float tMin = 0f;
            float tMax = 1f;
            if (!ClipSegmentAxis(innerMin.x, innerMax.x, from.x, delta.x, ref tMin, ref tMax) ||
                !ClipSegmentAxis(innerMin.y, innerMax.y, from.y, delta.y, ref tMin, ref tMax) ||
                !ClipSegmentAxis(innerMin.z, innerMax.z, from.z, delta.z, ref tMin, ref tMax))
            {
                return false;
            }

            return tMax > tMin;
        }

        private static bool ClipSegmentAxis(
            float min,
            float max,
            float origin,
            float direction,
            ref float tMin,
            ref float tMax)
        {
            if (Mathf.Abs(direction) < Epsilon)
            {
                return origin >= min && origin <= max;
            }

            float inv = 1f / direction;
            float t1 = (min - origin) * inv;
            float t2 = (max - origin) * inv;
            if (t1 > t2)
            {
                float tmp = t1;
                t1 = t2;
                t2 = tmp;
            }

            tMin = Mathf.Max(tMin, t1);
            tMax = Mathf.Min(tMax, t2);
            return tMax >= tMin;
        }

        private static void SeparateOverlappingSegments(List<DetailedPath> paths, RoutingConfig config)
        {
            if (paths == null || paths.Count <= 1)
            {
                return;
            }

            float lanePitch = Mathf.Max(0.001f, config.lanePitch);
            float minSegmentLength = Mathf.Max(Epsilon * 10f, config.minSegmentLen * 0.5f);
            float minOverlapLength = Mathf.Max(Epsilon * 10f, minSegmentLength * 0.25f);
            Dictionary<SegmentLineKey, List<SegmentRef>> groups = new Dictionary<SegmentLineKey, List<SegmentRef>>();

            for (int pathIndex = 0; pathIndex < paths.Count; pathIndex++)
            {
                DetailedPath path = paths[pathIndex];
                if (path == null || path.Points.Count < 2)
                {
                    continue;
                }

                for (int segmentIndex = 1; segmentIndex < path.Points.Count; segmentIndex++)
                {
                    Vector3 start = path.Points[segmentIndex - 1];
                    Vector3 end = path.Points[segmentIndex];
                    if (!TryGetAxisDirection(end - start, out Axis axis, out _, out float length) ||
                        length < minSegmentLength)
                    {
                        continue;
                    }

                    SegmentLineKey key = BuildSegmentLineKey(start, end, axis);
                    if (!groups.TryGetValue(key, out List<SegmentRef> segments))
                    {
                        segments = new List<SegmentRef>();
                        groups[key] = segments;
                    }

                    float a = GetAxis(start, axis);
                    float b = GetAxis(end, axis);
                    segments.Add(new SegmentRef(
                        pathIndex,
                        segmentIndex,
                        axis,
                        Mathf.Min(a, b),
                        Mathf.Max(a, b)));
                }
            }

            Dictionary<SegmentId, Vector3> offsets = new Dictionary<SegmentId, Vector3>();
            foreach (KeyValuePair<SegmentLineKey, List<SegmentRef>> pair in groups)
            {
                AssignOverlapOffsets(pair.Value, lanePitch, minOverlapLength, offsets);
            }

            if (offsets.Count == 0)
            {
                return;
            }

            ExpandOffsetsAlongStraightRuns(paths, offsets);
            ApplySegmentOffsets(paths, offsets, config);
        }

        private static SegmentLineKey BuildSegmentLineKey(Vector3 start, Vector3 end, Axis axis)
        {
            switch (axis)
            {
                case Axis.X:
                    return new SegmentLineKey(axis, QuantizeLineCoordinate(start.y), QuantizeLineCoordinate(start.z));
                case Axis.Y:
                    return new SegmentLineKey(axis, QuantizeLineCoordinate(start.x), QuantizeLineCoordinate(start.z));
                default:
                    return new SegmentLineKey(axis, QuantizeLineCoordinate(start.x), QuantizeLineCoordinate(start.y));
            }
        }

        private static int QuantizeLineCoordinate(float value)
        {
            return Mathf.RoundToInt(value * 1000f);
        }

        private static void AssignOverlapOffsets(
            List<SegmentRef> segments,
            float lanePitch,
            float minOverlapLength,
            Dictionary<SegmentId, Vector3> offsets)
        {
            if (segments == null || segments.Count <= 1)
            {
                return;
            }

            List<int>[] adjacency = new List<int>[segments.Count];
            for (int i = 0; i < segments.Count; i++)
            {
                for (int j = i + 1; j < segments.Count; j++)
                {
                    if (segments[i].PathIndex == segments[j].PathIndex ||
                        GetOverlapLength(segments[i], segments[j]) < minOverlapLength)
                    {
                        continue;
                    }

                    if (adjacency[i] == null)
                    {
                        adjacency[i] = new List<int>();
                    }

                    if (adjacency[j] == null)
                    {
                        adjacency[j] = new List<int>();
                    }

                    adjacency[i].Add(j);
                    adjacency[j].Add(i);
                }
            }

            bool[] visited = new bool[segments.Count];
            List<int> stack = new List<int>();
            List<int> component = new List<int>();
            for (int i = 0; i < segments.Count; i++)
            {
                if (visited[i] || adjacency[i] == null)
                {
                    continue;
                }

                stack.Clear();
                component.Clear();
                stack.Add(i);
                visited[i] = true;
                while (stack.Count > 0)
                {
                    int current = stack[stack.Count - 1];
                    stack.RemoveAt(stack.Count - 1);
                    component.Add(current);

                    List<int> neighbors = adjacency[current];
                    for (int n = 0; n < neighbors.Count; n++)
                    {
                        int next = neighbors[n];
                        if (visited[next])
                        {
                            continue;
                        }

                        visited[next] = true;
                        stack.Add(next);
                    }
                }

                AssignComponentOffsets(segments, component, lanePitch, offsets);
            }
        }

        private static float GetOverlapLength(SegmentRef first, SegmentRef second)
        {
            float min = Mathf.Max(first.Min, second.Min);
            float max = Mathf.Min(first.Max, second.Max);
            return Mathf.Max(0f, max - min);
        }

        private static void AssignComponentOffsets(
            List<SegmentRef> segments,
            List<int> component,
            float lanePitch,
            Dictionary<SegmentId, Vector3> offsets)
        {
            Dictionary<int, float> firstPositionByPath = new Dictionary<int, float>();
            for (int i = 0; i < component.Count; i++)
            {
                SegmentRef segment = segments[component[i]];
                if (!firstPositionByPath.TryGetValue(segment.PathIndex, out float existing) ||
                    segment.Min < existing)
                {
                    firstPositionByPath[segment.PathIndex] = segment.Min;
                }
            }

            if (firstPositionByPath.Count <= 1)
            {
                return;
            }

            List<int> orderedPaths = new List<int>(firstPositionByPath.Keys);
            orderedPaths.Sort((left, right) =>
            {
                int compare = firstPositionByPath[left].CompareTo(firstPositionByPath[right]);
                return compare != 0 ? compare : left.CompareTo(right);
            });

            Dictionary<int, Vector3> shiftByPath = new Dictionary<int, Vector3>(orderedPaths.Count);
            for (int i = 0; i < orderedPaths.Count; i++)
            {
                float offset = (i - (orderedPaths.Count - 1) * 0.5f) * lanePitch;
                Axis axis = segments[component[0]].Axis;
                shiftByPath[orderedPaths[i]] = GetSeparationShift(axis, offset);
            }

            for (int i = 0; i < component.Count; i++)
            {
                SegmentRef segment = segments[component[i]];
                if (!shiftByPath.TryGetValue(segment.PathIndex, out Vector3 shift) ||
                    shift.sqrMagnitude < Epsilon)
                {
                    continue;
                }

                offsets[new SegmentId(segment.PathIndex, segment.SegmentIndex)] = shift;
            }
        }

        private static Vector3 GetSeparationShift(Axis axis, float offset)
        {
            switch (axis)
            {
                case Axis.X:
                    return new Vector3(0f, 0f, offset);
                case Axis.Y:
                    return new Vector3(offset, 0f, 0f);
                default:
                    return new Vector3(offset, 0f, 0f);
            }
        }

        private static void ExpandOffsetsAlongStraightRuns(
            List<DetailedPath> paths,
            Dictionary<SegmentId, Vector3> offsets)
        {
            if (offsets.Count == 0)
            {
                return;
            }

            List<KeyValuePair<SegmentId, Vector3>> seeds = new List<KeyValuePair<SegmentId, Vector3>>(offsets);
            for (int i = 0; i < seeds.Count; i++)
            {
                SegmentId id = seeds[i].Key;
                Vector3 shift = seeds[i].Value;
                if (shift.sqrMagnitude < Epsilon ||
                    id.PathIndex < 0 ||
                    id.PathIndex >= paths.Count)
                {
                    continue;
                }

                DetailedPath path = paths[id.PathIndex];
                if (path == null ||
                    !TryGetSegmentLineKey(path.Points, id.SegmentIndex, out SegmentLineKey lineKey))
                {
                    continue;
                }

                ExtendOffsetAlongStraightRun(path.Points, id.PathIndex, id.SegmentIndex - 1, -1, lineKey, shift, offsets);
                ExtendOffsetAlongStraightRun(path.Points, id.PathIndex, id.SegmentIndex + 1, +1, lineKey, shift, offsets);
            }
        }

        private static void ExtendOffsetAlongStraightRun(
            List<Vector3> points,
            int pathIndex,
            int segmentIndex,
            int step,
            SegmentLineKey lineKey,
            Vector3 shift,
            Dictionary<SegmentId, Vector3> offsets)
        {
            for (int current = segmentIndex; current > 0 && current < points.Count; current += step)
            {
                if (!TryGetSegmentLineKey(points, current, out SegmentLineKey currentKey) ||
                    !currentKey.Equals(lineKey))
                {
                    break;
                }

                SegmentId currentId = new SegmentId(pathIndex, current);
                if (offsets.TryGetValue(currentId, out Vector3 existing))
                {
                    if ((existing - shift).sqrMagnitude > Epsilon)
                    {
                        break;
                    }

                    continue;
                }

                offsets[currentId] = shift;
            }
        }

        private static bool TryGetSegmentLineKey(
            List<Vector3> points,
            int segmentIndex,
            out SegmentLineKey key)
        {
            key = default;
            if (points == null || segmentIndex <= 0 || segmentIndex >= points.Count)
            {
                return false;
            }

            Vector3 start = points[segmentIndex - 1];
            Vector3 end = points[segmentIndex];
            if (!TryGetAxisDirection(end - start, out Axis axis, out _, out float length) ||
                length < Epsilon)
            {
                return false;
            }

            key = BuildSegmentLineKey(start, end, axis);
            return true;
        }

        private static void ApplySegmentOffsets(
            List<DetailedPath> paths,
            Dictionary<SegmentId, Vector3> offsets,
            RoutingConfig config)
        {
            for (int pathIndex = 0; pathIndex < paths.Count; pathIndex++)
            {
                DetailedPath path = paths[pathIndex];
                if (path == null || path.Points.Count < 2 || !HasSegmentOffset(pathIndex, path.Points.Count, offsets))
                {
                    continue;
                }

                List<Vector3> original = new List<Vector3>(path.Points);
                List<Vector3> rebuilt = new List<Vector3>(original.Count + 8) { original[0] };
                for (int segmentIndex = 1; segmentIndex < original.Count; segmentIndex++)
                {
                    offsets.TryGetValue(new SegmentId(pathIndex, segmentIndex), out Vector3 shift);
                    Vector3 shiftedStart = original[segmentIndex - 1] + shift;
                    Vector3 shiftedEnd = original[segmentIndex] + shift;
                    AppendAxisAlignedConnection(rebuilt, rebuilt[rebuilt.Count - 1], shiftedStart, config.minSegmentLen);
                    AppendAxisAlignedConnection(rebuilt, rebuilt[rebuilt.Count - 1], shiftedEnd, config.minSegmentLen);
                }

                AppendAxisAlignedConnection(rebuilt, rebuilt[rebuilt.Count - 1], original[original.Count - 1], config.minSegmentLen);
                path.Points.Clear();
                path.Points.AddRange(rebuilt);
                SnapNearlyAxisAligned(path.Points);
                SimplifyCollinear(path.Points);
            }
        }

        private static bool HasSegmentOffset(
            int pathIndex,
            int pointCount,
            Dictionary<SegmentId, Vector3> offsets)
        {
            for (int segmentIndex = 1; segmentIndex < pointCount; segmentIndex++)
            {
                if (offsets.ContainsKey(new SegmentId(pathIndex, segmentIndex)))
                {
                    return true;
                }
            }

            return false;
        }

        private static void SanitizeEndpointCorridor(
            List<Vector3> corridor,
            SlotPose endpoint,
            ModuleNode module,
            RoutingConfig config,
            bool fromStart)
        {
            if (corridor == null ||
                corridor.Count == 0 ||
                module == null ||
                endpoint.Normal.sqrMagnitude < 0.5f)
            {
                return;
            }

            Bounds bounds = module.GetWorldBounds();
            Vector3 normal = endpoint.Normal.normalized;
            float fanoutDistance = Mathf.Max(0f, config.fanoutLength);
            float padding = Mathf.Max(config.minSegmentLen, config.lanePitch) * 1.25f;
            int maxPoints = Mathf.Min(corridor.Count, 8);

            for (int localIndex = 0; localIndex < maxPoints; localIndex++)
            {
                int index = fromStart ? localIndex : corridor.Count - 1 - localIndex;
                Vector3 point = corridor[index];
                float outwardDistance = Vector3.Dot(point - endpoint.Position, normal);
                bool tangentiallyNear = IsTangentiallyNearBounds(point, bounds, normal, padding);
                bool shouldClamp =
                    outwardDistance < fanoutDistance - Epsilon &&
                    (localIndex == 0 || tangentiallyNear || IsInsideExpandedBounds(point, bounds, padding));

                if (shouldClamp)
                {
                    corridor[index] = ClampToEndpointFanoutPlane(point, endpoint.Position, normal, fanoutDistance);
                    continue;
                }

                if (localIndex > 0 && outwardDistance >= fanoutDistance - Epsilon && !tangentiallyNear)
                {
                    break;
                }
            }
        }

        private static Vector3 ClampToEndpointFanoutPlane(
            Vector3 point,
            Vector3 endpointPosition,
            Vector3 endpointNormal,
            float fanoutDistance)
        {
            float outwardDistance = Vector3.Dot(point - endpointPosition, endpointNormal);
            if (outwardDistance >= fanoutDistance - Epsilon)
            {
                return point;
            }

            return point + endpointNormal * (fanoutDistance - outwardDistance);
        }

        private static bool IsTangentiallyNearBounds(Vector3 point, Bounds bounds, Vector3 normal, float padding)
        {
            float absX = Mathf.Abs(normal.x);
            float absY = Mathf.Abs(normal.y);
            float absZ = Mathf.Abs(normal.z);

            if (absX >= absY && absX >= absZ)
            {
                return IsInRange(point.y, bounds.min.y - padding, bounds.max.y + padding) &&
                       IsInRange(point.z, bounds.min.z - padding, bounds.max.z + padding);
            }

            if (absY >= absX && absY >= absZ)
            {
                return IsInRange(point.x, bounds.min.x - padding, bounds.max.x + padding) &&
                       IsInRange(point.z, bounds.min.z - padding, bounds.max.z + padding);
            }

            return IsInRange(point.x, bounds.min.x - padding, bounds.max.x + padding) &&
                   IsInRange(point.y, bounds.min.y - padding, bounds.max.y + padding);
        }

        private static bool IsInsideExpandedBounds(Vector3 point, Bounds bounds, float padding)
        {
            return IsInRange(point.x, bounds.min.x - padding, bounds.max.x + padding) &&
                   IsInRange(point.y, bounds.min.y - padding, bounds.max.y + padding) &&
                   IsInRange(point.z, bounds.min.z - padding, bounds.max.z + padding);
        }

        private static bool IsInRange(float value, float min, float max)
        {
            return value >= min && value <= max;
        }

        private static Vector3 GetLaneShift(EdgeAxis axis, float offset)
        {
            return axis switch
            {
                EdgeAxis.X => new Vector3(0f, 0f, offset),
                EdgeAxis.Y => new Vector3(offset, 0f, 0f),
                _ => new Vector3(offset, 0f, 0f)
            };
        }

        private static void AppendAxisAlignedConnection(List<Vector3> points, Vector3 from, Vector3 to, float minSegmentLength)
        {
            if ((to - from).sqrMagnitude < Epsilon)
            {
                return;
            }

            if (IsAxisAlignedSegment(from, to))
            {
                AppendUnique(points, to);
                return;
            }

            AxisMask mask = GetAxisMask(from, to);
            Axis[] order = ChooseBestAxisOrder(from, to, mask, minSegmentLength);
            Vector3 current = from;
            for (int i = 0; i < order.Length; i++)
            {
                current = SetAxis(current, order[i], GetAxis(to, order[i]));
                AppendUnique(points, current);
            }
        }

        private static void AppendEndpointAwareConnection(
            List<Vector3> points,
            Vector3 from,
            Vector3 to,
            Vector3 endpointNormal,
            bool leavingEndpoint,
            RoutingConfig config)
        {
            if ((to - from).sqrMagnitude < Epsilon)
            {
                return;
            }

            if (!TryGetNormalAxis(endpointNormal, out Axis normalAxis, out int normalDirection))
            {
                AppendAxisAlignedConnection(points, from, to, config.minSegmentLen);
                return;
            }

            AxisMask mask = GetAxisMask(from, to);
            float endpointClearance = Mathf.Max(config.minSegmentLen, config.lanePitch);
            if ((mask & ToAxisMask(normalAxis)) == 0)
            {
                float endpointCoordinate = GetAxis(leavingEndpoint ? from : to, normalAxis);
                float railCoordinate = endpointCoordinate + normalDirection * endpointClearance;
                Vector3 railPoint = SetAxis(leavingEndpoint ? from : to, normalAxis, railCoordinate);
                if (leavingEndpoint)
                {
                    AppendUnique(points, railPoint);
                    AppendAxisAlignedConnection(points, railPoint, to, config.minSegmentLen);
                }
                else
                {
                    AppendConnectionWithAxisOrder(
                        points,
                        from,
                        railPoint,
                        BuildForcedAxisOrder(GetAxisMask(from, railPoint), normalAxis, true, config.minSegmentLen, from, railPoint),
                        config.minSegmentLen);
                    AppendUnique(points, to);
                }

                return;
            }

            Axis[] order = BuildForcedAxisOrder(
                mask,
                normalAxis,
                leavingEndpoint,
                config.minSegmentLen,
                from,
                to);
            AppendConnectionWithAxisOrder(points, from, to, order, config.minSegmentLen);
        }

        private static void AppendConnectionWithAxisOrder(
            List<Vector3> points,
            Vector3 from,
            Vector3 to,
            Axis[] order,
            float minSegmentLength)
        {
            if (order == null || order.Length == 0)
            {
                AppendAxisAlignedConnection(points, from, to, minSegmentLength);
                return;
            }

            Vector3 current = from;
            for (int i = 0; i < order.Length; i++)
            {
                current = SetAxis(current, order[i], GetAxis(to, order[i]));
                AppendUnique(points, current);
            }
        }

        private static Axis[] BuildForcedAxisOrder(
            AxisMask mask,
            Axis forcedAxis,
            bool forceFirst,
            float minSegmentLength,
            Vector3 from,
            Vector3 to)
        {
            AxisMask forcedMask = ToAxisMask(forcedAxis);
            if ((mask & forcedMask) == 0)
            {
                return ChooseBestAxisOrder(from, to, mask, minSegmentLength);
            }

            AxisMask remainingMask = mask & ~forcedMask;
            Axis[] remainingOrder = remainingMask == AxisMask.None
                ? new Axis[0]
                : ChooseBestAxisOrder(from, to, remainingMask, minSegmentLength);
            List<Axis> order = new List<Axis>(remainingOrder.Length + 1);
            if (forceFirst)
            {
                order.Add(forcedAxis);
                order.AddRange(remainingOrder);
            }
            else
            {
                order.AddRange(remainingOrder);
                order.Add(forcedAxis);
            }

            return order.ToArray();
        }

        private static Axis[] ChooseBestAxisOrder(Vector3 from, Vector3 to, AxisMask mask, float minSegmentLength)
        {
            List<Axis[]> candidates = BuildAxisOrderCandidates(mask);
            if (candidates.Count == 0)
            {
                return new[] { Axis.X };
            }

            Axis[] best = candidates[0];
            float bestScore = float.NegativeInfinity;
            for (int i = 0; i < candidates.Count; i++)
            {
                Axis[] candidate = candidates[i];
                float score = ScoreAxisOrder(from, to, candidate, minSegmentLength);
                if (score > bestScore)
                {
                    bestScore = score;
                    best = candidate;
                }
            }

            return best;
        }

        private static float ScoreAxisOrder(Vector3 from, Vector3 to, Axis[] order, float minSegmentLength)
        {
            Vector3 current = from;
            float minLen = float.PositiveInfinity;
            int shortSegments = 0;

            for (int i = 0; i < order.Length; i++)
            {
                Vector3 next = SetAxis(current, order[i], GetAxis(to, order[i]));
                float len = Vector3.Distance(current, next);
                minLen = Mathf.Min(minLen, len);
                if (len < minSegmentLength)
                {
                    shortSegments++;
                }

                current = next;
            }

            return minLen - shortSegments * 1000f;
        }

        private static List<Axis[]> BuildAxisOrderCandidates(AxisMask mask)
        {
            List<Axis> axes = new List<Axis>(3);
            if ((mask & AxisMask.X) != 0)
            {
                axes.Add(Axis.X);
            }

            if ((mask & AxisMask.Y) != 0)
            {
                axes.Add(Axis.Y);
            }

            if ((mask & AxisMask.Z) != 0)
            {
                axes.Add(Axis.Z);
            }

            List<Axis[]> result = new List<Axis[]>();
            if (axes.Count == 0)
            {
                return result;
            }

            Permute(axes, 0, result);
            return result;
        }

        private static bool TryGetNormalAxis(Vector3 normal, out Axis axis, out int direction)
        {
            axis = Axis.X;
            direction = 1;
            float absX = Mathf.Abs(normal.x);
            float absY = Mathf.Abs(normal.y);
            float absZ = Mathf.Abs(normal.z);
            if (absX < Epsilon && absY < Epsilon && absZ < Epsilon)
            {
                return false;
            }

            if (absX >= absY && absX >= absZ)
            {
                axis = Axis.X;
                direction = normal.x >= 0f ? 1 : -1;
                return true;
            }

            if (absY >= absZ)
            {
                axis = Axis.Y;
                direction = normal.y >= 0f ? 1 : -1;
                return true;
            }

            axis = Axis.Z;
            direction = normal.z >= 0f ? 1 : -1;
            return true;
        }

        private static AxisMask ToAxisMask(Axis axis)
        {
            return axis switch
            {
                Axis.X => AxisMask.X,
                Axis.Y => AxisMask.Y,
                _ => AxisMask.Z
            };
        }

        private static void Permute(List<Axis> axes, int index, List<Axis[]> output)
        {
            if (index >= axes.Count)
            {
                output.Add(axes.ToArray());
                return;
            }

            for (int i = index; i < axes.Count; i++)
            {
                (axes[index], axes[i]) = (axes[i], axes[index]);
                Permute(axes, index + 1, output);
                (axes[index], axes[i]) = (axes[i], axes[index]);
            }
        }

        private static void ApplyCornerRounding(
            List<Vector3> points,
            List<Vector3> cornerMarkers,
            float radius,
            float minSegmentLength)
        {
            if (points.Count < 3 || radius <= 0f)
            {
                return;
            }

            cornerMarkers.Clear();
            List<Vector3> rounded = new List<Vector3>(points.Count * 2) { points[0] };

            for (int i = 1; i < points.Count - 1; i++)
            {
                Vector3 prev = points[i - 1];
                Vector3 current = points[i];
                Vector3 next = points[i + 1];

                Vector3 inDir = current - prev;
                Vector3 outDir = next - current;
                float inLen = inDir.magnitude;
                float outLen = outDir.magnitude;
                if (inLen < Epsilon || outLen < Epsilon)
                {
                    continue;
                }

                inDir /= inLen;
                outDir /= outLen;
                if (!IsAxisAlignedDirection(inDir) || !IsAxisAlignedDirection(outDir))
                {
                    rounded.Add(current);
                    continue;
                }

                if (Vector3.Cross(inDir, outDir).sqrMagnitude < 0.001f)
                {
                    rounded.Add(current);
                    continue;
                }

                float trim = Mathf.Min(radius, inLen * 0.45f, outLen * 0.45f);
                if (trim < minSegmentLength * 0.2f)
                {
                    rounded.Add(current);
                    continue;
                }

                Vector3 start = current - inDir * trim;
                Vector3 end = current + outDir * trim;
                rounded.Add(start);
                rounded.Add(end);
                cornerMarkers.Add(current);
            }

            rounded.Add(points[points.Count - 1]);
            points.Clear();
            points.AddRange(rounded);
        }

        private static void SimplifyCollinear(List<Vector3> points)
        {
            if (points.Count < 3)
            {
                return;
            }

            for (int i = points.Count - 2; i > 0; i--)
            {
                Vector3 a = points[i - 1];
                Vector3 b = points[i];
                Vector3 c = points[i + 1];
                Vector3 ab = b - a;
                if (ab.sqrMagnitude < Epsilon)
                {
                    points.RemoveAt(i);
                    continue;
                }

                Vector3 bc = c - b;
                if (bc.sqrMagnitude < Epsilon)
                {
                    points.RemoveAt(i + 1);
                    continue;
                }

                Vector3 abDir = ab.normalized;
                Vector3 bcDir = bc.normalized;
                if (Vector3.Cross(abDir, bcDir).sqrMagnitude < 0.0005f && Vector3.Dot(abDir, bcDir) > 0.999f)
                {
                    points.RemoveAt(i);
                }
            }
        }

        private static void SnapNearlyAxisAligned(List<Vector3> points)
        {
            if (points.Count < 2)
            {
                return;
            }

            const float epsilon = 0.0005f;
            for (int i = 1; i < points.Count; i++)
            {
                Vector3 prev = points[i - 1];
                Vector3 current = points[i];
                float absDx = Mathf.Abs(current.x - prev.x);
                float absDy = Mathf.Abs(current.y - prev.y);
                float absDz = Mathf.Abs(current.z - prev.z);
                float dominant = Mathf.Max(absDx, Mathf.Max(absDy, absDz));

                if (dominant <= epsilon)
                {
                    current = prev;
                }
                else if (dominant == absDx)
                {
                    if (absDy <= epsilon || absDy < absDx * 0.02f)
                    {
                        current.y = prev.y;
                    }

                    if (absDz <= epsilon || absDz < absDx * 0.02f)
                    {
                        current.z = prev.z;
                    }
                }
                else if (dominant == absDy)
                {
                    if (absDx <= epsilon || absDx < absDy * 0.02f)
                    {
                        current.x = prev.x;
                    }

                    if (absDz <= epsilon || absDz < absDy * 0.02f)
                    {
                        current.z = prev.z;
                    }
                }
                else
                {
                    if (absDx <= epsilon || absDx < absDz * 0.02f)
                    {
                        current.x = prev.x;
                    }

                    if (absDy <= epsilon || absDy < absDz * 0.02f)
                    {
                        current.y = prev.y;
                    }
                }

                points[i] = current;
            }
        }

        private static bool IsAxisAlignedSegment(Vector3 from, Vector3 to)
        {
            AxisMask mask = GetAxisMask(from, to);
            int changes = 0;
            if ((mask & AxisMask.X) != 0)
            {
                changes++;
            }

            if ((mask & AxisMask.Y) != 0)
            {
                changes++;
            }

            if ((mask & AxisMask.Z) != 0)
            {
                changes++;
            }

            return changes <= 1;
        }

        private static bool IsAxisAlignedDirection(Vector3 dir)
        {
            float absX = Mathf.Abs(dir.x);
            float absY = Mathf.Abs(dir.y);
            float absZ = Mathf.Abs(dir.z);
            return Mathf.Abs(absX - 1f) < 0.0005f ||
                   Mathf.Abs(absY - 1f) < 0.0005f ||
                   Mathf.Abs(absZ - 1f) < 0.0005f;
        }

        private static AxisMask GetAxisMask(Vector3 a, Vector3 b)
        {
            AxisMask mask = AxisMask.None;
            if (Mathf.Abs(a.x - b.x) > Epsilon)
            {
                mask |= AxisMask.X;
            }

            if (Mathf.Abs(a.y - b.y) > Epsilon)
            {
                mask |= AxisMask.Y;
            }

            if (Mathf.Abs(a.z - b.z) > Epsilon)
            {
                mask |= AxisMask.Z;
            }

            return mask;
        }

        private static float GetAxis(Vector3 value, Axis axis)
        {
            return axis switch
            {
                Axis.X => value.x,
                Axis.Y => value.y,
                _ => value.z
            };
        }

        private static Vector3 SetAxis(Vector3 value, Axis axis, float newValue)
        {
            switch (axis)
            {
                case Axis.X:
                    value.x = newValue;
                    break;
                case Axis.Y:
                    value.y = newValue;
                    break;
                default:
                    value.z = newValue;
                    break;
            }

            return value;
        }

        private static void AppendUnique(List<Vector3> points, Vector3 point)
        {
            if (points.Count > 0 && (points[points.Count - 1] - point).sqrMagnitude < Epsilon)
            {
                return;
            }

            points.Add(point);
        }

        private static void CleanupLocalDetours(List<Vector3> points, float minSegmentLength, float lanePitch)
        {
            if (points.Count < 3)
            {
                return;
            }

            float threshold = Mathf.Max(minSegmentLength, lanePitch) * 1.4f;
            int safety = 0;
            bool changed;
            do
            {
                changed = false;
                changed |= CleanupOppositeAxisSpikes(points, threshold);
                changed |= CleanupOppositeDoglegs(points, threshold);
                if (changed)
                {
                    SnapNearlyAxisAligned(points);
                    SimplifyCollinear(points);
                }
            }
            while (changed && ++safety < 10);
        }

        private static void CleanupLaneTransitionDoglegs(List<Vector3> points, float minSegmentLength, float lanePitch)
        {
            if (points.Count < 4)
            {
                return;
            }

            float maxOuterLeg = Mathf.Max(minSegmentLength, lanePitch) * 3f;
            float maxMiddleLeg = Mathf.Max(minSegmentLength, lanePitch) * 8f;
            int safety = 0;
            bool changed;
            do
            {
                changed = CleanupLaneTransitionDoglegPass(points, maxOuterLeg, maxMiddleLeg);
                if (changed)
                {
                    SnapNearlyAxisAligned(points);
                    SimplifyCollinear(points);
                }
            }
            while (changed && ++safety < 8);
        }

        private static bool CleanupLaneTransitionDoglegPass(
            List<Vector3> points,
            float maxOuterLeg,
            float maxMiddleLeg)
        {
            bool changed = false;
            for (int i = 1; i < points.Count - 2; i++)
            {
                Vector3 a = points[i - 1];
                Vector3 b = points[i];
                Vector3 c = points[i + 1];
                Vector3 d = points[i + 2];

                if (!TryGetAxisDirection(b - a, out Axis axisAB, out int dirAB, out float lenAB) ||
                    !TryGetAxisDirection(c - b, out Axis axisBC, out _, out float lenBC) ||
                    !TryGetAxisDirection(d - c, out Axis axisCD, out int dirCD, out float lenCD))
                {
                    continue;
                }

                if (axisAB != axisCD || axisAB == axisBC || dirAB == dirCD)
                {
                    continue;
                }

                if (Mathf.Min(lenAB, lenCD) > maxOuterLeg || lenBC > maxMiddleLeg)
                {
                    continue;
                }

                if (!TryGetAxisDirection(d - a, out Axis axisAD, out _, out _) || axisAD != axisBC)
                {
                    continue;
                }

                points.RemoveAt(i + 1);
                points.RemoveAt(i);
                InsertAxisAlignedBridge(points, i, a, d);
                changed = true;
                i = Mathf.Max(0, i - 2);
            }

            return changed;
        }

        private static void CleanupSelfLoops(List<Vector3> points)
        {
            if (points.Count < 4)
            {
                return;
            }

            int safety = 0;
            bool changed;
            do
            {
                changed = TryRemoveOneSelfLoop(points);
                if (changed)
                {
                    SnapNearlyAxisAligned(points);
                    SimplifyCollinear(points);
                }
            }
            while (changed && ++safety < 32);
        }

        private static bool TryRemoveOneSelfLoop(List<Vector3> points)
        {
            for (int firstSegment = 0; firstSegment < points.Count - 2; firstSegment++)
            {
                Vector3 a0 = points[firstSegment];
                Vector3 a1 = points[firstSegment + 1];
                for (int secondSegment = firstSegment + 2; secondSegment < points.Count - 1; secondSegment++)
                {
                    if (!TryFindAxisAlignedSegmentIntersection(
                            a0,
                            a1,
                            points[secondSegment],
                            points[secondSegment + 1],
                            out Vector3 intersection))
                    {
                        continue;
                    }

                    if ((firstSegment == 0 && ApproximatelyEqual(intersection, points[0])) ||
                        (secondSegment == points.Count - 2 && ApproximatelyEqual(intersection, points[points.Count - 1])))
                    {
                        continue;
                    }

                    int removedPointCount = secondSegment - firstSegment;
                    if (removedPointCount <= 0)
                    {
                        continue;
                    }

                    List<Vector3> rebuilt = new List<Vector3>(points.Count - removedPointCount + 1);
                    for (int i = 0; i <= firstSegment; i++)
                    {
                        AppendUnique(rebuilt, points[i]);
                    }

                    AppendUnique(rebuilt, intersection);
                    for (int i = secondSegment + 1; i < points.Count; i++)
                    {
                        AppendUnique(rebuilt, points[i]);
                    }

                    if (rebuilt.Count >= points.Count)
                    {
                        continue;
                    }

                    points.Clear();
                    points.AddRange(rebuilt);
                    return true;
                }
            }

            return false;
        }

        private static bool TryFindAxisAlignedSegmentIntersection(
            Vector3 a0,
            Vector3 a1,
            Vector3 b0,
            Vector3 b1,
            out Vector3 intersection)
        {
            intersection = default;
            if (!TryGetAxisDirection(a1 - a0, out Axis axisA, out int dirA, out float lenA) ||
                !TryGetAxisDirection(b1 - b0, out Axis axisB, out _, out float lenB) ||
                lenA < Epsilon ||
                lenB < Epsilon)
            {
                return false;
            }

            if (axisA == axisB)
            {
                return TryFindCollinearOverlapPoint(a0, a1, b0, b1, axisA, dirA, out intersection);
            }

            Axis thirdAxis = GetRemainingAxis(axisA, axisB);
            if (Mathf.Abs(GetAxis(a0, thirdAxis) - GetAxis(b0, thirdAxis)) > 0.0005f)
            {
                return false;
            }

            Vector3 candidate = a0;
            candidate = SetAxis(candidate, axisA, GetAxis(b0, axisA));
            candidate = SetAxis(candidate, axisB, GetAxis(a0, axisB));
            candidate = SetAxis(candidate, thirdAxis, GetAxis(a0, thirdAxis));
            if (!IsPointOnAxisSegment(candidate, a0, a1) ||
                !IsPointOnAxisSegment(candidate, b0, b1))
            {
                return false;
            }

            intersection = candidate;
            return true;
        }

        private static bool TryFindCollinearOverlapPoint(
            Vector3 a0,
            Vector3 a1,
            Vector3 b0,
            Vector3 b1,
            Axis axis,
            int dirA,
            out Vector3 intersection)
        {
            intersection = default;
            if (!IsSameFixedLine(a0, b0, axis))
            {
                return false;
            }

            float aMin = Mathf.Min(GetAxis(a0, axis), GetAxis(a1, axis));
            float aMax = Mathf.Max(GetAxis(a0, axis), GetAxis(a1, axis));
            float bMin = Mathf.Min(GetAxis(b0, axis), GetAxis(b1, axis));
            float bMax = Mathf.Max(GetAxis(b0, axis), GetAxis(b1, axis));
            float overlapMin = Mathf.Max(aMin, bMin);
            float overlapMax = Mathf.Min(aMax, bMax);
            if (overlapMax < overlapMin - 0.0005f)
            {
                return false;
            }

            float coordinate = dirA >= 0 ? overlapMin : overlapMax;
            intersection = SetAxis(a0, axis, coordinate);
            return true;
        }

        private static bool IsSameFixedLine(Vector3 a, Vector3 b, Axis axis)
        {
            switch (axis)
            {
                case Axis.X:
                    return Mathf.Abs(a.y - b.y) <= 0.0005f && Mathf.Abs(a.z - b.z) <= 0.0005f;
                case Axis.Y:
                    return Mathf.Abs(a.x - b.x) <= 0.0005f && Mathf.Abs(a.z - b.z) <= 0.0005f;
                default:
                    return Mathf.Abs(a.x - b.x) <= 0.0005f && Mathf.Abs(a.y - b.y) <= 0.0005f;
            }
        }

        private static bool ApproximatelyEqual(Vector3 a, Vector3 b)
        {
            return (a - b).sqrMagnitude <= 0.00000025f;
        }

        private static bool IsPointOnAxisSegment(Vector3 point, Vector3 a, Vector3 b)
        {
            if (!TryGetAxisDirection(b - a, out Axis axis, out _, out _))
            {
                return false;
            }

            if (!IsSameFixedLine(point, a, axis))
            {
                return false;
            }

            float value = GetAxis(point, axis);
            float min = Mathf.Min(GetAxis(a, axis), GetAxis(b, axis)) - 0.0005f;
            float max = Mathf.Max(GetAxis(a, axis), GetAxis(b, axis)) + 0.0005f;
            return value >= min && value <= max;
        }

        private static Axis GetRemainingAxis(Axis first, Axis second)
        {
            if (first != Axis.X && second != Axis.X)
            {
                return Axis.X;
            }

            if (first != Axis.Y && second != Axis.Y)
            {
                return Axis.Y;
            }

            return Axis.Z;
        }

        private static bool CleanupOppositeAxisSpikes(List<Vector3> points, float maxSpikeLength)
        {
            bool changed = false;
            for (int i = 1; i < points.Count - 1; i++)
            {
                if (!TryGetAxisDirection(points[i] - points[i - 1], out Axis axisAB, out int dirAB, out float lenAB) ||
                    !TryGetAxisDirection(points[i + 1] - points[i], out Axis axisBC, out int dirBC, out float lenBC))
                {
                    continue;
                }

                if (axisAB != axisBC || dirAB == dirBC)
                {
                    continue;
                }

                if (Mathf.Min(lenAB, lenBC) > maxSpikeLength)
                {
                    continue;
                }

                points.RemoveAt(i);
                changed = true;
                i = Mathf.Max(0, i - 2);
            }

            return changed;
        }

        private static bool CleanupOppositeDoglegs(List<Vector3> points, float maxDetourLength)
        {
            bool changed = false;
            for (int i = 1; i < points.Count - 2; i++)
            {
                Vector3 a = points[i - 1];
                Vector3 b = points[i];
                Vector3 c = points[i + 1];
                Vector3 d = points[i + 2];

                if (!TryGetAxisDirection(b - a, out Axis axisAB, out int dirAB, out float lenAB) ||
                    !TryGetAxisDirection(c - b, out Axis axisBC, out _, out float lenBC) ||
                    !TryGetAxisDirection(d - c, out Axis axisCD, out int dirCD, out float lenCD))
                {
                    continue;
                }

                if (axisAB != axisCD || axisAB == axisBC || dirAB == dirCD)
                {
                    continue;
                }

                if (Mathf.Min(lenAB, lenCD) > maxDetourLength || lenBC > maxDetourLength * 1.7f)
                {
                    continue;
                }

                points.RemoveAt(i + 1);
                points.RemoveAt(i);
                InsertAxisAlignedBridge(points, i, a, d);
                changed = true;
                i = Mathf.Max(0, i - 2);
            }

            return changed;
        }

        private static void CleanupPostRoundingHooks(List<Vector3> points, float minSegmentLength, float lanePitch)
        {
            if (points.Count < 3)
            {
                return;
            }

            float maxLegLength = Mathf.Max(minSegmentLength, lanePitch) * 0.95f;
            float maxMiddleLength = maxLegLength * 2f;
            int safety = 0;
            bool changed;
            do
            {
                changed = false;
                changed |= CleanupShortReversePairs(points, maxLegLength);
                changed |= CleanupShortBacktrackingDoglegs(points, maxLegLength, maxMiddleLength);
                if (changed)
                {
                    SnapNearlyAxisAligned(points);
                    SimplifyCollinear(points);
                }
            }
            while (changed && ++safety < 8);
        }

        private static bool CleanupShortReversePairs(List<Vector3> points, float maxLegLength)
        {
            bool changed = false;
            for (int i = 1; i < points.Count - 1; i++)
            {
                Vector3 ab = points[i] - points[i - 1];
                Vector3 bc = points[i + 1] - points[i];
                float lenAB = ab.magnitude;
                float lenBC = bc.magnitude;
                if (lenAB < Epsilon)
                {
                    points.RemoveAt(i);
                    changed = true;
                    i = Mathf.Max(0, i - 2);
                    continue;
                }

                if (lenBC < Epsilon)
                {
                    points.RemoveAt(i + 1);
                    changed = true;
                    i = Mathf.Max(0, i - 2);
                    continue;
                }

                if (Mathf.Min(lenAB, lenBC) > maxLegLength)
                {
                    continue;
                }

                float dot = Vector3.Dot(ab / lenAB, bc / lenBC);
                if (dot > -0.84f)
                {
                    continue;
                }

                points.RemoveAt(i);
                changed = true;
                i = Mathf.Max(0, i - 2);
            }

            return changed;
        }

        private static bool CleanupShortBacktrackingDoglegs(List<Vector3> points, float maxLegLength, float maxMiddleLength)
        {
            bool changed = false;
            for (int i = 1; i < points.Count - 2; i++)
            {
                Vector3 a = points[i - 1];
                Vector3 b = points[i];
                Vector3 c = points[i + 1];
                Vector3 d = points[i + 2];

                Vector3 ab = b - a;
                Vector3 bc = c - b;
                Vector3 cd = d - c;
                float lenAB = ab.magnitude;
                float lenBC = bc.magnitude;
                float lenCD = cd.magnitude;
                if (lenAB < Epsilon || lenBC < Epsilon || lenCD < Epsilon)
                {
                    continue;
                }

                if (lenBC > maxMiddleLength || Mathf.Min(lenAB, lenCD) > maxLegLength)
                {
                    continue;
                }

                float outerDot = Vector3.Dot(ab / lenAB, cd / lenCD);
                if (outerDot > -0.38f)
                {
                    continue;
                }

                Vector3 ad = d - a;
                float adLen = ad.magnitude;
                if (adLen < Epsilon)
                {
                    continue;
                }

                Vector3 adDir = ad / adLen;
                float progressAB = Vector3.Dot(ab, adDir);
                float progressCD = Vector3.Dot(cd, adDir);
                if (progressAB * progressCD >= 0f)
                {
                    continue;
                }

                points.RemoveAt(i + 1);
                points.RemoveAt(i);
                InsertAxisAlignedBridge(points, i, a, d);
                changed = true;
                i = Mathf.Max(0, i - 2);
            }

            return changed;
        }

        private static void InsertAxisAlignedBridge(List<Vector3> points, int insertIndex, Vector3 from, Vector3 to)
        {
            AxisMask mask = GetAxisMask(from, to);
            if (mask == AxisMask.None || IsAxisAlignedSegment(from, to))
            {
                return;
            }

            Axis[] order = ChooseBestAxisOrder(from, to, mask, 0f);
            Vector3 current = from;
            for (int i = 0; i < order.Length - 1; i++)
            {
                current = SetAxis(current, order[i], GetAxis(to, order[i]));
                points.Insert(insertIndex + i, current);
            }
        }

        private static bool TryGetAxisDirection(Vector3 segment, out Axis axis, out int dir, out float length)
        {
            axis = Axis.X;
            dir = 0;
            length = 0f;
            float absX = Mathf.Abs(segment.x);
            float absY = Mathf.Abs(segment.y);
            float absZ = Mathf.Abs(segment.z);
            float dominant = Mathf.Max(absX, Mathf.Max(absY, absZ));
            if (dominant < Epsilon)
            {
                return false;
            }

            float crossTolerance = dominant * 0.02f + 0.0005f;
            if (dominant == absX)
            {
                if (absY > crossTolerance || absZ > crossTolerance)
                {
                    return false;
                }

                axis = Axis.X;
                dir = segment.x >= 0f ? 1 : -1;
                length = absX;
                return true;
            }

            if (dominant == absY)
            {
                if (absX > crossTolerance || absZ > crossTolerance)
                {
                    return false;
                }

                axis = Axis.Y;
                dir = segment.y >= 0f ? 1 : -1;
                length = absY;
                return true;
            }

            if (absX > crossTolerance || absY > crossTolerance)
            {
                return false;
            }

            axis = Axis.Z;
            dir = segment.z >= 0f ? 1 : -1;
            length = absZ;
            return true;
        }

        [System.Flags]
        private enum AxisMask
        {
            None = 0,
            X = 1,
            Y = 2,
            Z = 4
        }

        private enum Axis
        {
            X,
            Y,
            Z
        }
    }

    public sealed class GlobalRouter
    {
        private const int AxisStateCount = 4;

        private enum AxisState : byte
        {
            None = 0,
            X = 1,
            Y = 2,
            Z = 3
        }

        private readonly struct RouteConstraint
        {
            public RouteConstraint(bool usePlanar, float planeY, float tolerance)
            {
                UsePlanar = usePlanar;
                PlaneY = planeY;
                Tolerance = tolerance;
            }

            public bool UsePlanar { get; }
            public float PlaneY { get; }
            public float Tolerance { get; }
        }

        public GlobalRoute RouteFlow(
            CorridorGraph graph,
            FlowRequest flow,
            RoutingConfig config,
            int[] edgeUsage,
            float[] edgeHistory,
            Dictionary<int, List<int>> crossingMap,
            out float routeCost,
            long deadlineTicks = long.MaxValue)
        {
            routeCost = 0f;
            if (flow == null || graph == null || graph.Nodes.Count == 0 || graph.Edges.Count == 0)
            {
                return null;
            }

            if (!flow.TryResolve(out SlotPose src, out SlotPose dst))
            {
                return null;
            }

            RouteConstraint constraint = BuildRouteConstraint(flow, src, dst, config);
            Vector3 startAnchor = src.Position + src.Normal * Mathf.Max(config.clearance, config.fanoutLength);
            Vector3 endAnchor = dst.Position + dst.Normal * Mathf.Max(config.clearance, config.fanoutLength);
            if (constraint.UsePlanar)
            {
                startAnchor.y = constraint.PlaneY;
                endAnchor.y = constraint.PlaneY;
            }

            int startNode = FindClosestNode(graph, startAnchor, constraint, src.Position, src.Normal);
            int targetNode = FindClosestNode(graph, endAnchor, constraint, dst.Position, dst.Normal);
            if (startNode < 0 || targetNode < 0)
            {
                return null;
            }

            int stateCount = graph.Nodes.Count * AxisStateCount;
            float[] gScore = new float[stateCount];
            int[] previousState = new int[stateCount];
            int[] previousEdge = new int[stateCount];
            byte[] closed = new byte[stateCount];
            for (int i = 0; i < stateCount; i++)
            {
                gScore[i] = float.PositiveInfinity;
                previousState[i] = -1;
                previousEdge[i] = -1;
            }

            int startState = EncodeState(startNode, AxisState.None);
            float startHeuristic = Heuristic(graph.Nodes[startNode], graph.Nodes[targetNode], config);
            float maxPrunedHeuristic = startHeuristic * Mathf.Max(1f, config.heuristicPruneMultiplier) + Mathf.Max(0f, config.heuristicPrunePadding);
            gScore[startState] = 0f;

            MinHeap open = new MinHeap();
            open.Push(startState, startHeuristic);

            int expandedNodes = 0;
            int goalState = -1;

            while (open.Count > 0)
            {
                if (Stopwatch.GetTimestamp() >= deadlineTicks)
                {
                    return null;
                }

                int currentState = open.PopMin();
                if (closed[currentState] != 0)
                {
                    continue;
                }

                closed[currentState] = 1;
                expandedNodes++;
                if (expandedNodes > config.maxExpandedNodesPerFlow)
                {
                    return null;
                }

                int currentNode = DecodeNode(currentState);
                if (currentNode == targetNode)
                {
                    goalState = currentState;
                    break;
                }

                AxisState currentAxis = DecodeAxis(currentState);
                List<int> incidentEdges = graph.NodeToEdges[currentNode];
                for (int edgeIndex = 0; edgeIndex < incidentEdges.Count; edgeIndex++)
                {
                    int edgeId = incidentEdges[edgeIndex];
                    CorridorEdge edge = graph.Edges[edgeId];
                    int neighborNode = graph.GetOtherNode(edgeId, currentNode);
                    if (constraint.UsePlanar)
                    {
                        if (edge.Axis == EdgeAxis.Y)
                        {
                            continue;
                        }

                        if (Mathf.Abs(graph.Nodes[neighborNode].y - constraint.PlaneY) > constraint.Tolerance)
                        {
                            continue;
                        }
                    }

                    AxisState nextAxis = AxisFromEdge(edge.Axis);
                    int nextState = EncodeState(neighborNode, nextAxis);
                    if (closed[nextState] != 0)
                    {
                        continue;
                    }

                    float heuristic = Heuristic(graph.Nodes[neighborNode], graph.Nodes[targetNode], config);
                    if (config.useHeuristicPruning && heuristic > maxPrunedHeuristic)
                    {
                        continue;
                    }

                    float bendPenalty = currentAxis != AxisState.None && currentAxis != nextAxis ? config.wBend : 0f;
                    float presentCongestion = (float)edgeUsage[edgeId] / Mathf.Max(1, edge.Capacity);
                    int overflow = Mathf.Max(0, edgeUsage[edgeId] + 1 - edge.Capacity);
                    float crossingEstimate = EstimateCrossingPenalty(edgeId, edgeUsage, edgeHistory, crossingMap);
                    float weightedCrossingPenalty = config.wCrossing * crossingEstimate * (1f + crossingEstimate * 0.08f);
                    float crossingHotspotPenalty = config.wCrossing * EstimateCrossingHotspotPenalty(edgeId, crossingMap);

                    float stepCost =
                        config.wLength * edge.Length +
                        edge.BaseCost +
                        bendPenalty +
                        config.wCongestion * presentCongestion +
                        config.wHistory * edgeHistory[edgeId] +
                        config.wOverflow * overflow +
                        weightedCrossingPenalty +
                        crossingHotspotPenalty;

                    float tentativeG = gScore[currentState] + stepCost;
                    if (tentativeG >= gScore[nextState])
                    {
                        continue;
                    }

                    gScore[nextState] = tentativeG;
                    previousState[nextState] = currentState;
                    previousEdge[nextState] = edgeId;
                    open.Push(nextState, tentativeG + heuristic);
                }
            }

            if (goalState < 0)
            {
                return null;
            }

            GlobalRoute route = ReconstructRoute(flow, graph, goalState, previousState, previousEdge);
            if (route == null)
            {
                return null;
            }

            int crossings = CountCrossings(route.EdgeIds, edgeUsage, crossingMap);
            route.CrossingCount = crossings;
            route.UsesCrossingFallback = crossings > 0;
            if (!config.mustRenderAll && crossings > config.maxCrossingFallback)
            {
                return null;
            }

            routeCost = gScore[goalState];
            return route;
        }

        private static GlobalRoute ReconstructRoute(
            FlowRequest flow,
            CorridorGraph graph,
            int goalState,
            IReadOnlyList<int> previousState,
            IReadOnlyList<int> previousEdge)
        {
            List<int> reversedEdges = new List<int>();
            List<int> reversedNodes = new List<int> { DecodeNode(goalState) };

            int currentState = goalState;
            while (previousState[currentState] >= 0)
            {
                reversedEdges.Add(previousEdge[currentState]);
                currentState = previousState[currentState];
                reversedNodes.Add(DecodeNode(currentState));
            }

            GlobalRoute route = new GlobalRoute { Flow = flow };
            reversedEdges.Reverse();
            reversedNodes.Reverse();
            route.EdgeIds.AddRange(reversedEdges);
            route.NodeIds.AddRange(reversedNodes);

            float totalLength = 0f;
            int bends = 0;
            EdgeAxis? previousAxis = null;
            for (int i = 0; i < route.EdgeIds.Count; i++)
            {
                CorridorEdge edge = graph.Edges[route.EdgeIds[i]];
                totalLength += edge.Length;
                if (previousAxis.HasValue && previousAxis.Value != edge.Axis)
                {
                    bends++;
                }

                previousAxis = edge.Axis;
            }

            route.Length = totalLength;
            route.Bends = bends;
            return route;
        }

        private static RouteConstraint BuildRouteConstraint(FlowRequest flow, SlotPose src, SlotPose dst, RoutingConfig config)
        {
            if (config == null || !config.enforcePlanarRoutingOnSameHeight)
            {
                return new RouteConstraint(false, 0f, 0f);
            }

            float tolerance = Mathf.Max(0.001f, config.sameHeightTolerance);
            float sourceHeight = src.Position.y;
            float targetHeight = dst.Position.y;
            if (flow?.Source?.Module != null)
            {
                sourceHeight = flow.Source.Module.GetWorldBounds().center.y;
            }

            if (flow?.Target?.Module != null)
            {
                targetHeight = flow.Target.Module.GetWorldBounds().center.y;
            }

            if (Mathf.Abs(sourceHeight - targetHeight) > tolerance)
            {
                return new RouteConstraint(false, 0f, 0f);
            }

            float planeY = (sourceHeight + targetHeight) * 0.5f;
            float planeTolerance = Mathf.Max(tolerance, config.planarNodeTolerance);
            return new RouteConstraint(true, planeY, planeTolerance);
        }

        private static AxisState AxisFromEdge(EdgeAxis axis)
        {
            return axis switch
            {
                EdgeAxis.X => AxisState.X,
                EdgeAxis.Y => AxisState.Y,
                _ => AxisState.Z
            };
        }

        private static int EncodeState(int nodeId, AxisState axis)
        {
            return nodeId * AxisStateCount + (int)axis;
        }

        private static int DecodeNode(int state)
        {
            return state / AxisStateCount;
        }

        private static AxisState DecodeAxis(int state)
        {
            return (AxisState)(state % AxisStateCount);
        }

        private static int FindClosestNode(
            CorridorGraph graph,
            Vector3 point,
            RouteConstraint constraint,
            Vector3 endpointPosition,
            Vector3 endpointNormal)
        {
            int preferredNode = FindClosestNode(
                graph,
                point,
                constraint,
                endpointPosition,
                endpointNormal,
                requireEndpointSide: true);
            if (preferredNode >= 0)
            {
                return preferredNode;
            }

            return FindClosestNode(
                graph,
                point,
                constraint,
                endpointPosition,
                endpointNormal,
                requireEndpointSide: false);
        }

        private static int FindClosestNode(
            CorridorGraph graph,
            Vector3 point,
            RouteConstraint constraint,
            Vector3 endpointPosition,
            Vector3 endpointNormal,
            bool requireEndpointSide)
        {
            if (graph.TryGetNode(point, out int exactNode))
            {
                Vector3 exactPoint = graph.Nodes[exactNode];
                if ((!constraint.UsePlanar || Mathf.Abs(exactPoint.y - constraint.PlaneY) <= constraint.Tolerance) &&
                    IsOnEndpointOuterSide(exactPoint, endpointPosition, endpointNormal, requireEndpointSide))
                {
                    return exactNode;
                }
            }

            int bestNode = -1;
            float bestSqrDistance = float.PositiveInfinity;
            for (int i = 0; i < graph.Nodes.Count; i++)
            {
                Vector3 node = graph.Nodes[i];
                if (constraint.UsePlanar && Mathf.Abs(node.y - constraint.PlaneY) > constraint.Tolerance)
                {
                    continue;
                }

                if (!IsOnEndpointOuterSide(node, endpointPosition, endpointNormal, requireEndpointSide))
                {
                    continue;
                }

                float sqr = (node - point).sqrMagnitude;
                if (sqr < bestSqrDistance)
                {
                    bestSqrDistance = sqr;
                    bestNode = i;
                    if (bestSqrDistance <= 0.000001f)
                    {
                        break;
                    }
                }
            }

            return bestNode;
        }

        private static bool IsOnEndpointOuterSide(
            Vector3 point,
            Vector3 endpointPosition,
            Vector3 endpointNormal,
            bool requireEndpointSide)
        {
            if (!requireEndpointSide || endpointNormal.sqrMagnitude < 0.5f)
            {
                return true;
            }

            Vector3 normal = endpointNormal.normalized;
            return Vector3.Dot(point - endpointPosition, normal) >= -0.001f;
        }

        private static float EstimateCrossingPenalty(
            int edgeId,
            int[] edgeUsage,
            float[] edgeHistory,
            Dictionary<int, List<int>> crossingMap)
        {
            if (!crossingMap.TryGetValue(edgeId, out List<int> crossingEdges))
            {
                return 0f;
            }

            float conflicts = 0f;
            for (int i = 0; i < crossingEdges.Count; i++)
            {
                int crossingEdgeId = crossingEdges[i];
                int usage = edgeUsage[crossingEdgeId];
                if (usage <= 0)
                {
                    continue;
                }

                conflicts += 1f + 0.35f * (usage - 1f);
                conflicts += edgeHistory[crossingEdgeId] * 0.05f;
            }

            return conflicts;
        }

        private static int CountCrossings(IReadOnlyList<int> routeEdges, int[] edgeUsage, Dictionary<int, List<int>> crossingMap)
        {
            int crossings = 0;
            for (int i = 0; i < routeEdges.Count; i++)
            {
                if (!crossingMap.TryGetValue(routeEdges[i], out List<int> crossingEdges))
                {
                    continue;
                }

                for (int crossingIndex = 0; crossingIndex < crossingEdges.Count; crossingIndex++)
                {
                    int usage = edgeUsage[crossingEdges[crossingIndex]];
                    if (usage > 0)
                    {
                        crossings += usage;
                    }
                }
            }

            return crossings;
        }

        private static float EstimateCrossingHotspotPenalty(int edgeId, Dictionary<int, List<int>> crossingMap)
        {
            if (!crossingMap.TryGetValue(edgeId, out List<int> crossingEdges))
            {
                return 0f;
            }

            return crossingEdges.Count * 0.03f;
        }

        private static float Heuristic(Vector3 current, Vector3 target, RoutingConfig config)
        {
            return (Mathf.Abs(target.x - current.x) + Mathf.Abs(target.y - current.y) + Mathf.Abs(target.z - current.z)) * config.wLength;
        }

        private sealed class MinHeap
        {
            private readonly List<HeapEntry> items = new List<HeapEntry>(256);

            public int Count => items.Count;

            public void Push(int state, float score)
            {
                items.Add(new HeapEntry(state, score));
                SiftUp(items.Count - 1);
            }

            public int PopMin()
            {
                HeapEntry root = items[0];
                int lastIndex = items.Count - 1;
                items[0] = items[lastIndex];
                items.RemoveAt(lastIndex);
                if (items.Count > 0)
                {
                    SiftDown(0);
                }

                return root.State;
            }

            private void SiftUp(int index)
            {
                while (index > 0)
                {
                    int parent = (index - 1) >> 1;
                    if (items[parent].Score <= items[index].Score)
                    {
                        break;
                    }

                    (items[parent], items[index]) = (items[index], items[parent]);
                    index = parent;
                }
            }

            private void SiftDown(int index)
            {
                int count = items.Count;
                while (true)
                {
                    int left = (index << 1) + 1;
                    if (left >= count)
                    {
                        break;
                    }

                    int right = left + 1;
                    int smallest = left;
                    if (right < count && items[right].Score < items[left].Score)
                    {
                        smallest = right;
                    }

                    if (items[index].Score <= items[smallest].Score)
                    {
                        break;
                    }

                    (items[index], items[smallest]) = (items[smallest], items[index]);
                    index = smallest;
                }
            }

            private readonly struct HeapEntry
            {
                public HeapEntry(int state, float score)
                {
                    State = state;
                    Score = score;
                }

                public int State { get; }
                public float Score { get; }
            }
        }
    }

    public sealed class NegotiatedCongestionSolver
    {
        private readonly GlobalRouter globalRouter = new GlobalRouter();

        public NegotiationResult Solve(
            CorridorGraph graph,
            IReadOnlyList<FlowRequest> flowRequests,
            RoutingConfig config,
            int maxIterationsOverride = -1)
        {
            NegotiationResult bestResult = new NegotiationResult
            {
                EdgeUsage = new int[graph.Edges.Count],
                EdgeOverflow = new int[graph.Edges.Count],
                EdgeHistory = new float[graph.Edges.Count]
            };

            if (graph == null || graph.Edges.Count == 0 || flowRequests.Count == 0)
            {
                return bestResult;
            }

            Dictionary<int, List<int>> crossingMap = BuildCrossingMap(graph, config);
            float[] edgeHistory = new float[graph.Edges.Count];
            bool crossingMapDisabled =
                config.disableCrossingMapWhenLarge &&
                config.crossingMapMaxEdges > 0 &&
                graph.Edges.Count > config.crossingMapMaxEdges;

            int iterations = maxIterationsOverride > 0 ? maxIterationsOverride : Mathf.Max(1, config.maxNegotiationIters);
            float bestScore = float.PositiveInfinity;
            long now = Stopwatch.GetTimestamp();
            long solveBudgetTicks = (long)(Mathf.Max(0.5f, config.maxSolveTimeSeconds) * Stopwatch.Frequency);
            long solveDeadline = now + solveBudgetTicks;
            List<FlowRequest> sortedFlows = SortFlows(graph, flowRequests, config, solveDeadline);
            bool crossingWarningAdded = false;
            int crossingOptimizationItersUsed = 0;

            for (int iter = 0; iter < iterations; iter++)
            {
                if (Stopwatch.GetTimestamp() >= solveDeadline)
                {
                    bestResult.Warnings.Add($"Routing stopped by solve timeout ({config.maxSolveTimeSeconds:F1}s).");
                    break;
                }

                bool prioritizeCrossings =
                    config.optimizeCrossingsAfterOverflow &&
                    crossingMap.Count > 0 &&
                    crossingOptimizationItersUsed > 0;
                IterationResult iteration = ExecuteIteration(
                    graph,
                    sortedFlows,
                    config,
                    edgeHistory,
                    crossingMap,
                    solveDeadline,
                    prioritizeCrossings);
                float score = ComputeScore(iteration);

                if (score < bestScore)
                {
                    bestScore = score;
                    bestResult = CopyToNegotiationResult(iteration, edgeHistory, iter + 1);
                    ApplyCrossingMetadata(bestResult, crossingMap, crossingMapDisabled, graph.Edges.Count, config, crossingOptimizationItersUsed);
                    if (crossingMapDisabled && !crossingWarningAdded)
                    {
                        bestResult.Warnings.Add(
                            $"Crossing-map skipped for performance (edges={graph.Edges.Count}, limit={config.crossingMapMaxEdges}).");
                        crossingWarningAdded = true;
                    }
                }

                UpdateHistoryCosts(edgeHistory, iteration.EdgeOverflow);
                if (iteration.Metrics.OverflowCount > 0)
                {
                    crossingOptimizationItersUsed = 0;
                    continue;
                }

                if (iteration.Metrics.CrossingCount <= 0 || !config.optimizeCrossingsAfterOverflow || crossingMap.Count == 0)
                {
                    break;
                }

                crossingOptimizationItersUsed++;
                if (crossingOptimizationItersUsed >= Mathf.Max(0, config.maxCrossingOptimizationIters))
                {
                    bestResult.Warnings.Add(
                        $"Crossing optimization stopped after {crossingOptimizationItersUsed} iteration(s) (limit={config.maxCrossingOptimizationIters}).");
                    break;
                }
            }

            ApplyCrossingMetadata(bestResult, crossingMap, crossingMapDisabled, graph.Edges.Count, config, crossingOptimizationItersUsed);
            return bestResult;
        }

        public async UniTask SolveIncrementalAsync(
            CorridorGraph graph,
            IReadOnlyList<FlowRequest> flowRequests,
            RoutingConfig config,
            Action<NegotiationProgress> onProgress,
            CancellationToken cancellationToken,
            int maxIterationsOverride = -1)
        {
            NegotiationResult bestResult = new NegotiationResult
            {
                EdgeUsage = graph != null ? new int[graph.Edges.Count] : Array.Empty<int>(),
                EdgeOverflow = graph != null ? new int[graph.Edges.Count] : Array.Empty<int>(),
                EdgeHistory = graph != null ? new float[graph.Edges.Count] : Array.Empty<float>()
            };

            if (graph == null || graph.Edges.Count == 0 || flowRequests == null || flowRequests.Count == 0)
            {
                onProgress?.Invoke(new NegotiationProgress
                {
                    Phase = NegotiationProgressPhase.Completed,
                    BestResult = bestResult,
                    IsFinal = true,
                    Message = "No routable flows."
                });
                return;
            }

            Dictionary<int, List<int>> crossingMap = BuildCrossingMap(graph, config);
            float[] edgeHistory = new float[graph.Edges.Count];
            bool crossingMapDisabled =
                config.disableCrossingMapWhenLarge &&
                config.crossingMapMaxEdges > 0 &&
                graph.Edges.Count > config.crossingMapMaxEdges;

            int iterations = maxIterationsOverride > 0 ? maxIterationsOverride : Mathf.Max(1, config.maxNegotiationIters);
            float bestScore = float.PositiveInfinity;
            long now = Stopwatch.GetTimestamp();
            long solveBudgetTicks = (long)(Mathf.Max(0.5f, config.maxSolveTimeSeconds) * Stopwatch.Frequency);
            long solveDeadline = now + solveBudgetTicks;
            List<FlowRequest> sortedFlows = SortFlows(graph, flowRequests, config, solveDeadline);
            bool crossingWarningAdded = false;
            int crossingOptimizationItersUsed = 0;

            onProgress?.Invoke(new NegotiationProgress
            {
                Phase = NegotiationProgressPhase.Started,
                BestResult = bestResult,
                Iteration = 0,
                CompletedFlows = 0,
                TotalFlows = sortedFlows.Count,
                Message = "Incremental routing started."
            });
            await UniTask.Yield(cancellationToken);

            for (int iter = 0; iter < iterations; iter++)
            {
                cancellationToken.ThrowIfCancellationRequested();
                if (Stopwatch.GetTimestamp() >= solveDeadline)
                {
                    bestResult.Warnings.Add($"Routing stopped by solve timeout ({config.maxSolveTimeSeconds:F1}s).");
                    break;
                }

                bool prioritizeCrossings =
                    config.optimizeCrossingsAfterOverflow &&
                    crossingMap.Count > 0 &&
                    crossingOptimizationItersUsed > 0;

                onProgress?.Invoke(new NegotiationProgress
                {
                    Phase = NegotiationProgressPhase.IterationStarted,
                    BestResult = bestResult,
                    Iteration = iter + 1,
                    CompletedFlows = 0,
                    TotalFlows = sortedFlows.Count,
                    Message = $"Negotiation iteration {iter + 1} started."
                });
                await UniTask.Yield(cancellationToken);

                IterationResult iteration = new IterationResult
                {
                    EdgeUsage = new int[graph.Edges.Count],
                    EdgeOverflow = new int[graph.Edges.Count]
                };

                await ExecuteIterationIncrementalAsync(
                    graph,
                    sortedFlows,
                    config,
                    edgeHistory,
                    crossingMap,
                    solveDeadline,
                    prioritizeCrossings,
                    iteration,
                    onProgress,
                    iter + 1,
                    cancellationToken);

                float score = ComputeScore(iteration);
                if (score < bestScore)
                {
                    bestScore = score;
                    bestResult = CopyToNegotiationResult(iteration, edgeHistory, iter + 1);
                    ApplyCrossingMetadata(bestResult, crossingMap, crossingMapDisabled, graph.Edges.Count, config, crossingOptimizationItersUsed);
                    if (crossingMapDisabled && !crossingWarningAdded)
                    {
                        bestResult.Warnings.Add(
                            $"Crossing-map skipped for performance (edges={graph.Edges.Count}, limit={config.crossingMapMaxEdges}).");
                        crossingWarningAdded = true;
                    }
                }

                UpdateHistoryCosts(edgeHistory, iteration.EdgeOverflow);
                onProgress?.Invoke(new NegotiationProgress
                {
                    Phase = NegotiationProgressPhase.IterationCompleted,
                    Routes = new List<GlobalRoute>(bestResult.Routes),
                    BestResult = bestResult,
                    Iteration = iter + 1,
                    CompletedFlows = iteration.Routes.Count,
                    TotalFlows = sortedFlows.Count,
                    Message = $"Negotiation iteration {iter + 1} completed."
                });
                await UniTask.Yield(cancellationToken);

                if (iteration.Metrics.OverflowCount > 0)
                {
                    crossingOptimizationItersUsed = 0;
                    continue;
                }

                if (iteration.Metrics.CrossingCount <= 0 || !config.optimizeCrossingsAfterOverflow || crossingMap.Count == 0)
                {
                    break;
                }

                crossingOptimizationItersUsed++;
                if (crossingOptimizationItersUsed >= Mathf.Max(0, config.maxCrossingOptimizationIters))
                {
                    bestResult.Warnings.Add(
                        $"Crossing optimization stopped after {crossingOptimizationItersUsed} iteration(s) (limit={config.maxCrossingOptimizationIters}).");
                    break;
                }
            }

            ApplyCrossingMetadata(bestResult, crossingMap, crossingMapDisabled, graph.Edges.Count, config, crossingOptimizationItersUsed);
            onProgress?.Invoke(new NegotiationProgress
            {
                Phase = NegotiationProgressPhase.Completed,
                Routes = new List<GlobalRoute>(bestResult.Routes),
                BestResult = bestResult,
                Iteration = bestResult.IterationsUsed,
                CompletedFlows = bestResult.Routes.Count,
                TotalFlows = sortedFlows.Count,
                IsFinal = true,
                Message = "Incremental routing completed."
            });
        }

        private IterationResult ExecuteIteration(
            CorridorGraph graph,
            IReadOnlyList<FlowRequest> sortedFlows,
            RoutingConfig config,
            float[] edgeHistory,
            Dictionary<int, List<int>> crossingMap,
            long solveDeadlineTicks,
            bool prioritizeCrossings)
        {
            IterationResult result = new IterationResult
            {
                EdgeUsage = new int[graph.Edges.Count],
                EdgeOverflow = new int[graph.Edges.Count]
            };

            List<(GlobalRoute route, float cost)> routed = new List<(GlobalRoute route, float cost)>();

            for (int i = 0; i < sortedFlows.Count; i++)
            {
                if (Stopwatch.GetTimestamp() >= solveDeadlineTicks)
                {
                    result.FailedFlows.Add(sortedFlows[i]);
                    continue;
                }

                FlowRequest flow = sortedFlows[i];
                long routeBudgetTicks = (long)(Mathf.Max(0.01f, config.maxRouteTimeSeconds) * Stopwatch.Frequency);
                long routeDeadline = Stopwatch.GetTimestamp() + routeBudgetTicks;
                if (routeDeadline > solveDeadlineTicks)
                {
                    routeDeadline = solveDeadlineTicks;
                }

                GlobalRoute route = globalRouter.RouteFlow(
                    graph,
                    flow,
                    config,
                    result.EdgeUsage,
                    edgeHistory,
                    crossingMap,
                    out float routeCost,
                    routeDeadline);

                if (route == null)
                {
                    result.FailedFlows.Add(flow);
                    continue;
                }

                routed.Add((route, routeCost));
                IncrementUsage(result.EdgeUsage, route.EdgeIds, +1);
            }

            if (Stopwatch.GetTimestamp() < solveDeadlineTicks)
            {
                ApplyRipUpAndReroute(
                    graph,
                    routed,
                    result,
                    config,
                    edgeHistory,
                    crossingMap,
                    solveDeadlineTicks,
                    prioritizeCrossings);
            }

            for (int edgeId = 0; edgeId < graph.Edges.Count; edgeId++)
            {
                int overflow = Mathf.Max(0, result.EdgeUsage[edgeId] - graph.Edges[edgeId].Capacity);
                result.EdgeOverflow[edgeId] = overflow;
                result.Metrics.OverflowCount += overflow;
            }

            for (int i = 0; i < routed.Count; i++)
            {
                GlobalRoute route = routed[i].route;
                int crossingCount = CountCrossings(route.EdgeIds, result.EdgeUsage, crossingMap);
                route.CrossingCount = crossingCount;
                route.UsesCrossingFallback = crossingCount > 0;

                result.Routes.Add(route);
                result.Metrics.TotalLength += route.Length;
                result.Metrics.TotalBends += route.Bends;
                result.Metrics.CrossingCount += crossingCount;
            }

            result.Metrics.RoutedCount = result.Routes.Count;
            result.Metrics.FailedCount = result.FailedFlows.Count;
            return result;
        }

        private async UniTask ExecuteIterationIncrementalAsync(
            CorridorGraph graph,
            IReadOnlyList<FlowRequest> sortedFlows,
            RoutingConfig config,
            float[] edgeHistory,
            Dictionary<int, List<int>> crossingMap,
            long solveDeadlineTicks,
            bool prioritizeCrossings,
            IterationResult result,
            Action<NegotiationProgress> onProgress,
            int iterationIndex,
            CancellationToken cancellationToken)
        {
            List<(GlobalRoute route, float cost)> routed = new List<(GlobalRoute route, float cost)>();

            for (int i = 0; i < sortedFlows.Count; i++)
            {
                cancellationToken.ThrowIfCancellationRequested();
                FlowRequest flow = sortedFlows[i];
                if (Stopwatch.GetTimestamp() >= solveDeadlineTicks)
                {
                    result.FailedFlows.Add(flow);
                    onProgress?.Invoke(new NegotiationProgress
                    {
                        Phase = NegotiationProgressPhase.RouteCommitted,
                        RoutedPreviewCount = routed.Count,
                        LatestFlow = flow,
                        Iteration = iterationIndex,
                        CompletedFlows = i + 1,
                        TotalFlows = sortedFlows.Count,
                        Message = "Route skipped because solve timeout was reached."
                    });
                    await UniTask.Yield(cancellationToken);
                    continue;
                }

                long routeBudgetTicks = (long)(Mathf.Max(0.01f, config.maxRouteTimeSeconds) * Stopwatch.Frequency);
                long routeDeadline = Stopwatch.GetTimestamp() + routeBudgetTicks;
                if (routeDeadline > solveDeadlineTicks)
                {
                    routeDeadline = solveDeadlineTicks;
                }

                GlobalRoute route = globalRouter.RouteFlow(
                    graph,
                    flow,
                    config,
                    result.EdgeUsage,
                    edgeHistory,
                    crossingMap,
                    out float routeCost,
                    routeDeadline);

                if (route == null)
                {
                    result.FailedFlows.Add(flow);
                    onProgress?.Invoke(new NegotiationProgress
                    {
                        Phase = NegotiationProgressPhase.RouteCommitted,
                        RoutedPreviewCount = routed.Count,
                        LatestFlow = flow,
                        Iteration = iterationIndex,
                        CompletedFlows = i + 1,
                        TotalFlows = sortedFlows.Count,
                        Message = "Flow failed to route in this iteration."
                    });
                    await UniTask.Yield(cancellationToken);
                    continue;
                }

                routed.Add((route, routeCost));
                IncrementUsage(result.EdgeUsage, route.EdgeIds, +1);
                onProgress?.Invoke(new NegotiationProgress
                {
                    Phase = NegotiationProgressPhase.RouteCommitted,
                    RoutedPreviewCount = routed.Count,
                    LatestRoute = route,
                    LatestFlow = flow,
                    Iteration = iterationIndex,
                    CompletedFlows = i + 1,
                    TotalFlows = sortedFlows.Count,
                    Message = $"Routed {i + 1}/{sortedFlows.Count}."
                });
                await UniTask.Yield(cancellationToken);
            }

            if (Stopwatch.GetTimestamp() < solveDeadlineTicks)
            {
                await ApplyRipUpAndRerouteIncrementalAsync(
                    graph,
                    routed,
                    result,
                    config,
                    edgeHistory,
                    crossingMap,
                    solveDeadlineTicks,
                    prioritizeCrossings,
                    onProgress,
                    iterationIndex,
                    sortedFlows.Count,
                    cancellationToken);
            }

            for (int edgeId = 0; edgeId < graph.Edges.Count; edgeId++)
            {
                int overflow = Mathf.Max(0, result.EdgeUsage[edgeId] - graph.Edges[edgeId].Capacity);
                result.EdgeOverflow[edgeId] = overflow;
                result.Metrics.OverflowCount += overflow;
            }

            for (int i = 0; i < routed.Count; i++)
            {
                GlobalRoute route = routed[i].route;
                int crossingCount = CountCrossings(route.EdgeIds, result.EdgeUsage, crossingMap);
                route.CrossingCount = crossingCount;
                route.UsesCrossingFallback = crossingCount > 0;

                result.Routes.Add(route);
                result.Metrics.TotalLength += route.Length;
                result.Metrics.TotalBends += route.Bends;
                result.Metrics.CrossingCount += crossingCount;
            }

            result.Metrics.RoutedCount = result.Routes.Count;
            result.Metrics.FailedCount = result.FailedFlows.Count;
        }

        private void ApplyRipUpAndReroute(
            CorridorGraph graph,
            List<(GlobalRoute route, float cost)> routed,
            IterationResult result,
            RoutingConfig config,
            float[] edgeHistory,
            Dictionary<int, List<int>> crossingMap,
            long solveDeadlineTicks,
            bool prioritizeCrossings)
        {
            if (routed.Count == 0)
            {
                return;
            }

            bool hasOverflow = HasOverflow(graph, result.EdgeUsage);
            bool crossingMode = !hasOverflow && prioritizeCrossings && crossingMap.Count > 0;
            float ripUpRatio = hasOverflow ? config.ripUpPercent : config.crossingRipUpPercent;
            if (ripUpRatio <= 0f)
            {
                return;
            }

            int rerouteCount = Mathf.CeilToInt(routed.Count * ripUpRatio);
            rerouteCount = Mathf.Clamp(rerouteCount, 1, routed.Count);

            routed.Sort((left, right) =>
            {
                float leftScore = ComputeOverflowContribution(left.route, graph, result.EdgeUsage);
                float rightScore = ComputeOverflowContribution(right.route, graph, result.EdgeUsage);
                int leftCrossings = ComputeCrossingContribution(left.route, result.EdgeUsage, crossingMap);
                int rightCrossings = ComputeCrossingContribution(right.route, result.EdgeUsage, crossingMap);

                float leftRank = crossingMode
                    ? leftCrossings * Mathf.Max(1f, config.crossingRerouteBoost) + leftScore * 4f + left.route.Bends * 0.2f
                    : leftScore * 100f + leftCrossings * 0.5f + left.route.Bends;
                float rightRank = crossingMode
                    ? rightCrossings * Mathf.Max(1f, config.crossingRerouteBoost) + rightScore * 4f + right.route.Bends * 0.2f
                    : rightScore * 100f + rightCrossings * 0.5f + right.route.Bends;
                return rightRank.CompareTo(leftRank);
            });

            List<(GlobalRoute route, float cost)> selected = routed.GetRange(0, rerouteCount);
            for (int i = 0; i < selected.Count; i++)
            {
                IncrementUsage(result.EdgeUsage, selected[i].route.EdgeIds, -1);
            }

            for (int i = 0; i < selected.Count; i++)
            {
                FlowRequest flow = selected[i].route.Flow;
                if (Stopwatch.GetTimestamp() >= solveDeadlineTicks)
                {
                    result.FailedFlows.Add(flow);
                    continue;
                }

                long routeBudgetTicks = (long)(Mathf.Max(0.01f, config.maxRouteTimeSeconds) * Stopwatch.Frequency);
                long routeDeadline = Stopwatch.GetTimestamp() + routeBudgetTicks;
                if (routeDeadline > solveDeadlineTicks)
                {
                    routeDeadline = solveDeadlineTicks;
                }

                GlobalRoute rerouted = globalRouter.RouteFlow(
                    graph,
                    flow,
                    config,
                    result.EdgeUsage,
                    edgeHistory,
                    crossingMap,
                    out float cost,
                    routeDeadline);

                if (rerouted == null)
                {
                    result.FailedFlows.Add(flow);
                    continue;
                }

                selected[i] = (rerouted, cost);
                IncrementUsage(result.EdgeUsage, rerouted.EdgeIds, +1);
            }

            for (int i = 0; i < selected.Count; i++)
            {
                routed[i] = selected[i];
            }
        }

        private async UniTask ApplyRipUpAndRerouteIncrementalAsync(
            CorridorGraph graph,
            List<(GlobalRoute route, float cost)> routed,
            IterationResult result,
            RoutingConfig config,
            float[] edgeHistory,
            Dictionary<int, List<int>> crossingMap,
            long solveDeadlineTicks,
            bool prioritizeCrossings,
            Action<NegotiationProgress> onProgress,
            int iterationIndex,
            int totalFlows,
            CancellationToken cancellationToken)
        {
            if (routed.Count == 0)
            {
                return;
            }

            bool hasOverflow = HasOverflow(graph, result.EdgeUsage);
            bool crossingMode = !hasOverflow && prioritizeCrossings && crossingMap.Count > 0;
            float ripUpRatio = hasOverflow ? config.ripUpPercent : config.crossingRipUpPercent;
            if (ripUpRatio <= 0f)
            {
                return;
            }

            int rerouteCount = Mathf.CeilToInt(routed.Count * ripUpRatio);
            rerouteCount = Mathf.Clamp(rerouteCount, 1, routed.Count);

            routed.Sort((left, right) =>
            {
                float leftScore = ComputeOverflowContribution(left.route, graph, result.EdgeUsage);
                float rightScore = ComputeOverflowContribution(right.route, graph, result.EdgeUsage);
                int leftCrossings = ComputeCrossingContribution(left.route, result.EdgeUsage, crossingMap);
                int rightCrossings = ComputeCrossingContribution(right.route, result.EdgeUsage, crossingMap);

                float leftRank = crossingMode
                    ? leftCrossings * Mathf.Max(1f, config.crossingRerouteBoost) + leftScore * 4f + left.route.Bends * 0.2f
                    : leftScore * 100f + leftCrossings * 0.5f + left.route.Bends;
                float rightRank = crossingMode
                    ? rightCrossings * Mathf.Max(1f, config.crossingRerouteBoost) + rightScore * 4f + right.route.Bends * 0.2f
                    : rightScore * 100f + rightCrossings * 0.5f + right.route.Bends;
                return rightRank.CompareTo(leftRank);
            });

            for (int i = 0; i < rerouteCount; i++)
            {
                IncrementUsage(result.EdgeUsage, routed[i].route.EdgeIds, -1);
            }

            for (int i = 0; i < rerouteCount; i++)
            {
                cancellationToken.ThrowIfCancellationRequested();
                GlobalRoute previousRoute = routed[i].route;
                FlowRequest flow = previousRoute.Flow;
                if (Stopwatch.GetTimestamp() >= solveDeadlineTicks)
                {
                    IncrementUsage(result.EdgeUsage, previousRoute.EdgeIds, +1);
                    continue;
                }

                long routeBudgetTicks = (long)(Mathf.Max(0.01f, config.maxRouteTimeSeconds) * Stopwatch.Frequency);
                long routeDeadline = Stopwatch.GetTimestamp() + routeBudgetTicks;
                if (routeDeadline > solveDeadlineTicks)
                {
                    routeDeadline = solveDeadlineTicks;
                }

                GlobalRoute rerouted = globalRouter.RouteFlow(
                    graph,
                    flow,
                    config,
                    result.EdgeUsage,
                    edgeHistory,
                    crossingMap,
                    out float cost,
                    routeDeadline);

                if (rerouted == null)
                {
                    IncrementUsage(result.EdgeUsage, previousRoute.EdgeIds, +1);
                    continue;
                }

                routed[i] = (rerouted, cost);
                IncrementUsage(result.EdgeUsage, rerouted.EdgeIds, +1);
                onProgress?.Invoke(new NegotiationProgress
                {
                    Phase = NegotiationProgressPhase.RouteCommitted,
                    RoutedPreviewCount = routed.Count,
                    LatestRoute = rerouted,
                    LatestFlow = flow,
                    Iteration = iterationIndex,
                    CompletedFlows = Mathf.Min(totalFlows, totalFlows - rerouteCount + i + 1),
                    TotalFlows = totalFlows,
                    Message = $"Rerouted {i + 1}/{rerouteCount} selected flow(s)."
                });
                await UniTask.Yield(cancellationToken);
            }
        }

        private static void IncrementUsage(int[] edgeUsage, IReadOnlyList<int> edges, int delta)
        {
            for (int i = 0; i < edges.Count; i++)
            {
                int edgeId = edges[i];
                edgeUsage[edgeId] = Mathf.Max(0, edgeUsage[edgeId] + delta);
            }
        }

        private static float ComputeOverflowContribution(GlobalRoute route, CorridorGraph graph, int[] edgeUsage)
        {
            float score = 0f;
            for (int i = 0; i < route.EdgeIds.Count; i++)
            {
                int edgeId = route.EdgeIds[i];
                CorridorEdge edge = graph.Edges[edgeId];
                int overflow = Mathf.Max(0, edgeUsage[edgeId] - edge.Capacity);
                score += overflow;
            }

            return score * 10f + route.Bends;
        }

        private static bool HasOverflow(CorridorGraph graph, int[] edgeUsage)
        {
            for (int edgeId = 0; edgeId < graph.Edges.Count; edgeId++)
            {
                if (edgeUsage[edgeId] > graph.Edges[edgeId].Capacity)
                {
                    return true;
                }
            }

            return false;
        }

        private static int ComputeCrossingContribution(
            GlobalRoute route,
            int[] edgeUsage,
            Dictionary<int, List<int>> crossingMap)
        {
            if (route == null || crossingMap == null || crossingMap.Count == 0)
            {
                return 0;
            }

            int crossings = 0;
            for (int i = 0; i < route.EdgeIds.Count; i++)
            {
                int edgeId = route.EdgeIds[i];
                if (!crossingMap.TryGetValue(edgeId, out List<int> crossingEdges))
                {
                    continue;
                }

                for (int crossingIndex = 0; crossingIndex < crossingEdges.Count; crossingIndex++)
                {
                    int crossingEdgeId = crossingEdges[crossingIndex];
                    int usage = edgeUsage[crossingEdgeId];
                    if (usage > 0)
                    {
                        crossings += usage;
                    }
                }
            }

            return crossings;
        }

        private static int CountCrossings(
            IReadOnlyList<int> routeEdges,
            int[] edgeUsage,
            Dictionary<int, List<int>> crossingMap)
        {
            if (routeEdges == null || crossingMap == null || crossingMap.Count == 0)
            {
                return 0;
            }

            int crossings = 0;
            for (int i = 0; i < routeEdges.Count; i++)
            {
                int edgeId = routeEdges[i];
                if (!crossingMap.TryGetValue(edgeId, out List<int> crossingEdges))
                {
                    continue;
                }

                for (int crossingIndex = 0; crossingIndex < crossingEdges.Count; crossingIndex++)
                {
                    int usage = edgeUsage[crossingEdges[crossingIndex]];
                    if (usage > 0)
                    {
                        crossings += usage;
                    }
                }
            }

            return crossings;
        }

        private List<FlowRequest> SortFlows(
            CorridorGraph graph,
            IReadOnlyList<FlowRequest> flowRequests,
            RoutingConfig config,
            long solveDeadlineTicks)
        {
            List<(FlowRequest flow, float length)> sorted = new List<(FlowRequest flow, float length)>(flowRequests.Count);
            int edgeCount = graph != null ? graph.Edges.Count : 0;
            int[] emptyUsage = edgeCount > 0 ? new int[edgeCount] : Array.Empty<int>();
            float[] emptyHistory = edgeCount > 0 ? new float[edgeCount] : Array.Empty<float>();
            Dictionary<int, List<int>> emptyCrossingMap = new Dictionary<int, List<int>>();
            for (int i = 0; i < flowRequests.Count; i++)
            {
                FlowRequest flow = flowRequests[i];
                if (flow == null)
                {
                    continue;
                }

                float length = float.PositiveInfinity;
                if (graph != null && edgeCount > 0 && config != null && Stopwatch.GetTimestamp() < solveDeadlineTicks)
                {
                    long routeBudgetTicks = (long)(Mathf.Max(0.01f, config.maxRouteTimeSeconds) * Stopwatch.Frequency);
                    long routeDeadline = Stopwatch.GetTimestamp() + routeBudgetTicks;
                    if (routeDeadline > solveDeadlineTicks)
                    {
                        routeDeadline = solveDeadlineTicks;
                    }

                    GlobalRoute estimate = globalRouter.RouteFlow(
                        graph,
                        flow,
                        config,
                        emptyUsage,
                        emptyHistory,
                        emptyCrossingMap,
                        out _,
                        routeDeadline);

                    if (estimate != null)
                    {
                        length = estimate.Length;
                    }
                }

                sorted.Add((flow, length));
            }

            sorted.Sort((left, right) =>
            {
                int lengthCompare = left.length.CompareTo(right.length);
                if (lengthCompare != 0)
                {
                    return lengthCompare;
                }

                string leftId = string.IsNullOrWhiteSpace(left.flow.Id) ? string.Empty : left.flow.Id;
                string rightId = string.IsNullOrWhiteSpace(right.flow.Id) ? string.Empty : right.flow.Id;
                return string.CompareOrdinal(leftId, rightId);
            });

            List<FlowRequest> result = new List<FlowRequest>(sorted.Count);
            for (int i = 0; i < sorted.Count; i++)
            {
                result.Add(sorted[i].flow);
            }

            return result;
        }

        private static void ApplyCrossingMetadata(
            NegotiationResult result,
            Dictionary<int, List<int>> crossingMap,
            bool crossingMapSkippedByLimit,
            int graphEdgeCount,
            RoutingConfig config,
            int crossingOptimizationIterationsUsed)
        {
            if (result == null)
            {
                return;
            }

            result.Metrics.CrossingMapEnabled = crossingMap != null && crossingMap.Count > 0;
            result.Metrics.CrossingMapSkippedByLimit = crossingMapSkippedByLimit;
            result.Metrics.CrossingMapEdges = graphEdgeCount;
            result.Metrics.CrossingOptimizationIterationsUsed = crossingOptimizationIterationsUsed;
            result.Metrics.RequestedCrossingOptimizationIterations = config != null
                ? Mathf.Max(0, config.maxCrossingOptimizationIters)
                : 0;
        }

        private static Dictionary<int, List<int>> BuildCrossingMap(CorridorGraph graph, RoutingConfig config)
        {
            if (config.disableCrossingMapWhenLarge &&
                config.crossingMapMaxEdges > 0 &&
                graph.Edges.Count > config.crossingMapMaxEdges)
            {
                return new Dictionary<int, List<int>>();
            }

            Dictionary<int, List<int>> map = new Dictionary<int, List<int>>();
            for (int i = 0; i < graph.Edges.Count; i++)
            {
                map[i] = new List<int>();
            }

            for (int i = 0; i < graph.Edges.Count; i++)
            {
                CorridorEdge first = graph.Edges[i];
                Vector3 a1 = graph.Nodes[first.NodeA];
                Vector3 a2 = graph.Nodes[first.NodeB];

                for (int j = i + 1; j < graph.Edges.Count; j++)
                {
                    CorridorEdge second = graph.Edges[j];
                    if (first.Axis == second.Axis)
                    {
                        continue;
                    }

                    Vector3 b1 = graph.Nodes[second.NodeA];
                    Vector3 b2 = graph.Nodes[second.NodeB];

                    if (!EdgesCrossInterior(first.Axis, a1, a2, second.Axis, b1, b2))
                    {
                        continue;
                    }

                    map[i].Add(j);
                    map[j].Add(i);
                }
            }

            return map;
        }

        private static bool EdgesCrossInterior(
            EdgeAxis axisA,
            Vector3 a1,
            Vector3 a2,
            EdgeAxis axisB,
            Vector3 b1,
            Vector3 b2)
        {
            const float epsilon = 0.0001f;
            if (ShareEndpoint(a1, a2, b1, b2, epsilon))
            {
                return false;
            }

            if (axisA == EdgeAxis.X && axisB == EdgeAxis.Y)
            {
                return CrossXY(a1, a2, b1, b2, epsilon);
            }
            if (axisA == EdgeAxis.Y && axisB == EdgeAxis.X)
            {
                return CrossXY(b1, b2, a1, a2, epsilon);
            }
            if (axisA == EdgeAxis.X && axisB == EdgeAxis.Z)
            {
                return CrossXZ(a1, a2, b1, b2, epsilon);
            }
            if (axisA == EdgeAxis.Z && axisB == EdgeAxis.X)
            {
                return CrossXZ(b1, b2, a1, a2, epsilon);
            }
            if (axisA == EdgeAxis.Y && axisB == EdgeAxis.Z)
            {
                return CrossYZ(a1, a2, b1, b2, epsilon);
            }
            if (axisA == EdgeAxis.Z && axisB == EdgeAxis.Y)
            {
                return CrossYZ(b1, b2, a1, a2, epsilon);
            }

            return false;
        }

        private static bool CrossXY(Vector3 x0, Vector3 x1, Vector3 y0, Vector3 y1, float epsilon)
        {
            if (Mathf.Abs(x0.z - y0.z) > epsilon)
            {
                return false;
            }

            float x = y0.x;
            float y = x0.y;
            return IsInsideRange(x, x0.x, x1.x, epsilon) &&
                   IsInsideRange(y, y0.y, y1.y, epsilon);
        }

        private static bool CrossXZ(Vector3 x0, Vector3 x1, Vector3 z0, Vector3 z1, float epsilon)
        {
            if (Mathf.Abs(x0.y - z0.y) > epsilon)
            {
                return false;
            }

            float x = z0.x;
            float z = x0.z;
            return IsInsideRange(x, x0.x, x1.x, epsilon) &&
                   IsInsideRange(z, z0.z, z1.z, epsilon);
        }

        private static bool CrossYZ(Vector3 y0, Vector3 y1, Vector3 z0, Vector3 z1, float epsilon)
        {
            if (Mathf.Abs(y0.x - z0.x) > epsilon)
            {
                return false;
            }

            float y = z0.y;
            float z = y0.z;
            return IsInsideRange(y, y0.y, y1.y, epsilon) &&
                   IsInsideRange(z, z0.z, z1.z, epsilon);
        }

        private static bool IsInsideRange(float value, float a, float b, float epsilon)
        {
            float min = Mathf.Min(a, b);
            float max = Mathf.Max(a, b);
            return value > min + epsilon && value < max - epsilon;
        }

        private static bool ShareEndpoint(Vector3 a0, Vector3 a1, Vector3 b0, Vector3 b1, float epsilon)
        {
            return ApproximatelyEqual(a0, b0, epsilon) ||
                   ApproximatelyEqual(a0, b1, epsilon) ||
                   ApproximatelyEqual(a1, b0, epsilon) ||
                   ApproximatelyEqual(a1, b1, epsilon);
        }

        private static bool ApproximatelyEqual(Vector3 a, Vector3 b, float epsilon)
        {
            return (a - b).sqrMagnitude <= epsilon * epsilon;
        }

        private static float ComputeScore(IterationResult iteration)
        {
            return iteration.Metrics.OverflowCount * 10000f +
                   iteration.Metrics.FailedCount * 1000f +
                   iteration.Metrics.CrossingCount * 250f +
                   iteration.Metrics.TotalLength;
        }

        private static void UpdateHistoryCosts(float[] edgeHistory, int[] overflow)
        {
            for (int i = 0; i < edgeHistory.Length; i++)
            {
                if (overflow[i] > 0)
                {
                    edgeHistory[i] += overflow[i];
                }
            }
        }

        private static NegotiationResult CopyToNegotiationResult(IterationResult iteration, float[] historySnapshot, int iterationsUsed)
        {
            NegotiationResult copy = new NegotiationResult
            {
                EdgeUsage = (int[])iteration.EdgeUsage.Clone(),
                EdgeOverflow = (int[])iteration.EdgeOverflow.Clone(),
                EdgeHistory = (float[])historySnapshot.Clone(),
                IterationsUsed = iterationsUsed
            };

            copy.Routes.AddRange(iteration.Routes);
            copy.FailedFlows.AddRange(iteration.FailedFlows);
            copy.Metrics = new RoutingMetrics
            {
                RoutedCount = iteration.Metrics.RoutedCount,
                FailedCount = iteration.Metrics.FailedCount,
                TotalLength = iteration.Metrics.TotalLength,
                TotalBends = iteration.Metrics.TotalBends,
                OverflowCount = iteration.Metrics.OverflowCount,
                CrossingCount = iteration.Metrics.CrossingCount,
                ModuleCollisionCount = iteration.Metrics.ModuleCollisionCount
            };

            if (copy.Metrics.OverflowCount > 0)
            {
                copy.Warnings.Add($"Negotiated congestion converged with overflow={copy.Metrics.OverflowCount}.");
            }

            if (copy.Metrics.FailedCount > 0)
            {
                copy.Warnings.Add($"Failed to route {copy.Metrics.FailedCount} flow(s).");
            }

            return copy;
        }

        private sealed class IterationResult
        {
            public readonly List<GlobalRoute> Routes = new List<GlobalRoute>();
            public readonly List<FlowRequest> FailedFlows = new List<FlowRequest>();
            public readonly RoutingMetrics Metrics = new RoutingMetrics();
            public int[] EdgeUsage = Array.Empty<int>();
            public int[] EdgeOverflow = Array.Empty<int>();
        }
    }

    public sealed class RouteValidator
    {
        private const float Epsilon = 0.0001f;

        public RouteValidationReport Validate(
            IReadOnlyList<ModuleNode> modules,
            IReadOnlyList<DetailedPath> paths,
            RoutingConfig config = null)
        {
            RouteValidationReport report = new RouteValidationReport();
            if (paths == null)
            {
                return report;
            }

            int checks = 0;
            int checkBudget = config != null ? Mathf.Max(1000, config.maxValidationChecks) : int.MaxValue;
            bool truncated = false;

            ValidateModuleCollisions(modules, paths, report, ref checks, checkBudget, ref truncated);
            if (!truncated)
            {
                ValidateFlowCrossings(paths, report, ref checks, checkBudget, ref truncated);
            }

            if (report.ModuleCollisionCount > 0)
            {
                report.Errors.Add($"Detected {report.ModuleCollisionCount} module collision segment(s).");
            }

            if (report.FlowCrossingCount > 0)
            {
                report.Warnings.Add($"Detected {report.FlowCrossingCount} flow crossing segment(s).");
            }

            if (truncated)
            {
                report.Warnings.Add($"Validation truncated by performance limit (maxValidationChecks={checkBudget}).");
            }

            return report;
        }

        private static void ValidateModuleCollisions(
            IReadOnlyList<ModuleNode> modules,
            IReadOnlyList<DetailedPath> paths,
            RouteValidationReport report,
            ref int checks,
            int checkBudget,
            ref bool truncated)
        {
            if (modules == null)
            {
                return;
            }

            for (int pathIndex = 0; pathIndex < paths.Count; pathIndex++)
            {
                IReadOnlyList<Vector3> points = paths[pathIndex].Points;
                for (int pointIndex = 1; pointIndex < points.Count; pointIndex++)
                {
                    Vector3 a = points[pointIndex - 1];
                    Vector3 b = points[pointIndex];
                    for (int moduleIndex = 0; moduleIndex < modules.Count; moduleIndex++)
                    {
                        if (++checks > checkBudget)
                        {
                            truncated = true;
                            return;
                        }

                        ModuleNode module = modules[moduleIndex];
                        if (module == null)
                        {
                            continue;
                        }

                        Bounds bounds = module.GetWorldBounds();
                        if (SegmentIntersectsBoundsInterior(a, b, bounds))
                        {
                            report.ModuleCollisionCount++;
                            report.ModuleCollisionSegments.Add(new SegmentIssue(a, b));
                            break;
                        }
                    }
                }
            }
        }

        private static void ValidateFlowCrossings(
            IReadOnlyList<DetailedPath> paths,
            RouteValidationReport report,
            ref int checks,
            int checkBudget,
            ref bool truncated)
        {
            for (int i = 0; i < paths.Count; i++)
            {
                IReadOnlyList<Vector3> aPoints = paths[i].Points;
                for (int j = i + 1; j < paths.Count; j++)
                {
                    IReadOnlyList<Vector3> bPoints = paths[j].Points;
                    for (int a = 1; a < aPoints.Count; a++)
                    {
                        Vector3 a0 = aPoints[a - 1];
                        Vector3 a1 = aPoints[a];
                        for (int b = 1; b < bPoints.Count; b++)
                        {
                            if (++checks > checkBudget)
                            {
                                truncated = true;
                                return;
                            }

                            Vector3 b0 = bPoints[b - 1];
                            Vector3 b1 = bPoints[b];
                            if (ShareEndpoint(a0, a1, b0, b1))
                            {
                                continue;
                            }

                            if (!SegmentsIntersect3D(a0, a1, b0, b1))
                            {
                                continue;
                            }

                            report.FlowCrossingCount++;
                            report.FlowCrossingSegments.Add(new SegmentIssue(a0, a1));
                            report.FlowCrossingSegments.Add(new SegmentIssue(b0, b1));
                        }
                    }
                }
            }
        }

        private static bool SegmentIntersectsBoundsInterior(Vector3 a, Vector3 b, Bounds bounds)
        {
            Vector3 innerMin = bounds.min + Vector3.one * Epsilon;
            Vector3 innerMax = bounds.max - Vector3.one * Epsilon;
            if (innerMin.x >= innerMax.x || innerMin.y >= innerMax.y || innerMin.z >= innerMax.z)
            {
                return false;
            }

            return SegmentIntersectsAabb(a, b, innerMin, innerMax);
        }

        private static bool SegmentIntersectsAabb(Vector3 a, Vector3 b, Vector3 min, Vector3 max)
        {
            Vector3 d = b - a;
            float tMin = 0f;
            float tMax = 1f;

            if (!ClipAxis(min.x, max.x, a.x, d.x, ref tMin, ref tMax) ||
                !ClipAxis(min.y, max.y, a.y, d.y, ref tMin, ref tMax) ||
                !ClipAxis(min.z, max.z, a.z, d.z, ref tMin, ref tMax))
            {
                return false;
            }

            return tMax > tMin;
        }

        private static bool ClipAxis(float min, float max, float origin, float direction, ref float tMin, ref float tMax)
        {
            if (Mathf.Abs(direction) < Epsilon)
            {
                return origin >= min && origin <= max;
            }

            float inv = 1f / direction;
            float t1 = (min - origin) * inv;
            float t2 = (max - origin) * inv;
            if (t1 > t2)
            {
                float tmp = t1;
                t1 = t2;
                t2 = tmp;
            }

            tMin = Mathf.Max(tMin, t1);
            tMax = Mathf.Min(tMax, t2);
            return tMax >= tMin;
        }

        private static bool ShareEndpoint(Vector3 a0, Vector3 a1, Vector3 b0, Vector3 b1)
        {
            return ApproximatelyEqual(a0, b0) ||
                   ApproximatelyEqual(a0, b1) ||
                   ApproximatelyEqual(a1, b0) ||
                   ApproximatelyEqual(a1, b1);
        }

        private static bool SegmentsIntersect3D(Vector3 p1, Vector3 q1, Vector3 p2, Vector3 q2)
        {
            float sqDist = SegmentSegmentSqrDistance(p1, q1, p2, q2);
            return sqDist <= Epsilon * Epsilon;
        }

        private static float SegmentSegmentSqrDistance(Vector3 p1, Vector3 q1, Vector3 p2, Vector3 q2)
        {
            Vector3 d1 = q1 - p1;
            Vector3 d2 = q2 - p2;
            Vector3 r = p1 - p2;
            float a = Vector3.Dot(d1, d1);
            float e = Vector3.Dot(d2, d2);
            float f = Vector3.Dot(d2, r);

            float s;
            float t;
            if (a <= Epsilon && e <= Epsilon)
            {
                return (p1 - p2).sqrMagnitude;
            }

            if (a <= Epsilon)
            {
                s = 0f;
                t = Mathf.Clamp01(f / e);
            }
            else
            {
                float c = Vector3.Dot(d1, r);
                if (e <= Epsilon)
                {
                    t = 0f;
                    s = Mathf.Clamp01(-c / a);
                }
                else
                {
                    float b = Vector3.Dot(d1, d2);
                    float denom = a * e - b * b;

                    if (denom != 0f)
                    {
                        s = Mathf.Clamp01((b * f - c * e) / denom);
                    }
                    else
                    {
                        s = 0f;
                    }

                    t = (b * s + f) / e;
                    if (t < 0f)
                    {
                        t = 0f;
                        s = Mathf.Clamp01(-c / a);
                    }
                    else if (t > 1f)
                    {
                        t = 1f;
                        s = Mathf.Clamp01((b - c) / a);
                    }
                }
            }

            Vector3 c1 = p1 + d1 * s;
            Vector3 c2 = p2 + d2 * t;
            return (c1 - c2).sqrMagnitude;
        }

        private static bool ApproximatelyEqual(Vector3 a, Vector3 b)
        {
            return (a - b).sqrMagnitude < 0.0004f;
        }
    }

    public enum EdgeAxis
    {
        X,
        Y,
        Z
    }

    [Serializable]
    public struct CorridorEdge
    {
        public int Id;
        public int NodeA;
        public int NodeB;
        public float Length;
        public int Capacity;
        public float BaseCost;
        public float ProximityPenalty;
        public EdgeAxis Axis;
    }

    public sealed class CorridorGraph
    {
        private readonly Dictionary<Vector3Int, int> nodeLookup = new Dictionary<Vector3Int, int>();
        private readonly Dictionary<long, int> edgeLookup = new Dictionary<long, int>();

        public readonly List<Vector3> Nodes = new List<Vector3>();
        public readonly List<CorridorEdge> Edges = new List<CorridorEdge>();
        public readonly List<List<int>> NodeToEdges = new List<List<int>>();

        public Bounds WorkArea { get; set; }
        public bool TruncatedByLimits { get; set; }

        public int AddNode(Vector3 position)
        {
            Vector3Int key = Quantize(position);
            if (nodeLookup.TryGetValue(key, out int existingId))
            {
                return existingId;
            }

            int nodeId = Nodes.Count;
            Nodes.Add(position);
            NodeToEdges.Add(new List<int>(8));
            nodeLookup[key] = nodeId;
            return nodeId;
        }

        public bool TryGetNode(Vector3 position, out int nodeId)
        {
            return nodeLookup.TryGetValue(Quantize(position), out nodeId);
        }

        public int AddEdge(int nodeA, int nodeB, float baseCost, int capacity, float proximityPenalty)
        {
            if (nodeA == nodeB)
            {
                return -1;
            }

            long key = PairKey(nodeA, nodeB);
            if (edgeLookup.TryGetValue(key, out int existingId))
            {
                return existingId;
            }

            Vector3 a = Nodes[nodeA];
            Vector3 b = Nodes[nodeB];
            EdgeAxis axis = DetermineAxis(a, b);
            float length = Vector3.Distance(a, b);

            CorridorEdge edge = new CorridorEdge
            {
                Id = Edges.Count,
                NodeA = nodeA,
                NodeB = nodeB,
                Length = length,
                Capacity = Mathf.Max(1, capacity),
                BaseCost = Mathf.Max(0.001f, baseCost),
                ProximityPenalty = Mathf.Max(0f, proximityPenalty),
                Axis = axis
            };

            Edges.Add(edge);
            edgeLookup[key] = edge.Id;
            NodeToEdges[nodeA].Add(edge.Id);
            NodeToEdges[nodeB].Add(edge.Id);
            return edge.Id;
        }

        public bool TryGetEdgeId(int nodeA, int nodeB, out int edgeId)
        {
            return edgeLookup.TryGetValue(PairKey(nodeA, nodeB), out edgeId);
        }

        public int GetOtherNode(int edgeId, int nodeId)
        {
            CorridorEdge edge = Edges[edgeId];
            if (edge.NodeA == nodeId)
            {
                return edge.NodeB;
            }

            return edge.NodeA;
        }

        private static EdgeAxis DetermineAxis(Vector3 a, Vector3 b)
        {
            Vector3 delta = b - a;
            float absX = Mathf.Abs(delta.x);
            float absY = Mathf.Abs(delta.y);
            float absZ = Mathf.Abs(delta.z);
            if (absX >= absY && absX >= absZ)
            {
                return EdgeAxis.X;
            }

            if (absY >= absX && absY >= absZ)
            {
                return EdgeAxis.Y;
            }

            return EdgeAxis.Z;
        }

        private static Vector3Int Quantize(Vector3 value)
        {
            return new Vector3Int(
                Mathf.RoundToInt(value.x * 1000f),
                Mathf.RoundToInt(value.y * 1000f),
                Mathf.RoundToInt(value.z * 1000f));
        }

        private static long PairKey(int a, int b)
        {
            int min = Mathf.Min(a, b);
            int max = Mathf.Max(a, b);
            return ((long)min << 32) | (uint)max;
        }
    }

    [Serializable]
    public sealed class GlobalRoute
    {
        public FlowRequest Flow;
        public readonly List<int> NodeIds = new List<int>();
        public readonly List<int> EdgeIds = new List<int>();
        public float Length;
        public int Bends;
        public int CrossingCount;
        public bool UsesCrossingFallback;
    }

    [Serializable]
    public sealed class RoutingMetrics
    {
        public int RoutedCount;
        public int FailedCount;
        public float TotalLength;
        public int TotalBends;
        public int OverflowCount;
        public int CrossingCount;
        public int ModuleCollisionCount;
        public bool CrossingMapEnabled;
        public bool CrossingMapSkippedByLimit;
        public int CrossingMapEdges;
        public int CrossingOptimizationIterationsUsed;
        public int RequestedCrossingOptimizationIterations;
    }

    [Serializable]
    public sealed class NegotiationResult
    {
        public readonly List<GlobalRoute> Routes = new List<GlobalRoute>();
        public readonly List<FlowRequest> FailedFlows = new List<FlowRequest>();
        public readonly List<string> Warnings = new List<string>();
        public int[] EdgeUsage = Array.Empty<int>();
        public int[] EdgeOverflow = Array.Empty<int>();
        public float[] EdgeHistory = Array.Empty<float>();
        public int IterationsUsed;
        public RoutingMetrics Metrics = new RoutingMetrics();
    }

    public enum NegotiationProgressPhase
    {
        Started,
        IterationStarted,
        RouteCommitted,
        IterationCompleted,
        Completed
    }

    public sealed class NegotiationProgress
    {
        public NegotiationProgressPhase Phase;
        public IReadOnlyList<GlobalRoute> Routes = Array.Empty<GlobalRoute>();
        public NegotiationResult BestResult;
        public GlobalRoute LatestRoute;
        public FlowRequest LatestFlow;
        public int RoutedPreviewCount;
        public int Iteration;
        public int CompletedFlows;
        public int TotalFlows;
        public bool IsFinal;
        public string Message;
    }

    [Serializable]
    public sealed class TrunkLane
    {
        public FlowRequest Flow;
        public int Order;
        public float Offset;
    }

    [Serializable]
    public sealed class TrunkSegment
    {
        public int EdgeId;
        public readonly List<TrunkLane> Lanes = new List<TrunkLane>();
    }

    [Serializable]
    public sealed class BundleRoutingResult
    {
        public readonly Dictionary<int, TrunkSegment> TrunksByEdge = new Dictionary<int, TrunkSegment>();
        public bool UsedTurnAwareLaneOrdering;
        public int RequestedLaneAlignmentPasses;
        public int AppliedLaneAlignmentPasses;
        public int LaneAlignmentChangeCount;

        public float GetOffset(FlowRequest flow, int edgeId)
        {
            if (!TrunksByEdge.TryGetValue(edgeId, out TrunkSegment segment))
            {
                return 0f;
            }

            for (int i = 0; i < segment.Lanes.Count; i++)
            {
                if (segment.Lanes[i].Flow == flow)
                {
                    return segment.Lanes[i].Offset;
                }
            }

            return 0f;
        }

        public int GetOrder(FlowRequest flow, int edgeId)
        {
            if (!TrunksByEdge.TryGetValue(edgeId, out TrunkSegment segment))
            {
                return 0;
            }

            for (int i = 0; i < segment.Lanes.Count; i++)
            {
                if (segment.Lanes[i].Flow == flow)
                {
                    return segment.Lanes[i].Order;
                }
            }

            return 0;
        }
    }

    [Serializable]
    public sealed class DetailedPath
    {
        public FlowRequest Flow;
        public readonly List<Vector3> Points = new List<Vector3>();
        public readonly List<Vector3> CornerMarkers = new List<Vector3>();
        public bool UsesCrossingFallback;
    }

    [Serializable]
    public readonly struct SegmentIssue
    {
        public SegmentIssue(Vector3 start, Vector3 end)
        {
            Start = start;
            End = end;
        }

        public Vector3 Start { get; }
        public Vector3 End { get; }
    }

    [Serializable]
    public sealed class RouteValidationReport
    {
        public readonly List<string> Errors = new List<string>();
        public readonly List<string> Warnings = new List<string>();
        public readonly List<SegmentIssue> ModuleCollisionSegments = new List<SegmentIssue>();
        public readonly List<SegmentIssue> FlowCrossingSegments = new List<SegmentIssue>();
        public int ModuleCollisionCount;
        public int FlowCrossingCount;
    }
    public class FlowRoutingSystem : MonoBehaviour
    {
        [Header("Scene Data")]
        [SerializeField] private RoutingConfig config;
        [SerializeField] private bool autoCollectModules = true;
        [SerializeField] private List<ModuleNode> modules = new List<ModuleNode>();
        [SerializeField] private List<FlowRequest> flowRequests = new List<FlowRequest>();

        [Header("Random Generation")]
        [Min(1)]
        [SerializeField] private int randomConnectionCount = 20;
        [SerializeField] private int randomSeed = 12345;

        [Header("Runtime Report")]
        [TextArea(5, 20)]
        [SerializeField] private string lastReport = "No routing has been run yet.";
        [Tooltip("Optional vertical visualization offset for all debug gizmos.")]
        [SerializeField] private float gizmoHeight = 0f;

        [Header("Line Renderer Output")]
        [SerializeField] private bool renderFlowsWithLineRenderers = true;
        [Min(0.001f)]
        [SerializeField] private float lineRendererWidth = 0.035f;
        [SerializeField] private Material lineRendererMaterial;
        [SerializeField] private float lineRendererHeightOffset = 0.02f;

        [Header("Incremental Generation")]
        [SerializeField] private bool renderIncrementalProgress = true;
        [Min(0f)]
        [SerializeField] private float incrementalPreviewIntervalSeconds = 0.05f;
        [Min(2)]
        [SerializeField] private int incrementalPreviewMaxNodes = 256;
        [Min(0f)]
        [SerializeField] private float incrementalReportIntervalSeconds = 0.1f;

        private readonly CorridorGraphBuilder corridorGraphBuilder = new CorridorGraphBuilder();
        private readonly NegotiatedCongestionSolver congestionSolver = new NegotiatedCongestionSolver();
        private readonly BundleRouter bundleRouter = new BundleRouter();
        private readonly DetailedGeometryBuilder detailedGeometryBuilder = new DetailedGeometryBuilder();
        private readonly RouteValidator routeValidator = new RouteValidator();
        private readonly Dictionary<FlowRequest, LineRenderer> generatedLineRenderers =
            new Dictionary<FlowRequest, LineRenderer>();

        private CorridorGraph corridorGraph;
        private List<Bounds> inflatedObstacles = new List<Bounds>();
        private NegotiationResult negotiationResult;
        private BundleRoutingResult bundleResult;
        private List<DetailedPath> detailedPaths = new List<DetailedPath>();
        private RouteValidationReport validationReport;
        private long lastPipelineDurationMs;
        private long lastGraphBuildMs;
        private long lastSolveMs;
        private long lastBundleMs;
        private long lastDetailedGeometryMs;
        private long lastValidationMs;
        private long lastLineRenderMs;
        private Transform lineRendererRoot;
        private CancellationTokenSource activeRoutingCts;
        private bool isRoutingInProgress;
        private bool routingCancelRequested;
        private long lastIncrementalPreviewTicks;
        private long lastIncrementalReportTicks;

        public RoutingConfig Config => config;
        public IList<ModuleNode> Modules => modules;
        public IList<FlowRequest> FlowRequests => flowRequests;
        public string LastReport => lastReport;
        public bool IsRoutingInProgress => isRoutingInProgress;
        public int RandomConnectionCount { get => randomConnectionCount; set => randomConnectionCount = Mathf.Max(1, value); }
        public int RandomSeed { get => randomSeed; set => randomSeed = value; }

        private void OnDestroy()
        {
            CancelActiveRouting(false);
            DestroyGeneratedLineRenderers();
        }

        [ContextMenu("Build Corridors Only")]
        public void BuildCorridorsOnly()
        {
            CancelActiveRouting(false);
            using (ModuleNode.UseRoutingSpace(transform))
            {
                if (!PrepareSceneData())
                {
                    return;
                }

                long started = System.Diagnostics.Stopwatch.GetTimestamp();
                ResetStageTimings();
                long stageStarted = System.Diagnostics.Stopwatch.GetTimestamp();
                corridorGraph = corridorGraphBuilder.Build(modules, flowRequests, config, out inflatedObstacles);
                lastGraphBuildMs = TicksToMs(System.Diagnostics.Stopwatch.GetTimestamp() - stageStarted);
                lastPipelineDurationMs = (long)((System.Diagnostics.Stopwatch.GetTimestamp() - started) * 1000.0 / System.Diagnostics.Stopwatch.Frequency);
                negotiationResult = null;
                bundleResult = null;
                detailedPaths.Clear();
                validationReport = null;
                DestroyGeneratedLineRenderers();
                lastIncrementalPreviewTicks = 0L;
                lastIncrementalReportTicks = 0L;

                lastReport = $"Corridor graph built: nodes={corridorGraph.Nodes.Count}, edges={corridorGraph.Edges.Count}.";
                Debug.Log($"[FlowRoutingSystem] {lastReport}", this);
            }
        }

        [ContextMenu("Run One Negotiation Iteration")]
        public void RunOneNegotiationIteration()
        {
            CancelActiveRouting(false);
            using (ModuleNode.UseRoutingSpace(transform))
            {
                if (!PrepareSceneData())
                {
                    return;
                }

                long started = System.Diagnostics.Stopwatch.GetTimestamp();
                ResetStageTimings();
                long stageStarted = System.Diagnostics.Stopwatch.GetTimestamp();
                EnsureCorridorsBuilt();
                lastGraphBuildMs = TicksToMs(System.Diagnostics.Stopwatch.GetTimestamp() - stageStarted);
                stageStarted = System.Diagnostics.Stopwatch.GetTimestamp();
                negotiationResult = congestionSolver.Solve(corridorGraph, flowRequests, config, 1);
                lastSolveMs = TicksToMs(System.Diagnostics.Stopwatch.GetTimestamp() - stageStarted);
                stageStarted = System.Diagnostics.Stopwatch.GetTimestamp();
                bundleResult = bundleRouter.Build(corridorGraph, negotiationResult.Routes, config);
                lastBundleMs = TicksToMs(System.Diagnostics.Stopwatch.GetTimestamp() - stageStarted);
                stageStarted = System.Diagnostics.Stopwatch.GetTimestamp();
                detailedPaths = detailedGeometryBuilder.Build(corridorGraph, negotiationResult.Routes, bundleResult, modules, config);
                lastDetailedGeometryMs = TicksToMs(System.Diagnostics.Stopwatch.GetTimestamp() - stageStarted);
                stageStarted = System.Diagnostics.Stopwatch.GetTimestamp();
                validationReport = routeValidator.Validate(modules, detailedPaths, config);
                lastValidationMs = TicksToMs(System.Diagnostics.Stopwatch.GetTimestamp() - stageStarted);
                negotiationResult.Metrics.ModuleCollisionCount = validationReport.ModuleCollisionCount;
                stageStarted = System.Diagnostics.Stopwatch.GetTimestamp();
                RebuildGeneratedLineRenderers();
                lastLineRenderMs = TicksToMs(System.Diagnostics.Stopwatch.GetTimestamp() - stageStarted);
                lastPipelineDurationMs = (long)((System.Diagnostics.Stopwatch.GetTimestamp() - started) * 1000.0 / System.Diagnostics.Stopwatch.Frequency);

                PublishReport("One-iteration route report");
            }
        }

        [ContextMenu("Generate Random Connections")]
        public void GenerateRandomConnections()
        {
            GenerateRandomConnections(randomConnectionCount, randomSeed);
        }

        public void GenerateRandomConnections(int count, int seed)
        {
            CancelActiveRouting(false);
            using (ModuleNode.UseRoutingSpace(transform))
            {
                if (!PrepareSceneData())
                {
                    return;
                }

                System.Random random = new System.Random(seed);
                List<ModuleNode> sourceCandidates = new List<ModuleNode>();
                List<ModuleNode> targetCandidates = new List<ModuleNode>();

                CollectModuleCandidates(sourceCandidates, targetCandidates);
                if (sourceCandidates.Count == 0 || targetCandidates.Count == 0)
                {
                    Debug.LogWarning("[FlowRoutingSystem] No module candidates found for random generation.", this);
                    return;
                }

                flowRequests.Clear();
                for (int i = 0; i < count; i++)
                {
                    ModuleNode source = sourceCandidates[random.Next(sourceCandidates.Count)];
                    ModuleNode target = targetCandidates[random.Next(targetCandidates.Count)];

                    if (source == target && sourceCandidates.Count > 1)
                    {
                        int safety = 0;
                        while (target == source && safety++ < 8)
                        {
                            target = targetCandidates[random.Next(targetCandidates.Count)];
                        }
                    }

                    FlowRequest request = new FlowRequest
                    {
                        Id = $"flow_{i + 1:D3}",
                        FlowClass = "random",
                        DebugColor = Color.HSVToRGB((i * 0.6180339f) % 1f, 0.65f, 0.95f)
                    };
                    request.Source.Module = source;
                    request.Target.Module = target;
                    flowRequests.Add(request);
                }

                RecomputeAutoSlotsFromConnectivity();
                corridorGraph = null;
                DestroyGeneratedLineRenderers();
                lastReport = $"Generated {flowRequests.Count} random flow request(s) with seed={seed}.";
                Debug.Log($"[FlowRoutingSystem] {lastReport}", this);
            }
        }

        [ContextMenu("Generate And Draw Flows")]
        public void GenerateAndDrawFlows()
        {
            if (Application.isPlaying)
            {
                GenerateAndDrawFlowsIncrementally();
                return;
            }

            GenerateAndDrawFlowsImmediate();
        }

        [ContextMenu("Generate And Draw Flows Immediate")]
        public void GenerateAndDrawFlowsImmediate()
        {
            CancelActiveRouting(false);
            using (ModuleNode.UseRoutingSpace(transform))
            {
                if (!PrepareSceneData())
                {
                    return;
                }

                long started = System.Diagnostics.Stopwatch.GetTimestamp();
                ResetStageTimings();
                long stageStarted = System.Diagnostics.Stopwatch.GetTimestamp();
                EnsureCorridorsBuilt();
                lastGraphBuildMs = TicksToMs(System.Diagnostics.Stopwatch.GetTimestamp() - stageStarted);
                stageStarted = System.Diagnostics.Stopwatch.GetTimestamp();
                negotiationResult = congestionSolver.Solve(corridorGraph, flowRequests, config);
                lastSolveMs = TicksToMs(System.Diagnostics.Stopwatch.GetTimestamp() - stageStarted);
                stageStarted = System.Diagnostics.Stopwatch.GetTimestamp();
                bundleResult = bundleRouter.Build(corridorGraph, negotiationResult.Routes, config);
                lastBundleMs = TicksToMs(System.Diagnostics.Stopwatch.GetTimestamp() - stageStarted);
                stageStarted = System.Diagnostics.Stopwatch.GetTimestamp();
                detailedPaths = detailedGeometryBuilder.Build(corridorGraph, negotiationResult.Routes, bundleResult, modules, config);
                lastDetailedGeometryMs = TicksToMs(System.Diagnostics.Stopwatch.GetTimestamp() - stageStarted);
                stageStarted = System.Diagnostics.Stopwatch.GetTimestamp();
                validationReport = routeValidator.Validate(modules, detailedPaths, config);
                lastValidationMs = TicksToMs(System.Diagnostics.Stopwatch.GetTimestamp() - stageStarted);
                negotiationResult.Metrics.ModuleCollisionCount = validationReport.ModuleCollisionCount;
                stageStarted = System.Diagnostics.Stopwatch.GetTimestamp();
                RebuildGeneratedLineRenderers();
                lastLineRenderMs = TicksToMs(System.Diagnostics.Stopwatch.GetTimestamp() - stageStarted);
                lastPipelineDurationMs = (long)((System.Diagnostics.Stopwatch.GetTimestamp() - started) * 1000.0 / System.Diagnostics.Stopwatch.Frequency);

                foreach (string warning in validationReport.Warnings)
                {
                    negotiationResult.Warnings.Add(warning);
                }

                foreach (string error in validationReport.Errors)
                {
                    negotiationResult.Warnings.Add($"ERROR: {error}");
                }

                PublishReport("Full route generation report");
            }
        }

        [ContextMenu("Generate And Draw Flows Incrementally")]
        public void GenerateAndDrawFlowsIncrementally()
        {
            if (!Application.isPlaying)
            {
                Debug.LogWarning(
                    "[FlowRoutingSystem] Incremental routing outside Play Mode is started by the custom inspector editor runner.",
                    this);
                return;
            }

            CancelActiveRouting(false);
            GenerateAndDrawFlowsIncrementallyAsync().Forget();
        }

        public async UniTask GenerateAndDrawFlowsIncrementallyAsync()
        {
            CancelActiveRouting(false);
            routingCancelRequested = false;
            isRoutingInProgress = true;
            activeRoutingCts = CancellationTokenSource.CreateLinkedTokenSource(this.GetCancellationTokenOnDestroy());
            CancellationToken cancellationToken = activeRoutingCts.Token;

            using (ModuleNode.UseRoutingSpace(transform))
            {
                if (!PrepareSceneData())
                {
                    isRoutingInProgress = false;
                    DisposeActiveRoutingCts();
                    return;
                }

                long started = System.Diagnostics.Stopwatch.GetTimestamp();
                ResetStageTimings();
                negotiationResult = null;
                bundleResult = null;
                detailedPaths.Clear();
                validationReport = null;
                DestroyGeneratedLineRenderers();

                lastReport = "Incremental route generation started.";
                try
                {
                    await UniTask.Yield(cancellationToken);

                    long stageStarted = System.Diagnostics.Stopwatch.GetTimestamp();
                    EnsureCorridorsBuilt();
                    lastGraphBuildMs = TicksToMs(System.Diagnostics.Stopwatch.GetTimestamp() - stageStarted);
                    if (routingCancelRequested)
                    {
                        FinishCancelledRouting();
                        return;
                    }

                    lastReport = $"Corridor graph built: nodes={corridorGraph.Nodes.Count}, edges={corridorGraph.Edges.Count}. Routing flows incrementally...";
                    await UniTask.Yield(cancellationToken);

                    stageStarted = System.Diagnostics.Stopwatch.GetTimestamp();
                    await congestionSolver.SolveIncrementalAsync(
                        corridorGraph,
                        flowRequests,
                        config,
                        progress => HandleIncrementalRoutingProgress(progress, started),
                        cancellationToken);
                    lastSolveMs = TicksToMs(System.Diagnostics.Stopwatch.GetTimestamp() - stageStarted);

                    if (negotiationResult == null)
                    {
                        negotiationResult = new NegotiationResult();
                    }

                    stageStarted = System.Diagnostics.Stopwatch.GetTimestamp();
                    bundleResult = bundleRouter.Build(corridorGraph, negotiationResult.Routes, config);
                    lastBundleMs = TicksToMs(System.Diagnostics.Stopwatch.GetTimestamp() - stageStarted);
                    stageStarted = System.Diagnostics.Stopwatch.GetTimestamp();
                    detailedPaths = detailedGeometryBuilder.Build(corridorGraph, negotiationResult.Routes, bundleResult, modules, config);
                    lastDetailedGeometryMs = TicksToMs(System.Diagnostics.Stopwatch.GetTimestamp() - stageStarted);
                    stageStarted = System.Diagnostics.Stopwatch.GetTimestamp();
                    validationReport = routeValidator.Validate(modules, detailedPaths, config);
                    lastValidationMs = TicksToMs(System.Diagnostics.Stopwatch.GetTimestamp() - stageStarted);
                    negotiationResult.Metrics.ModuleCollisionCount = validationReport.ModuleCollisionCount;
                    stageStarted = System.Diagnostics.Stopwatch.GetTimestamp();
                    RebuildGeneratedLineRenderers();
                    lastLineRenderMs = TicksToMs(System.Diagnostics.Stopwatch.GetTimestamp() - stageStarted);
                    lastPipelineDurationMs = (long)((System.Diagnostics.Stopwatch.GetTimestamp() - started) * 1000.0 / System.Diagnostics.Stopwatch.Frequency);

                    foreach (string warning in validationReport.Warnings)
                    {
                        negotiationResult.Warnings.Add(warning);
                    }

                    foreach (string error in validationReport.Errors)
                    {
                        negotiationResult.Warnings.Add($"ERROR: {error}");
                    }

                    isRoutingInProgress = false;
                    DisposeActiveRoutingCts();
                    PublishReport("Full route generation report");
                }
                catch (System.OperationCanceledException)
                {
                    FinishCancelledRouting();
                }
            }
        }

        [ContextMenu("Clear")]
        public void Clear()
        {
            CancelActiveRouting(false);
            flowRequests.Clear();
            corridorGraph = null;
            inflatedObstacles.Clear();
            negotiationResult = null;
            bundleResult = null;
            detailedPaths.Clear();
            validationReport = null;
            lastPipelineDurationMs = 0L;
            ResetStageTimings();
            DestroyGeneratedLineRenderers();

            lastReport = "Cleared flow requests, cached routes, and debug overlays.";
            Debug.Log($"[FlowRoutingSystem] {lastReport}", this);
        }

        public void RefreshModuleList()
        {
            using (ModuleNode.UseRoutingSpace(transform))
            {
                EnsureModuleList();
                RecomputeAutoSlotsFromConnectivity();
                corridorGraph = null;
            }
        }

        private void EnsureCorridorsBuilt()
        {
            if (corridorGraph == null)
            {
                corridorGraph = corridorGraphBuilder.Build(modules, flowRequests, config, out inflatedObstacles);
            }
        }

        private bool PrepareSceneData()
        {
            if (config == null)
            {
                Debug.LogError("[FlowRoutingSystem] RoutingConfig is not assigned.", this);
                return false;
            }

            EnsureModuleList();
            if (modules.Count == 0)
            {
                Debug.LogError("[FlowRoutingSystem] No ModuleNode found in scene.", this);
                return false;
            }

            flowRequests.RemoveAll(item => item == null);
            RecomputeAutoSlotsFromConnectivity();
            corridorGraph = null;
            return true;
        }

        private void RebuildGeneratedLineRenderers()
        {
            ClearGeneratedLineRenderers(destroyRoot: false);
            if (!renderFlowsWithLineRenderers || detailedPaths == null || detailedPaths.Count == 0)
            {
                return;
            }

            for (int i = 0; i < detailedPaths.Count; i++)
            {
                UpsertGeneratedLineRenderer(detailedPaths[i], i);
            }
        }

        private void HandleIncrementalRoutingProgress(NegotiationProgress progress, long started)
        {
            if (progress == null)
            {
                return;
            }

            if (progress.BestResult != null)
            {
                negotiationResult = progress.BestResult;
            }

            long now = System.Diagnostics.Stopwatch.GetTimestamp();
            if (renderIncrementalProgress && progress.LatestRoute != null && ShouldUpdateIncrementalPreview(progress, now))
            {
                RenderIncrementalRoute(progress.LatestRoute, progress.CompletedFlows - 1);
            }

            if (!ShouldUpdateIncrementalReport(progress, now))
            {
                return;
            }

            long elapsedMs = (long)((now - started) * 1000.0 / System.Diagnostics.Stopwatch.Frequency);
            int routedCount = progress.RoutedPreviewCount > 0
                ? progress.RoutedPreviewCount
                : (progress.Routes != null ? progress.Routes.Count : (negotiationResult?.Routes.Count ?? 0));
            lastReport =
                "Incremental route generation in progress\n" +
                $"Iteration: {Mathf.Max(0, progress.Iteration)}\n" +
                $"Routed preview: {routedCount}/{progress.TotalFlows}\n" +
                $"Elapsed: {elapsedMs} ms\n" +
                progress.Message;
        }

        private bool ShouldUpdateIncrementalPreview(NegotiationProgress progress, long now)
        {
            if (progress.IsFinal || progress.CompletedFlows >= progress.TotalFlows)
            {
                lastIncrementalPreviewTicks = now;
                return true;
            }

            float interval = Mathf.Max(0f, incrementalPreviewIntervalSeconds);
            if (interval <= 0f || lastIncrementalPreviewTicks == 0L)
            {
                lastIncrementalPreviewTicks = now;
                return true;
            }

            long intervalTicks = (long)(interval * System.Diagnostics.Stopwatch.Frequency);
            if (now - lastIncrementalPreviewTicks < intervalTicks)
            {
                return false;
            }

            lastIncrementalPreviewTicks = now;
            return true;
        }

        private bool ShouldUpdateIncrementalReport(NegotiationProgress progress, long now)
        {
            if (progress.IsFinal || progress.Phase == NegotiationProgressPhase.IterationCompleted)
            {
                lastIncrementalReportTicks = now;
                return true;
            }

            float interval = Mathf.Max(0f, incrementalReportIntervalSeconds);
            if (interval <= 0f || lastIncrementalReportTicks == 0L)
            {
                lastIncrementalReportTicks = now;
                return true;
            }

            long intervalTicks = (long)(interval * System.Diagnostics.Stopwatch.Frequency);
            if (now - lastIncrementalReportTicks < intervalTicks)
            {
                return false;
            }

            lastIncrementalReportTicks = now;
            return true;
        }

        private void RenderIncrementalRoute(GlobalRoute route, int routeIndex)
        {
            if (route == null || corridorGraph == null)
            {
                return;
            }

            DetailedPath previewPath = BuildLightweightPreviewPath(route);
            UpsertPreviewDetailedPath(previewPath);
            if (renderFlowsWithLineRenderers)
            {
                UpsertGeneratedLineRenderer(previewPath, Mathf.Max(0, routeIndex));
            }
        }

        private DetailedPath BuildLightweightPreviewPath(GlobalRoute route)
        {
            DetailedPath path = new DetailedPath
            {
                Flow = route.Flow,
                UsesCrossingFallback = route.UsesCrossingFallback
            };

            if (route.NodeIds.Count == 0 || corridorGraph == null)
            {
                return path;
            }

            int maxNodes = Mathf.Max(2, incrementalPreviewMaxNodes);
            int stride = Mathf.Max(1, Mathf.CeilToInt(route.NodeIds.Count / (float)maxNodes));
            for (int i = 0; i < route.NodeIds.Count; i += stride)
            {
                int nodeId = route.NodeIds[i];
                if (nodeId >= 0 && nodeId < corridorGraph.Nodes.Count)
                {
                    path.Points.Add(corridorGraph.Nodes[nodeId]);
                }
            }

            int lastNodeId = route.NodeIds[route.NodeIds.Count - 1];
            if (lastNodeId >= 0 && lastNodeId < corridorGraph.Nodes.Count)
            {
                Vector3 lastPoint = corridorGraph.Nodes[lastNodeId];
                if (path.Points.Count == 0 || (path.Points[path.Points.Count - 1] - lastPoint).sqrMagnitude > 0.000001f)
                {
                    path.Points.Add(lastPoint);
                }
            }

            return path;
        }

        private void UpsertPreviewDetailedPath(DetailedPath path)
        {
            if (path == null || path.Flow == null)
            {
                return;
            }

            for (int i = 0; i < detailedPaths.Count; i++)
            {
                if (detailedPaths[i]?.Flow == path.Flow)
                {
                    detailedPaths[i] = path;
                    return;
                }
            }

            detailedPaths.Add(path);
        }

        private void UpsertGeneratedLineRenderer(DetailedPath path, int index)
        {
            if (!renderFlowsWithLineRenderers || path == null || path.Points.Count < 2)
            {
                return;
            }

            Transform root = EnsureLineRendererRoot();
            LineRenderer line = null;
            if (path.Flow != null)
            {
                generatedLineRenderers.TryGetValue(path.Flow, out line);
            }

            if (line == null)
            {
                GameObject lineObject = new GameObject(GetLineObjectName(path, index));
                lineObject.transform.SetParent(root, false);
                line = lineObject.AddComponent<LineRenderer>();
                if (path.Flow != null)
                {
                    generatedLineRenderers[path.Flow] = line;
                }
            }
            else
            {
                line.gameObject.name = GetLineObjectName(path, index);
            }

            line.transform.localPosition = Vector3.zero;
            line.transform.localRotation = Quaternion.identity;
            line.transform.localScale = Vector3.one;
            line.useWorldSpace = false;
            line.sharedMaterial = GetLineRendererMaterial();
            line.widthMultiplier = Mathf.Max(0.001f, lineRendererWidth);
            line.numCornerVertices = 4;
            line.numCapVertices = 2;
            line.positionCount = path.Points.Count;

            Color color = path.Flow != null ? path.Flow.DebugColor : new Color(0.3f, 0.85f, 1f, 1f);
            line.startColor = color;
            line.endColor = color;

            for (int pointIndex = 0; pointIndex < path.Points.Count; pointIndex++)
            {
                line.SetPosition(pointIndex, ToLineRendererPosition(path.Points[pointIndex]));
            }
        }

        public void CancelActiveRouting()
        {
            CancelActiveRouting(true);
        }

        private void CancelActiveRouting(bool updateReport)
        {
            routingCancelRequested = true;
            if (activeRoutingCts != null)
            {
                activeRoutingCts.Cancel();
                if (!isRoutingInProgress)
                {
                    DisposeActiveRoutingCts();
                }
            }

            if (isRoutingInProgress)
            {
                isRoutingInProgress = false;
                if (updateReport)
                {
                    lastReport = "Incremental route generation cancelled.";
                }
            }
        }

        private void FinishCancelledRouting()
        {
            isRoutingInProgress = false;
            DisposeActiveRoutingCts();
            routingCancelRequested = false;
            lastReport = "Incremental route generation cancelled.";
        }

        private void DisposeActiveRoutingCts()
        {
            if (activeRoutingCts == null)
            {
                return;
            }

            activeRoutingCts.Dispose();
            activeRoutingCts = null;
        }

        private Transform EnsureLineRendererRoot()
        {
            const string rootName = "Generated Flow Lines";
            if (lineRendererRoot != null)
            {
                return lineRendererRoot;
            }

            Transform existing = transform.Find(rootName);
            if (existing != null)
            {
                lineRendererRoot = existing;
                lineRendererRoot.localPosition = Vector3.zero;
                lineRendererRoot.localRotation = Quaternion.identity;
                lineRendererRoot.localScale = Vector3.one;
                return lineRendererRoot;
            }

            GameObject rootObject = new GameObject(rootName);
            rootObject.transform.SetParent(transform, false);
            lineRendererRoot = rootObject.transform;
            lineRendererRoot.localPosition = Vector3.zero;
            lineRendererRoot.localRotation = Quaternion.identity;
            lineRendererRoot.localScale = Vector3.one;
            return lineRendererRoot;
        }

        private void DestroyGeneratedLineRenderers()
        {
            ClearGeneratedLineRenderers(destroyRoot: true);
        }

        private void ClearGeneratedLineRenderers(bool destroyRoot)
        {
            const string rootName = "Generated Flow Lines";
            generatedLineRenderers.Clear();
            Transform root = lineRendererRoot != null ? lineRendererRoot : transform.Find(rootName);
            if (root == null)
            {
                lineRendererRoot = null;
                return;
            }

#if UNITY_EDITOR
            ClearEditorSelectionIfDestroying(root);
#endif
            if (destroyRoot)
            {
                lineRendererRoot = null;
                if (Application.isPlaying)
                {
                    Destroy(root.gameObject);
                }
                else
                {
                    DestroyImmediate(root.gameObject);
                }
                return;
            }

            for (int i = root.childCount - 1; i >= 0; i--)
            {
                GameObject child = root.GetChild(i).gameObject;
                if (Application.isPlaying)
                {
                    Destroy(child);
                }
                else
                {
                    DestroyImmediate(child);
                }
            }
        }

#if UNITY_EDITOR
        private static void ClearEditorSelectionIfDestroying(Transform root)
        {
            if (root == null || Selection.activeObject == null)
            {
                return;
            }

            GameObject selectedGameObject = Selection.activeObject as GameObject;
            if (selectedGameObject == null && Selection.activeObject is Component selectedComponent)
            {
                selectedGameObject = selectedComponent.gameObject;
            }

            if (selectedGameObject != null && selectedGameObject.transform.IsChildOf(root))
            {
                Selection.activeObject = null;
            }
        }
#endif

        private Material GetLineRendererMaterial()
        {
            if (lineRendererMaterial != null)
            {
                return lineRendererMaterial;
            }

            Shader shader = Shader.Find("Sprites/Default");
            if (shader == null)
            {
                shader = Shader.Find("Universal Render Pipeline/Unlit");
            }

            if (shader == null)
            {
                shader = Shader.Find("Unlit/Color");
            }

            lineRendererMaterial = shader != null
                ? new Material(shader) { name = "Generated Flow Line Material" }
                : null;
            return lineRendererMaterial;
        }

        private static string GetLineObjectName(DetailedPath path, int index)
        {
            string flowId = path.Flow != null && !string.IsNullOrWhiteSpace(path.Flow.Id)
                ? path.Flow.Id
                : $"flow_{index + 1:D3}";
            return $"Flow Line - {flowId}";
        }

        private void EnsureModuleList()
        {
            if (autoCollectModules)
            {
                modules.Clear();
                ModuleNode[] discovered =
#if UNITY_2022_2_OR_NEWER
                    FindObjectsByType<ModuleNode>(FindObjectsInactive.Include, FindObjectsSortMode.None);
#else
                    FindObjectsOfType<ModuleNode>(true);
#endif
                for (int i = 0; i < discovered.Length; i++)
                {
                    modules.Add(discovered[i]);
                }

                modules.Sort((left, right) =>
                {
                    string leftName = left != null ? left.name : string.Empty;
                    string rightName = right != null ? right.name : string.Empty;
                    return string.CompareOrdinal(leftName, rightName);
                });
            }
            else
            {
                modules.RemoveAll(item => item == null);
            }
        }

        private void CollectModuleCandidates(
            List<ModuleNode> sourceCandidates,
            List<ModuleNode> targetCandidates)
        {
            for (int moduleIndex = 0; moduleIndex < modules.Count; moduleIndex++)
            {
                ModuleNode module = modules[moduleIndex];
                if (module == null)
                {
                    continue;
                }

                sourceCandidates.Add(module);
                targetCandidates.Add(module);
            }
        }

        private void RecomputeAutoSlotsFromConnectivity()
        {
            Dictionary<ModuleNode, List<FlowRequest>> outputsByModule = new Dictionary<ModuleNode, List<FlowRequest>>();
            Dictionary<ModuleNode, List<FlowRequest>> inputsByModule = new Dictionary<ModuleNode, List<FlowRequest>>();

            for (int i = 0; i < flowRequests.Count; i++)
            {
                FlowRequest flow = flowRequests[i];
                if (flow == null)
                {
                    continue;
                }

                AddFlowGroup(outputsByModule, flow.Source.Module, flow);
                AddFlowGroup(inputsByModule, flow.Target.Module, flow);
            }

            Dictionary<FlowRequest, int> sourceIndexByFlow = new Dictionary<FlowRequest, int>(flowRequests.Count);
            Dictionary<FlowRequest, int> sourceTotalByFlow = new Dictionary<FlowRequest, int>(flowRequests.Count);
            Dictionary<FlowRequest, int> targetIndexByFlow = new Dictionary<FlowRequest, int>(flowRequests.Count);
            Dictionary<FlowRequest, int> targetTotalByFlow = new Dictionary<FlowRequest, int>(flowRequests.Count);

            foreach (KeyValuePair<ModuleNode, List<FlowRequest>> pair in outputsByModule)
            {
                ModuleNode module = pair.Key;
                List<FlowRequest> groupedFlows = pair.Value;
                groupedFlows.Sort((left, right) => CompareFlowOrderForModule(module, left, right, SlotKind.Out));
                int total = groupedFlows.Count;
                for (int i = 0; i < groupedFlows.Count; i++)
                {
                    FlowRequest flow = groupedFlows[i];
                    sourceIndexByFlow[flow] = i;
                    sourceTotalByFlow[flow] = total;
                }
            }

            foreach (KeyValuePair<ModuleNode, List<FlowRequest>> pair in inputsByModule)
            {
                ModuleNode module = pair.Key;
                List<FlowRequest> groupedFlows = pair.Value;
                groupedFlows.Sort((left, right) => CompareFlowOrderForModule(module, left, right, SlotKind.In));
                int total = groupedFlows.Count;
                for (int i = 0; i < groupedFlows.Count; i++)
                {
                    FlowRequest flow = groupedFlows[i];
                    targetIndexByFlow[flow] = i;
                    targetTotalByFlow[flow] = total;
                }
            }

            for (int i = 0; i < flowRequests.Count; i++)
            {
                FlowRequest flow = flowRequests[i];
                if (flow == null)
                {
                    continue;
                }

                int sourceIndex = sourceIndexByFlow.TryGetValue(flow, out int sourceSlotIndex) ? sourceSlotIndex : 0;
                int sourceTotal = sourceTotalByFlow.TryGetValue(flow, out int sourceSlotTotal) ? sourceSlotTotal : 0;
                int targetIndex = targetIndexByFlow.TryGetValue(flow, out int targetSlotIndex) ? targetSlotIndex : 0;
                int targetTotal = targetTotalByFlow.TryGetValue(flow, out int targetSlotTotal) ? targetSlotTotal : 0;
                flow.AssignAutoSlots(sourceIndex, sourceTotal, targetIndex, targetTotal);
            }
        }

        private static void AddFlowGroup(Dictionary<ModuleNode, List<FlowRequest>> groups, ModuleNode module, FlowRequest flow)
        {
            if (module == null || flow == null)
            {
                return;
            }

            if (!groups.TryGetValue(module, out List<FlowRequest> list))
            {
                list = new List<FlowRequest>();
                groups[module] = list;
            }

            list.Add(flow);
        }

        private static int CompareFlowOrderForModule(ModuleNode module, FlowRequest left, FlowRequest right, SlotKind slotKind)
        {
            ComputeModuleSortKeys(module, left, slotKind, out float leftPrimary, out float leftSecondary);
            ComputeModuleSortKeys(module, right, slotKind, out float rightPrimary, out float rightSecondary);

            int compare = leftPrimary.CompareTo(rightPrimary);
            if (compare != 0)
            {
                return compare;
            }

            compare = leftSecondary.CompareTo(rightSecondary);
            if (compare != 0)
            {
                return compare;
            }

            string leftId = left != null && !string.IsNullOrWhiteSpace(left.Id) ? left.Id : "flow";
            string rightId = right != null && !string.IsNullOrWhiteSpace(right.Id) ? right.Id : "flow";
            return string.CompareOrdinal(leftId, rightId);
        }

        private static void ComputeModuleSortKeys(ModuleNode module, FlowRequest flow, SlotKind slotKind, out float primary, out float secondary)
        {
            primary = 0f;
            secondary = 0f;
            if (module == null || flow == null)
            {
                return;
            }

            Vector3 counterpartCenter = GetCounterpartCenter(module, flow, slotKind);
            SlotSide side = slotKind == SlotKind.Out ? module.DefaultOutputSide : module.DefaultInputSide;
            switch (side)
            {
                case SlotSide.Left:
                case SlotSide.Right:
                    primary = counterpartCenter.z;
                    secondary = counterpartCenter.y;
                    break;
                case SlotSide.Top:
                case SlotSide.Bottom:
                    primary = counterpartCenter.x;
                    secondary = counterpartCenter.y;
                    break;
                case SlotSide.Up:
                case SlotSide.Down:
                    primary = counterpartCenter.z;
                    secondary = counterpartCenter.x;
                    break;
            }
        }

        private static Vector3 GetCounterpartCenter(ModuleNode module, FlowRequest flow, SlotKind slotKind)
        {
            ModuleNode counterpart = slotKind == SlotKind.Out ? flow.Target.Module : flow.Source.Module;
            if (counterpart != null)
            {
                return counterpart.GetWorldBounds().center;
            }

            if (module != null)
            {
                return module.GetWorldBounds().center;
            }

            return Vector3.zero;
        }

        private void PublishReport(string title)
        {
            List<string> warnings = new List<string>();
            if (corridorGraph != null && corridorGraph.TruncatedByLimits)
            {
                warnings.Add("Corridor graph was truncated by performance limits (maxGraphNodes/maxGraphEdges).");
            }

            if (negotiationResult != null && negotiationResult.Warnings.Count > 0)
            {
                warnings.AddRange(negotiationResult.Warnings);
            }

            StringBuilder builder = new StringBuilder();
            builder.AppendLine(title);
            builder.AppendLine($"Modules: {modules.Count}, Flows: {flowRequests.Count}");
            builder.AppendLine($"Corridor graph: nodes={corridorGraph?.Nodes.Count ?? 0}, edges={corridorGraph?.Edges.Count ?? 0}");
            if (lastPipelineDurationMs > 0)
            {
                builder.AppendLine($"Elapsed: {lastPipelineDurationMs} ms");
                builder.AppendLine(
                    $"Stage timings: graph={lastGraphBuildMs} ms, solve={lastSolveMs} ms, bundle={lastBundleMs} ms, detailed={lastDetailedGeometryMs} ms, validation={lastValidationMs} ms, lineRender={lastLineRenderMs} ms");
            }

            if (bundleResult != null)
            {
                builder.AppendLine(
                    $"Bundling: turnAware={(bundleResult.UsedTurnAwareLaneOrdering ? "on" : "off")}, alignmentPasses={bundleResult.AppliedLaneAlignmentPasses}/{bundleResult.RequestedLaneAlignmentPasses}, laneOrderChanges={bundleResult.LaneAlignmentChangeCount}");
            }

            if (negotiationResult != null)
            {
                RoutingMetrics metrics = negotiationResult.Metrics;
                string crossingMapStatus = metrics.CrossingMapSkippedByLimit
                    ? $"skipped by limit (edges={metrics.CrossingMapEdges}, limit={config.crossingMapMaxEdges})"
                    : (metrics.CrossingMapEnabled ? $"active (edges={metrics.CrossingMapEdges})" : "empty/off");
                builder.AppendLine(
                    $"Crossing optimization: map={crossingMapStatus}, postOverflow={(config.optimizeCrossingsAfterOverflow ? "on" : "off")}, iterations={metrics.CrossingOptimizationIterationsUsed}/{metrics.RequestedCrossingOptimizationIterations}");
                if (config.optimizeCrossingsAfterOverflow && metrics.CrossingMapSkippedByLimit)
                {
                    builder.AppendLine("- NOTE: Optimize Crossings After Overflow is ignored because the crossing map was skipped by performance limits.");
                }

                if (!config.optimizeCrossingsAfterOverflow && metrics.RequestedCrossingOptimizationIterations > 0)
                {
                    builder.AppendLine("- NOTE: Max Crossing Optimization Iters is ignored while Optimize Crossings After Overflow is disabled.");
                }
            }

            if (negotiationResult != null)
            {
                builder.AppendLine($"Negotiation iterations: {negotiationResult.IterationsUsed}");
                builder.AppendLine($"Routed: {negotiationResult.Metrics.RoutedCount}, Failed: {negotiationResult.Metrics.FailedCount}");
                builder.AppendLine($"Length: {negotiationResult.Metrics.TotalLength:F2}, Bends: {negotiationResult.Metrics.TotalBends}");
                builder.AppendLine($"Overflow: {negotiationResult.Metrics.OverflowCount}, Crossings: {negotiationResult.Metrics.CrossingCount}");
            }

            if (validationReport != null)
            {
                builder.AppendLine($"Module collisions: {validationReport.ModuleCollisionCount}");
                builder.AppendLine($"Flow crossings: {validationReport.FlowCrossingCount}");
            }

            if (warnings.Count > 0)
            {
                builder.AppendLine("Warnings:");
                for (int i = 0; i < warnings.Count; i++)
                {
                    builder.AppendLine($"- {warnings[i]}");
                }
            }

            lastReport = builder.ToString().TrimEnd();
            Debug.Log($"[FlowRoutingSystem]\n{lastReport}", this);
        }

        private void ResetStageTimings()
        {
            lastGraphBuildMs = 0L;
            lastSolveMs = 0L;
            lastBundleMs = 0L;
            lastDetailedGeometryMs = 0L;
            lastValidationMs = 0L;
            lastLineRenderMs = 0L;
        }

        private static long TicksToMs(long ticks)
        {
            return (long)(ticks * 1000.0 / System.Diagnostics.Stopwatch.Frequency);
        }

        private void OnDrawGizmos()
        {
            if (config == null)
            {
                return;
            }

            DrawObstacles();
            DrawCorridorGraph();
            DrawGlobalRoutes();
            DrawTrunks();
            DrawDetailedPaths();
            DrawConflicts();
        }

        private void DrawObstacles()
        {
            if (!config.showObstacles || inflatedObstacles == null)
            {
                return;
            }

            Gizmos.color = new Color(0.95f, 0.35f, 0.2f, 0.35f);
            for (int i = 0; i < inflatedObstacles.Count; i++)
            {
                DrawBounds(inflatedObstacles[i]);
            }
        }

        private void DrawCorridorGraph()
        {
            if (!config.showCorridorGraph || corridorGraph == null)
            {
                return;
            }

            int step = GetGizmoStride(corridorGraph.Edges.Count);
            for (int edgeId = 0; edgeId < corridorGraph.Edges.Count; edgeId += step)
            {
                CorridorEdge edge = corridorGraph.Edges[edgeId];
                Vector3 a = ToV3(corridorGraph.Nodes[edge.NodeA]);
                Vector3 b = ToV3(corridorGraph.Nodes[edge.NodeB]);

                if (config.showCongestion && negotiationResult != null && negotiationResult.EdgeUsage.Length == corridorGraph.Edges.Count)
                {
                    int usage = negotiationResult.EdgeUsage[edgeId];
                    int overflow = negotiationResult.EdgeOverflow[edgeId];
                    float t = Mathf.Clamp01((float)usage / Mathf.Max(1, edge.Capacity));
                    Gizmos.color = overflow > 0
                        ? Color.red
                        : Color.Lerp(new Color(0.2f, 0.95f, 0.2f, 0.9f), new Color(1f, 0.85f, 0.15f, 0.9f), t);
                }
                else
                {
                    Gizmos.color = new Color(0.5f, 0.5f, 0.5f, 0.6f);
                }

                Gizmos.DrawLine(a, b);
            }
        }

        private void DrawGlobalRoutes()
        {
            if (!config.showGlobalRoutes || negotiationResult == null)
            {
                return;
            }

            int segmentBudget = Mathf.Max(1, config.maxGizmoSegments);
            int drawnSegments = 0;
            for (int routeIndex = 0; routeIndex < negotiationResult.Routes.Count; routeIndex++)
            {
                GlobalRoute route = negotiationResult.Routes[routeIndex];
                if (route.NodeIds.Count <= 1)
                {
                    continue;
                }

                Gizmos.color = new Color(route.Flow.DebugColor.r, route.Flow.DebugColor.g, route.Flow.DebugColor.b, 0.35f);
                for (int i = 1; i < route.NodeIds.Count; i++)
                {
                    if (drawnSegments++ >= segmentBudget)
                    {
                        return;
                    }

                    Vector3 a = ToV3(corridorGraph.Nodes[route.NodeIds[i - 1]]);
                    Vector3 b = ToV3(corridorGraph.Nodes[route.NodeIds[i]]);
                    Gizmos.DrawLine(a, b);
                }
            }
        }

        private void DrawTrunks()
        {
            if (!config.showTrunks || bundleResult == null || corridorGraph == null)
            {
                return;
            }

            Gizmos.color = new Color(0.1f, 0.75f, 1f, 0.85f);
            foreach (KeyValuePair<int, TrunkSegment> pair in bundleResult.TrunksByEdge)
            {
                CorridorEdge edge = corridorGraph.Edges[pair.Key];
                Vector3 a = corridorGraph.Nodes[edge.NodeA];
                Vector3 b = corridorGraph.Nodes[edge.NodeB];
                Vector3 center = (a + b) * 0.5f;
                Vector3 axisNormal = GetTrunkNormal(edge.Axis);
                float span = pair.Value.Lanes.Count * config.lanePitch * 0.5f;
                Gizmos.DrawLine(ToV3(center - axisNormal * span), ToV3(center + axisNormal * span));
            }
        }

        private void DrawDetailedPaths()
        {
            if (!config.showDetailedPaths || detailedPaths == null)
            {
                return;
            }

            int segmentBudget = Mathf.Max(1, config.maxGizmoSegments);
            int drawnSegments = 0;
            for (int i = 0; i < detailedPaths.Count; i++)
            {
                DetailedPath path = detailedPaths[i];
                Gizmos.color = path.Flow != null ? path.Flow.DebugColor : new Color(0.3f, 0.85f, 1f, 1f);
                for (int pointIndex = 1; pointIndex < path.Points.Count; pointIndex++)
                {
                    if (drawnSegments++ >= segmentBudget)
                    {
                        return;
                    }

                    Gizmos.DrawLine(ToV3(path.Points[pointIndex - 1]), ToV3(path.Points[pointIndex]));
                }

                if (config.showRoundedCorners)
                {
                    Gizmos.color = Color.white;
                    for (int markerIndex = 0; markerIndex < path.CornerMarkers.Count; markerIndex++)
                    {
                        Gizmos.DrawSphere(ToV3(path.CornerMarkers[markerIndex]), config.lanePitch * 0.2f);
                    }
                }

                DrawPathOrderLabels(path, i + 1);
            }
        }

        private void DrawPathOrderLabels(DetailedPath path, int order)
        {
#if UNITY_EDITOR
            if (!config.showPathOrderLabels || path == null || path.Points.Count == 0)
            {
                return;
            }

            string label = $"#{order}";
            Vector3 labelLift = Vector3.up * Mathf.Max(0.08f, config.lanePitch * 0.35f);
            GUIStyle style = new GUIStyle(EditorStyles.boldLabel)
            {
                alignment = TextAnchor.MiddleCenter
            };
            Color color = path.Flow != null ? path.Flow.DebugColor : Color.white;
            style.normal.textColor = new Color(color.r, color.g, color.b, 1f);

            Handles.Label(ToV3(path.Points[0]) + labelLift, label, style);
            if (path.Points.Count > 1)
            {
                Handles.Label(ToV3(path.Points[path.Points.Count - 1]) + labelLift, label, style);
            }
#endif
        }

        private void DrawConflicts()
        {
            if (!config.showConflicts || validationReport == null)
            {
                return;
            }

            int segmentBudget = Mathf.Max(1, config.maxGizmoSegments);
            int drawnSegments = 0;
            Gizmos.color = new Color(1f, 0f, 0f, 0.9f);
            for (int i = 0; i < validationReport.ModuleCollisionSegments.Count; i++)
            {
                if (drawnSegments++ >= segmentBudget)
                {
                    return;
                }

                SegmentIssue issue = validationReport.ModuleCollisionSegments[i];
                Gizmos.DrawLine(ToV3(issue.Start), ToV3(issue.End));
            }

            Gizmos.color = new Color(1f, 0.5f, 0f, 0.85f);
            for (int i = 0; i < validationReport.FlowCrossingSegments.Count; i++)
            {
                if (drawnSegments++ >= segmentBudget)
                {
                    return;
                }

                SegmentIssue issue = validationReport.FlowCrossingSegments[i];
                Gizmos.DrawLine(ToV3(issue.Start), ToV3(issue.End));
            }
        }

        private Vector3 ToV3(Vector3 point)
        {
            return transform.TransformPoint(point + Vector3.up * gizmoHeight);
        }

        private Vector3 ToLineRendererPosition(Vector3 point)
        {
            return point + Vector3.up * lineRendererHeightOffset;
        }

        private static Vector3 GetTrunkNormal(EdgeAxis axis)
        {
            return axis switch
            {
                EdgeAxis.X => Vector3.forward,
                EdgeAxis.Y => Vector3.right,
                _ => Vector3.right
            };
        }

        private void DrawBounds(Bounds bounds)
        {
            Matrix4x4 previous = Gizmos.matrix;
            Gizmos.matrix = transform.localToWorldMatrix;
            Gizmos.DrawWireCube(bounds.center + Vector3.up * gizmoHeight, bounds.size);
            Gizmos.matrix = previous;
        }

        private int GetGizmoStride(int count)
        {
            int maxSegments = Mathf.Max(1, config.maxGizmoSegments);
            return Mathf.Max(1, Mathf.CeilToInt(count / (float)maxSegments));
        }
    }

    [CustomEditor(typeof(FlowRoutingSystem))]
    [CanEditMultipleObjects]
    public class FlowRoutingSystemEditor : UnityEditor.Editor
    {
        public override void OnInspectorGUI()
        {
            serializedObject.Update();
            DrawDefaultInspector();
            serializedObject.ApplyModifiedProperties();

            EditorGUILayout.Space(8f);
            EditorGUILayout.LabelField("Routing Tools", EditorStyles.boldLabel);

            if (GUILayout.Button("Generate Random Connections"))
            {
                RunForTargets("Generate Random Connections", system => system.GenerateRandomConnections());
            }

            if (GUILayout.Button("Build Corridors Only"))
            {
                RunForTargets("Build Corridors Only", system => system.BuildCorridorsOnly());
            }

            if (GUILayout.Button("Run One Negotiation Iteration"))
            {
                RunForTargets("Run One Negotiation Iteration", system => system.RunOneNegotiationIteration());
            }

            if (GUILayout.Button("Generate And Draw Flows"))
            {
                RunIncrementalForTargets("Generate And Draw Flows");
            }

            if (GUILayout.Button("Cancel Incremental Generation"))
            {
                RunForTargets("Cancel Incremental Generation", system =>
                {
                    system.CancelActiveRouting();
                });
            }

            if (GUILayout.Button("Clear"))
            {
                RunForTargets("Clear Routing Data", system => system.Clear());
            }

            EditorGUILayout.Space(6f);
            EditorGUILayout.HelpBox(
                "Use Build Corridors Only and Run One Negotiation Iteration for step-by-step debug. " +
                "Generate And Draw Flows runs the route pipeline incrementally and paints LineRenderers as routes arrive.",
                MessageType.Info);
        }

        private void RunIncrementalForTargets(string undoLabel)
        {
            for (int i = 0; i < targets.Length; i++)
            {
                FlowRoutingSystem system = targets[i] as FlowRoutingSystem;
                if (system == null)
                {
                    continue;
                }

                Undo.RecordObject(system, undoLabel);
                if (Application.isPlaying)
                {
                    system.GenerateAndDrawFlowsIncrementally();
                }
                else
                {
                    system.GenerateAndDrawFlowsIncrementallyAsync().Forget();
                }

                EditorUtility.SetDirty(system);
            }
        }

        private void RunForTargets(string undoLabel, System.Action<FlowRoutingSystem> action)
        {
            for (int i = 0; i < targets.Length; i++)
            {
                FlowRoutingSystem system = targets[i] as FlowRoutingSystem;
                if (system == null)
                {
                    continue;
                }

                Undo.RecordObject(system, undoLabel);
                action(system);
                EditorUtility.SetDirty(system);
            }
        }
    }

    [Serializable]
    public class FlowEndpoint
    {
        [SerializeField] private ModuleNode module;
        [SerializeField, HideInInspector] private int autoSlotIndex;
        [SerializeField, HideInInspector] private int autoSlotCount;

        public ModuleNode Module
        {
            get => module;
            set => module = value;
        }

        public int AutoSlotIndex
        {
            get => autoSlotIndex;
            set => autoSlotIndex = Mathf.Max(0, value);
        }

        public int AutoSlotCount
        {
            get => autoSlotCount;
            set => autoSlotCount = Mathf.Max(0, value);
        }

        public void AssignAutoSlot(int slotIndex, int slotCount)
        {
            autoSlotIndex = Mathf.Max(0, slotIndex);
            autoSlotCount = Mathf.Max(0, slotCount);
        }

        public bool TryGetPose(SlotKind slotKind, out SlotPose pose)
        {
            pose = default;
            return module != null && autoSlotCount > 0 && module.TryGetSlotPose(slotKind, autoSlotIndex, autoSlotCount, out pose);
        }
    }

    public class ModuleNode : MonoBehaviour
    {
        private static Transform activeRoutingSpace;

        [SerializeField] private bool includeChildrenInBounds = true;
        [SerializeField] private SlotSide defaultInputSide = SlotSide.Left;
        [SerializeField] private SlotSide defaultOutputSide = SlotSide.Right;

        public Vector3 Size => GetWorldBounds().size;
        public SlotSide DefaultInputSide => defaultInputSide;
        public SlotSide DefaultOutputSide => defaultOutputSide;

        public static IDisposable UseRoutingSpace(Transform routingSpace)
        {
            Transform previous = activeRoutingSpace;
            activeRoutingSpace = routingSpace;
            return new RoutingSpaceScope(previous);
        }

        public Bounds GetWorldBounds()
        {
            if (TryGetComponentBounds(out Bounds bounds))
            {
                return bounds;
            }

            Vector3 position = activeRoutingSpace != null
                ? activeRoutingSpace.InverseTransformPoint(transform.position)
                : transform.position;
            return new Bounds(position, Vector3.zero);
        }

        public Bounds GetInflatedBounds(float clearance)
        {
            Bounds inflated = GetWorldBounds();
            inflated.Expand(Mathf.Max(0f, clearance) * 2f);
            return inflated;
        }

        public Rect GetWorldRectAabb()
        {
            Bounds bounds = GetWorldBounds();
            return Rect.MinMaxRect(bounds.min.x, bounds.min.z, bounds.max.x, bounds.max.z);
        }

        public Rect GetInflatedRect(float clearance)
        {
            Bounds bounds = GetInflatedBounds(clearance);
            return Rect.MinMaxRect(bounds.min.x, bounds.min.z, bounds.max.x, bounds.max.z);
        }

        public bool TryGetSlotPose(SlotKind slotKind, int slotIndex, int totalSlots, out SlotPose pose)
        {
            pose = default;
            if (slotIndex < 0 || totalSlots <= 0 || slotIndex >= totalSlots)
            {
                return false;
            }

            Bounds bounds = GetWorldBounds();
            Vector3 point;
            Vector3 normal;
            float linearT = (slotIndex + 1f) / (totalSlots + 1f);
            GetGridCoordinates(slotIndex, totalSlots, out float u, out float v);
            SlotSide side = slotKind == SlotKind.In ? defaultInputSide : defaultOutputSide;

            switch (side)
            {
                case SlotSide.Left:
                    point = new Vector3(
                        bounds.min.x,
                        bounds.center.y,
                        Mathf.Lerp(bounds.min.z, bounds.max.z, linearT));
                    normal = Vector3.left;
                    break;
                case SlotSide.Right:
                    point = new Vector3(
                        bounds.max.x,
                        bounds.center.y,
                        Mathf.Lerp(bounds.min.z, bounds.max.z, linearT));
                    normal = Vector3.right;
                    break;
                case SlotSide.Top:
                    point = new Vector3(
                        Mathf.Lerp(bounds.min.x, bounds.max.x, linearT),
                        bounds.center.y,
                        bounds.max.z);
                    normal = Vector3.forward;
                    break;
                case SlotSide.Bottom:
                    point = new Vector3(
                        Mathf.Lerp(bounds.min.x, bounds.max.x, linearT),
                        bounds.center.y,
                        bounds.min.z);
                    normal = Vector3.back;
                    break;
                case SlotSide.Up:
                    point = new Vector3(
                        Mathf.Lerp(bounds.min.x, bounds.max.x, u),
                        bounds.max.y,
                        Mathf.Lerp(bounds.min.z, bounds.max.z, v));
                    normal = Vector3.up;
                    break;
                default: // Down
                    point = new Vector3(
                        Mathf.Lerp(bounds.min.x, bounds.max.x, u),
                        bounds.min.y,
                        Mathf.Lerp(bounds.min.z, bounds.max.z, v));
                    normal = Vector3.down;
                    break;
            }

            pose = new SlotPose(point, normal);
            return true;
        }

        private static void GetGridCoordinates(int slotIndex, int totalSlots, out float u, out float v)
        {
            int safeTotal = Mathf.Max(1, totalSlots);
            int columns = Mathf.Max(1, Mathf.CeilToInt(Mathf.Sqrt(safeTotal)));
            int rows = Mathf.Max(1, Mathf.CeilToInt(safeTotal / (float)columns));
            int row = slotIndex / columns;
            int column = slotIndex % columns;

            u = (column + 1f) / (columns + 1f);
            v = (row + 1f) / (rows + 1f);
        }

        private bool TryGetComponentBounds(out Bounds bounds)
        {
            bool hasBounds = false;
            bounds = default;

            Renderer[] renderers = includeChildrenInBounds
                ? GetComponentsInChildren<Renderer>(true)
                : GetComponents<Renderer>();
            for (int i = 0; i < renderers.Length; i++)
            {
                if (renderers[i] == null)
                {
                    continue;
                }

                EncapsulateBounds(ref hasBounds, ref bounds, GetRendererBounds(renderers[i]));
            }

            Collider[] colliders = includeChildrenInBounds
                ? GetComponentsInChildren<Collider>(true)
                : GetComponents<Collider>();
            for (int i = 0; i < colliders.Length; i++)
            {
                if (colliders[i] == null)
                {
                    continue;
                }

                EncapsulateBounds(ref hasBounds, ref bounds, GetColliderBounds(colliders[i]));
            }

            return hasBounds;
        }

        private static Bounds GetRendererBounds(Renderer renderer)
        {
            if (activeRoutingSpace == null)
            {
                return renderer.bounds;
            }

            return TransformLocalBounds(renderer.transform, renderer.localBounds, activeRoutingSpace);
        }

        private static Bounds GetColliderBounds(Collider collider)
        {
            if (activeRoutingSpace == null)
            {
                return collider.bounds;
            }

            if (collider is BoxCollider box)
            {
                return TransformLocalBounds(box.transform, new Bounds(box.center, box.size), activeRoutingSpace);
            }

            return TransformWorldBounds(collider.bounds, activeRoutingSpace);
        }

        private static Bounds TransformLocalBounds(Transform source, Bounds localBounds, Transform targetSpace)
        {
            Vector3 min = localBounds.min;
            Vector3 max = localBounds.max;
            Bounds result = default;
            bool hasPoint = false;

            EncapsulateCorner(ref hasPoint, ref result, source.TransformPoint(new Vector3(min.x, min.y, min.z)), targetSpace);
            EncapsulateCorner(ref hasPoint, ref result, source.TransformPoint(new Vector3(min.x, min.y, max.z)), targetSpace);
            EncapsulateCorner(ref hasPoint, ref result, source.TransformPoint(new Vector3(min.x, max.y, min.z)), targetSpace);
            EncapsulateCorner(ref hasPoint, ref result, source.TransformPoint(new Vector3(min.x, max.y, max.z)), targetSpace);
            EncapsulateCorner(ref hasPoint, ref result, source.TransformPoint(new Vector3(max.x, min.y, min.z)), targetSpace);
            EncapsulateCorner(ref hasPoint, ref result, source.TransformPoint(new Vector3(max.x, min.y, max.z)), targetSpace);
            EncapsulateCorner(ref hasPoint, ref result, source.TransformPoint(new Vector3(max.x, max.y, min.z)), targetSpace);
            EncapsulateCorner(ref hasPoint, ref result, source.TransformPoint(new Vector3(max.x, max.y, max.z)), targetSpace);

            return result;
        }

        private static Bounds TransformWorldBounds(Bounds worldBounds, Transform targetSpace)
        {
            Vector3 min = worldBounds.min;
            Vector3 max = worldBounds.max;
            Bounds result = default;
            bool hasPoint = false;

            EncapsulateCorner(ref hasPoint, ref result, new Vector3(min.x, min.y, min.z), targetSpace);
            EncapsulateCorner(ref hasPoint, ref result, new Vector3(min.x, min.y, max.z), targetSpace);
            EncapsulateCorner(ref hasPoint, ref result, new Vector3(min.x, max.y, min.z), targetSpace);
            EncapsulateCorner(ref hasPoint, ref result, new Vector3(min.x, max.y, max.z), targetSpace);
            EncapsulateCorner(ref hasPoint, ref result, new Vector3(max.x, min.y, min.z), targetSpace);
            EncapsulateCorner(ref hasPoint, ref result, new Vector3(max.x, min.y, max.z), targetSpace);
            EncapsulateCorner(ref hasPoint, ref result, new Vector3(max.x, max.y, min.z), targetSpace);
            EncapsulateCorner(ref hasPoint, ref result, new Vector3(max.x, max.y, max.z), targetSpace);

            return result;
        }

        private static void EncapsulateCorner(ref bool hasPoint, ref Bounds bounds, Vector3 worldPoint, Transform targetSpace)
        {
            Vector3 point = targetSpace.InverseTransformPoint(worldPoint);
            if (!hasPoint)
            {
                bounds = new Bounds(point, Vector3.zero);
                hasPoint = true;
                return;
            }

            bounds.Encapsulate(point);
        }

        private static void EncapsulateBounds(ref bool hasBounds, ref Bounds bounds, Bounds value)
        {
            if (!hasBounds)
            {
                bounds = value;
                hasBounds = true;
                return;
            }

            bounds.Encapsulate(value);
        }

        private readonly struct RoutingSpaceScope : IDisposable
        {
            private readonly Transform previous;

            public RoutingSpaceScope(Transform previous)
            {
                this.previous = previous;
            }

            public void Dispose()
            {
                activeRoutingSpace = previous;
            }
        }
    }

    [CreateAssetMenu(menuName = "Routing/Lightweight PCB Routing Config", fileName = "RoutingConfig")]
    public class RoutingConfig : ScriptableObject
    {
        [Header("Geometry")]
        [Min(0f)] public float clearance = 0.4f;
        [Min(0.02f)] public float lanePitch = 0.25f;
        [Min(0.02f)] public float minSegmentLen = 0.2f;
        [Min(0f)] public float cornerRadius = 0.2f;
        [Min(0f)] public float fanoutLength = 0.6f;
        [Min(0f)] public float workAreaPadding = 2f;
        [Min(0f)] public float proximityPenaltyDistance = 1.5f;

        [Header("Routing Modes")]
        public bool enforcePlanarRoutingOnSameHeight = true;
        [Min(0f)] public float sameHeightTolerance = 0.05f;
        [Min(0f)] public float planarNodeTolerance = 0.08f;

        [Header("Bundling")]
        public bool useTurnAwareLaneOrdering = true;
        [Min(0)] public int laneAlignmentPasses = 2;

        [Header("Cost Weights")]
        [Min(0f)] public float wLength = 1f;
        [Min(0f)] public float wBend = 0.8f;
        [Min(0f)] public float wCongestion = 4f;
        [Min(0f)] public float wOverflow = 10f;
        [Min(0f)] public float wCrossing = 8f;
        [Min(0f)] public float wHistory = 1.2f;

        [Header("Negotiation")]
        [Min(1)] public int maxNegotiationIters = 16;
        [Range(0f, 1f)] public float ripUpPercent = 0.35f;
        [Min(0)] public int maxCrossingFallback = 12;
        public bool mustRenderAll = true;

        [Header("Crossing Optimization")]
        public bool optimizeCrossingsAfterOverflow = true;
        [Range(0f, 1f)] public float crossingRipUpPercent = 0.28f;
        [Min(0)] public int maxCrossingOptimizationIters = 4;
        [Min(0f)] public float crossingRerouteBoost = 5f;

        [Header("Performance & Timeouts")]
        [Min(8)] public int maxGuideCoordsPerAxis = 40;
        [Min(1000)] public int maxGraphNodes = 80000;
        [Min(1000)] public int maxGraphEdges = 220000;
        [Min(0.1f)] public float maxGraphBuildTimeSeconds = 6f;
        [Min(1000)] public int maxExpandedNodesPerFlow = 35000;
        [Min(0.05f)] public float maxRouteTimeSeconds = 0.15f;
        [Min(0.5f)] public float maxSolveTimeSeconds = 30f;
        public bool useHeuristicPruning = true;
        [Min(1f)] public float heuristicPruneMultiplier = 2.4f;
        [Min(0f)] public float heuristicPrunePadding = 4f;
        public bool disableCrossingMapWhenLarge = true;
        [Min(0)] public int crossingMapMaxEdges = 3000;
        [Min(1000)] public int maxValidationChecks = 120000;

        [Header("Debug Flags")]
        public bool showObstacles = true;
        public bool showCorridorGraph = true;
        public bool showCongestion = true;
        public bool showGlobalRoutes = true;
        public bool showTrunks = true;
        public bool showDetailedPaths = true;
        public bool showPathOrderLabels = true;
        public bool showRoundedCorners = false;
        public bool showConflicts = true;
        [Min(100)] public int maxGizmoSegments = 8000;
    }

    public enum SlotKind
    {
        In,
        Out
    }

    public enum SlotSide
    {
        Left,
        Right,
        Top,
        Bottom,
        Up,
        Down
    }

    public readonly struct SlotPose
    {
        public SlotPose(Vector3 position, Vector3 normal)
        {
            Position = position;
            Normal = normal.sqrMagnitude > 0f ? normal.normalized : Vector3.right;
        }

        public Vector3 Position { get; }
        public Vector3 Normal { get; }
    }