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


using System.Collections.Generic;
using Sirenix.OdinInspector;
using UnityEngine;
using Random = System.Random;

public class InfoflowGridGraph : MonoBehaviour
{
    [Header("Bounds")]
    [SerializeField] private Transform boundsRoot;
    [SerializeField] private float cellSize = 1f;
    [SerializeField] private float verticalOffset;

    private Bounds rootBounds;

    private float FloorLocalY => verticalOffset;

    [Header("Modules")]
    [SerializeField] private List<Transform> sourceModules;
    [SerializeField] private List<Transform> targetModules;
    [SerializeField] private List<Transform> consumerModules;

    [Header("Common path")]
    [SerializeField] private List<Vector3> commonPath = new List<Vector3>();

    [SerializeField] private int commonPathOffsetCellsZ = 2;
    [SerializeField] private int commonPathOffsetCellsX = 2;
    [SerializeField] private int commonPathOffsetCellsY = 2;

    [SerializeField] private int sourceRowToleranceCells = 1;
    [SerializeField] private int targetColumnToleranceCells = 1;
    [SerializeField] private int consumerColumnToleranceCells = 1;

    [SerializeField] private List<Vector3> sourceModuleConnectionPoints = new List<Vector3>();
    [SerializeField] private List<Vector3> sourceRowMergePoints = new List<Vector3>();
    [SerializeField] private List<Vector3> targetColumnConnectionPoints = new List<Vector3>();

    [SerializeField] private bool drawSourceModuleConnectionPoints;
    [SerializeField] private bool drawSourceRowMergePoints;
    [SerializeField] private bool drawTargetColumnConnectionPoints;
    [SerializeField] private bool drawCommonPath;

    [Header("Flow connections")]
    [SerializeField] private int randomConnectionSeed = 12345;
    [SerializeField] private int maximumTargetConnectionsPerSource = 3;
    [SerializeField] private int maximumConsumerConnectionsPerSource = 2;

    [SerializeField] private List<InfoflowModuleConnection> moduleConnections = new List<InfoflowModuleConnection>();
    [SerializeField] private List<InfoflowModuleConnection> consumerModuleConnections = new List<InfoflowModuleConnection>();

    [Header("Detailed flows")]
    [SerializeField] private List<InfoflowDetailedFlow> detailedFlows = new List<InfoflowDetailedFlow>();
    [SerializeField] private List<InfoflowDetailedFlow> consumerDetailedFlows = new List<InfoflowDetailedFlow>();

    [SerializeField] private float detailedFlowSpacingCells = 0.18f;

    [Tooltip("Local Y offset for consumer flows on the XZ floor plane.")]
    [SerializeField] private float consumerFlowVerticalOffset = 0.25f;

    [SerializeField] private bool drawDetailedFlows;
    [SerializeField] private bool drawConsumerDetailedFlows = true;
    [SerializeField] private float detailedFlowPointRadius = 0.04f;

    [SerializeField] private bool useRoundedDetailedFlowPoints = true;
    [SerializeField] private float detailedFlowCornerRadiusCells = 0.45f;
    [SerializeField] private int detailedFlowCornerSegments = 8;

    [SerializeField, Range(0f, 1f)] private float detailedFlowColorSaturationMin = 0.65f;
    [SerializeField, Range(0f, 1f)] private float detailedFlowColorSaturationMax = 1f;

    [SerializeField, Range(0f, 1f)] private float detailedFlowColorValueMin = 0.75f;
    [SerializeField, Range(0f, 1f)] private float detailedFlowColorValueMax = 1f;

    [Header("Detailed flow LineRenderers")]
    [SerializeField] private Transform lineRendererRoot;
    [SerializeField] private Material detailedFlowLineMaterial;
    [SerializeField] private float detailedFlowLineWidth = 0.025f;
    [SerializeField] private bool generateTargetFlowLineRenderers = true;
    [SerializeField] private bool generateConsumerFlowLineRenderers = true;

    [Button()]
    public void UpdateRootBounds()
    {
        Transform targetBoundsRoot = boundsRoot != null ? boundsRoot : transform;
        rootBounds = GetLocalBoundsWithChildren(targetBoundsRoot.gameObject);
    }

    [Button()]
    public void BuildCommonFlowPath()
    {
        if (cellSize <= 0f)
        {
            Debug.LogWarning($"{nameof(InfoflowGridGraph)}: Cell size must be greater than zero.", this);
            return;
        }

        UpdateRootBounds();

        commonPath = new List<Vector3>();
        sourceModuleConnectionPoints = new List<Vector3>();
        sourceRowMergePoints = new List<Vector3>();
        targetColumnConnectionPoints = new List<Vector3>();

        List<InfoflowModuleBounds> sourceModuleBounds = GetModuleBounds(sourceModules, ModuleProjection.XZ);
        List<InfoflowModuleBounds> targetModuleBounds = GetModuleBounds(targetModules, ModuleProjection.XY);

        if (sourceModuleBounds.Count == 0)
        {
            Debug.LogWarning($"{nameof(InfoflowGridGraph)}: Source modules are empty.", this);
            return;
        }

        if (targetModuleBounds.Count == 0)
        {
            Debug.LogWarning($"{nameof(InfoflowGridGraph)}: Target modules are empty.", this);
            return;
        }

        float offsetLocalX = GetOffsetLocal(commonPathOffsetCellsX);
        float offsetLocalY = GetOffsetLocal(commonPathOffsetCellsY);
        float offsetLocalZ = GetOffsetLocal(commonPathOffsetCellsZ);

        float sourceBusLocalX = GetRightMostLocalMaxX(sourceModuleBounds) + offsetLocalX;

        List<SourceRowData> sourceRows = BuildSourceRows(sourceModuleBounds, offsetLocalZ);
        sourceRows.Sort((firstRow, secondRow) => firstRow.RowLocalZ.CompareTo(secondRow.RowLocalZ));

        foreach (SourceRowData sourceRow in sourceRows)
        {
            sourceRow.Modules.Sort((firstModule, secondModule) =>
                firstModule.LocalBounds.center.x.CompareTo(secondModule.LocalBounds.center.x));

            foreach (InfoflowModuleBounds moduleBounds in sourceRow.Modules)
            {
                Vector3 sourceConnectionPoint = new Vector3(
                    moduleBounds.LocalBounds.center.x,
                    FloorLocalY,
                    sourceRow.RowLocalZ);

                sourceModuleConnectionPoints.Add(sourceConnectionPoint);
            }

            Vector3 sourceRowMergePoint = new Vector3(
                sourceBusLocalX,
                FloorLocalY,
                sourceRow.RowLocalZ);

            sourceRowMergePoints.Add(sourceRowMergePoint);
            AddLocalPointToCommonPath(sourceRowMergePoint);
        }

        float targetWallLocalZ = GetAverageNearFaceLocalZ(targetModuleBounds);
        float targetVerticalBusLocalX = GetLeftMostLocalMinX(targetModuleBounds) - offsetLocalX;

        if (targetVerticalBusLocalX < sourceBusLocalX)
        {
            targetVerticalBusLocalX = sourceBusLocalX;
        }

        float targetBusLocalY = GetLowestLocalMinY(targetModuleBounds) - offsetLocalY;

        AddLocalPointToCommonPath(new Vector3(sourceBusLocalX, FloorLocalY, targetWallLocalZ));
        AddLocalPointToCommonPath(new Vector3(targetVerticalBusLocalX, FloorLocalY, targetWallLocalZ));
        AddLocalPointToCommonPath(new Vector3(targetVerticalBusLocalX, targetBusLocalY, targetWallLocalZ));

        List<TargetColumnData> targetColumns = BuildTargetColumns(targetModuleBounds);
        targetColumns.Sort((firstColumn, secondColumn) =>
            firstColumn.ColumnLocalX.CompareTo(secondColumn.ColumnLocalX));

        foreach (TargetColumnData targetColumn in targetColumns)
        {
            Vector3 targetColumnConnectionPoint = new Vector3(
                targetColumn.ColumnLocalX,
                targetBusLocalY,
                targetWallLocalZ);

            targetColumnConnectionPoints.Add(targetColumnConnectionPoint);
            AddLocalPointToCommonPath(targetColumnConnectionPoint);
        }
    }

    [Button()]
    public void GenerateRandomConnections()
    {
        GenerateRandomConnections(randomConnectionSeed);
    }

