Загрузка данных
public class HexInsideHexGenerator : IInitializable
{
private const float Sqrt3 = 1.7320508f;
private const float HexApothemFactor = Sqrt3 * 0.5f;
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 BusinesCapabilitiesFragmentFactory fragmentFactory;
private MainConfigHolder mainConfigHolder;
private 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.IsEmpty())
{
Debug.LogWarning("ids is empty");
return new List<FbcDataFragment>();
}
if (root == null)
{
Debug.LogWarning("cannot resolve generated root.");
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>();
}
List<Vector2Int> cells = BuildSpiralCells(count);
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);
var tmpScale = prefab.transform.localScale;
var tmpPos = prefab.transform.position;
prefab.transform.localScale *= finalScale;
var prefabBounds = ObjectToFrustumFitter.GetBoundsWithChildren(prefab.gameObject);
var outerBounds = ObjectToFrustumFitter.GetBoundsWithChildren(outerHex.gameObject);
prefab.transform.SetY(outerBounds.max.y - prefabBounds.size.y);
var localY = prefab.transform.localPosition.y;
prefab.transform.localScale = tmpScale;
prefab.transform.position = tmpPos;
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.transform, 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--)
{
var 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.gameObject.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> 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 Vector2Int Multiply(Vector2Int value, int multiplier)
{
return new Vector2Int(value.x * multiplier, value.y * multiplier);
}
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];
float angleOffsetDegrees = 0f;
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 bool IsPointInsideHex(Vector2 point, float radius)
{
float epsilon = Mathf.Epsilon;
float apothem = radius * HexApothemFactor;
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;
}
}