Загрузка данных
// 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;
}
}