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


// VERSION: PROCEDURAL LARGE HEXES + DECORATIVE HEXES WITH INDEPENDENT PERCENT GAP
// DecorativeGapRatio is a percentage of the decorative hex flat-to-flat size.
// Decorative hexes are pulled toward their neighbouring large hexes until this gap is reached.

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

    // 0.12 = отступ между границами равен 12% размера маленького гекса
    // между его параллельными гранями (flat-to-flat).
    private const float DecorativeGapRatio = 0.12f;

    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;


    private struct DecorativePlacement
    {
        public Vector2Int Cell;
        public Vector2 Position;

        public DecorativePlacement(Vector2Int cell, Vector2 position)
        {
            Cell = cell;
            Position = position;
        }
    }

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

        bool generateDecorativeHexes = count > 1 && emptyPrefab != null;
        float decorativeBaseRadius = 0f;

        if (generateDecorativeHexes)
        {
            if (!TryCalculatePrefabLocalBounds(emptyPrefab, outerHex, out Bounds decorativeLocalBounds))
            {
                Debug.LogWarning("cannot calculate decorative prefab bounds. Decorative hexes were not generated.");
                generateDecorativeHexes = false;
            }
            else
            {
                decorativeBaseRadius = ResolveHexRadius(decorativeLocalBounds);

                if (decorativeBaseRadius <= 0f)
                {
                    Debug.LogWarning("decorative prefab radius is zero. Decorative hexes were not generated.");
                    generateDecorativeHexes = false;
                }
            }
        }
        else if (count > 1 && emptyPrefab == null)
        {
            Debug.LogWarning("empty prefab is not assigned. Decorative hexes were not generated.");
        }

        List<Vector2Int> cells = BuildOuterAlignedCells(count, config.InHexGapRatio);
        List<Vector2Int> decorativeCells = generateDecorativeHexes
            ? BuildDecorativeCells(cells)
            : new List<Vector2Int>();

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

        float finalInnerRadius = innerBaseRadius * finalScale;
        float finalDecorativeRadius = decorativeBaseRadius * 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);
        }

        if (generateDecorativeHexes && decorativeCells.Count > 0)
        {
            List<DecorativePlacement> decorativePlacements = BuildDecorativePlacements(
                cells,
                decorativeCells,
                finalInnerRadius,
                finalDecorativeRadius,
                config.InHexGapRatio,
                DecorativeGapRatio);

            SpawnDecorativeHexes(
                decorativePlacements,
                emptyPrefab,
                root,
                outerCenterXZ,
                finalScale,
                outerTopWorldY);
        }

        return results;
    }

    private void SpawnDecorativeHexes(
        IReadOnlyList<DecorativePlacement> placements,
        GameObject emptyPrefab,
        Transform root,
        Vector2 outerCenterXZ,
        float finalScale,
        float outerTopWorldY)
    {
        if (placements == null || placements.Count == 0 || emptyPrefab == null)
            return;

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

        for (int i = 0; i < placements.Count; i++)
        {
            Vector2 finalXZ = outerCenterXZ + placements[i].Position;

            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)
    {
        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)
        {
            if (CountOccupiedNeighbours(candidate, occupied) >= 2)
                result.Add(candidate);
        }

        result.Sort((left, right) => CompareCellsByAngle(left, right, 1f));
        return result;
    }

    private List<DecorativePlacement> BuildDecorativePlacements(
        IReadOnlyList<Vector2Int> largeCells,
        IReadOnlyList<Vector2Int> decorativeCells,
        float largeRadius,
        float decorativeRadius,
        float largeGapRatio,
        float decorativeGapRatio)
    {
        List<DecorativePlacement> result = new List<DecorativePlacement>(decorativeCells.Count);

        if (decorativeCells.Count == 0)
            return result;

        float largeGap = CalculateGapUnits(largeRadius, largeGapRatio);
        float pitchRadius = largeRadius + largeGap / Sqrt3;
        Vector2 largeGroupCenter = CalculateCellsCenterXZ(largeCells, pitchRadius);

        Dictionary<Vector2Int, Vector2> largePositions = new Dictionary<Vector2Int, Vector2>(largeCells.Count);
        List<Vector2> allLargePositions = new List<Vector2>(largeCells.Count);
        HashSet<Vector2Int> occupied = new HashSet<Vector2Int>();

        for (int i = 0; i < largeCells.Count; i++)
        {
            Vector2Int cell = largeCells[i];
            Vector2 position = AxialToXZ(cell, pitchRadius) - largeGroupCenter;

            occupied.Add(cell);
            largePositions[cell] = position;
            allLargePositions.Add(position);
        }

        float decorativeFlatToFlat = decorativeRadius * Sqrt3;
        float targetGap = decorativeFlatToFlat * Mathf.Max(0f, decorativeGapRatio);

        for (int i = 0; i < decorativeCells.Count; i++)
        {
            Vector2Int decorativeCell = decorativeCells[i];
            Vector2 gridPosition = AxialToXZ(decorativeCell, pitchRadius) - largeGroupCenter;
            List<Vector2> neighbourPositions = GetOccupiedNeighbourPositions(
                decorativeCell,
                occupied,
                largePositions);

            Vector2 pulledPosition = CalculatePulledDecorativePosition(
                gridPosition,
                neighbourPositions,
                allLargePositions,
                largeRadius,
                decorativeRadius,
                targetGap);

            result.Add(new DecorativePlacement(decorativeCell, pulledPosition));
        }

        return result;
    }

    private List<Vector2> GetOccupiedNeighbourPositions(
        Vector2Int decorativeCell,
        HashSet<Vector2Int> occupied,
        Dictionary<Vector2Int, Vector2> largePositions)
    {
        List<Vector2> result = new List<Vector2>(3);

        for (int directionIndex = 0; directionIndex < AxialDirections.Length; directionIndex++)
        {
            Vector2Int neighbour = decorativeCell + AxialDirections[directionIndex];

            if (occupied.Contains(neighbour))
                result.Add(largePositions[neighbour]);
        }

        return result;
    }

    private Vector2 CalculatePulledDecorativePosition(
        Vector2 gridPosition,
        IReadOnlyList<Vector2> neighbourPositions,
        IReadOnlyList<Vector2> allLargePositions,
        float largeRadius,
        float decorativeRadius,
        float targetGap)
    {
        if (neighbourPositions == null || neighbourPositions.Count == 0)
            return gridPosition;

        Vector2 neighboursCenter = Vector2.zero;

        for (int i = 0; i < neighbourPositions.Count; i++)
            neighboursCenter += neighbourPositions[i];

        neighboursCenter /= neighbourPositions.Count;

        Vector2 pullVector = neighboursCenter - gridPosition;

        if (pullVector.sqrMagnitude <= GeometryEpsilon * GeometryEpsilon)
            return gridPosition;

        // В нулевой точке декоративный гекс находится в центре свободной
        // ячейки большой сетки. Затем он притягивается к соседним большим
        // гексам до тех пор, пока отступ от каждого из них не станет равен
        // targetGap или больше.
        float low = 0f;
        float high = 1f;

        for (int iteration = 0; iteration < DecorativePullIterations; iteration++)
        {
            float middle = (low + high) * 0.5f;
            Vector2 testPosition = gridPosition + pullVector * middle;

            if (HasRequiredDecorativeGap(
                    testPosition,
                    allLargePositions,
                    largeRadius,
                    decorativeRadius,
                    targetGap))
            {
                low = middle;
            }
            else
            {
                high = middle;
            }
        }

        return gridPosition + pullVector * low;
    }

    private bool HasRequiredDecorativeGap(
        Vector2 decorativePosition,
        IReadOnlyList<Vector2> allLargePositions,
        float largeRadius,
        float decorativeRadius,
        float targetGap)
    {
        for (int i = 0; i < allLargePositions.Count; i++)
        {
            float actualGap = CalculateHexToHexDistance(
                decorativePosition,
                decorativeRadius,
                allLargePositions[i],
                largeRadius);

            if (actualGap + GeometryEpsilon < targetGap)
                return false;
        }

        return true;
    }

    private float CalculateHexToHexDistance(
        Vector2 firstCenter,
        float firstRadius,
        Vector2 secondCenter,
        float secondRadius)
    {
        if (HexesOverlapOrTouch(firstCenter, firstRadius, secondCenter, secondRadius))
            return 0f;

        Vector2[] firstVertices = CreateHexVertices(firstCenter, firstRadius);
        Vector2[] secondVertices = CreateHexVertices(secondCenter, secondRadius);
        float minimumDistance = float.MaxValue;

        for (int vertexIndex = 0; vertexIndex < firstVertices.Length; vertexIndex++)
        {
            Vector2 point = firstVertices[vertexIndex];

            for (int edgeIndex = 0; edgeIndex < secondVertices.Length; edgeIndex++)
            {
                Vector2 edgeStart = secondVertices[edgeIndex];
                Vector2 edgeEnd = secondVertices[(edgeIndex + 1) % secondVertices.Length];
                minimumDistance = Mathf.Min(
                    minimumDistance,
                    DistancePointToSegment(point, edgeStart, edgeEnd));
            }
        }

        for (int vertexIndex = 0; vertexIndex < secondVertices.Length; vertexIndex++)
        {
            Vector2 point = secondVertices[vertexIndex];

            for (int edgeIndex = 0; edgeIndex < firstVertices.Length; edgeIndex++)
            {
                Vector2 edgeStart = firstVertices[edgeIndex];
                Vector2 edgeEnd = firstVertices[(edgeIndex + 1) % firstVertices.Length];
                minimumDistance = Mathf.Min(
                    minimumDistance,
                    DistancePointToSegment(point, edgeStart, edgeEnd));
            }
        }

        return minimumDistance;
    }

    private bool HexesOverlapOrTouch(
        Vector2 firstCenter,
        float firstRadius,
        Vector2 secondCenter,
        float secondRadius)
    {
        Vector2 delta = secondCenter - firstCenter;
        float combinedApothem = (firstRadius + secondRadius) * HexApothemFactor;

        return Mathf.Abs(delta.y) <= combinedApothem + GeometryEpsilon &&
               Mathf.Abs(HexApothemFactor * delta.x + 0.5f * delta.y) <= combinedApothem + GeometryEpsilon &&
               Mathf.Abs(HexApothemFactor * delta.x - 0.5f * delta.y) <= combinedApothem + GeometryEpsilon;
    }

    private float DistancePointToSegment(Vector2 point, Vector2 segmentStart, Vector2 segmentEnd)
    {
        Vector2 segment = segmentEnd - segmentStart;
        float squaredLength = segment.sqrMagnitude;

        if (squaredLength <= GeometryEpsilon * GeometryEpsilon)
            return Vector2.Distance(point, segmentStart);

        float t = Vector2.Dot(point - segmentStart, segment) / squaredLength;
        t = Mathf.Clamp01(t);

        Vector2 closestPoint = segmentStart + segment * t;
        return Vector2.Distance(point, closestPoint);
    }

    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> largeCells,
        IReadOnlyList<Vector2Int> decorativeCells,
        float outerRadius,
        float largeBaseRadius,
        float decorativeBaseRadius,
        float largeGapRatio,
        float decorativeGapRatio)
    {
        float low = 0f;
        float high = outerRadius / largeBaseRadius;

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

            if (CanFitScale(
                    largeCells,
                    decorativeCells,
                    outerRadius,
                    largeBaseRadius,
                    decorativeBaseRadius,
                    largeGapRatio,
                    decorativeGapRatio,
                    middle))
            {
                low = middle;
            }
            else
            {
                high = middle;
            }
        }

        return low;
    }

    private bool CanFitScale(
        IReadOnlyList<Vector2Int> largeCells,
        IReadOnlyList<Vector2Int> decorativeCells,
        float outerRadius,
        float largeBaseRadius,
        float decorativeBaseRadius,
        float largeGapRatio,
        float decorativeGapRatio,
        float scale)
    {
        float largeRadius = largeBaseRadius * scale;
        float largeGap = CalculateGapUnits(largeRadius, largeGapRatio);
        float pitchRadius = largeRadius + largeGap / Sqrt3;
        Vector2 largeGroupCenter = CalculateCellsCenterXZ(largeCells, pitchRadius);

        for (int i = 0; i < largeCells.Count; i++)
        {
            Vector2 position = AxialToXZ(largeCells[i], pitchRadius) - largeGroupCenter;
            Vector2[] vertices = CreateHexVertices(position, largeRadius);

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

        if (decorativeCells == null || decorativeCells.Count == 0 || decorativeBaseRadius <= 0f)
            return true;

        float decorativeRadius = decorativeBaseRadius * scale;
        List<DecorativePlacement> placements = BuildDecorativePlacements(
            largeCells,
            decorativeCells,
            largeRadius,
            decorativeRadius,
            largeGapRatio,
            decorativeGapRatio);

        for (int i = 0; i < placements.Count; i++)
        {
            Vector2[] vertices = CreateHexVertices(placements[i].Position, decorativeRadius);

            for (int vertexIndex = 0; vertexIndex < vertices.Length; vertexIndex++)
            {
                if (!IsPointInsideHex(vertices[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;
    }
}