    public void GenerateRandomConnections(int seed)
    {
        moduleConnections = new List<InfoflowModuleConnection>();
        consumerModuleConnections = new List<InfoflowModuleConnection>();

        if (sourceModules == null || sourceModules.Count == 0)
        {
            Debug.LogWarning($"{nameof(InfoflowGridGraph)}: Source modules are empty.", this);
            return;
        }

        List<Transform> validTargetModules = GetValidModules(targetModules);
        List<Transform> validConsumerModules = GetValidModules(consumerModules);

        int clampedMaximumTargetConnectionsPerSource = Mathf.Clamp(
            maximumTargetConnectionsPerSource,
            0,
            validTargetModules.Count);

        int clampedMaximumConsumerConnectionsPerSource = Mathf.Clamp(
            maximumConsumerConnectionsPerSource,
            0,
            validConsumerModules.Count);

        Random random = new Random(seed);

        foreach (Transform sourceModule in sourceModules)
        {
            if (sourceModule == null)
                continue;

            AddRandomConnectionsForSource(
                sourceModule,
                validTargetModules,
                clampedMaximumTargetConnectionsPerSource,
                moduleConnections,
                random);

            AddRandomConnectionsForSource(
                sourceModule,
                validConsumerModules,
                clampedMaximumConsumerConnectionsPerSource,
                consumerModuleConnections,
                random);
        }

        Debug.Log(
            $"{nameof(InfoflowGridGraph)}: Generated {moduleConnections.Count} target connections and {consumerModuleConnections.Count} consumer connections. Seed: {seed}.",
            this);
    }

    private void AddRandomConnectionsForSource(
        Transform sourceModule,
        List<Transform> possibleModules,
        int maximumConnections,
        List<InfoflowModuleConnection> resultConnections,
        Random random)
    {
        if (possibleModules == null || possibleModules.Count == 0)
            return;

        if (maximumConnections <= 0)
            return;

        int connectionCount = random.Next(0, maximumConnections + 1);

        if (connectionCount == 0)
            return;

        List<Transform> shuffledModules = GetShuffledModules(possibleModules, random);

        for (int connectionIndex = 0; connectionIndex < connectionCount; connectionIndex++)
        {
            Transform targetModule = shuffledModules[connectionIndex];

            resultConnections.Add(new InfoflowModuleConnection
            {
                SourceModule = sourceModule,
                TargetModule = targetModule
            });
        }
    }

    [Button()]
    public void BuildDetailedFlows()
    {
        if (cellSize <= 0f)
        {
            Debug.LogWarning($"{nameof(InfoflowGridGraph)}: Cell size must be greater than zero.", this);
            return;
        }

        UpdateRootBounds();

        detailedFlows = new List<InfoflowDetailedFlow>();
        consumerDetailedFlows = new List<InfoflowDetailedFlow>();

        BuildTargetDetailedFlowsInternal();
        BuildConsumerDetailedFlowsInternal();

        Debug.Log(
            $"{nameof(InfoflowGridGraph)}: Generated {detailedFlows.Count} target detailed flows and {consumerDetailedFlows.Count} consumer detailed flows.",
            this);
    }

    private void BuildTargetDetailedFlowsInternal()
    {
        if (moduleConnections == null || moduleConnections.Count == 0)
            return;

        List<InfoflowModuleBounds> sourceModuleBounds = GetModuleBounds(sourceModules, ModuleProjection.XZ);
        List<InfoflowModuleBounds> targetModuleBounds = GetModuleBounds(targetModules, ModuleProjection.XY);

        Dictionary<Transform, InfoflowModuleBounds> sourceBoundsByModule = BuildBoundsDictionary(sourceModuleBounds);
        Dictionary<Transform, InfoflowModuleBounds> targetBoundsByModule = BuildBoundsDictionary(targetModuleBounds);

        List<DetailedFlowBuildData> flowBuildDataList =
            BuildDetailedFlowData(sourceBoundsByModule, targetBoundsByModule, moduleConnections);

        if (flowBuildDataList.Count == 0)
            return;

        AssignSourcePortPositions(flowBuildDataList);
        AssignTargetPortPositions(flowBuildDataList);

        flowBuildDataList.Sort(CompareDetailedFlowOrder);

        float spacingLocal = GetSpacingLocal();
        float maximumLaneOffsetLocal = flowBuildDataList.Count * spacingLocal;

        for (int flowIndex = 0; flowIndex < flowBuildDataList.Count; flowIndex++)
        {
            DetailedFlowBuildData flowBuildData = flowBuildDataList[flowIndex];

            flowBuildData.LaneIndex = flowIndex;
            flowBuildData.LaneOffsetLocal = (flowIndex + 1) * spacingLocal;
        }

        AssignSourceRowLaneOffsets(flowBuildDataList, spacingLocal);

        TargetRouteData targetRouteData = BuildTargetRouteData(sourceModuleBounds, targetModuleBounds);
        Random detailedFlowColorRandom = new Random(randomConnectionSeed);

        foreach (DetailedFlowBuildData flowBuildData in flowBuildDataList)
        {
            List<Vector3> sourceToTargetLocalPath =
                BuildTargetLocalPath(
                    flowBuildData,
                    targetRouteData,
                    flowBuildData.SourceRowLaneOffsetLocal,
                    flowBuildData.LaneOffsetLocal,
                    maximumLaneOffsetLocal);

            sourceToTargetLocalPath.Reverse();

            InfoflowDetailedFlow detailedFlow = new InfoflowDetailedFlow
            {
                SourceModule = flowBuildData.Connection.SourceModule,
                TargetModule = flowBuildData.Connection.TargetModule,
                LaneIndex = flowBuildData.LaneIndex,
                Color = GetRandomDetailedFlowColor(detailedFlowColorRandom),
                Points = sourceToTargetLocalPath,
                RoundedPoints = BuildRoundedLocalPath(sourceToTargetLocalPath)
            };

            detailedFlows.Add(detailedFlow);
        }
    }

    private void BuildConsumerDetailedFlowsInternal()
    {
        if (consumerModuleConnections == null || consumerModuleConnections.Count == 0)
            return;

        List<InfoflowModuleBounds> sourceModuleBounds = GetModuleBounds(sourceModules, ModuleProjection.XZ);
        List<InfoflowModuleBounds> consumerModuleBounds = GetModuleBounds(consumerModules, ModuleProjection.ZY);

        Dictionary<Transform, InfoflowModuleBounds> sourceBoundsByModule = BuildBoundsDictionary(sourceModuleBounds);
        Dictionary<Transform, InfoflowModuleBounds> consumerBoundsByModule = BuildBoundsDictionary(consumerModuleBounds);

        List<ConsumerFlowBuildData> consumerFlowBuildDataList =
            BuildConsumerFlowData(sourceBoundsByModule, consumerBoundsByModule);

        if (consumerFlowBuildDataList.Count == 0)
            return;

        AssignConsumerSourcePortPositions(consumerFlowBuildDataList);
        AssignConsumerPortPositions(consumerFlowBuildDataList);

        ConsumerRouteData consumerRouteData = BuildConsumerRouteData(
            sourceModuleBounds,
            consumerModuleBounds,
            consumerFlowBuildDataList);

        float spacingLocal = GetSpacingLocal();

        AssignConsumerDirectionalLaneOffsets(
            consumerFlowBuildDataList,
            consumerRouteData,
            spacingLocal);

        consumerFlowBuildDataList.Sort(CompareConsumerFlowOrder);

        float maximumLaneOffsetLocal = consumerFlowBuildDataList.Count * spacingLocal;

        for (int flowIndex = 0; flowIndex < consumerFlowBuildDataList.Count; flowIndex++)
        {
            ConsumerFlowBuildData flowBuildData = consumerFlowBuildDataList[flowIndex];

            flowBuildData.LaneIndex = flowIndex;
            flowBuildData.LaneOffsetLocal = (flowIndex + 1) * spacingLocal;
        }

        AssignConsumerSourceRowLaneOffsets(consumerFlowBuildDataList, spacingLocal);

        Random detailedFlowColorRandom = new Random(randomConnectionSeed + 7919);

        foreach (ConsumerFlowBuildData flowBuildData in consumerFlowBuildDataList)
        {
            List<Vector3> sourceToConsumerLocalPath =
                BuildConsumerLocalPath(
                    flowBuildData,
                    consumerRouteData,
                    flowBuildData.SourceRowLaneOffsetLocal,
                    flowBuildData.LaneOffsetLocal,
                    maximumLaneOffsetLocal);

            sourceToConsumerLocalPath.Reverse();

            InfoflowDetailedFlow detailedFlow = new InfoflowDetailedFlow
            {
                SourceModule = flowBuildData.Connection.SourceModule,
                TargetModule = flowBuildData.Connection.TargetModule,
                LaneIndex = flowBuildData.LaneIndex,
                Color = GetRandomDetailedFlowColor(detailedFlowColorRandom),
                Points = sourceToConsumerLocalPath,
                RoundedPoints = BuildRoundedLocalPath(sourceToConsumerLocalPath)
            };

            consumerDetailedFlows.Add(detailedFlow);
        }
    }

    private TargetRouteData BuildTargetRouteData(
        List<InfoflowModuleBounds> sourceModuleBounds,
        List<InfoflowModuleBounds> targetModuleBounds)
    {
        float offsetLocalX = GetOffsetLocal(commonPathOffsetCellsX);
        float offsetLocalY = GetOffsetLocal(commonPathOffsetCellsY);

        float sourceBusLocalX = GetRightMostLocalMaxX(sourceModuleBounds) + offsetLocalX;
        float targetVerticalBusLocalX = GetLeftMostLocalMinX(targetModuleBounds) - offsetLocalX;

        if (targetVerticalBusLocalX < sourceBusLocalX)
        {
            targetVerticalBusLocalX = sourceBusLocalX;
        }

        return new TargetRouteData
        {
            SourceBusLocalX = sourceBusLocalX,
            TargetVerticalBusLocalX = targetVerticalBusLocalX,
            TargetWallLocalZ = GetAverageNearFaceLocalZ(targetModuleBounds),
            TargetBusLocalY = GetLowestLocalMinY(targetModuleBounds) - offsetLocalY
        };
    }

