Загрузка данных
public class HexInsideHexGenerator : IInitializable
{
private const float Sqrt3 = 1.7320508f;
private const float HexApothemFactor = Sqrt3 * 0.5f;
private const float ShellEpsilon = 0.0001f;
private readonly BusinesCapabilitiesFragmentFactory fragmentFactory;
private readonly MainConfigHolder mainConfigHolder;
private readonly DataManager dataManager;
private BusinessCapabilitiesInDepartmentConfig config;
private struct CellCandidate
{
public Vector2Int Cell;
public float RequiredOuterRadius;
public float Angle;
public CellCandidate(Vector2Int cell, float requiredOuterRadius, float angle)
{
Cell = cell;
RequiredOuterRadius = requiredOuterRadius;
Angle = angle;
}
}
public HexInsideHexGenerator(
BusinesCapabilitiesFragmentFactory fragmentFactory,
MainConfigHolder mainConfigHolder,
DataManager dataManager)
{
this.fragmentFactory = fragmentFactory;
this.mainConfigHolder = mainConfigHolder;
this.dataManager = dataManager;
}
public void Initialize()
{
config = mainConfigHolder.BusinessCapabilitiesInDepartmentConfig;
}
public List<FbcDataFragment> Generate(
int count,
List<long> ids,
FbcDataFragment prefab,
GameObject emptyPrefab,
Transform root,
Transform outerHex)
{
if (prefab == null)
{
Debug.LogWarning("inner hex prefab is not assigned.");
return new List<FbcDataFragment>();
}
if (ids == null || ids.Count == 0)
{
Debug.LogWarning("ids is empty");
return new List<FbcDataFragment>();
}
if (count <= 0)
{
Debug.LogWarning("generation count is zero.");
return new List<FbcDataFragment>();
}
count = Mathf.Min(count, ids.Count);
if (root == null)
{
Debug.LogWarning("cannot resolve generated root.");
return new List<FbcDataFragment>();
}
if (outerHex == null)
{
Debug.LogWarning("outer hex is not assigned.");
return new List<FbcDataFragment>();
}
ClearChildren(root, prefab);
Transform actualOuterVisualRoot = outerHex;
if (!TryCalculateLocalBounds(
actualOuterVisualRoot,
outerHex,
out Bounds outerLocalBounds,
root))
{
Debug.LogWarning("cannot calculate outer bounds.");
return new List<FbcDataFragment>();
}
if (!TryCalculatePrefabLocalBounds(
prefab.gameObject,
outerHex,
out Bounds innerLocalBounds))
{
Debug.LogWarning("cannot calculate inner prefab bounds.");
return new List<FbcDataFragment>();
}
float outerRadius = ResolveHexRadius(outerLocalBounds);
float innerBaseRadius = ResolveHexRadius(innerLocalBounds);
float effectiveOuterRadius = ApplyPaddingRatioToHexRadius(
outerRadius,
config.InHexPadding);
if (outerRadius <= 0f)
{
Debug.LogWarning("outer radius is zero.");
return new List<FbcDataFragment>();
}
if (effectiveOuterRadius <= 0f)
{
Debug.LogWarning("effective outer radius is zero. Reduce outer padding ratio.");
return new List<FbcDataFragment>();
}
if (innerBaseRadius <= 0f)
{
Debug.LogWarning("inner radius is zero.");
return new List<FbcDataFragment>();
}
// В отличие от аксиальной спирали, этот набор ячеек формирует
// общий контур в той же flat-top ориентации, что и внешний гекс.
List<Vector2Int> cells = BuildOuterAlignedCells(
count,
config.InHexGapRatio);
float finalScale = FindMaximumFittingScale(
cells,
effectiveOuterRadius,
innerBaseRadius,
config.InHexGapRatio);
float finalInnerRadius = innerBaseRadius * finalScale;
Vector2 outerCenterXZ = GetXZPosition(outerLocalBounds.center);
List<Vector2> localXZPositions = BuildCenteredXZPositions(
cells,
finalInnerRadius,
config.InHexGapRatio);
Vector3 temporaryScale = prefab.transform.localScale;
Vector3 temporaryPosition = prefab.transform.position;
prefab.transform.localScale *= finalScale;
Bounds prefabBounds = ObjectToFrustumFitter.GetBoundsWithChildren(prefab.gameObject);
Bounds outerBounds = ObjectToFrustumFitter.GetBoundsWithChildren(outerHex.gameObject);
prefab.transform.SetY(outerBounds.max.y - prefabBounds.size.y);
float localY = prefab.transform.localPosition.y;
prefab.transform.localScale = temporaryScale;
prefab.transform.position = temporaryPosition;
List<FbcDataFragment> results = new List<FbcDataFragment>(
localXZPositions.Count);
for (int i = 0; i < localXZPositions.Count; i++)
{
Vector2 finalXZPosition = outerCenterXZ + localXZPositions[i];
FbcDataFragment fragment = fragmentFactory.CreateFBC(
prefab,
new Vector3(
finalXZPosition.x,
config.InHexSurfaceOffset,
finalXZPosition.y),
prefab.transform.localRotation,
finalScale,
root,
ids[i]);
fragment.name = $"{prefab.name} {i}";
fragment.transform.localPosition = new Vector3(
finalXZPosition.x,
localY,
finalXZPosition.y);
fragment.transform.localRotation = prefab.transform.localRotation;
fragment.transform.localScale = prefab.transform.localScale * finalScale;
results.Add(fragment);
}
return results;
}
private void ClearChildren(Transform root, FbcDataFragment prefab)
{
for (int childIndex = root.childCount - 1; childIndex >= 0; childIndex--)
{
GameObject target = root.GetChild(childIndex).gameObject;
if (prefab.gameObject == target)
continue;
DestroySmart(target);
}
}
private void DestroySmart(Object target)
{
if (target == null)
return;
if (Application.isPlaying)
Object.Destroy(target);
else
Object.DestroyImmediate(target);
}
private bool TryCalculatePrefabLocalBounds(
GameObject prefab,
Transform outerHex,
out Bounds localBounds)
{
GameObject probe = UnityEngine.Object.Instantiate(
prefab,
outerHex,
false);
probe.name = $"{prefab.name}_BoundsProbe";
probe.SetActive(true);
bool result = TryCalculateLocalBounds(
probe.transform,
outerHex,
out localBounds,
null);
DestroySmart(probe);
return result;
}
private bool TryCalculateLocalBounds(
Transform sourceRoot,
Transform relativeTo,
out Bounds localBounds,
Transform excludedRoot)
{
localBounds = default;
bool hasBounds = false;
Renderer[] renderers = sourceRoot.GetComponentsInChildren<Renderer>(true);
foreach (Renderer renderer in renderers)
{
if (excludedRoot != null && renderer.transform.IsChildOf(excludedRoot))
continue;
EncapsulateWorldBoundsAsLocal(
renderer.bounds,
relativeTo,
ref localBounds,
ref hasBounds);
}
if (!hasBounds)
{
Collider[] colliders = sourceRoot.GetComponentsInChildren<Collider>(true);
foreach (Collider collider in colliders)
{
if (excludedRoot != null && collider.transform.IsChildOf(excludedRoot))
continue;
EncapsulateWorldBoundsAsLocal(
collider.bounds,
relativeTo,
ref localBounds,
ref hasBounds);
}
}
return hasBounds;
}
private void EncapsulateWorldBoundsAsLocal(
Bounds worldBounds,
Transform relativeTo,
ref Bounds localBounds,
ref bool hasBounds)
{
Vector3 min = worldBounds.min;
Vector3 max = worldBounds.max;
for (int xIndex = 0; xIndex <= 1; xIndex++)
{
for (int yIndex = 0; yIndex <= 1; yIndex++)
{
for (int zIndex = 0; zIndex <= 1; zIndex++)
{
Vector3 worldCorner = new Vector3(
xIndex == 0 ? min.x : max.x,
yIndex == 0 ? min.y : max.y,
zIndex == 0 ? min.z : max.z);
Vector3 localCorner = relativeTo.InverseTransformPoint(worldCorner);
if (!hasBounds)
{
localBounds = new Bounds(localCorner, Vector3.zero);
hasBounds = true;
}
else
{
localBounds.Encapsulate(localCorner);
}
}
}
}
}
private float ResolveHexRadius(Bounds localBounds)
{
Vector2 extentsXZ = GetXZExtents(localBounds);
return Mathf.Min(extentsXZ.x, extentsXZ.y * 2f / Sqrt3);
}
private Vector2 GetXZExtents(Bounds bounds)
{
return new Vector2(bounds.extents.x, bounds.extents.z);
}
private Vector2 GetXZPosition(Vector3 position)
{
return new Vector2(position.x, position.z);
}
private float ApplyPaddingRatioToHexRadius(float radius, float paddingRatio)
{
float clampedPaddingRatio = Mathf.Clamp01(paddingRatio);
return radius * (1f - clampedPaddingRatio);
}
private float CalculateGapUnits(float innerRadius, float gapRatio)
{
float safeGapRatio = Mathf.Max(0f, gapRatio);
float innerFlatToFlatSize = innerRadius * Sqrt3;
return innerFlatToFlatSize * safeGapRatio;
}
private List<Vector2Int> BuildOuterAlignedCells(int count, float gapRatio)
{
if (count <= 0)
return new List<Vector2Int>();
int searchRadius = Mathf.Max(
2,
Mathf.CeilToInt(Mathf.Sqrt(count)) * 2);
while (true)
{
List<CellCandidate> candidates = BuildCellCandidates(
searchRadius,
gapRatio);
candidates.Sort(CompareCandidates);
if (candidates.Count < count)
{
searchRadius *= 2;
continue;
}
float cutoffRadius = candidates[count - 1].RequiredOuterRadius;
bool touchesSearchBoundary = false;
for (int i = 0; i < candidates.Count; i++)
{
CellCandidate candidate = candidates[i];
if (candidate.RequiredOuterRadius > cutoffRadius + ShellEpsilon)
break;
if (Mathf.Abs(candidate.Cell.x) == searchRadius ||
Mathf.Abs(candidate.Cell.y) == searchRadius)
{
touchesSearchBoundary = true;
break;
}
}
if (touchesSearchBoundary)
{
searchRadius *= 2;
continue;
}
return SelectBalancedCandidates(candidates, count);
}
}
private List<CellCandidate> BuildCellCandidates(
int searchRadius,
float gapRatio)
{
int sideLength = searchRadius * 2 + 1;
List<CellCandidate> candidates = new List<CellCandidate>(
sideLength * sideLength);
const float unitInnerRadius = 1f;
float unitGap = CalculateGapUnits(unitInnerRadius, gapRatio);
float unitPitchRadius = unitInnerRadius + unitGap / Sqrt3;
for (int q = -searchRadius; q <= searchRadius; q++)
{
for (int r = -searchRadius; r <= searchRadius; r++)
{
Vector2Int cell = new Vector2Int(q, r);
Vector2 position = AxialToXZ(cell, unitPitchRadius);
float requiredOuterRadius = CalculateRequiredOuterRadius(
position,
unitInnerRadius);
float angle = Mathf.Repeat(
Mathf.Atan2(position.y, position.x) * Mathf.Rad2Deg,
360f);
candidates.Add(new CellCandidate(
cell,
requiredOuterRadius,
angle));
}
}
return candidates;
}
private int CompareCandidates(CellCandidate left, CellCandidate right)
{
int radiusComparison = left.RequiredOuterRadius.CompareTo(
right.RequiredOuterRadius);
if (radiusComparison != 0)
return radiusComparison;
int angleComparison = left.Angle.CompareTo(right.Angle);
if (angleComparison != 0)
return angleComparison;
int qComparison = left.Cell.x.CompareTo(right.Cell.x);
if (qComparison != 0)
return qComparison;
return left.Cell.y.CompareTo(right.Cell.y);
}
private List<Vector2Int> SelectBalancedCandidates(
List<CellCandidate> candidates,
int count)
{
List<Vector2Int> result = new List<Vector2Int>(count);
int candidateIndex = 0;
while (result.Count < count)
{
int shellStart = candidateIndex;
float shellRadius = candidates[shellStart].RequiredOuterRadius;
candidateIndex++;
while (candidateIndex < candidates.Count &&
Mathf.Abs(
candidates[candidateIndex].RequiredOuterRadius -
shellRadius) <= ShellEpsilon)
{
candidateIndex++;
}
int shellCount = candidateIndex - shellStart;
int remaining = count - result.Count;
if (shellCount <= remaining)
{
for (int i = shellStart; i < candidateIndex; i++)
result.Add(candidates[i].Cell);
continue;
}
// Если последний слой помещается не полностью,
// распределяем ячейки равномерно по всему периметру.
for (int i = 0; i < remaining; i++)
{
float normalizedIndex = (i + 0.5f) / remaining;
int localIndex = Mathf.FloorToInt(
normalizedIndex * shellCount);
localIndex = Mathf.Clamp(
localIndex,
0,
shellCount - 1);
result.Add(candidates[shellStart + localIndex].Cell);
}
}
return result;
}
private float CalculateRequiredOuterRadius(
Vector2 center,
float innerRadius)
{
float requiredApothem = 0f;
for (int vertexIndex = 0; vertexIndex < 6; vertexIndex++)
{
float angleRadians = Mathf.Deg2Rad * (60f * vertexIndex);
Vector2 point = center + new Vector2(
Mathf.Cos(angleRadians) * innerRadius,
Mathf.Sin(angleRadians) * innerRadius);
float firstPlane = Mathf.Abs(point.y);
float secondPlane = Mathf.Abs(
HexApothemFactor * point.x + 0.5f * point.y);
float thirdPlane = Mathf.Abs(
HexApothemFactor * point.x - 0.5f * point.y);
requiredApothem = Mathf.Max(
requiredApothem,
Mathf.Max(
firstPlane,
Mathf.Max(secondPlane, thirdPlane)));
}
return requiredApothem / HexApothemFactor;
}
private List<Vector2> BuildCenteredXZPositions(
IReadOnlyList<Vector2Int> cells,
float innerRadius,
float gapRatio)
{
float gap = CalculateGapUnits(innerRadius, gapRatio);
float pitchRadius = innerRadius + gap / Sqrt3;
List<Vector2> positions = new List<Vector2>(cells.Count);
Vector2 positionsSum = Vector2.zero;
for (int cellIndex = 0; cellIndex < cells.Count; cellIndex++)
{
Vector2 position = AxialToXZ(cells[cellIndex], pitchRadius);
positions.Add(position);
positionsSum += position;
}
Vector2 groupCenter = positionsSum / cells.Count;
for (int positionIndex = 0; positionIndex < positions.Count; positionIndex++)
positions[positionIndex] -= groupCenter;
return positions;
}
private Vector2 AxialToXZ(Vector2Int axial, float pitchRadius)
{
float q = axial.x;
float r = axial.y;
return new Vector2(
1.5f * pitchRadius * q,
Sqrt3 * pitchRadius * (r + q * 0.5f));
}
private float FindMaximumFittingScale(
IReadOnlyList<Vector2Int> cells,
float outerRadius,
float innerBaseRadius,
float gapRatio)
{
float low = 0f;
float high = outerRadius / innerBaseRadius;
for (int iteration = 0; iteration < 48; iteration++)
{
float middle = (low + high) * 0.5f;
bool fits = CanFitScale(
cells,
outerRadius,
innerBaseRadius,
gapRatio,
middle);
if (fits)
low = middle;
else
high = middle;
}
return low;
}
private bool CanFitScale(
IReadOnlyList<Vector2Int> cells,
float outerRadius,
float innerBaseRadius,
float gapRatio,
float scale)
{
float innerRadius = innerBaseRadius * scale;
List<Vector2> positions = BuildCenteredXZPositions(
cells,
innerRadius,
gapRatio);
for (int positionIndex = 0; positionIndex < positions.Count; positionIndex++)
{
Vector2[] innerVertices = CreateHexVertices(
positions[positionIndex],
innerRadius);
for (int vertexIndex = 0; vertexIndex < innerVertices.Length; vertexIndex++)
{
if (!IsPointInsideHex(innerVertices[vertexIndex], outerRadius))
return false;
}
}
return true;
}
private Vector2[] CreateHexVertices(Vector2 center, float radius)
{
Vector2[] vertices = new Vector2[6];
for (int vertexIndex = 0; vertexIndex < vertices.Length; vertexIndex++)
{
float angleRadians = Mathf.Deg2Rad * (60f * vertexIndex);
vertices[vertexIndex] = center + new Vector2(
Mathf.Cos(angleRadians) * radius,
Mathf.Sin(angleRadians) * radius);
}
return vertices;
}
private bool IsPointInsideHex(Vector2 point, float radius)
{
float epsilon = Mathf.Epsilon;
float apothem = radius * HexApothemFactor;
return Mathf.Abs(point.y) <= apothem + epsilon &&
Mathf.Abs(
HexApothemFactor * point.x + 0.5f * point.y) <=
apothem + epsilon &&
Mathf.Abs(
HexApothemFactor * point.x - 0.5f * point.y) <=
apothem + epsilon;
}
}