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


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

[ExecuteAlways]
public sealed class HexInsideHexLayout : MonoBehaviour
{
    public enum HexOrientation
    {
        FlatTop,
        PointyTop
    }

    private const float Sqrt3 = 1.7320508f;
    private const float HexApothemFactor = Sqrt3 * 0.5f;
    private const string GeneratedRootName = "Generated Inner Hexes";

    private static 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)
    };

    [Header("Sources")]
    [SerializeField] private Transform outerVisualRoot;
    [SerializeField] private GameObject innerHexPrefab;
    [SerializeField] private Transform generatedRoot;

    [Header("Layout")]
    [Min(1)]
    [SerializeField] private int innerHexCount = 7;

    [Tooltip("Относительный зазор между внутренними гексами. 0.1 = зазор 10% от итогового flat-to-flat размера внутреннего гекса.")]
    [Min(0f)]
    [SerializeField] private float gapRatio = 0f;

    [Tooltip("Относительный отступ от границы внешнего гекса. 0.1 = доступная область уменьшается примерно на 10%.")]
    [Range(0f, 0.95f)]
    [SerializeField] private float outerPaddingRatio = 0f;

    [Tooltip("Локальная высота размещения внутренних гексов по Y.")]
    [SerializeField] private float localY = 0f;

    [SerializeField] private HexOrientation orientation = HexOrientation.FlatTop;

    [Header("Optional radius overrides")]
    [Tooltip("Если больше 0, используется вместо радиуса, вычисленного по bounds внешнего гекса.")]
    [SerializeField] private float outerRadiusOverride = 0f;

    [Tooltip("Если больше 0, используется вместо радиуса, вычисленного по bounds внутреннего prefab.")]
    [SerializeField] private float innerRadiusOverride = 0f;

    [Header("Debug")]
    [SerializeField] private float lastGeneratedScale;
    [SerializeField] private float lastGeneratedInnerRadius;
    [SerializeField] private float lastGeneratedGap;
    [SerializeField] private float lastOuterRadius;
    [SerializeField] private float lastEffectiveOuterRadius;
    [SerializeField] private float lastOuterPadding;

    [Button("Generate")]
    public void Generate()
    {
        if (innerHexPrefab == null)
        {
            Debug.LogWarning($"{nameof(HexInsideHexLayout)}: inner hex prefab is not assigned.", this);
            return;
        }

        Transform root = ResolveGeneratedRoot(true);

        if (root == null)
        {
            Debug.LogWarning($"{nameof(HexInsideHexLayout)}: cannot resolve generated root.", this);
            return;
        }

        PrepareGeneratedRoot(root);
        ClearChildren(root);

        Transform actualOuterVisualRoot = outerVisualRoot != null ? outerVisualRoot : transform;

        if (!TryCalculateLocalBounds(actualOuterVisualRoot, transform, out Bounds outerLocalBounds, root))
        {
            Debug.LogWarning($"{nameof(HexInsideHexLayout)}: cannot calculate outer bounds.", this);
            return;
        }

        if (!TryCalculatePrefabLocalBounds(innerHexPrefab, out Bounds innerLocalBounds))
        {
            Debug.LogWarning($"{nameof(HexInsideHexLayout)}: cannot calculate inner prefab bounds.", this);
            return;
        }

        float outerRadius = ResolveHexRadius(outerLocalBounds, true);
        float innerBaseRadius = ResolveHexRadius(innerLocalBounds, false);

        float effectiveOuterRadius = ApplyPaddingRatioToHexRadius(
            outerRadius,
            outerPaddingRatio);

        lastOuterRadius = outerRadius;
        lastEffectiveOuterRadius = effectiveOuterRadius;
        lastOuterPadding = CalculateOuterPaddingUnits(outerRadius, outerPaddingRatio);

        if (outerRadius <= 0f)
        {
            Debug.LogWarning($"{nameof(HexInsideHexLayout)}: outer radius is zero.", this);
            return;
        }

        if (effectiveOuterRadius <= 0f)
        {
            Debug.LogWarning(
                $"{nameof(HexInsideHexLayout)}: effective outer radius is zero. Reduce outer padding ratio.",
                this);

            return;
        }

        if (innerBaseRadius <= 0f)
        {
            Debug.LogWarning($"{nameof(HexInsideHexLayout)}: inner radius is zero.", this);
            return;
        }

        int count = Mathf.Max(1, innerHexCount);

        List<Vector2Int> cells = BuildSpiralCells(count);

        float finalScale = FindMaximumFittingScale(
            cells,
            effectiveOuterRadius,
            innerBaseRadius,
            gapRatio,
            orientation);

        float finalInnerRadius = innerBaseRadius * finalScale;
        float finalGap = CalculateGapUnits(finalInnerRadius, gapRatio);

        lastGeneratedScale = finalScale;
        lastGeneratedInnerRadius = finalInnerRadius;
        lastGeneratedGap = finalGap;

        Vector2 outerCenterXZ = GetXZPosition(outerLocalBounds.center);

        List<Vector2> localXZPositions = BuildCenteredXZPositions(
            cells,
            finalInnerRadius,
            gapRatio,
            orientation);

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

            GameObject instance = Instantiate(innerHexPrefab, root, false);
            instance.name = $"{innerHexPrefab.name} {index:000}";

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

            instance.transform.localRotation = innerHexPrefab.transform.localRotation;
            instance.transform.localScale = innerHexPrefab.transform.localScale * finalScale;
        }
    }

    [Button("Clear Generated")]
    public void ClearGenerated()
    {
        Transform root = ResolveGeneratedRoot(false);

        if (root == null)
        {
            return;
        }

        ClearChildren(root);
    }

    private Transform ResolveGeneratedRoot(bool createIfMissing)
    {
        if (IsValidGeneratedRoot(generatedRoot))
        {
            return generatedRoot;
        }

        Transform existingRoot = transform.Find(GeneratedRootName);

        if (existingRoot != null)
        {
            generatedRoot = existingRoot;
            return generatedRoot;
        }

        if (!createIfMissing)
        {
            return null;
        }

        GameObject rootObject = new GameObject(GeneratedRootName);
        Transform rootTransform = rootObject.transform;
        rootTransform.SetParent(transform, false);

        generatedRoot = rootTransform;

        return generatedRoot;
    }

    private bool IsValidGeneratedRoot(Transform candidate)
    {
        if (candidate == null)
        {
            return false;
        }

        if (candidate == transform)
        {
            return false;
        }

        if (outerVisualRoot != null && candidate == outerVisualRoot)
        {
            return false;
        }

        if (outerVisualRoot != null && outerVisualRoot.IsChildOf(candidate))
        {
            return false;
        }

        return true;
    }

    private void PrepareGeneratedRoot(Transform root)
    {
        if (root == transform)
        {
            return;
        }

        root.SetParent(transform, false);
        root.localPosition = Vector3.zero;
        root.localRotation = Quaternion.identity;
        root.localScale = Vector3.one;
    }

    private static void ClearChildren(Transform root)
    {
        for (int childIndex = root.childCount - 1; childIndex >= 0; childIndex--)
        {
            DestroySmart(root.GetChild(childIndex).gameObject);
        }
    }

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

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

    private bool TryCalculatePrefabLocalBounds(GameObject prefab, out Bounds localBounds)
    {
        GameObject probe = Instantiate(prefab, transform, false);
        probe.name = $"{prefab.name}_BoundsProbe";
        probe.hideFlags = HideFlags.HideAndDontSave;
        probe.SetActive(true);

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

        DestroySmart(probe);

        return result;
    }

    private static 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 static 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, bool isOuter)
    {
        float radiusOverride = isOuter ? outerRadiusOverride : innerRadiusOverride;

        if (radiusOverride > 0f)
        {
            return radiusOverride;
        }

        Vector2 extentsXZ = GetXZExtents(localBounds);

        if (orientation == HexOrientation.FlatTop)
        {
            return Mathf.Min(
                extentsXZ.x,
                extentsXZ.y * 2f / Sqrt3);
        }

        return Mathf.Min(
            extentsXZ.x * 2f / Sqrt3,
            extentsXZ.y);
    }

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

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

    private static float ApplyPaddingRatioToHexRadius(
        float radius,
        float paddingRatio)
    {
        float clampedPaddingRatio = Mathf.Clamp01(paddingRatio);

        return radius * (1f - clampedPaddingRatio);
    }

    private static float CalculateOuterPaddingUnits(
        float outerRadius,
        float paddingRatio)
    {
        float clampedPaddingRatio = Mathf.Clamp01(paddingRatio);
        float outerApothem = outerRadius * HexApothemFactor;

        return outerApothem * clampedPaddingRatio;
    }

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

        return innerFlatToFlatSize * safeGapRatio;
    }

    private static List<Vector2Int> BuildSpiralCells(int count)
    {
        List<Vector2Int> cells = new List<Vector2Int>(count)
        {
            Vector2Int.zero
        };

        for (int radius = 1; cells.Count < count; radius++)
        {
            Vector2Int cell = Multiply(AxialDirections[4], radius);

            for (int sideIndex = 0; sideIndex < 6 && cells.Count < count; sideIndex++)
            {
                for (int stepIndex = 0; stepIndex < radius && cells.Count < count; stepIndex++)
                {
                    cells.Add(cell);
                    cell += AxialDirections[sideIndex];
                }
            }
        }

        return cells;
    }

    private static Vector2Int Multiply(Vector2Int value, int multiplier)
    {
        return new Vector2Int(
            value.x * multiplier,
            value.y * multiplier);
    }

    private static List<Vector2> BuildCenteredXZPositions(
        IReadOnlyList<Vector2Int> cells,
        float innerRadius,
        float gapRatio,
        HexOrientation orientation)
    {
        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,
                orientation);

            positions.Add(position);
            positionsSum += position;
        }

        Vector2 groupCenter = positionsSum / cells.Count;

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

        return positions;
    }

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

        if (orientation == HexOrientation.FlatTop)
        {
            return new Vector2(
                1.5f * pitchRadius * q,
                Sqrt3 * pitchRadius * (r + q * 0.5f));
        }

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

    private static float FindMaximumFittingScale(
        IReadOnlyList<Vector2Int> cells,
        float outerRadius,
        float innerBaseRadius,
        float gapRatio,
        HexOrientation orientation)
    {
        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,
                orientation,
                middle);

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

        return low;
    }

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

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

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

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

        return true;
    }

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

        float angleOffsetDegrees = orientation == HexOrientation.FlatTop ? 0f : 30f;

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

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

        return vertices;
    }

    private static bool IsPointInsideHex(
        Vector2 point,
        float radius,
        HexOrientation orientation)
    {
        const float epsilon = 0.0001f;

        float apothem = radius * HexApothemFactor;

        if (orientation == HexOrientation.FlatTop)
        {
            return Mathf.Abs(point.y) <= apothem + epsilon
                && Mathf.Abs(Sqrt3 * 0.5f * point.x + 0.5f * point.y) <= apothem + epsilon
                && Mathf.Abs(Sqrt3 * 0.5f * point.x - 0.5f * point.y) <= apothem + epsilon;
        }

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