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


public class HexInsideHexGenerator : IInitializable
{
    private const float Sqrt3 = 1.7320508f;
    private const float HexApothemFactor = Sqrt3 * 0.5f;
    private const float ShellEpsilon = 0.0001f;

    private readonly BusinesCapabilitiesFragmentFactory fragmentFactory;
    private readonly MainConfigHolder mainConfigHolder;
    private readonly DataManager dataManager;

    private BusinessCapabilitiesInDepartmentConfig config;

    private struct CellCandidate
    {
        public Vector2Int Cell;
        public float RequiredOuterRadius;
        public float Angle;

        public CellCandidate(Vector2Int cell, float requiredOuterRadius, float angle)
        {
            Cell = cell;
            RequiredOuterRadius = requiredOuterRadius;
            Angle = angle;
        }
    }

    public HexInsideHexGenerator(
        BusinesCapabilitiesFragmentFactory fragmentFactory,
        MainConfigHolder mainConfigHolder,
        DataManager dataManager)
    {
        this.fragmentFactory = fragmentFactory;
        this.mainConfigHolder = mainConfigHolder;
        this.dataManager = dataManager;
    }

    public void Initialize()
    {
        config = mainConfigHolder.BusinessCapabilitiesInDepartmentConfig;
    }

    public List<FbcDataFragment> Generate(
        int count,
        List<long> ids,
        FbcDataFragment prefab,
        GameObject emptyPrefab,
        Transform root,
        Transform outerHex)
    {
        if (prefab == null)
        {
            Debug.LogWarning("inner hex prefab is not assigned.");
            return new List<FbcDataFragment>();
        }

        if (ids == null || ids.Count == 0)
        {
            Debug.LogWarning("ids is empty");
            return new List<FbcDataFragment>();
        }

        if (count <= 0)
        {
            Debug.LogWarning("generation count is zero.");
            return new List<FbcDataFragment>();
        }

        count = Mathf.Min(count, ids.Count);

        if (root == null)
        {
            Debug.LogWarning("cannot resolve generated root.");
            return new List<FbcDataFragment>();
        }

        if (outerHex == null)
        {
            Debug.LogWarning("outer hex is not assigned.");
            return new List<FbcDataFragment>();
        }

        ClearChildren(root, prefab);

        Transform actualOuterVisualRoot = outerHex;
        if (!TryCalculateLocalBounds(
                actualOuterVisualRoot,
                outerHex,
                out Bounds outerLocalBounds,
                root))
        {
            Debug.LogWarning("cannot calculate outer bounds.");
            return new List<FbcDataFragment>();
        }

        if (!TryCalculatePrefabLocalBounds(
                prefab.gameObject,
                outerHex,
                out Bounds innerLocalBounds))
        {
            Debug.LogWarning("cannot calculate inner prefab bounds.");
            return new List<FbcDataFragment>();
        }

        float outerRadius = ResolveHexRadius(outerLocalBounds);
        float innerBaseRadius = ResolveHexRadius(innerLocalBounds);
        float effectiveOuterRadius = ApplyPaddingRatioToHexRadius(
            outerRadius,
            config.InHexPadding);

        if (outerRadius <= 0f)
        {
            Debug.LogWarning("outer radius is zero.");
            return new List<FbcDataFragment>();
        }

        if (effectiveOuterRadius <= 0f)
        {
            Debug.LogWarning("effective outer radius is zero. Reduce outer padding ratio.");
            return new List<FbcDataFragment>();
        }

        if (innerBaseRadius <= 0f)
        {
            Debug.LogWarning("inner radius is zero.");
            return new List<FbcDataFragment>();
        }

        // В отличие от аксиальной спирали, этот набор ячеек формирует
        // общий контур в той же flat-top ориентации, что и внешний гекс.
        List<Vector2Int> cells = BuildOuterAlignedCells(
            count,
            config.InHexGapRatio);

        float finalScale = FindMaximumFittingScale(
            cells,
            effectiveOuterRadius,
            innerBaseRadius,
            config.InHexGapRatio);

        float finalInnerRadius = innerBaseRadius * finalScale;
        Vector2 outerCenterXZ = GetXZPosition(outerLocalBounds.center);

        List<Vector2> localXZPositions = BuildCenteredXZPositions(
            cells,
            finalInnerRadius,
            config.InHexGapRatio);

        Vector3 temporaryScale = prefab.transform.localScale;
        Vector3 temporaryPosition = prefab.transform.position;

        prefab.transform.localScale *= finalScale;

        Bounds prefabBounds = ObjectToFrustumFitter.GetBoundsWithChildren(prefab.gameObject);
        Bounds outerBounds = ObjectToFrustumFitter.GetBoundsWithChildren(outerHex.gameObject);

        prefab.transform.SetY(outerBounds.max.y - prefabBounds.size.y);
        float localY = prefab.transform.localPosition.y;

        prefab.transform.localScale = temporaryScale;
        prefab.transform.position = temporaryPosition;

        List<FbcDataFragment> results = new List<FbcDataFragment>(
            localXZPositions.Count);

        for (int i = 0; i < localXZPositions.Count; i++)
        {
            Vector2 finalXZPosition = outerCenterXZ + localXZPositions[i];

            FbcDataFragment fragment = fragmentFactory.CreateFBC(
                prefab,
                new Vector3(
                    finalXZPosition.x,
                    config.InHexSurfaceOffset,
                    finalXZPosition.y),
                prefab.transform.localRotation,
                finalScale,
                root,
                ids[i]);

            fragment.name = $"{prefab.name} {i}";

            fragment.transform.localPosition = new Vector3(
                finalXZPosition.x,
                localY,
                finalXZPosition.y);

            fragment.transform.localRotation = prefab.transform.localRotation;
            fragment.transform.localScale = prefab.transform.localScale * finalScale;

            results.Add(fragment);
        }

        return results;
    }