    private ConsumerRouteData BuildConsumerRouteData(
        List<InfoflowModuleBounds> sourceModuleBounds,
        List<InfoflowModuleBounds> consumerModuleBounds,
        List<ConsumerFlowBuildData> consumerFlowBuildDataList)
    {
        float offsetLocalX = GetOffsetLocal(commonPathOffsetCellsX);
        float offsetLocalY = GetOffsetLocal(commonPathOffsetCellsY);
        float offsetLocalZ = GetOffsetLocal(commonPathOffsetCellsZ);

        float sourceLeftBusLocalX = GetLeftMostLocalMinX(sourceModuleBounds) - offsetLocalX;

        float minSourceRowLocalZ = float.MaxValue;
        float maxSourceRowLocalZ = float.MinValue;

        foreach (ConsumerFlowBuildData flowBuildData in consumerFlowBuildDataList)
        {
            float sourceConnectionLocalZ = GetSourceConnectionLocalZ(flowBuildData.SourceBounds, offsetLocalZ);

            minSourceRowLocalZ = Mathf.Min(minSourceRowLocalZ, sourceConnectionLocalZ);
            maxSourceRowLocalZ = Mathf.Max(maxSourceRowLocalZ, sourceConnectionLocalZ);
        }

        if (minSourceRowLocalZ == float.MaxValue)
        {
            minSourceRowLocalZ = rootBounds.center.z;
            maxSourceRowLocalZ = rootBounds.center.z;
        }

        float middleSourceRowsLocalZ = (minSourceRowLocalZ + maxSourceRowLocalZ) * 0.5f;

        return new ConsumerRouteData
        {
            SourceLeftBusLocalX = sourceLeftBusLocalX,
            MiddleSourceRowsLocalZ = middleSourceRowsLocalZ,
            ConsumerLeftFaceLocalX = GetLeftMostLocalMinX(consumerModuleBounds),
            ConsumerBusLocalY = GetLowestLocalMinY(consumerModuleBounds) - offsetLocalY
        };
    }

    private List<Vector3> BuildTargetLocalPath(
        DetailedFlowBuildData flowBuildData,
        TargetRouteData targetRouteData,
        float sourceRowOffsetLocal,
        float commonOffsetLocal,
        float maximumLaneOffsetLocal)
    {
        float offsetLocalY = GetOffsetLocal(commonPathOffsetCellsY);
        float offsetLocalZ = GetOffsetLocal(commonPathOffsetCellsZ);

        float sourceLaneOffsetLocal = Mathf.Max(0f, sourceRowOffsetLocal);
        float invertedTurnOffsetLocal = Mathf.Max(0f, maximumLaneOffsetLocal - commonOffsetLocal);

        float sourceEntryLocalX = flowBuildData.SourcePortLocalX;
        float sourceEntryLocalZ = flowBuildData.SourceBounds.LocalBounds.min.z;

        float sourcePreEntryLocalZ = sourceEntryLocalZ - offsetLocalZ;
        float sourceLaneLocalZ = sourcePreEntryLocalZ - sourceLaneOffsetLocal;

        float sourceTurnLocalX = targetRouteData.SourceBusLocalX + invertedTurnOffsetLocal;
        float targetTurnLocalX = targetRouteData.TargetVerticalBusLocalX + invertedTurnOffsetLocal;

        float targetEntryLocalX = flowBuildData.TargetPortLocalX;
        float targetEntryLocalY = flowBuildData.TargetBounds.LocalBounds.min.y;
        float targetPreEntryLocalY = targetEntryLocalY - offsetLocalY;

        float targetBusLaneLocalY = targetRouteData.TargetBusLocalY - commonOffsetLocal;
        float targetWallLocalZ = targetRouteData.TargetWallLocalZ;

        List<Vector3> path = new List<Vector3>
        {
            new Vector3(sourceEntryLocalX, FloorLocalY, sourceEntryLocalZ),
            new Vector3(sourceEntryLocalX, FloorLocalY, sourcePreEntryLocalZ),
            new Vector3(sourceEntryLocalX, FloorLocalY, sourceLaneLocalZ),
            new Vector3(sourceTurnLocalX, FloorLocalY, sourceLaneLocalZ),
            new Vector3(sourceTurnLocalX, FloorLocalY, targetWallLocalZ),
            new Vector3(targetTurnLocalX, FloorLocalY, targetWallLocalZ),
            new Vector3(targetTurnLocalX, targetBusLaneLocalY, targetWallLocalZ),
            new Vector3(targetEntryLocalX, targetBusLaneLocalY, targetWallLocalZ),
            new Vector3(targetEntryLocalX, targetPreEntryLocalY, targetWallLocalZ),
            new Vector3(targetEntryLocalX, targetEntryLocalY, targetWallLocalZ)
        };

        return RemoveDuplicateLocalPoints(path);
    }

    private List<Vector3> BuildConsumerLocalPath(
        ConsumerFlowBuildData flowBuildData,
        ConsumerRouteData consumerRouteData,
        float sourceRowOffsetLocal,
        float commonOffsetLocal,
        float maximumLaneOffsetLocal)
    {
        float offsetLocalZ = GetOffsetLocal(commonPathOffsetCellsZ);

        float sourceLaneOffsetLocal = Mathf.Max(0f, sourceRowOffsetLocal);
        float invertedTurnOffsetLocal = Mathf.Max(0f, maximumLaneOffsetLocal - commonOffsetLocal);
        float directionalLaneOffsetLocal = Mathf.Max(0f, flowBuildData.ConsumerDirectionalLaneOffsetLocal);

        float floorFlowLocalY = FloorLocalY + consumerFlowVerticalOffset;

        float sourceEntryLocalX = flowBuildData.SourcePortLocalX;
        float sourceEntryLocalZ = flowBuildData.SourceBounds.LocalBounds.min.z;

        float sourcePreEntryLocalZ = sourceEntryLocalZ - offsetLocalZ;
        float sourceLaneLocalZ = sourcePreEntryLocalZ - sourceLaneOffsetLocal;

        float sourceTurnLocalX = consumerRouteData.SourceLeftBusLocalX - invertedTurnOffsetLocal;

        float middleLaneLocalZ =
            consumerRouteData.MiddleSourceRowsLocalZ +
            flowBuildData.ConsumerDirection * directionalLaneOffsetLocal;

        float consumerLeftFaceLocalX = flowBuildData.ConsumerBounds.LocalBounds.min.x;
        float consumerColumnLocalZ = flowBuildData.ConsumerPortLocalZ;
        float consumerEntryLocalY = flowBuildData.ConsumerBounds.LocalBounds.min.y;

        float consumerBusLaneLocalY = consumerRouteData.ConsumerBusLocalY - directionalLaneOffsetLocal;

        List<Vector3> path = new List<Vector3>
        {
            new Vector3(sourceEntryLocalX, floorFlowLocalY, sourceEntryLocalZ),
            new Vector3(sourceEntryLocalX, floorFlowLocalY, sourcePreEntryLocalZ),
            new Vector3(sourceEntryLocalX, floorFlowLocalY, sourceLaneLocalZ),
            new Vector3(sourceTurnLocalX, floorFlowLocalY, sourceLaneLocalZ),
            new Vector3(sourceTurnLocalX, floorFlowLocalY, middleLaneLocalZ),
            new Vector3(consumerRouteData.ConsumerLeftFaceLocalX, floorFlowLocalY, middleLaneLocalZ),
            new Vector3(consumerRouteData.ConsumerLeftFaceLocalX, consumerBusLaneLocalY, middleLaneLocalZ),
            new Vector3(consumerRouteData.ConsumerLeftFaceLocalX, consumerBusLaneLocalY, consumerColumnLocalZ),
            new Vector3(consumerLeftFaceLocalX, consumerBusLaneLocalY, consumerColumnLocalZ),
            new Vector3(consumerLeftFaceLocalX, consumerEntryLocalY, consumerColumnLocalZ)
        };

        return RemoveDuplicateLocalPoints(path);
    }

    private List<DetailedFlowBuildData> BuildDetailedFlowData(
        Dictionary<Transform, InfoflowModuleBounds> sourceBoundsByModule,
        Dictionary<Transform, InfoflowModuleBounds> targetBoundsByModule,
        List<InfoflowModuleConnection> connections)
    {
        List<DetailedFlowBuildData> flowBuildDataList = new List<DetailedFlowBuildData>();

        foreach (InfoflowModuleConnection connection in connections)
        {
            if (connection == null)
                continue;

            if (connection.SourceModule == null || connection.TargetModule == null)
                continue;

            if (sourceBoundsByModule.TryGetValue(connection.SourceModule, out InfoflowModuleBounds sourceBounds) == false)
                continue;

            if (targetBoundsByModule.TryGetValue(connection.TargetModule, out InfoflowModuleBounds targetBounds) == false)
                continue;

            flowBuildDataList.Add(new DetailedFlowBuildData
            {
                Connection = connection,
                SourceBounds = sourceBounds,
                TargetBounds = targetBounds
            });
        }

        return flowBuildDataList;
    }

