// 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]); } } } }