    private void ClearChildren(Transform root, FbcDataFragment prefab)
    {
        for (int childIndex = root.childCount - 1; childIndex >= 0; childIndex--)
        {
            GameObject target = root.GetChild(childIndex).gameObject;

            if (prefab.gameObject == target)
                continue;

            DestroySmart(target);
        }
    }

    private void DestroySmart(Object target)
    {
        if (target == null)
            return;

        if (Application.isPlaying)
            Object.Destroy(target);
        else
            Object.DestroyImmediate(target);
    }

    private bool TryCalculatePrefabLocalBounds(
        GameObject prefab,
        Transform outerHex,
        out Bounds localBounds)
    {
        GameObject probe = UnityEngine.Object.Instantiate(
            prefab,
            outerHex,
            false);

        probe.name = $"{prefab.name}_BoundsProbe";
        probe.SetActive(true);

        bool result = TryCalculateLocalBounds(
            probe.transform,
            outerHex,
            out localBounds,
            null);

        DestroySmart(probe);
        return result;
    }

    private bool TryCalculateLocalBounds(
        Transform sourceRoot,
        Transform relativeTo,
        out Bounds localBounds,
        Transform excludedRoot)
    {
        localBounds = default;
        bool hasBounds = false;

        Renderer[] renderers = sourceRoot.GetComponentsInChildren<Renderer>(true);
        foreach (Renderer renderer in renderers)
        {
            if (excludedRoot != null && renderer.transform.IsChildOf(excludedRoot))
                continue;

            EncapsulateWorldBoundsAsLocal(
                renderer.bounds,
                relativeTo,
                ref localBounds,
                ref hasBounds);
        }

        if (!hasBounds)
        {
            Collider[] colliders = sourceRoot.GetComponentsInChildren<Collider>(true);
            foreach (Collider collider in colliders)
            {
                if (excludedRoot != null && collider.transform.IsChildOf(excludedRoot))
                    continue;

                EncapsulateWorldBoundsAsLocal(
                    collider.bounds,
                    relativeTo,
                    ref localBounds,
                    ref hasBounds);
            }
        }

        return hasBounds;
    }

    private void EncapsulateWorldBoundsAsLocal(
        Bounds worldBounds,
        Transform relativeTo,
        ref Bounds localBounds,
        ref bool hasBounds)
    {
        Vector3 min = worldBounds.min;
        Vector3 max = worldBounds.max;

        for (int xIndex = 0; xIndex <= 1; xIndex++)
        {
            for (int yIndex = 0; yIndex <= 1; yIndex++)
            {
                for (int zIndex = 0; zIndex <= 1; zIndex++)
                {
                    Vector3 worldCorner = new Vector3(
                        xIndex == 0 ? min.x : max.x,
                        yIndex == 0 ? min.y : max.y,
                        zIndex == 0 ? min.z : max.z);

                    Vector3 localCorner = relativeTo.InverseTransformPoint(worldCorner);

                    if (!hasBounds)
                    {
                        localBounds = new Bounds(localCorner, Vector3.zero);
                        hasBounds = true;
                    }
                    else
                    {
                        localBounds.Encapsulate(localCorner);
                    }
                }
            }
        }
    }

    private float ResolveHexRadius(Bounds localBounds)
    {
        Vector2 extentsXZ = GetXZExtents(localBounds);
        return Mathf.Min(extentsXZ.x, extentsXZ.y * 2f / Sqrt3);
    }

    private Vector2 GetXZExtents(Bounds bounds)
    {
        return new Vector2(bounds.extents.x, bounds.extents.z);
    }