    private List<ConsumerFlowBuildData> BuildConsumerFlowData(
        Dictionary<Transform, InfoflowModuleBounds> sourceBoundsByModule,
        Dictionary<Transform, InfoflowModuleBounds> consumerBoundsByModule)
    {
        List<ConsumerFlowBuildData> consumerFlowBuildDataList = new List<ConsumerFlowBuildData>();

        foreach (InfoflowModuleConnection connection in consumerModuleConnections)
        {
            if (connection == null)
                continue;

            if (connection.SourceModule == null || connection.TargetModule == null)
                continue;

            if (sourceBoundsByModule.TryGetValue(connection.SourceModule, out InfoflowModuleBounds sourceBounds) == false)
                continue;

            if (consumerBoundsByModule.TryGetValue(connection.TargetModule, out InfoflowModuleBounds consumerBounds) == false)
                continue;

            consumerFlowBuildDataList.Add(new ConsumerFlowBuildData
            {
                Connection = connection,
                SourceBounds = sourceBounds,
                ConsumerBounds = consumerBounds
            });
        }

        return consumerFlowBuildDataList;
    }

    private void AssignSourcePortPositions(List<DetailedFlowBuildData> flowBuildDataList)
    {
        Dictionary<Transform, List<DetailedFlowBuildData>> flowsBySourceModule =
            new Dictionary<Transform, List<DetailedFlowBuildData>>();

        foreach (DetailedFlowBuildData flowBuildData in flowBuildDataList)
        {
            Transform sourceModule = flowBuildData.Connection.SourceModule;

            if (flowsBySourceModule.ContainsKey(sourceModule) == false)
            {
                flowsBySourceModule.Add(sourceModule, new List<DetailedFlowBuildData>());
            }

            flowsBySourceModule[sourceModule].Add(flowBuildData);
        }

        foreach (KeyValuePair<Transform, List<DetailedFlowBuildData>> pair in flowsBySourceModule)
        {
            List<DetailedFlowBuildData> flows = pair.Value;

            flows.Sort((firstFlow, secondFlow) =>
            {
                int targetXComparison = secondFlow.TargetBounds.LocalBounds.center.x.CompareTo(
                    firstFlow.TargetBounds.LocalBounds.center.x);

                if (targetXComparison != 0)
                    return targetXComparison;

                return secondFlow.TargetBounds.LocalBounds.center.y.CompareTo(
                    firstFlow.TargetBounds.LocalBounds.center.y);
            });

            float sourceCenterLocalX = flows[0].SourceBounds.LocalBounds.center.x;

            for (int flowIndex = 0; flowIndex < flows.Count; flowIndex++)
            {
                DetailedFlowBuildData flowBuildData = flows[flowIndex];

                flowBuildData.SourcePortLocalX = GetCenteredPortPosition(
                    sourceCenterLocalX,
                    flowIndex,
                    flows.Count,
                    GetSpacingLocal());
            }
        }
    }

    private void AssignConsumerSourcePortPositions(List<ConsumerFlowBuildData> flowBuildDataList)
    {
        Dictionary<Transform, List<ConsumerFlowBuildData>> flowsBySourceModule =
            new Dictionary<Transform, List<ConsumerFlowBuildData>>();

        foreach (ConsumerFlowBuildData flowBuildData in flowBuildDataList)
        {
            Transform sourceModule = flowBuildData.Connection.SourceModule;

            if (flowsBySourceModule.ContainsKey(sourceModule) == false)
            {
                flowsBySourceModule.Add(sourceModule, new List<ConsumerFlowBuildData>());
            }

            flowsBySourceModule[sourceModule].Add(flowBuildData);
        }

        foreach (KeyValuePair<Transform, List<ConsumerFlowBuildData>> pair in flowsBySourceModule)
        {
            List<ConsumerFlowBuildData> flows = pair.Value;

            flows.Sort((firstFlow, secondFlow) =>
            {
                int consumerZComparison = secondFlow.ConsumerBounds.LocalBounds.center.z.CompareTo(
                    firstFlow.ConsumerBounds.LocalBounds.center.z);

                if (consumerZComparison != 0)
                    return consumerZComparison;

                return secondFlow.ConsumerBounds.LocalBounds.center.y.CompareTo(
                    firstFlow.ConsumerBounds.LocalBounds.center.y);
            });

            float sourceCenterLocalX = flows[0].SourceBounds.LocalBounds.center.x;

            for (int flowIndex = 0; flowIndex < flows.Count; flowIndex++)
            {
                ConsumerFlowBuildData flowBuildData = flows[flowIndex];

                flowBuildData.SourcePortLocalX = GetCenteredPortPosition(
                    sourceCenterLocalX,
                    flowIndex,
                    flows.Count,
                    GetSpacingLocal());
            }
        }
    }

    private void AssignTargetPortPositions(List<DetailedFlowBuildData> flowBuildDataList)
    {
        List<TargetFlowColumnData> targetFlowColumns = new List<TargetFlowColumnData>();

        foreach (DetailedFlowBuildData flowBuildData in flowBuildDataList)
        {
            float targetColumnLocalX = flowBuildData.TargetBounds.LocalBounds.center.x;

            TargetFlowColumnData targetFlowColumn = FindTargetFlowColumn(targetFlowColumns, targetColumnLocalX);

            if (targetFlowColumn == null)
            {
                targetFlowColumn = new TargetFlowColumnData
                {
                    ColumnLocalX = targetColumnLocalX
                };

                targetFlowColumns.Add(targetFlowColumn);
            }

            targetFlowColumn.Flows.Add(flowBuildData);
        }

        foreach (TargetFlowColumnData targetFlowColumn in targetFlowColumns)
        {
            targetFlowColumn.Flows.Sort((firstFlow, secondFlow) =>
            {
                int targetYComparison = firstFlow.TargetBounds.LocalBounds.min.y.CompareTo(
                    secondFlow.TargetBounds.LocalBounds.min.y);

                if (targetYComparison != 0)
                    return targetYComparison;

                int sourceXComparison = firstFlow.SourceBounds.LocalBounds.center.x.CompareTo(
                    secondFlow.SourceBounds.LocalBounds.center.x);

                if (sourceXComparison != 0)
                    return sourceXComparison;

                return firstFlow.SourceBounds.LocalBounds.center.z.CompareTo(
                    secondFlow.SourceBounds.LocalBounds.center.z);
            });

            for (int flowIndex = 0; flowIndex < targetFlowColumn.Flows.Count; flowIndex++)
            {
                DetailedFlowBuildData flowBuildData = targetFlowColumn.Flows[flowIndex];

                flowBuildData.TargetPortLocalX = GetCenteredPortPosition(
                    targetFlowColumn.ColumnLocalX,
                    flowIndex,
                    targetFlowColumn.Flows.Count,
                    GetSpacingLocal());
            }
        }
    }

    private void AssignConsumerPortPositions(List<ConsumerFlowBuildData> flowBuildDataList)
    {
        List<ConsumerFlowColumnData> consumerFlowColumns = new List<ConsumerFlowColumnData>();

        foreach (ConsumerFlowBuildData flowBuildData in flowBuildDataList)
        {
            float consumerColumnLocalZ = flowBuildData.ConsumerBounds.LocalBounds.center.z;

            ConsumerFlowColumnData consumerFlowColumn = FindConsumerFlowColumn(
                consumerFlowColumns,
                consumerColumnLocalZ);

            if (consumerFlowColumn == null)
            {
                consumerFlowColumn = new ConsumerFlowColumnData
                {
                    ColumnLocalZ = consumerColumnLocalZ
                };

                consumerFlowColumns.Add(consumerFlowColumn);
            }

            consumerFlowColumn.Flows.Add(flowBuildData);
        }

        foreach (ConsumerFlowColumnData consumerFlowColumn in consumerFlowColumns)
        {
            consumerFlowColumn.Flows.Sort((firstFlow, secondFlow) =>
            {
                int consumerYComparison = firstFlow.ConsumerBounds.LocalBounds.min.y.CompareTo(
                    secondFlow.ConsumerBounds.LocalBounds.min.y);

                if (consumerYComparison != 0)
                    return consumerYComparison;

                int sourceXComparison = firstFlow.SourceBounds.LocalBounds.center.x.CompareTo(
                    secondFlow.SourceBounds.LocalBounds.center.x);

                if (sourceXComparison != 0)
                    return sourceXComparison;

                return firstFlow.SourceBounds.LocalBounds.center.z.CompareTo(
                    secondFlow.SourceBounds.LocalBounds.center.z);
            });

            for (int flowIndex = 0; flowIndex < consumerFlowColumn.Flows.Count; flowIndex++)
            {
                ConsumerFlowBuildData flowBuildData = consumerFlowColumn.Flows[flowIndex];

                flowBuildData.ConsumerPortLocalZ = GetCenteredPortPosition(
                    consumerFlowColumn.ColumnLocalZ,
                    flowIndex,
                    consumerFlowColumn.Flows.Count,
                    GetSpacingLocal());
            }
        }
    }

