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


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

    private readonly Vector2Int[] AxialDirections =
    {
        new Vector2Int(1, 0),
        new Vector2Int(1, -1),
        new Vector2Int(0, -1),
        new Vector2Int(-1, 0),
        new Vector2Int(-1, 1),
        new Vector2Int(0, 1)
    };

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

    private BusinessCapabilitiesInDepartmentConfig config;

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

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

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

        ClearChildren(root, prefab, emptyPrefab);

        if (!TryCalculateLocalBounds(outerHex, 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;
        float gap = CalculateGapUnits(finalInnerRadius, config.InHexGapRatio);
        float pitchRadius = finalInnerRadius + gap / Sqrt3;

        Vector2 outerCenterXZ = GetXZPosition(outerLocalBounds.center);
        Vector2 groupCenterXZ = CalculateCellsCenterXZ(cells, pitchRadius);
        float outerTopWorldY = CalculateWorldMaxY(outerLocalBounds, outerHex);

        if (!TryResolveScaledPrefabLocalY(
                prefab.gameObject,
                finalScale,
                root,
                outerTopWorldY,
                out float largeHexLocalY))
        {
            Debug.LogWarning("cannot resolve inner prefab Y position.");
            return new List<FbcDataFragment>();
        }

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

        for (int i = 0; i < cells.Count; i++)
        {
            Vector2 localXZ = AxialToXZ(cells[i], pitchRadius) - groupCenterXZ;
            Vector2 finalXZ = outerCenterXZ + localXZ;

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

            fragment.name = $"{prefab.name} {i}";
            fragment.transform.localPosition = new Vector3(finalXZ.x, largeHexLocalY, finalXZ.y);
            fragment.transform.localRotation = prefab.transform.localRotation;
            fragment.transform.localScale = prefab.transform.localScale * finalScale;

            results.Add(fragment);
        }

        SpawnDecorativeHexes(
            cells,
            emptyPrefab,
            root,
            outerCenterXZ,
            groupCenterXZ,
            pitchRadius,
            finalScale,
            outerTopWorldY);

        return results;
    }

    private void SpawnDecorativeHexes(
        IReadOnlyList<Vector2Int> largeCells,
        GameObject emptyPrefab,
        Transform root,
        Vector2 outerCenterXZ,
        Vector2 largeGroupCenterXZ,
        float pitchRadius,
        float finalScale,
        float outerTopWorldY)
    {
        // Для одного большого гекса декоративные элементы не создаются.
        if (largeCells == null || largeCells.Count <= 1)
            return;

        if (emptyPrefab == null)
        {
            Debug.LogWarning("empty prefab is not assigned. Decorative hexes were not generated.");
            return;
        }

        if (!TryResolveScaledPrefabLocalY(
                emptyPrefab,
                finalScale,
                root,
                outerTopWorldY,
                out float emptyHexLocalY))
        {
            Debug.LogWarning("cannot resolve decorative prefab Y position.");
            return;
        }

        List<Vector2Int> decorativeCells = BuildDecorativeCells(largeCells, pitchRadius);

        for (int i = 0; i < decorativeCells.Count; i++)
        {
            Vector2 localXZ = AxialToXZ(decorativeCells[i], pitchRadius) - largeGroupCenterXZ;
            Vector2 finalXZ = outerCenterXZ + localXZ;

            GameObject decorativeHex = UnityEngine.Object.Instantiate(emptyPrefab, root, false);
            decorativeHex.name = $"{emptyPrefab.name} {i}";
            decorativeHex.transform.localPosition = new Vector3(finalXZ.x, emptyHexLocalY, finalXZ.y);
            decorativeHex.transform.localRotation = emptyPrefab.transform.localRotation;
            decorativeHex.transform.localScale = emptyPrefab.transform.localScale * finalScale;
            decorativeHex.SetActive(true);
        }
    }

    private List<Vector2Int> BuildDecorativeCells(
        IReadOnlyList<Vector2Int> largeCells,
        float pitchRadius)
    {
        HashSet<Vector2Int> occupied = new HashSet<Vector2Int>();
        for (int i = 0; i < largeCells.Count; i++)
            occupied.Add(largeCells[i]);

        HashSet<Vector2Int> candidates = new HashSet<Vector2Int>();

        foreach (Vector2Int cell in occupied)
        {
            for (int directionIndex = 0; directionIndex < AxialDirections.Length; directionIndex++)
            {
                Vector2Int neighbour = cell + AxialDirections[directionIndex];

                if (!occupied.Contains(neighbour))
                    candidates.Add(neighbour);
            }
        }

        List<Vector2Int> result = new List<Vector2Int>();

        foreach (Vector2Int candidate in candidates)
        {
            int occupiedNeighbourCount = CountOccupiedNeighbours(candidate, occupied);

            // Маленький гекс появляется только в свободной ячейке,
            // которая касается минимум двух больших гексов.
            if (occupiedNeighbourCount >= 2)
                result.Add(candidate);
        }

        // HashSet не гарантирует порядок. Сортировка делает генерацию стабильной.
        result.Sort((left, right) => CompareCellsByAngle(left, right, pitchRadius));
        return result;
    }

    private int CountOccupiedNeighbours(Vector2Int cell, HashSet<Vector2Int> occupied)
    {
        int count = 0;

        for (int directionIndex = 0; directionIndex < AxialDirections.Length; directionIndex++)
        {
            if (occupied.Contains(cell + AxialDirections[directionIndex]))
                count++;
        }

        return count;
    }

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

            if (prefab != null && prefab.gameObject == target)
                continue;

            if (emptyPrefab != null && emptyPrefab == 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 TryResolveScaledPrefabLocalY(
        GameObject sourcePrefab,
        float scale,
        Transform root,
        float outerTopWorldY,
        out float localY)
    {
        localY = 0f;

        if (sourcePrefab == null || root == null)
            return false;

        GameObject probe = UnityEngine.Object.Instantiate(sourcePrefab, root, false);
        probe.name = $"{sourcePrefab.name}_YProbe";
        probe.SetActive(true);
        probe.transform.localScale *= scale;

        Bounds prefabBounds = ObjectToFrustumFitter.GetBoundsWithChildren(probe);

        if (prefabBounds.size == Vector3.zero)
        {
            DestroySmart(probe);
            return false;
        }

        // Сохраняет исходную логику вертикального размещения:
        // верх префаба совмещается с верхней поверхностью внешнего объекта
        // с учётом высоты модели и её исходного pivot.
        probe.transform.SetY(outerTopWorldY - prefabBounds.size.y);
        localY = probe.transform.localPosition.y;

        DestroySmart(probe);
        return true;
    }

    private float CalculateWorldMaxY(Bounds localBounds, Transform relativeTo)
    {
        Vector3 min = localBounds.min;
        Vector3 max = localBounds.max;
        float maxWorldY = float.MinValue;

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

                    float worldY = relativeTo.TransformPoint(localCorner).y;
                    maxWorldY = Mathf.Max(maxWorldY, worldY);
                }
            }
        }

        return maxWorldY;
    }

    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<Vector2Int> candidates = BuildSearchCells(searchRadius);
            candidates.Sort((left, right) => CompareOuterAlignedCells(left, right, 1f));

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

            int cutoffLayer = GetOuterAlignedLayer(candidates[count - 1]);

            if (LayerTouchesSearchBoundary(candidates, cutoffLayer, searchRadius))
            {
                searchRadius *= 2;
                continue;
            }

            return SelectCellsByLayers(candidates, count, gapRatio);
        }
    }

    private List<Vector2Int> BuildSearchCells(int searchRadius)
    {
        int sideLength = searchRadius * 2 + 1;
        List<Vector2Int> result = new List<Vector2Int>(sideLength * sideLength);

        for (int q = -searchRadius; q <= searchRadius; q++)
        {
            for (int r = -searchRadius; r <= searchRadius; r++)
                result.Add(new Vector2Int(q, r));
        }

        return result;
    }

    private bool LayerTouchesSearchBoundary(
        IReadOnlyList<Vector2Int> candidates,
        int cutoffLayer,
        int searchRadius)
    {
        for (int i = 0; i < candidates.Count; i++)
        {
            Vector2Int cell = candidates[i];

            if (GetOuterAlignedLayer(cell) > cutoffLayer)
                break;

            if (Mathf.Abs(cell.x) == searchRadius || Mathf.Abs(cell.y) == searchRadius)
                return true;
        }

        return false;
    }

    private List<Vector2Int> SelectCellsByLayers(
        IReadOnlyList<Vector2Int> orderedCandidates,
        int count,
        float gapRatio)
    {
        List<Vector2Int> selected = new List<Vector2Int>(count);
        HashSet<Vector2Int> occupied = new HashSet<Vector2Int>();

        int candidateIndex = 0;

        while (selected.Count < count)
        {
            int layerStart = candidateIndex;
            int layer = GetOuterAlignedLayer(orderedCandidates[layerStart]);

            candidateIndex++;

            while (candidateIndex < orderedCandidates.Count &&
                   GetOuterAlignedLayer(orderedCandidates[candidateIndex]) == layer)
            {
                candidateIndex++;
            }

            int layerCount = candidateIndex - layerStart;
            int remaining = count - selected.Count;

            if (layerCount <= remaining)
            {
                for (int i = layerStart; i < candidateIndex; i++)
                {
                    Vector2Int cell = orderedCandidates[i];
                    selected.Add(cell);
                    occupied.Add(cell);
                }

                continue;
            }

            List<Vector2Int> partialLayer = new List<Vector2Int>(layerCount);
            for (int i = layerStart; i < candidateIndex; i++)
                partialLayer.Add(orderedCandidates[i]);

            SelectCompactPartialLayer(
                selected,
                occupied,
                partialLayer,
                remaining,
                gapRatio);
        }

        return selected;
    }

    private void SelectCompactPartialLayer(
        List<Vector2Int> selected,
        HashSet<Vector2Int> occupied,
        List<Vector2Int> available,
        int amount,
        float gapRatio)
    {
        for (int selectionIndex = 0; selectionIndex < amount; selectionIndex++)
        {
            int bestIndex = -1;
            int bestNeighbourCount = int.MinValue;
            float bestRequiredRadius = float.MaxValue;
            float bestAngle = float.MaxValue;

            for (int candidateIndex = 0; candidateIndex < available.Count; candidateIndex++)
            {
                Vector2Int candidate = available[candidateIndex];
                int neighbourCount = CountOccupiedNeighbours(candidate, occupied);
                float requiredRadius = CalculateRequiredOuterRadiusAfterAdding(
                    selected,
                    candidate,
                    gapRatio);
                float angle = GetCellAngle(candidate, 1f);

                bool isBetter = neighbourCount > bestNeighbourCount;

                if (!isBetter && neighbourCount == bestNeighbourCount)
                {
                    isBetter = requiredRadius < bestRequiredRadius - ScoreEpsilon;
                }

                if (!isBetter &&
                    neighbourCount == bestNeighbourCount &&
                    Mathf.Abs(requiredRadius - bestRequiredRadius) <= ScoreEpsilon)
                {
                    isBetter = angle < bestAngle;
                }

                if (!isBetter)
                    continue;

                bestIndex = candidateIndex;
                bestNeighbourCount = neighbourCount;
                bestRequiredRadius = requiredRadius;
                bestAngle = angle;
            }

            Vector2Int selectedCell = available[bestIndex];
            available.RemoveAt(bestIndex);
            selected.Add(selectedCell);
            occupied.Add(selectedCell);
        }
    }

    private float CalculateRequiredOuterRadiusAfterAdding(
        IReadOnlyList<Vector2Int> selected,
        Vector2Int candidate,
        float gapRatio)
    {
        List<Vector2Int> testCells = new List<Vector2Int>(selected.Count + 1);

        for (int i = 0; i < selected.Count; i++)
            testCells.Add(selected[i]);

        testCells.Add(candidate);

        const float unitInnerRadius = 1f;
        List<Vector2> positions = BuildCenteredXZPositions(testCells, unitInnerRadius, gapRatio);
        float requiredRadius = 0f;

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

            for (int vertexIndex = 0; vertexIndex < vertices.Length; vertexIndex++)
            {
                float vertexRequiredRadius = CalculateRequiredOuterRadiusForPoint(vertices[vertexIndex]);
                requiredRadius = Mathf.Max(requiredRadius, vertexRequiredRadius);
            }
        }

        return requiredRadius;
    }

    private int GetOuterAlignedLayer(Vector2Int cell)
    {
        int first = Mathf.Abs(2 * cell.y + cell.x);
        int second = Mathf.Abs(2 * cell.x + cell.y);
        int third = Mathf.Abs(cell.x - cell.y);

        return Mathf.Max(first, Mathf.Max(second, third));
    }

    private int CompareOuterAlignedCells(Vector2Int left, Vector2Int right, float pitchRadius)
    {
        int layerComparison = GetOuterAlignedLayer(left).CompareTo(GetOuterAlignedLayer(right));
        if (layerComparison != 0)
            return layerComparison;

        return CompareCellsByAngle(left, right, pitchRadius);
    }

    private int CompareCellsByAngle(Vector2Int left, Vector2Int right, float pitchRadius)
    {
        float leftAngle = GetCellAngle(left, pitchRadius);
        float rightAngle = GetCellAngle(right, pitchRadius);

        int angleComparison = leftAngle.CompareTo(rightAngle);
        if (angleComparison != 0)
            return angleComparison;

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

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

    private float GetCellAngle(Vector2Int cell, float pitchRadius)
    {
        Vector2 position = AxialToXZ(cell, pitchRadius);
        return Mathf.Repeat(Mathf.Atan2(position.y, position.x) * Mathf.Rad2Deg, 360f);
    }

    private Vector2 CalculateCellsCenterXZ(IReadOnlyList<Vector2Int> cells, float pitchRadius)
    {
        Vector2 sum = Vector2.zero;

        for (int i = 0; i < cells.Count; i++)
            sum += AxialToXZ(cells[i], pitchRadius);

        return sum / cells.Count;
    }

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

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

        for (int i = 0; i < cells.Count; i++)
            positions.Add(AxialToXZ(cells[i], pitchRadius) - center);

        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;

            if (CanFitScale(cells, outerRadius, innerBaseRadius, gapRatio, middle))
                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 float CalculateRequiredOuterRadiusForPoint(Vector2 point)
    {
        float requiredApothem = Mathf.Max(
            Mathf.Abs(point.y),
            Mathf.Max(
                Mathf.Abs(HexApothemFactor * point.x + 0.5f * point.y),
                Mathf.Abs(HexApothemFactor * point.x - 0.5f * point.y)));

        return requiredApothem / HexApothemFactor;
    }

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