    private Vector2 GetXZPosition(Vector3 position)
    {
        return new Vector2(position.x, position.z);
    }

    private float ApplyPaddingRatioToHexRadius(float radius, float paddingRatio)
    {
        float clampedPaddingRatio = Mathf.Clamp01(paddingRatio);
        return radius * (1f - clampedPaddingRatio);
    }

    private float CalculateGapUnits(float innerRadius, float gapRatio)
    {
        float safeGapRatio = Mathf.Max(0f, gapRatio);
        float innerFlatToFlatSize = innerRadius * Sqrt3;
        return innerFlatToFlatSize * safeGapRatio;
    }

    private List<Vector2Int> BuildOuterAlignedCells(int count, float gapRatio)
    {
        if (count <= 0)
            return new List<Vector2Int>();

        int searchRadius = Mathf.Max(
            2,
            Mathf.CeilToInt(Mathf.Sqrt(count)) * 2);

        while (true)
        {
            List<CellCandidate> candidates = BuildCellCandidates(
                searchRadius,
                gapRatio);

            candidates.Sort(CompareCandidates);

            if (candidates.Count < count)
            {
                searchRadius *= 2;
                continue;
            }

            float cutoffRadius = candidates[count - 1].RequiredOuterRadius;
            bool touchesSearchBoundary = false;

            for (int i = 0; i < candidates.Count; i++)
            {
                CellCandidate candidate = candidates[i];

                if (candidate.RequiredOuterRadius > cutoffRadius + ShellEpsilon)
                    break;

                if (Mathf.Abs(candidate.Cell.x) == searchRadius ||
                    Mathf.Abs(candidate.Cell.y) == searchRadius)
                {
                    touchesSearchBoundary = true;
                    break;
                }
            }

            if (touchesSearchBoundary)
            {
                searchRadius *= 2;
                continue;
            }

            return SelectBalancedCandidates(candidates, count);
        }
    }

    private List<CellCandidate> BuildCellCandidates(
        int searchRadius,
        float gapRatio)
    {
        int sideLength = searchRadius * 2 + 1;
        List<CellCandidate> candidates = new List<CellCandidate>(
            sideLength * sideLength);

        const float unitInnerRadius = 1f;

        float unitGap = CalculateGapUnits(unitInnerRadius, gapRatio);
        float unitPitchRadius = unitInnerRadius + unitGap / Sqrt3;

        for (int q = -searchRadius; q <= searchRadius; q++)
        {
            for (int r = -searchRadius; r <= searchRadius; r++)
            {
                Vector2Int cell = new Vector2Int(q, r);
                Vector2 position = AxialToXZ(cell, unitPitchRadius);

                float requiredOuterRadius = CalculateRequiredOuterRadius(
                    position,
                    unitInnerRadius);

                float angle = Mathf.Repeat(
                    Mathf.Atan2(position.y, position.x) * Mathf.Rad2Deg,
                    360f);

                candidates.Add(new CellCandidate(
                    cell,
                    requiredOuterRadius,
                    angle));
            }
        }

        return candidates;
    }

    private int CompareCandidates(CellCandidate left, CellCandidate right)
    {
        int radiusComparison = left.RequiredOuterRadius.CompareTo(
            right.RequiredOuterRadius);

        if (radiusComparison != 0)
            return radiusComparison;

        int angleComparison = left.Angle.CompareTo(right.Angle);
        if (angleComparison != 0)
            return angleComparison;

        int qComparison = left.Cell.x.CompareTo(right.Cell.x);
        if (qComparison != 0)
            return qComparison;

        return left.Cell.y.CompareTo(right.Cell.y);
    }

    private List<Vector2Int> SelectBalancedCandidates(
        List<CellCandidate> candidates,
        int count)
    {
        List<Vector2Int> result = new List<Vector2Int>(count);
        int candidateIndex = 0;

        while (result.Count < count)
        {
            int shellStart = candidateIndex;
            float shellRadius = candidates[shellStart].RequiredOuterRadius;

            candidateIndex++;

            while (candidateIndex < candidates.Count &&
                   Mathf.Abs(
                       candidates[candidateIndex].RequiredOuterRadius -
                       shellRadius) <= ShellEpsilon)
            {
                candidateIndex++;
            }

            int shellCount = candidateIndex - shellStart;
            int remaining = count - result.Count;

            if (shellCount <= remaining)
            {
                for (int i = shellStart; i < candidateIndex; i++)
                    result.Add(candidates[i].Cell);

                continue;
            }

            // Если последний слой помещается не полностью,
            // распределяем ячейки равномерно по всему периметру.
            for (int i = 0; i < remaining; i++)
            {
                float normalizedIndex = (i + 0.5f) / remaining;

                int localIndex = Mathf.FloorToInt(
                    normalizedIndex * shellCount);

                localIndex = Mathf.Clamp(
                    localIndex,
                    0,
                    shellCount - 1);

                result.Add(candidates[shellStart + localIndex].Cell);
            }
        }

        return result;
    }