    private void AssignConsumerDirectionalLaneOffsets(
        List<ConsumerFlowBuildData> flowBuildDataList,
        ConsumerRouteData consumerRouteData,
        float spacingLocal)
    {
        List<ConsumerFlowBuildData> negativeDirectionFlows = new List<ConsumerFlowBuildData>();
        List<ConsumerFlowBuildData> positiveDirectionFlows = new List<ConsumerFlowBuildData>();

        foreach (ConsumerFlowBuildData flowBuildData in flowBuildDataList)
        {
            float distanceToSplitPoint = Mathf.Abs(
                flowBuildData.ConsumerPortLocalZ - consumerRouteData.MiddleSourceRowsLocalZ);

            flowBuildData.ConsumerDistanceToSplitZ = distanceToSplitPoint;
            flowBuildData.ConsumerDirection =
                flowBuildData.ConsumerPortLocalZ >= consumerRouteData.MiddleSourceRowsLocalZ ? 1 : -1;

            if (flowBuildData.ConsumerDirection > 0)
            {
                positiveDirectionFlows.Add(flowBuildData);
            }
            else
            {
                negativeDirectionFlows.Add(flowBuildData);
            }
        }

        AssignConsumerDirectionalLaneOffsetsForGroup(positiveDirectionFlows, spacingLocal);
        AssignConsumerDirectionalLaneOffsetsForGroup(negativeDirectionFlows, spacingLocal);
    }

    private void AssignConsumerDirectionalLaneOffsetsForGroup(
        List<ConsumerFlowBuildData> directionalFlows,
        float spacingLocal)
    {
        directionalFlows.Sort((firstFlow, secondFlow) =>
        {
            int distanceComparison = firstFlow.ConsumerDistanceToSplitZ.CompareTo(
                secondFlow.ConsumerDistanceToSplitZ);

            if (distanceComparison != 0)
                return distanceComparison;

            int consumerYComparison = firstFlow.ConsumerBounds.LocalBounds.min.y.CompareTo(
                secondFlow.ConsumerBounds.LocalBounds.min.y);

            if (consumerYComparison != 0)
                return consumerYComparison;

            int sourceXComparison = firstFlow.SourceBounds.LocalBounds.center.x.CompareTo(
                secondFlow.SourceBounds.LocalBounds.center.x);

            if (sourceXComparison != 0)
                return sourceXComparison;

            return firstFlow.SourceBounds.LocalBounds.center.z.CompareTo(
                secondFlow.SourceBounds.LocalBounds.center.z);
        });

        for (int flowIndex = 0; flowIndex < directionalFlows.Count; flowIndex++)
        {
            ConsumerFlowBuildData flowBuildData = directionalFlows[flowIndex];

            flowBuildData.ConsumerDirectionalLaneIndex = flowIndex;
            flowBuildData.ConsumerDirectionalLaneOffsetLocal = (flowIndex + 1) * spacingLocal;
        }
    }

    private void AssignSourceRowLaneOffsets(List<DetailedFlowBuildData> flowBuildDataList, float spacingLocal)
    {
        List<SourceFlowRowData> sourceFlowRows = new List<SourceFlowRowData>();
        float offsetLocalZ = GetOffsetLocal(commonPathOffsetCellsZ);

        foreach (DetailedFlowBuildData flowBuildData in flowBuildDataList)
        {
            float sourceConnectionLocalZ = GetSourceConnectionLocalZ(flowBuildData.SourceBounds, offsetLocalZ);

            SourceFlowRowData sourceFlowRow = FindSourceFlowRow(sourceFlowRows, sourceConnectionLocalZ);

            if (sourceFlowRow == null)
            {
                sourceFlowRow = new SourceFlowRowData
                {
                    RowLocalZ = sourceConnectionLocalZ
                };

                sourceFlowRows.Add(sourceFlowRow);
            }

            sourceFlowRow.TargetFlows.Add(flowBuildData);
        }

        sourceFlowRows.Sort((firstRow, secondRow) =>
            firstRow.RowLocalZ.CompareTo(secondRow.RowLocalZ));

        for (int rowIndex = 0; rowIndex < sourceFlowRows.Count; rowIndex++)
        {
            SourceFlowRowData sourceFlowRow = sourceFlowRows[rowIndex];

            sourceFlowRow.TargetFlows.Sort((firstFlow, secondFlow) =>
            {
                int sourcePortXComparison = secondFlow.SourcePortLocalX.CompareTo(firstFlow.SourcePortLocalX);

                if (sourcePortXComparison != 0)
                    return sourcePortXComparison;

                return firstFlow.LaneIndex.CompareTo(secondFlow.LaneIndex);
            });

            for (int flowIndex = 0; flowIndex < sourceFlowRow.TargetFlows.Count; flowIndex++)
            {
                DetailedFlowBuildData flowBuildData = sourceFlowRow.TargetFlows[flowIndex];

                flowBuildData.SourceRowIndex = rowIndex;
                flowBuildData.SourceRowLaneOffsetLocal = (flowIndex + 1) * spacingLocal;
            }
        }
    }

    private void AssignConsumerSourceRowLaneOffsets(List<ConsumerFlowBuildData> flowBuildDataList, float spacingLocal)
    {
        List<SourceFlowRowData> sourceFlowRows = new List<SourceFlowRowData>();
        float offsetLocalZ = GetOffsetLocal(commonPathOffsetCellsZ);

        foreach (ConsumerFlowBuildData flowBuildData in flowBuildDataList)
        {
            float sourceConnectionLocalZ = GetSourceConnectionLocalZ(flowBuildData.SourceBounds, offsetLocalZ);

            SourceFlowRowData sourceFlowRow = FindSourceFlowRow(sourceFlowRows, sourceConnectionLocalZ);

            if (sourceFlowRow == null)
            {
                sourceFlowRow = new SourceFlowRowData
                {
                    RowLocalZ = sourceConnectionLocalZ
                };

                sourceFlowRows.Add(sourceFlowRow);
            }

            sourceFlowRow.ConsumerFlows.Add(flowBuildData);
        }

        sourceFlowRows.Sort((firstRow, secondRow) =>
            firstRow.RowLocalZ.CompareTo(secondRow.RowLocalZ));

        for (int rowIndex = 0; rowIndex < sourceFlowRows.Count; rowIndex++)
        {
            SourceFlowRowData sourceFlowRow = sourceFlowRows[rowIndex];

            sourceFlowRow.ConsumerFlows.Sort((firstFlow, secondFlow) =>
            {
                int sourcePortXComparison = firstFlow.SourcePortLocalX.CompareTo(secondFlow.SourcePortLocalX);

                if (sourcePortXComparison != 0)
                    return sourcePortXComparison;

                return firstFlow.LaneIndex.CompareTo(secondFlow.LaneIndex);
            });

            for (int flowIndex = 0; flowIndex < sourceFlowRow.ConsumerFlows.Count; flowIndex++)
            {
                ConsumerFlowBuildData flowBuildData = sourceFlowRow.ConsumerFlows[flowIndex];

                flowBuildData.SourceRowIndex = rowIndex;
                flowBuildData.SourceRowLaneOffsetLocal = (flowIndex + 1) * spacingLocal;
            }
        }
    }

    private List<SourceRowData> BuildSourceRows(
        List<InfoflowModuleBounds> sourceModuleBounds,
        float offsetLocalZ)
    {
        List<SourceRowData> sourceRows = new List<SourceRowData>();

        foreach (InfoflowModuleBounds moduleBounds in sourceModuleBounds)
        {
            float sourceConnectionLocalZ = GetSourceConnectionLocalZ(moduleBounds, offsetLocalZ);
            SourceRowData sourceRow = FindSourceRow(sourceRows, sourceConnectionLocalZ);

            if (sourceRow == null)
            {
                sourceRow = new SourceRowData
                {
                    RowLocalZ = sourceConnectionLocalZ
                };

                sourceRows.Add(sourceRow);
            }

            sourceRow.Modules.Add(moduleBounds);
        }

        return sourceRows;
    }

    private SourceRowData FindSourceRow(List<SourceRowData> sourceRows, float sourceConnectionLocalZ)
    {
        float toleranceLocal = GetOffsetLocal(sourceRowToleranceCells);

        SourceRowData closestRow = null;
        float closestDistance = float.MaxValue;

        foreach (SourceRowData sourceRow in sourceRows)
        {
            float distance = Mathf.Abs(sourceRow.RowLocalZ - sourceConnectionLocalZ);

            if (distance > toleranceLocal)
                continue;

            if (distance >= closestDistance)
                continue;

            closestDistance = distance;
            closestRow = sourceRow;
        }

        return closestRow;
    }

