// 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 System.ComponentModel;
using DataStructure.Adapters;
using Helper;
using UnityEngine;
using Utils;
#if UNITY_EDITOR
#endif
///
/// Provides extension methods for Unity types including GameObject, Transform, Bounds, Rect,
/// Vector types, UI components, and advanced geometric algorithms such as convex/concave hull generation.
///
///
/// Thread Safety: Most methods require execution on the Unity main thread due to Unity API calls.
/// Performance: Methods use object pooling where possible to minimize allocations.
/// This class contains geometric algorithms adapted from various sources (see method-level comments).
///
/// Concave Hull Architecture
///
/// The concave hull system is organized as a partial class across multiple files, each handling a specific aspect:
///
///
///
/// File
/// Responsibility
///
/// -
/// GeometryConcaveHull.cs
/// Core types: enum, struct.
/// This is the architecture documentation entry point.
///
/// -
/// GridConcaveHull.cs
/// Main entry point for Vector2 inputs. Strategy dispatcher that routes to the
/// appropriate algorithm based on . Also contains the
/// post-processing repair logic that fixes axis-aligned hull artifacts.
///
/// -
/// GeometryConcaveHullGrid.cs
/// Grid-aware entry points for FastVector3Int inputs with Unity Grid context.
/// Converts grid positions to world coordinates for hull computation.
///
/// -
/// GridConcaveHullKnn.cs
/// K-Nearest Neighbors (KNN) algorithm implementation ().
/// Iteratively builds the hull by selecting the next point from k nearest neighbors that produces
/// the maximum right turn without intersecting existing edges. Good for general point clouds.
///
/// -
/// GridConcaveHullEdgeSplit.cs
/// Edge-splitting algorithm implementation ().
/// Starts with convex hull, then recursively splits the longest edges by inserting interior points.
/// Better for grid-aligned data with uniform density. Contains HullEdge struct and comparer.
///
/// -
/// GridConcaveHullDiagnostics.cs
/// Diagnostics and repair utilities. Contains ConcaveHullRepairStats for tracking
/// repair operations (enabled via ENABLE_CONCAVE_HULL_STATS), axis-corner repair methods,
/// and shared geometry helpers (intersection tests, angle calculations).
///
/// -
/// GridConcaveHullLineDivision.cs
/// Legacy line-division algorithm (retired/obsolete). Throws NotSupportedException;
/// kept only for API compatibility guidance.
///
///
///
/// Algorithm Selection Guidelines
///
/// - KNN (default): Best for irregular point distributions. More robust but slower for large datasets.
/// - EdgeSplit: Best for grid-aligned/lattice data. Faster but may produce artifacts on irregular data.
///
///
/// Post-Processing
///
/// Both algorithms apply optional axis-corner repair to fix common artifacts in grid-aligned data.
/// The repair pass inserts missing axis-aligned corners and removes redundant colinear points.
///
///
public static partial class UnityExtensions
{
public enum ConcaveHullStrategy
{
[Obsolete("Do not use default value; specify a strategy explicitly.")]
Unknown = 0,
Knn = 1,
EdgeSplit = 2,
}
public readonly struct ConcaveHullOptions
{
private const int DefaultNearestNeighbors = 3;
private const int DefaultBucketSize = 40;
private const float DefaultAngleThreshold = 90f;
private static readonly ConcaveHullOptions DefaultOptions = new(
ConcaveHullStrategy.Knn,
DefaultNearestNeighbors,
DefaultBucketSize,
DefaultAngleThreshold,
initialized: true
);
private readonly ConcaveHullStrategy _strategy;
private readonly int _nearestNeighbors;
private readonly int _bucketSize;
private readonly float _angleThreshold;
private readonly bool _initialized;
public ConcaveHullStrategy Strategy =>
_initialized ? _strategy : ConcaveHullStrategy.Knn;
public int NearestNeighbors =>
_initialized ? _nearestNeighbors : DefaultNearestNeighbors;
public int BucketSize => _initialized ? _bucketSize : DefaultBucketSize;
public float AngleThreshold => _initialized ? _angleThreshold : DefaultAngleThreshold;
public ConcaveHullOptions(
ConcaveHullStrategy strategy,
int nearestNeighbors = DefaultNearestNeighbors,
int bucketSize = DefaultBucketSize,
float angleThreshold = DefaultAngleThreshold
)
: this(strategy, nearestNeighbors, bucketSize, angleThreshold, initialized: true)
{ }
private ConcaveHullOptions(
ConcaveHullStrategy strategy,
int nearestNeighbors,
int bucketSize,
float angleThreshold,
bool initialized
)
{
_strategy = strategy;
_nearestNeighbors = Math.Max(DefaultNearestNeighbors, nearestNeighbors);
_bucketSize = Math.Max(1, bucketSize);
_angleThreshold = angleThreshold;
_initialized = initialized;
}
public static ConcaveHullOptions Default => DefaultOptions;
public static Builder CreateBuilder()
{
return new Builder(Default);
}
public static ConcaveHullOptions ForKnn(
int nearestNeighbors = DefaultNearestNeighbors,
float angleThreshold = DefaultAngleThreshold
)
{
return Default
.WithStrategy(ConcaveHullStrategy.Knn)
.WithNearestNeighbors(nearestNeighbors)
.WithAngleThreshold(angleThreshold);
}
public static ConcaveHullOptions ForEdgeSplit(
int bucketSize = DefaultBucketSize,
float angleThreshold = DefaultAngleThreshold
)
{
return Default
.WithStrategy(ConcaveHullStrategy.EdgeSplit)
.WithBucketSize(bucketSize)
.WithAngleThreshold(angleThreshold);
}
public ConcaveHullOptions WithStrategy(ConcaveHullStrategy strategy)
{
return new ConcaveHullOptions(
strategy,
NearestNeighbors,
BucketSize,
AngleThreshold
);
}
public ConcaveHullOptions WithNearestNeighbors(int nearestNeighbors)
{
return new ConcaveHullOptions(
Strategy,
nearestNeighbors,
BucketSize,
AngleThreshold
);
}
public ConcaveHullOptions WithBucketSize(int bucketSize)
{
return new ConcaveHullOptions(
Strategy,
NearestNeighbors,
bucketSize,
AngleThreshold
);
}
public ConcaveHullOptions WithAngleThreshold(float angleThreshold)
{
return new ConcaveHullOptions(
Strategy,
NearestNeighbors,
BucketSize,
angleThreshold
);
}
public struct Builder
{
private ConcaveHullStrategy _strategy;
private int _nearestNeighbors;
private int _bucketSize;
private float _angleThreshold;
internal Builder(ConcaveHullOptions seed)
{
_strategy = seed.Strategy;
_nearestNeighbors = seed.NearestNeighbors;
_bucketSize = seed.BucketSize;
_angleThreshold = seed.AngleThreshold;
}
public Builder WithStrategy(ConcaveHullStrategy strategy)
{
_strategy = strategy;
return this;
}
public Builder WithNearestNeighbors(int nearestNeighbors)
{
_nearestNeighbors = nearestNeighbors;
return this;
}
public Builder WithBucketSize(int bucketSize)
{
_bucketSize = bucketSize;
return this;
}
public Builder WithAngleThreshold(float angleThreshold)
{
_angleThreshold = angleThreshold;
return this;
}
public ConcaveHullOptions Build()
{
return new ConcaveHullOptions(
_strategy,
_nearestNeighbors,
_bucketSize,
_angleThreshold
);
}
}
}
public static List BuildConcaveHull(
this IReadOnlyCollection positions,
ConcaveHullOptions? options = null
)
{
if (positions == null)
{
throw new ArgumentNullException(nameof(positions));
}
ConcaveHullOptions appliedOptions = options ?? ConcaveHullOptions.Default;
using PooledResource> sourcePointsLease =
Buffers.List.Get(out List sourcePoints);
sourcePoints.AddRange(positions);
using PooledResource> vectorPointsResource = Buffers.List.Get(
out List vectorPoints
);
using PooledResource> mappingResource =
DictionaryBuffer.Dictionary.Get(
out Dictionary mapping
);
PopulateVectorBuffers(positions, vectorPoints, mapping, out int fallbackZ);
List vectorHull = BuildConcaveHullRaw(vectorPoints, appliedOptions);
List fastHull = ConvertVector2HullToFastVector3(
vectorHull,
mapping,
fallbackZ
);
#if ENABLE_CONCAVE_HULL_STATS
ConcaveHullRepairStats repairStats = new(fastHull.Count, sourcePoints.Count);
MaybeRepairConcaveCorners(
fastHull,
sourcePoints,
appliedOptions.Strategy,
appliedOptions.AngleThreshold,
repairStats
);
TrackHullRepairStats(fastHull, repairStats);
#else
MaybeRepairConcaveCorners(
fastHull,
sourcePoints,
appliedOptions.Strategy,
appliedOptions.AngleThreshold
);
#endif
return fastHull;
}
public static List BuildConcaveHullKnn(
this IReadOnlyCollection positions,
int nearestNeighbors = 3
)
{
ConcaveHullOptions options = ConcaveHullOptions
.Default.WithStrategy(ConcaveHullStrategy.Knn)
.WithNearestNeighbors(Math.Max(3, nearestNeighbors));
return positions.BuildConcaveHull(options);
}
public static List BuildConcaveHullEdgeSplit(
this IReadOnlyCollection positions,
int bucketSize = 40,
float angleThreshold = 90f
)
{
int clampedBucketSize = Math.Max(1, bucketSize);
float effectiveAngleThreshold = clampedBucketSize <= 1 ? 0f : angleThreshold;
ConcaveHullOptions options = ConcaveHullOptions
.Default.WithStrategy(ConcaveHullStrategy.EdgeSplit)
.WithBucketSize(clampedBucketSize)
.WithAngleThreshold(effectiveAngleThreshold);
return positions.BuildConcaveHull(options);
}
private static List BuildConvexHullJarvis(
IEnumerable pointsSet,
bool includeColinearPoints
)
{
using PooledResource> ptsRes = Buffers.List.Get(
out List points
);
points.AddRange(pointsSet);
int pointCount = points.Count;
List hull = new(pointCount > 0 ? pointCount + 1 : 0);
if (pointCount == 0)
{
return hull;
}
points.Sort(Vector2LexicographicalComparison);
DeduplicateSortedVector2(points);
pointCount = points.Count;
if (pointCount <= 2)
{
hull.AddRange(points);
return hull;
}
Vector2 start = points[0];
bool allColinear = true;
for (int i = 1; i < pointCount - 1; ++i)
{
if (AreApproximatelyColinear(start, points[^1], points[i]))
{
continue;
}
allColinear = false;
break;
}
if (allColinear)
{
if (includeColinearPoints)
{
hull.AddRange(points);
return hull;
}
Vector2 min = start;
Vector2 max = start;
foreach (Vector2 w in points)
{
if (w.x < min.x || (Mathf.Approximately(w.x, min.x) && w.y < min.y))
{
min = w;
}
if (w.x > max.x || (Mathf.Approximately(w.x, max.x) && w.y > max.y))
{
max = w;
}
}
hull.Add(min);
if (max != min)
{
hull.Add(max);
}
return hull;
}
hull.Capacity = Math.Max(hull.Capacity, pointCount + 1);
Vector2 current = start;
int guard = 0;
int guardMax = Math.Max(8, pointCount * 8);
do
{
hull.Add(current);
if (!includeColinearPoints)
{
TrimTailColinear(hull);
}
Vector2 candidate =
points[0] == current && points.Count > 1 ? points[1] : points[0];
for (int i = 0; i < points.Count; ++i)
{
Vector2 p = points[i];
if (p == current)
{
continue;
}
double rel = Geometry.IsAPointLeftOfVectorOrOnTheLineDouble(
current,
candidate,
p
);
double tolerance = ComputeAreaTolerance(current, candidate, p);
if (rel > tolerance)
{
candidate = p;
}
else if (Math.Abs(rel) <= tolerance)
{
float distCandidate = (candidate - current).sqrMagnitude;
float distP = (p - current).sqrMagnitude;
if (distP > distCandidate)
{
candidate = p;
}
}
}
if (includeColinearPoints)
{
using PooledResource> colinearRes = Buffers.List.Get(
out List colinear
);
colinear.Clear();
for (int i = 0; i < points.Count; ++i)
{
Vector2 p = points[i];
if (p == current || p == candidate)
{
continue;
}
double rel = Geometry.IsAPointLeftOfVectorOrOnTheLineDouble(
current,
candidate,
p
);
double tolerance = ComputeAreaTolerance(current, candidate, p);
if (Math.Abs(rel) <= tolerance)
{
colinear.Add(p);
}
}
if (colinear.Count > 0)
{
SortByDistanceAscending(colinear, current);
using PooledResource> hullSetRes =
Buffers.HashSet.Get(out HashSet hullSet);
foreach (Vector2 h in hull)
{
hullSet.Add(h);
}
foreach (Vector2 p in colinear)
{
if (!hullSet.Contains(p))
{
hull.Add(p);
}
}
current = candidate;
}
else
{
current = candidate;
}
}
else
{
current = candidate;
}
if (++guard > guardMax)
{
break;
}
} while (current != start);
if (hull.Count > 1 && hull[0] == hull[^1])
{
hull.RemoveAt(hull.Count - 1);
}
if (!includeColinearPoints && hull.Count > 2)
{
PruneColinearOnHull(hull);
}
return hull;
}
private static void PruneColinearOnHull(List hull)
{
if (hull == null || hull.Count <= 2)
{
return;
}
bool removed;
do
{
removed = false;
for (int i = 0; hull.Count > 2 && i < hull.Count; ++i)
{
int prev = (i - 1 + hull.Count) % hull.Count;
int next = (i + 1) % hull.Count;
if (AreApproximatelyColinear(hull[prev], hull[i], hull[next]))
{
hull.RemoveAt(i);
removed = true;
break;
}
}
} while (removed);
}
private static void TrimTailColinear(List hull)
{
if (hull == null)
{
return;
}
while (hull.Count >= 3)
{
int count = hull.Count;
if (AreApproximatelyColinear(hull[count - 3], hull[count - 2], hull[count - 1]))
{
hull.RemoveAt(count - 2);
continue;
}
break;
}
}
private static void TrimTailColinear(List hull)
{
if (hull == null)
{
return;
}
while (hull.Count >= 3)
{
int count = hull.Count;
FastVector3Int a = hull[count - 3];
FastVector3Int b = hull[count - 2];
FastVector3Int c = hull[count - 1];
if (Cross(a, b, c) == 0)
{
hull.RemoveAt(count - 2);
continue;
}
break;
}
}
private static List BuildConvexHullMonotoneChain(
IEnumerable pointsSet,
bool includeColinearPoints
)
{
using PooledResource> pointsResource = Buffers.List.Get(
out List points
);
points.AddRange(pointsSet);
points.Sort(Vector2LexicographicalComparison);
DeduplicateSortedVector2(points);
int pointCount = points.Count;
List hull = new(pointCount > 0 ? pointCount : 0);
if (pointCount <= 1)
{
hull.AddRange(points);
return hull;
}
if (pointCount >= 2)
{
Vector2 first = points[0];
Vector2 last = points[^1];
bool allColinear = true;
for (int i = 1; i < pointCount - 1; ++i)
{
if (!AreApproximatelyColinear(first, last, points[i]))
{
allColinear = false;
break;
}
}
if (allColinear)
{
if (includeColinearPoints)
{
hull.AddRange(points);
return hull;
}
else
{
hull.Add(points[0]);
if (points.Count > 1)
{
hull.Add(points[^1]);
}
return hull;
}
}
}
using PooledResource> lowerRes = Buffers.List.Get(
out List lower
);
using PooledResource> upperRes = Buffers.List.Get(
out List upper
);
foreach (Vector2 p in points)
{
while (lower.Count >= 2)
{
Vector2 a = lower[^2];
Vector2 b = lower[^1];
double cross = Geometry.IsAPointLeftOfVectorOrOnTheLineDouble(a, b, p);
double tolerance = ComputeAreaTolerance(a, b, p);
bool isRightTurn = cross < -tolerance;
bool isColinear = Math.Abs(cross) <= tolerance;
if (isRightTurn || (!includeColinearPoints && isColinear))
{
lower.RemoveAt(lower.Count - 1);
continue;
}
break;
}
lower.Add(p);
}
for (int i = points.Count - 1; i >= 0; --i)
{
Vector2 p = points[i];
while (upper.Count >= 2)
{
Vector2 a = upper[^2];
Vector2 b = upper[^1];
double cross = Geometry.IsAPointLeftOfVectorOrOnTheLineDouble(a, b, p);
double tolerance = ComputeAreaTolerance(a, b, p);
bool isRightTurn = cross < -tolerance;
bool isColinear = Math.Abs(cross) <= tolerance;
if (isRightTurn || (!includeColinearPoints && isColinear))
{
upper.RemoveAt(upper.Count - 1);
continue;
}
break;
}
upper.Add(p);
}
int targetCapacity = Math.Max(0, lower.Count + upper.Count - 2);
hull.Clear();
if (hull.Capacity < targetCapacity)
{
hull.Capacity = targetCapacity;
}
for (int i = 0; i < lower.Count; ++i)
{
hull.Add(lower[i]);
}
for (int i = 1; i < upper.Count - 1; ++i)
{
hull.Add(upper[i]);
}
if (!includeColinearPoints && hull.Count > 2)
{
PruneColinearOnHull(hull);
}
return hull;
}
// ===================== Vector2 Convex Hulls =====================
///
/// Builds a convex hull from a set of Vector2 points using the Monotone Chain algorithm.
///
/// The collection of points.
/// When true, includes colinear points along edges.
public static List BuildConvexHull(
this IEnumerable pointsSet,
bool includeColinearPoints = true
)
{
return BuildConvexHullMonotoneChain(pointsSet, includeColinearPoints);
}
///
/// Builds a convex hull from Vector2 with an explicit algorithm selection.
///
public static List BuildConvexHull(
this IEnumerable pointsSet,
bool includeColinearPoints,
ConvexHullAlgorithm algorithm
)
{
switch (algorithm)
{
case ConvexHullAlgorithm.MonotoneChain:
return BuildConvexHullMonotoneChain(pointsSet, includeColinearPoints);
case ConvexHullAlgorithm.Jarvis:
return BuildConvexHullJarvis(pointsSet, includeColinearPoints);
default:
throw new InvalidEnumArgumentException(
nameof(algorithm),
(int)algorithm,
typeof(ConvexHullAlgorithm)
);
}
}
}
}