    private float CalculateRequiredOuterRadius(
        Vector2 center,
        float innerRadius)
    {
        float requiredApothem = 0f;

        for (int vertexIndex = 0; vertexIndex < 6; vertexIndex++)
        {
            float angleRadians = Mathf.Deg2Rad * (60f * vertexIndex);

            Vector2 point = center + new Vector2(
                Mathf.Cos(angleRadians) * innerRadius,
                Mathf.Sin(angleRadians) * innerRadius);

            float firstPlane = Mathf.Abs(point.y);

            float secondPlane = Mathf.Abs(
                HexApothemFactor * point.x + 0.5f * point.y);

            float thirdPlane = Mathf.Abs(
                HexApothemFactor * point.x - 0.5f * point.y);

            requiredApothem = Mathf.Max(
                requiredApothem,
                Mathf.Max(
                    firstPlane,
                    Mathf.Max(secondPlane, thirdPlane)));
        }

        return requiredApothem / HexApothemFactor;
    }

    private List<Vector2> BuildCenteredXZPositions(
        IReadOnlyList<Vector2Int> cells,
        float innerRadius,
        float gapRatio)
    {
        float gap = CalculateGapUnits(innerRadius, gapRatio);
        float pitchRadius = innerRadius + gap / Sqrt3;

        List<Vector2> positions = new List<Vector2>(cells.Count);
        Vector2 positionsSum = Vector2.zero;

        for (int cellIndex = 0; cellIndex < cells.Count; cellIndex++)
        {
            Vector2 position = AxialToXZ(cells[cellIndex], pitchRadius);
            positions.Add(position);
            positionsSum += position;
        }

        Vector2 groupCenter = positionsSum / cells.Count;

        for (int positionIndex = 0; positionIndex < positions.Count; positionIndex++)
            positions[positionIndex] -= groupCenter;

        return positions;
    }

    private Vector2 AxialToXZ(Vector2Int axial, float pitchRadius)
    {
        float q = axial.x;
        float r = axial.y;

        return new Vector2(
            1.5f * pitchRadius * q,
            Sqrt3 * pitchRadius * (r + q * 0.5f));
    }

    private float FindMaximumFittingScale(
        IReadOnlyList<Vector2Int> cells,
        float outerRadius,
        float innerBaseRadius,
        float gapRatio)
    {
        float low = 0f;
        float high = outerRadius / innerBaseRadius;

        for (int iteration = 0; iteration < 48; iteration++)
        {
            float middle = (low + high) * 0.5f;

            bool fits = CanFitScale(
                cells,
                outerRadius,
                innerBaseRadius,
                gapRatio,
                middle);

            if (fits)
                low = middle;
            else
                high = middle;
        }

        return low;
    }

    private bool CanFitScale(
        IReadOnlyList<Vector2Int> cells,
        float outerRadius,
        float innerBaseRadius,
        float gapRatio,
        float scale)
    {
        float innerRadius = innerBaseRadius * scale;

        List<Vector2> positions = BuildCenteredXZPositions(
            cells,
            innerRadius,
            gapRatio);

        for (int positionIndex = 0; positionIndex < positions.Count; positionIndex++)
        {
            Vector2[] innerVertices = CreateHexVertices(
                positions[positionIndex],
                innerRadius);

            for (int vertexIndex = 0; vertexIndex < innerVertices.Length; vertexIndex++)
            {
                if (!IsPointInsideHex(innerVertices[vertexIndex], outerRadius))
                    return false;
            }
        }

        return true;
    }

    private Vector2[] CreateHexVertices(Vector2 center, float radius)
    {
        Vector2[] vertices = new Vector2[6];

        for (int vertexIndex = 0; vertexIndex < vertices.Length; vertexIndex++)
        {
            float angleRadians = Mathf.Deg2Rad * (60f * vertexIndex);

            vertices[vertexIndex] = center + new Vector2(
                Mathf.Cos(angleRadians) * radius,
                Mathf.Sin(angleRadians) * radius);
        }

        return vertices;
    }

    private bool IsPointInsideHex(Vector2 point, float radius)
    {
        float epsilon = Mathf.Epsilon;
        float apothem = radius * HexApothemFactor;

        return Mathf.Abs(point.y) <= apothem + epsilon &&
               Mathf.Abs(
                   HexApothemFactor * point.x + 0.5f * point.y) <=
               apothem + epsilon &&
               Mathf.Abs(
                   HexApothemFactor * point.x - 0.5f * point.y) <=
               apothem + epsilon;
    }
}