    private SourceFlowRowData FindSourceFlowRow(
        List<SourceFlowRowData> sourceFlowRows,
        float sourceConnectionLocalZ)
    {
        float toleranceLocal = GetOffsetLocal(sourceRowToleranceCells);

        SourceFlowRowData closestRow = null;
        float closestDistance = float.MaxValue;

        foreach (SourceFlowRowData sourceFlowRow in sourceFlowRows)
        {
            float distance = Mathf.Abs(sourceFlowRow.RowLocalZ - sourceConnectionLocalZ);

            if (distance > toleranceLocal)
                continue;

            if (distance >= closestDistance)
                continue;

            closestDistance = distance;
            closestRow = sourceFlowRow;
        }

        return closestRow;
    }

    private List<TargetColumnData> BuildTargetColumns(List<InfoflowModuleBounds> targetModuleBounds)
    {
        List<TargetColumnData> targetColumns = new List<TargetColumnData>();

        foreach (InfoflowModuleBounds moduleBounds in targetModuleBounds)
        {
            float columnLocalX = moduleBounds.LocalBounds.center.x;
            TargetColumnData targetColumn = FindTargetColumn(targetColumns, columnLocalX);

            if (targetColumn == null)
            {
                targetColumn = new TargetColumnData
                {
                    ColumnLocalX = columnLocalX
                };

                targetColumns.Add(targetColumn);
            }

            targetColumn.Modules.Add(moduleBounds);
        }

        return targetColumns;
    }

    private TargetColumnData FindTargetColumn(List<TargetColumnData> targetColumns, float columnLocalX)
    {
        float toleranceLocal = GetOffsetLocal(targetColumnToleranceCells);

        TargetColumnData closestColumn = null;
        float closestDistance = float.MaxValue;

        foreach (TargetColumnData targetColumn in targetColumns)
        {
            float distance = Mathf.Abs(targetColumn.ColumnLocalX - columnLocalX);

            if (distance > toleranceLocal)
                continue;

            if (distance >= closestDistance)
                continue;

            closestDistance = distance;
            closestColumn = targetColumn;
        }

        return closestColumn;
    }

    private TargetFlowColumnData FindTargetFlowColumn(
        List<TargetFlowColumnData> targetFlowColumns,
        float targetColumnLocalX)
    {
        float toleranceLocal = GetOffsetLocal(targetColumnToleranceCells);

        TargetFlowColumnData closestColumn = null;
        float closestDistance = float.MaxValue;

        foreach (TargetFlowColumnData targetFlowColumn in targetFlowColumns)
        {
            float distance = Mathf.Abs(targetFlowColumn.ColumnLocalX - targetColumnLocalX);

            if (distance > toleranceLocal)
                continue;

            if (distance >= closestDistance)
                continue;

            closestDistance = distance;
            closestColumn = targetFlowColumn;
        }

        return closestColumn;
    }

    private ConsumerFlowColumnData FindConsumerFlowColumn(
        List<ConsumerFlowColumnData> consumerFlowColumns,
        float consumerColumnLocalZ)
    {
        float toleranceLocal = GetOffsetLocal(consumerColumnToleranceCells);

        ConsumerFlowColumnData closestColumn = null;
        float closestDistance = float.MaxValue;

        foreach (ConsumerFlowColumnData consumerFlowColumn in consumerFlowColumns)
        {
            float distance = Mathf.Abs(consumerFlowColumn.ColumnLocalZ - consumerColumnLocalZ);

            if (distance > toleranceLocal)
                continue;

            if (distance >= closestDistance)
                continue;

            closestDistance = distance;
            closestColumn = consumerFlowColumn;
        }

        return closestColumn;
    }

    private int CompareDetailedFlowOrder(DetailedFlowBuildData firstFlow, DetailedFlowBuildData secondFlow)
    {
        int targetPortXComparison = secondFlow.TargetPortLocalX.CompareTo(firstFlow.TargetPortLocalX);

        if (targetPortXComparison != 0)
            return targetPortXComparison;

        int targetYComparison = secondFlow.TargetBounds.LocalBounds.min.y.CompareTo(
            firstFlow.TargetBounds.LocalBounds.min.y);

        if (targetYComparison != 0)
            return targetYComparison;

        int sourceXComparison = secondFlow.SourcePortLocalX.CompareTo(firstFlow.SourcePortLocalX);

        if (sourceXComparison != 0)
            return sourceXComparison;

        return secondFlow.SourceBounds.LocalBounds.center.z.CompareTo(firstFlow.SourceBounds.LocalBounds.center.z);
    }

    private int CompareConsumerFlowOrder(ConsumerFlowBuildData firstFlow, ConsumerFlowBuildData secondFlow)
    {
        int directionComparison = secondFlow.ConsumerDirection.CompareTo(firstFlow.ConsumerDirection);

        if (directionComparison != 0)
            return directionComparison;

        int directionalLaneComparison =
            firstFlow.ConsumerDirectionalLaneIndex.CompareTo(secondFlow.ConsumerDirectionalLaneIndex);

        if (directionalLaneComparison != 0)
            return directionalLaneComparison;

        int consumerYComparison = secondFlow.ConsumerBounds.LocalBounds.min.y.CompareTo(
            firstFlow.ConsumerBounds.LocalBounds.min.y);

        if (consumerYComparison != 0)
            return consumerYComparison;

        int sourceXComparison = secondFlow.SourcePortLocalX.CompareTo(firstFlow.SourcePortLocalX);

        if (sourceXComparison != 0)
            return sourceXComparison;

        return secondFlow.SourceBounds.LocalBounds.center.z.CompareTo(firstFlow.SourceBounds.LocalBounds.center.z);
    }

    private void AddLocalPointToCommonPath(Vector3 localPosition)
    {
        if (commonPath.Count > 0)
        {
            Vector3 previousPoint = commonPath[commonPath.Count - 1];

            if ((previousPoint - localPosition).sqrMagnitude < 0.0001f)
                return;
        }

        commonPath.Add(localPosition);
    }

    private float GetSourceConnectionLocalZ(InfoflowModuleBounds moduleBounds, float offsetLocalZ)
    {
        return moduleBounds.LocalBounds.min.z - offsetLocalZ;
    }

    private float GetOffsetLocal(int offsetCells)
    {
        return Mathf.Max(0, offsetCells) * cellSize;
    }

    private float GetSpacingLocal()
    {
        return Mathf.Max(0f, detailedFlowSpacingCells) * cellSize;
    }

    private float GetCenteredPortPosition(float centerPosition, int portIndex, int portCount, float spacingLocal)
    {
        if (portCount <= 1)
            return centerPosition;

        float safeSpacingLocal = Mathf.Max(0.001f, spacingLocal);
        float totalWidth = (portCount - 1) * safeSpacingLocal;
        float firstPortPosition = centerPosition - totalWidth * 0.5f;

        return firstPortPosition + portIndex * safeSpacingLocal;
    }

    private float GetRightMostLocalMaxX(List<InfoflowModuleBounds> moduleBounds)
    {
        float rightMostLocalX = float.MinValue;

        foreach (InfoflowModuleBounds bounds in moduleBounds)
        {
            rightMostLocalX = Mathf.Max(rightMostLocalX, bounds.LocalBounds.max.x);
        }

        return rightMostLocalX;
    }

    private float GetLeftMostLocalMinX(List<InfoflowModuleBounds> moduleBounds)
    {
        float leftMostLocalX = float.MaxValue;

        foreach (InfoflowModuleBounds bounds in moduleBounds)
        {
            leftMostLocalX = Mathf.Min(leftMostLocalX, bounds.LocalBounds.min.x);
        }

        return leftMostLocalX;
    }

    private float GetLowestLocalMinY(List<InfoflowModuleBounds> moduleBounds)
    {
        float lowestLocalY = float.MaxValue;

        foreach (InfoflowModuleBounds bounds in moduleBounds)
        {
            lowestLocalY = Mathf.Min(lowestLocalY, bounds.LocalBounds.min.y);
        }

        return lowestLocalY;
    }

    private float GetAverageNearFaceLocalZ(List<InfoflowModuleBounds> moduleBounds)
    {
        if (moduleBounds == null || moduleBounds.Count == 0)
            return rootBounds.max.z;

        float sum = 0f;
        int count = 0;

        foreach (InfoflowModuleBounds bounds in moduleBounds)
        {
            sum += GetNearFaceLocalZ(bounds);
            count++;
        }

        if (count == 0)
            return rootBounds.max.z;

        return sum / count;
    }

    private float GetNearFaceLocalZ(InfoflowModuleBounds moduleBounds)
    {
        if (moduleBounds.LocalBounds.center.z >= rootBounds.center.z)
            return moduleBounds.LocalBounds.min.z;

        return moduleBounds.LocalBounds.max.z;
    }

    [Button()]
    public void RefreshRoundedDetailedFlowPoints()
    {
        RefreshRoundedFlowPoints(detailedFlows);
        RefreshRoundedFlowPoints(consumerDetailedFlows);
    }

