// MIT License - Copyright (c) 2025 wallstop
// Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE
// ReSharper disable once CheckNamespace
namespace WallstopStudios.UnityHelpers.Core.Extension
{
using System;
using System.Collections.Generic;
using DataStructure.Adapters;
using UnityEngine;
using Utils;
// GridConcaveHullKnn.cs - K-Nearest Neighbors algorithm implementation
// See GeometryConcaveHull.cs for full concave hull architecture documentation
///
/// KNN-based concave hull builders for Vector2 and FastVector3Int grids.
/// Iteratively selects next hull point from k nearest neighbors using maximum right turn.
///
public static partial class UnityExtensions
{
public static List BuildConcaveHullKnn(
this IReadOnlyCollection points,
int nearestNeighbors = 3
)
{
ConcaveHullOptions options = ConcaveHullOptions
.Default.WithStrategy(ConcaveHullStrategy.Knn)
.WithNearestNeighbors(Math.Max(3, nearestNeighbors));
return points.BuildConcaveHull(options);
}
// KNN-style concave hull for Vector2 (port of BuildConcaveHull2)
private static List BuildConcaveHull2(
this IReadOnlyCollection input,
int nearestNeighbors
)
{
const int minimumNearestNeighbors = 3;
nearestNeighbors = Math.Max(minimumNearestNeighbors, nearestNeighbors);
using PooledResource> dataSetRes = Buffers.List.Get(
out List dataSet
);
using PooledResource> uniqueRes = Buffers.HashSet.Get(
out HashSet unique
);
using PooledResource> originalRes = Buffers.List.Get(
out List original
);
original.AddRange(input);
foreach (Vector2 point in original)
{
if (unique.Add(point))
{
dataSet.Add(point);
}
}
int totalPoints = dataSet.Count;
if (totalPoints <= 4)
{
return input.BuildConvexHull(includeColinearPoints: false);
}
int maximumNearestNeighbors = totalPoints;
int attemptNearestNeighbors = Math.Min(totalPoints, nearestNeighbors);
int firstIndex = FindLowestPointIndex(dataSet);
List hull = new(totalPoints);
if (firstIndex < 0)
{
hull.AddRange(dataSet);
return hull;
}
Vector2 firstPoint = dataSet[firstIndex];
int maxSteps = Math.Max(16, totalPoints * 6);
using PooledArray availabilityResource = SystemArrayPool.Get(
totalPoints,
out bool[] availability
);
using PooledResource> neighborIndicesRes = Buffers.List.Get(
out List neighborIndices
);
using PooledArray distanceBufferRes = SystemArrayPool.Get(
totalPoints,
out float[] neighborDistances
);
while (true)
{
hull.Clear();
hull.Add(firstPoint);
if (neighborIndices.Capacity < attemptNearestNeighbors)
{
neighborIndices.Capacity = attemptNearestNeighbors;
}
bool success = TryBuildConcaveHull2Attempt(
dataSet,
hull,
availability,
neighborIndices,
neighborDistances,
attemptNearestNeighbors,
firstIndex,
firstPoint,
maxSteps
);
if (success)
{
PruneColinearOnHull(hull);
return hull;
}
if (attemptNearestNeighbors >= maximumNearestNeighbors)
{
return BuildConvexHullJarvisFallback(
dataSet,
hull,
includeColinearPoints: false,
neighborIndices,
neighborDistances,
availability
);
}
++attemptNearestNeighbors;
}
}
///
/// Builds a concave hull using a k-nearest neighbors approach for grid points.
///
///
/// Obsolete: prefer or
/// .
///
[Obsolete("Use BuildConcaveHullKnn or BuildConcaveHull with options.")]
public static List BuildConcaveHull2(
this IReadOnlyCollection gridPositions,
Grid grid,
int nearestNeighbors = 3
)
{
const int minimumNearestNeighbors = 3;
nearestNeighbors = Math.Max(minimumNearestNeighbors, nearestNeighbors);
if (grid == null)
{
throw new ArgumentNullException(nameof(grid));
}
using PooledResource> dataSetResource =
Buffers.List.Get(out List dataSet);
using PooledResource> uniquePositionsResource =
Buffers.HashSet.Get(out HashSet uniquePositions);
using PooledResource> originalGridPositionsBuffer =
Buffers.List.Get(out List originalGridPositions);
originalGridPositions.AddRange(gridPositions);
foreach (FastVector3Int gridPosition in originalGridPositions)
{
if (uniquePositions.Add(gridPosition))
{
dataSet.Add(gridPosition);
}
}
int totalPoints = dataSet.Count;
List hull = new(totalPoints);
if (totalPoints <= 4)
{
return BuildConvexHullMonotoneChain(
gridPositions,
grid,
includeColinearPoints: false,
resultBuffer: hull
);
}
BuildConvexHullMonotoneChain(
dataSet,
grid,
includeColinearPoints: false,
resultBuffer: hull
);
if (AreAllPointsOnHullEdges(dataSet, hull))
{
return hull;
}
hull.Clear();
int maximumNearestNeighbors = totalPoints;
int attemptNearestNeighbors = Math.Min(totalPoints, nearestNeighbors);
int firstIndex = FindLowestGridPointIndex(dataSet, grid);
if (firstIndex < 0)
{
hull.AddRange(dataSet);
return hull;
}
FastVector3Int firstPoint = dataSet[firstIndex];
int maxSteps = Math.Max(16, dataSet.Count * 6);
using PooledArray availabilityResource = SystemArrayPool.Get(
totalPoints,
out bool[] availability
);
using PooledResource> neighborIndicesResource = Buffers.List.Get(
out List neighborIndices
);
using PooledArray distanceBufferResource = SystemArrayPool.Get(
totalPoints,
out float[] neighborDistances
);
using PooledArray worldPositionsResource = SystemArrayPool.Get(
totalPoints,
out Vector2[] worldPositions
);
for (int i = 0; i < totalPoints; ++i)
{
worldPositions[i] = grid.CellToWorld(dataSet[i]);
}
while (true)
{
hull.Clear();
hull.Add(firstPoint);
if (neighborIndices.Capacity < attemptNearestNeighbors)
{
neighborIndices.Capacity = attemptNearestNeighbors;
}
bool success = TryBuildGridConcaveHullAttempt(
dataSet,
hull,
grid,
availability,
neighborIndices,
neighborDistances,
worldPositions,
attemptNearestNeighbors,
firstIndex,
firstPoint,
maxSteps
);
if (success)
{
PruneColinearOnHull(hull);
return hull;
}
if (attemptNearestNeighbors >= maximumNearestNeighbors)
{
return BuildGridConvexHullJarvisFallback(
dataSet,
worldPositions,
hull,
includeColinearPoints: false,
neighborIndices,
neighborDistances,
availability
);
}
++attemptNearestNeighbors;
}
}
private static void SortByDistanceAscending(List points, Vector2 origin)
{
int count = points.Count;
if (count <= 1)
{
return;
}
using PooledArray distancesResource = SystemArrayPool.Get(
count,
out float[] distances
);
for (int i = 0; i < count; ++i)
{
distances[i] = (points[i] - origin).sqrMagnitude;
}
SelectionSort(points, distances, count);
}
private static void SortByRightHandTurn(
List points,
Vector2 current,
float previousAngle
)
{
int count = points.Count;
if (count <= 1)
{
return;
}
using PooledArray angleRes = SystemArrayPool.Get(
count,
out float[] angles
);
for (int i = 0; i < count; ++i)
{
float candidateAngle = CalculateAngle(current, points[i]);
angles[i] = -AngleDifference(previousAngle, candidateAngle);
}
SelectionSort(points, angles, count);
}
private static void SortByDistanceAscending(
List points,
Grid grid,
Vector2 origin
)
{
int count = points.Count;
if (count <= 1)
{
return;
}
using PooledArray distancesResource = SystemArrayPool.Get(
count,
out float[] distances
);
for (int i = 0; i < count; ++i)
{
Vector2 worldPosition = grid.CellToWorld(points[i]);
distances[i] = (worldPosition - origin).sqrMagnitude;
}
SelectionSort(points, distances, count);
}
private static void SortByDistanceAscending(
List points,
Grid grid,
Vector2 origin
)
{
int count = points.Count;
if (count <= 1)
{
return;
}
using PooledArray distancesResource = SystemArrayPool.Get(
count,
out float[] distances
);
for (int i = 0; i < count; ++i)
{
Vector2 worldPosition = grid.CellToWorld(points[i]);
distances[i] = (worldPosition - origin).sqrMagnitude;
}
SelectionSort(points, distances, count);
}
private static void SortByRightHandTurn(
List points,
Grid grid,
FastVector3Int current,
float previousAngle
)
{
int count = points.Count;
if (count <= 1)
{
return;
}
using PooledArray angleBufferResource = SystemArrayPool.Get(
count,
out float[] angles
);
Vector2 currentPoint = grid.CellToWorld(current);
for (int i = 0; i < count; ++i)
{
Vector2 candidatePoint = grid.CellToWorld(points[i]);
float candidateAngle = CalculateAngle(currentPoint, candidatePoint);
angles[i] = -AngleDifference(previousAngle, candidateAngle);
}
SelectionSort(points, angles, count);
}
private static void SelectionSort(List points, float[] distances, int count)
{
for (int i = 0; i < count - 1; ++i)
{
int minIndex = i;
float minDistance = distances[i];
for (int j = i + 1; j < count; ++j)
{
float candidateDistance = distances[j];
if (candidateDistance < minDistance)
{
minDistance = candidateDistance;
minIndex = j;
}
}
if (minIndex != i)
{
(points[i], points[minIndex]) = (points[minIndex], points[i]);
(distances[i], distances[minIndex]) = (distances[minIndex], distances[i]);
}
}
}
private static void RemovePoints(List source, List toRemove)
{
for (int i = toRemove.Count - 1; i >= 0; --i)
{
_ = source.Remove(toRemove[i]);
}
}
}
}