    private void RefreshRoundedFlowPoints(List<InfoflowDetailedFlow> flows)
    {
        if (flows == null)
            return;

        foreach (InfoflowDetailedFlow flow in flows)
        {
            if (flow == null)
                continue;

            flow.RoundedPoints = BuildRoundedLocalPath(flow.Points);
        }
    }

    private List<Vector3> GetRenderableFlowPoints(InfoflowDetailedFlow detailedFlow)
    {
        if (detailedFlow == null)
            return null;

        if (useRoundedDetailedFlowPoints &&
            detailedFlow.RoundedPoints != null &&
            detailedFlow.RoundedPoints.Count >= 2)
        {
            return detailedFlow.RoundedPoints;
        }

        return detailedFlow.Points;
    }

    private List<Vector3> BuildRoundedLocalPath(List<Vector3> points)
    {
        if (points == null)
            return new List<Vector3>();

        if (points.Count < 3)
            return new List<Vector3>(points);

        float radius = Mathf.Max(0f, detailedFlowCornerRadiusCells) * cellSize;
        int segmentCount = Mathf.Max(1, detailedFlowCornerSegments);

        if (radius <= 0.0001f)
            return new List<Vector3>(points);

        List<Vector3> roundedPoints = new List<Vector3>();

        AddUniqueLocalPoint(roundedPoints, points[0]);

        for (int pointIndex = 1; pointIndex < points.Count - 1; pointIndex++)
        {
            Vector3 previousPoint = points[pointIndex - 1];
            Vector3 currentPoint = points[pointIndex];
            Vector3 nextPoint = points[pointIndex + 1];

            Vector3 incomingVector = currentPoint - previousPoint;
            Vector3 outgoingVector = nextPoint - currentPoint;

            float incomingLength = incomingVector.magnitude;
            float outgoingLength = outgoingVector.magnitude;

            if (incomingLength < 0.0001f || outgoingLength < 0.0001f)
                continue;

            Vector3 incomingDirection = incomingVector / incomingLength;
            Vector3 outgoingDirection = outgoingVector / outgoingLength;

            bool isStraightLine = Vector3.Cross(incomingDirection, outgoingDirection).sqrMagnitude < 0.0001f;

            if (isStraightLine)
            {
                AddUniqueLocalPoint(roundedPoints, currentPoint);
                continue;
            }

            float cornerRadius = Mathf.Min(
                radius,
                incomingLength * 0.5f,
                outgoingLength * 0.5f);

            if (cornerRadius < 0.0001f)
            {
                AddUniqueLocalPoint(roundedPoints, currentPoint);
                continue;
            }

            Vector3 cornerStartPoint = currentPoint - incomingDirection * cornerRadius;
            Vector3 cornerEndPoint = currentPoint + outgoingDirection * cornerRadius;

            AddUniqueLocalPoint(roundedPoints, cornerStartPoint);

            for (int segmentIndex = 1; segmentIndex <= segmentCount; segmentIndex++)
            {
                float t = segmentIndex / (float)segmentCount;
                Vector3 roundedPoint = GetQuadraticBezierPoint(cornerStartPoint, currentPoint, cornerEndPoint, t);

                AddUniqueLocalPoint(roundedPoints, roundedPoint);
            }
        }

        AddUniqueLocalPoint(roundedPoints, points[points.Count - 1]);

        return roundedPoints;
    }

    private Vector3 GetQuadraticBezierPoint(Vector3 startPoint, Vector3 controlPoint, Vector3 endPoint, float t)
    {
        float inverseT = 1f - t;

        return inverseT * inverseT * startPoint +
               2f * inverseT * t * controlPoint +
               t * t * endPoint;
    }

    private void AddUniqueLocalPoint(List<Vector3> points, Vector3 point)
    {
        if (points.Count > 0)
        {
            Vector3 previousPoint = points[points.Count - 1];

            if ((previousPoint - point).sqrMagnitude < 0.000001f)
                return;
        }

        points.Add(point);
    }

    [Button()]
    public void GenerateDetailedFlowLineRenderers()
    {
        if ((detailedFlows == null || detailedFlows.Count == 0) &&
            (consumerDetailedFlows == null || consumerDetailedFlows.Count == 0))
        {
            BuildDetailedFlows();
        }

        RefreshRoundedDetailedFlowPoints();
        ClearDetailedFlowLineRenderers();

        Transform root = GetOrCreateLineRendererRoot();

        if (generateTargetFlowLineRenderers)
        {
            GenerateLineRenderersForFlowList(
                detailedFlows,
                root,
                "Target flow");
        }

        if (generateConsumerFlowLineRenderers)
        {
            GenerateLineRenderersForFlowList(
                consumerDetailedFlows,
                root,
                "Consumer flow");
        }
    }

    [Button()]
    public void ClearDetailedFlowLineRenderers()
    {
        Transform root = GetOrCreateLineRendererRoot();

        for (int childIndex = root.childCount - 1; childIndex >= 0; childIndex--)
        {
            Transform child = root.GetChild(childIndex);

            if (Application.isPlaying)
            {
                Destroy(child.gameObject);
            }
            else
            {
                DestroyImmediate(child.gameObject);
            }
        }
    }

    private Transform GetOrCreateLineRendererRoot()
    {
        if (lineRendererRoot != null)
        {
            NormalizeLineRendererRootTransform();
            return lineRendererRoot;
        }

        const string rootName = "Generated Infoflow LineRenderers";

        Transform existingRoot = transform.Find(rootName);

        if (existingRoot != null)
        {
            lineRendererRoot = existingRoot;
            NormalizeLineRendererRootTransform();
            return lineRendererRoot;
        }

        GameObject rootGameObject = new GameObject(rootName);
        rootGameObject.transform.SetParent(transform, false);

        lineRendererRoot = rootGameObject.transform;
        NormalizeLineRendererRootTransform();

        return lineRendererRoot;
    }

    private void NormalizeLineRendererRootTransform()
    {
        if (lineRendererRoot == null)
            return;

        if (lineRendererRoot.parent != transform)
        {
            lineRendererRoot.SetParent(transform, false);
        }

        lineRendererRoot.localPosition = Vector3.zero;
        lineRendererRoot.localRotation = Quaternion.identity;
        lineRendererRoot.localScale = Vector3.one;
    }

    private void GenerateLineRenderersForFlowList(
        List<InfoflowDetailedFlow> flows,
        Transform root,
        string namePrefix)
    {
        if (flows == null)
            return;

        for (int flowIndex = 0; flowIndex < flows.Count; flowIndex++)
        {
            InfoflowDetailedFlow flow = flows[flowIndex];

            if (flow == null)
                continue;

            List<Vector3> points = GetRenderableFlowPoints(flow);

            if (points == null || points.Count < 2)
                continue;

            CreateLineRendererForFlow(
                flow,
                points,
                root,
                $"{namePrefix} {flowIndex:000}");
        }
    }

    private void CreateLineRendererForFlow(
        InfoflowDetailedFlow flow,
        List<Vector3> points,
        Transform root,
        string objectName)
    {
        GameObject lineRendererGameObject = new GameObject(objectName);
        lineRendererGameObject.transform.SetParent(root, false);

        lineRendererGameObject.transform.localPosition = Vector3.zero;
        lineRendererGameObject.transform.localRotation = Quaternion.identity;
        lineRendererGameObject.transform.localScale = Vector3.one;

        LineRenderer lineRenderer = lineRendererGameObject.AddComponent<LineRenderer>();

        lineRenderer.useWorldSpace = false;
        lineRenderer.positionCount = points.Count;
        lineRenderer.SetPositions(points.ToArray());

        lineRenderer.startWidth = detailedFlowLineWidth;
        lineRenderer.endWidth = detailedFlowLineWidth;

        lineRenderer.startColor = flow.Color;
        lineRenderer.endColor = flow.Color;

        lineRenderer.numCornerVertices = 0;
        lineRenderer.numCapVertices = 4;

        lineRenderer.material = GetLineRendererMaterial();
    }

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

        Shader shader = Shader.Find("Sprites/Default");

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

        if (shader == null)
            shader = Shader.Find("Standard");

        detailedFlowLineMaterial = new Material(shader)
        {
            name = "Generated Infoflow Line Material"
        };

        return detailedFlowLineMaterial;
    }

    private List<Vector3> RemoveDuplicateLocalPoints(List<Vector3> points)
    {
        List<Vector3> result = new List<Vector3>();

        foreach (Vector3 point in points)
        {
            if (result.Count > 0)
            {
                Vector3 previousPoint = result[result.Count - 1];

                if ((previousPoint - point).sqrMagnitude < 0.0001f)
                    continue;
            }

            result.Add(point);
        }

        return result;
    }

    private List<InfoflowModuleBounds> GetModuleBounds(
        List<Transform> modules,
        ModuleProjection moduleProjection)
    {
        List<InfoflowModuleBounds> moduleBounds = new List<InfoflowModuleBounds>();

        if (modules == null)
            return moduleBounds;

        foreach (Transform module in modules)
        {
            if (module == null)
                continue;

            Bounds moduleLocalBounds = GetLocalBoundsWithChildren(module.gameObject);

            moduleBounds.Add(new InfoflowModuleBounds
            {
                Module = module,
                LocalBounds = moduleLocalBounds,
                Projection = moduleProjection
            });
        }

        return moduleBounds;
    }

    private Dictionary<Transform, InfoflowModuleBounds> BuildBoundsDictionary(
        List<InfoflowModuleBounds> moduleBounds)
    {
        Dictionary<Transform, InfoflowModuleBounds> boundsByModule =
            new Dictionary<Transform, InfoflowModuleBounds>();

        foreach (InfoflowModuleBounds bounds in moduleBounds)
        {
            if (bounds.Module == null)
                continue;

            if (boundsByModule.ContainsKey(bounds.Module))
                continue;

            boundsByModule.Add(bounds.Module, bounds);
        }

        return boundsByModule;
    }

    private List<Transform> GetValidModules(List<Transform> modules)
    {
        List<Transform> validModules = new List<Transform>();

        if (modules == null)
            return validModules;

        foreach (Transform module in modules)
        {
            if (module == null)
                continue;

            validModules.Add(module);
        }

        return validModules;
    }

    private List<Transform> GetShuffledModules(List<Transform> modules, Random random)
    {
        List<Transform> shuffledModules = new List<Transform>(modules);

        for (int moduleIndex = shuffledModules.Count - 1; moduleIndex > 0; moduleIndex--)
        {
            int randomIndex = random.Next(0, moduleIndex + 1);

            Transform temporaryModule = shuffledModules[moduleIndex];
            shuffledModules[moduleIndex] = shuffledModules[randomIndex];
            shuffledModules[randomIndex] = temporaryModule;
        }

        return shuffledModules;
    }

    private Color GetRandomDetailedFlowColor(Random random)
    {
        float hue = (float)random.NextDouble();

        float saturation = Mathf.Lerp(
            detailedFlowColorSaturationMin,
            detailedFlowColorSaturationMax,
            (float)random.NextDouble());

        float value = Mathf.Lerp(
            detailedFlowColorValueMin,
            detailedFlowColorValueMax,
            (float)random.NextDouble());

        return Color.HSVToRGB(hue, saturation, value);
    }

    private void OnDrawGizmos()
    {
        if (drawCommonPath && commonPath != null)
        {
            Gizmos.color = Color.green;

            for (int pointIndex = 1; pointIndex < commonPath.Count; pointIndex++)
            {
                Gizmos.DrawLine(
                    transform.TransformPoint(commonPath[pointIndex - 1]),
                    transform.TransformPoint(commonPath[pointIndex]));
            }

            foreach (Vector3 point in commonPath)
            {
                Gizmos.DrawSphere(transform.TransformPoint(point), cellSize * 0.2f);
            }
        }

        if (drawSourceModuleConnectionPoints)
            DrawPointList(sourceModuleConnectionPoints, Color.cyan, cellSize * 0.16f);

        if (drawSourceRowMergePoints)
            DrawPointList(sourceRowMergePoints, Color.yellow, cellSize * 0.18f);

        if (drawTargetColumnConnectionPoints)
            DrawPointList(targetColumnConnectionPoints, Color.magenta, cellSize * 0.18f);

        if (drawDetailedFlows)
            DrawFlowList(detailedFlows);

        if (drawConsumerDetailedFlows)
            DrawFlowList(consumerDetailedFlows);
    }

    private void DrawPointList(List<Vector3> points, Color color, float radius)
    {
        if (points == null)
            return;

        Gizmos.color = color;

        foreach (Vector3 point in points)
        {
            Gizmos.DrawSphere(transform.TransformPoint(point), radius);
        }
    }

    private void DrawFlowList(List<InfoflowDetailedFlow> flows)
    {
        if (flows == null)
            return;

        foreach (InfoflowDetailedFlow detailedFlow in flows)
        {
            if (detailedFlow == null)
                continue;

            List<Vector3> points = GetRenderableFlowPoints(detailedFlow);

            if (points == null || points.Count < 2)
                continue;

            Gizmos.color = detailedFlow.Color;

            for (int pointIndex = 1; pointIndex < points.Count; pointIndex++)
            {
                Gizmos.DrawLine(
                    transform.TransformPoint(points[pointIndex - 1]),
                    transform.TransformPoint(points[pointIndex]));
            }

            foreach (Vector3 point in points)
            {
                Gizmos.DrawSphere(transform.TransformPoint(point), detailedFlowPointRadius);
            }
        }
    }

    public Bounds GetLocalBoundsWithChildren(GameObject targetGameObject)
    {
        Renderer[] renderers = targetGameObject.GetComponentsInChildren<Renderer>();

        if (renderers.Length == 0)
        {
            Vector3 localPosition = transform.InverseTransformPoint(targetGameObject.transform.position);
            return new Bounds(localPosition, Vector3.zero);
        }

        Bounds resultBounds = new Bounds();
        bool hasBounds = false;

        foreach (Renderer targetRenderer in renderers)
        {
            Bounds rendererLocalBounds = targetRenderer.localBounds;

            Vector3 center = rendererLocalBounds.center;
            Vector3 extents = rendererLocalBounds.extents;

            for (int x = -1; x <= 1; x += 2)
            {
                for (int y = -1; y <= 1; y += 2)
                {
                    for (int z = -1; z <= 1; z += 2)
                    {
                        Vector3 rendererLocalCorner = center + new Vector3(
                            extents.x * x,
                            extents.y * y,
                            extents.z * z);

                        Vector3 worldCorner = targetRenderer.transform.TransformPoint(rendererLocalCorner);
                        Vector3 graphLocalCorner = transform.InverseTransformPoint(worldCorner);

                        if (hasBounds == false)
                        {
                            resultBounds = new Bounds(graphLocalCorner, Vector3.zero);
                            hasBounds = true;
                        }
                        else
                        {
                            resultBounds.Encapsulate(graphLocalCorner);
                        }
                    }
                }
            }
        }

        return resultBounds;
    }
}

public enum ModuleProjection
{
    XZ,
    XY,
    ZY
}

[System.Serializable]
public struct InfoflowModuleBounds
{
    public Transform Module;
    public Bounds LocalBounds;
    public ModuleProjection Projection;
}

[System.Serializable]
public struct TargetRouteData
{
    public float SourceBusLocalX;
    public float TargetVerticalBusLocalX;
    public float TargetWallLocalZ;
    public float TargetBusLocalY;
}

[System.Serializable]
public struct ConsumerRouteData
{
    public float SourceLeftBusLocalX;
    public float MiddleSourceRowsLocalZ;
    public float ConsumerLeftFaceLocalX;
    public float ConsumerBusLocalY;
}

[System.Serializable]
public class SourceRowData
{
    public float RowLocalZ;
    public List<InfoflowModuleBounds> Modules = new List<InfoflowModuleBounds>();
}

[System.Serializable]
public class TargetColumnData
{
    public float ColumnLocalX;
    public List<InfoflowModuleBounds> Modules = new List<InfoflowModuleBounds>();
}

[System.Serializable]
public class SourceFlowRowData
{
    public float RowLocalZ;
    public List<DetailedFlowBuildData> TargetFlows = new List<DetailedFlowBuildData>();
    public List<ConsumerFlowBuildData> ConsumerFlows = new List<ConsumerFlowBuildData>();
}

[System.Serializable]
public class TargetFlowColumnData
{
    public float ColumnLocalX;
    public List<DetailedFlowBuildData> Flows = new List<DetailedFlowBuildData>();
}

[System.Serializable]
public class ConsumerFlowColumnData
{
    public float ColumnLocalZ;
    public List<ConsumerFlowBuildData> Flows = new List<ConsumerFlowBuildData>();
}

[System.Serializable]
public class InfoflowModuleConnection
{
    public Transform SourceModule;
    public Transform TargetModule;
}

[System.Serializable]
public class InfoflowDetailedFlow
{
    public Transform SourceModule;
    public Transform TargetModule;
    public int LaneIndex;
    public Color Color = Color.white;
    public List<Vector3> Points = new List<Vector3>();
    public List<Vector3> RoundedPoints = new List<Vector3>();
}

public class DetailedFlowBuildData
{
    public InfoflowModuleConnection Connection;

    public InfoflowModuleBounds SourceBounds;
    public InfoflowModuleBounds TargetBounds;

    public float SourcePortLocalX;
    public float TargetPortLocalX;

    public int LaneIndex;
    public float LaneOffsetLocal;

    public int SourceRowIndex;
    public float SourceRowLaneOffsetLocal;
}

public class ConsumerFlowBuildData
{
    public InfoflowModuleConnection Connection;

    public InfoflowModuleBounds SourceBounds;
    public InfoflowModuleBounds ConsumerBounds;

    public float SourcePortLocalX;
    public float ConsumerPortLocalZ;

    public int LaneIndex;
    public float LaneOffsetLocal;

    public int SourceRowIndex;
    public float SourceRowLaneOffsetLocal;

    public int ConsumerDirection;
    public float ConsumerDistanceToSplitZ;
    public int ConsumerDirectionalLaneIndex;
    public float ConsumerDirectionalLaneOffsetLocal;
}