// MIT License - Copyright (c) 2025 wallstop
// Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE
#if UNITY_EDITOR || UNITY_INCLUDE_TESTS || WALLSTOP_CONCAVE_HULL_STATS
#define ENABLE_CONCAVE_HULL_STATS
#endif
// ReSharper disable once CheckNamespace
namespace WallstopStudios.UnityHelpers.Core.Extension
{
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using DataStructure.Adapters;
using UnityEngine;
using Utils;
// GridConcaveHullDiagnostics.cs - Diagnostics, repair, and shared geometry utilities
// See GeometryConcaveHull.cs for full concave hull architecture documentation
///
/// Concave hull diagnostics, repair helpers, and shared geometry utilities.
/// Contains repair stats tracking and axis-corner repair logic for grid-aligned data.
///
public static partial class UnityExtensions
{
#if ENABLE_CONCAVE_HULL_STATS
public sealed class ConcaveHullRepairStats
{
public ConcaveHullRepairStats(int startHullCount, int originalPointsCount)
{
StartHullCount = startHullCount;
OriginalPointsCount = originalPointsCount;
}
public int StartHullCount { get; }
public int OriginalPointsCount { get; }
public int FinalHullCount { get; private set; }
public int AxisCornerInsertions { get; private set; }
public int AxisPathInsertions { get; private set; }
public int CandidateConnections { get; private set; }
public int DuplicateRemovals { get; private set; }
public int DiagonalPruned { get; private set; }
public int AxisNeighborVisits { get; private set; }
public int MaxFrontierSize { get; private set; }
internal ConcaveHullRepairStats Clone()
{
ConcaveHullRepairStats clone = new(StartHullCount, OriginalPointsCount)
{
FinalHullCount = FinalHullCount,
AxisCornerInsertions = AxisCornerInsertions,
AxisPathInsertions = AxisPathInsertions,
CandidateConnections = CandidateConnections,
DuplicateRemovals = DuplicateRemovals,
DiagonalPruned = DiagonalPruned,
AxisNeighborVisits = AxisNeighborVisits,
MaxFrontierSize = MaxFrontierSize,
};
return clone;
}
internal void IncrementAxisCornerInsertions()
{
AxisCornerInsertions++;
}
internal void IncrementAxisPathInsertions(int amount)
{
if (amount > 0)
{
AxisPathInsertions += amount;
}
}
internal void IncrementCandidateConnections()
{
CandidateConnections++;
}
internal void IncrementDuplicateRemovals()
{
DuplicateRemovals++;
}
internal void IncrementDiagonalPruned()
{
DiagonalPruned++;
}
internal void IncrementAxisNeighborVisits()
{
AxisNeighborVisits++;
}
internal void MaybeRecordFrontierSize(int size)
{
if (size > MaxFrontierSize)
{
MaxFrontierSize = size;
}
}
internal void MarkFinalHullCount(int count)
{
FinalHullCount = count;
}
}
#endif
#if ENABLE_CONCAVE_HULL_STATS
private static readonly ConditionalWeakTable<
List,
ConcaveHullRepairStats
> HullRepairStatsCache = new();
private static void TrackHullRepairStats(
List hull,
ConcaveHullRepairStats stats
)
{
if (hull == null || stats == null)
{
return;
}
ConcaveHullRepairStats clone = stats.Clone();
if (HullRepairStatsCache.TryGetValue(hull, out _))
{
HullRepairStatsCache.Remove(hull);
}
HullRepairStatsCache.Add(hull, clone);
}
private static bool TryGetTrackedHullRepairStats(
List hull,
out ConcaveHullRepairStats stats
)
{
if (
hull != null
&& HullRepairStatsCache.TryGetValue(hull, out ConcaveHullRepairStats cached)
)
{
stats = cached.Clone();
return true;
}
stats = null;
return false;
}
#endif
private const float ConcaveCornerRepairThresholdDegrees = 90f;
private const int AxisCornerRepairIterationMultiplier = 4;
private const int AxisCornerRepairIterationMinimum = 1024;
///
/// Calculates the cosine of the angle at point o formed by points a and b using the law of cosines.
///
public static double GetCosine(Vector2 a, Vector3 b, Vector3 o)
{
double aPow2 = (a.x - o.x) * (a.x - o.x) + (a.y - o.y) * (a.y - o.y);
double bPow2 = (b.x - o.x) * (b.x - o.x) + (b.y - o.y) * (b.y - o.y);
double cPow2 = (a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y);
double cos = (aPow2 + bPow2 - cPow2) / (2 * Math.Sqrt(aPow2 * bPow2));
return Math.Round(cos, 4);
}
public static bool IsPositionInside(List hull, Vector2 position)
{
bool isPositionInside = false;
for (int i = 0; i < hull.Count; ++i)
{
Vector2 oldVector = hull[i];
int nextIndex = (i + 1) % hull.Count;
Vector2 newVector = hull[nextIndex];
Vector2 lhs;
Vector2 rhs;
if (oldVector.x < newVector.x)
{
lhs = oldVector;
rhs = newVector;
}
else
{
lhs = newVector;
rhs = oldVector;
}
if (
(newVector.x < position.x) == (position.x <= oldVector.x)
&& (position.y - (long)lhs.y) * (rhs.x - lhs.x)
< (rhs.y - (long)lhs.y) * (position.x - lhs.x)
)
{
isPositionInside = !isPositionInside;
}
}
return isPositionInside;
}
public static bool IsPositionInside(
List hull,
FastVector3Int gridPosition,
Grid grid
)
{
bool isPositionInside = false;
Vector2 position = grid.CellToWorld(gridPosition);
for (int i = 0; i < hull.Count; ++i)
{
Vector2 oldVector = grid.CellToWorld(hull[i]);
int nextIndex = (i + 1) % hull.Count;
Vector2 newVector = grid.CellToWorld(hull[nextIndex]);
Vector2 lhs;
Vector2 rhs;
if (oldVector.x < newVector.x)
{
lhs = oldVector;
rhs = newVector;
}
else
{
lhs = newVector;
rhs = oldVector;
}
if (
newVector.x < position.x == position.x <= oldVector.x
&& (position.y - (long)lhs.y) * (rhs.x - lhs.x)
< (rhs.y - (long)lhs.y) * (position.x - lhs.x)
)
{
isPositionInside = !isPositionInside;
}
}
return isPositionInside;
}
private static bool ShouldRepairConcaveCorners(float angleThreshold)
{
return angleThreshold >= ConcaveCornerRepairThresholdDegrees;
}
private static void MaybeRepairConcaveCorners(
List hull,
List originalPoints,
ConcaveHullStrategy strategy,
float angleThreshold
#if ENABLE_CONCAVE_HULL_STATS
,
ConcaveHullRepairStats stats = null
#endif
)
{
if (
hull == null
|| originalPoints == null
|| (
strategy != ConcaveHullStrategy.Knn
&& !ShouldRepairConcaveCorners(angleThreshold)
)
)
{
return;
}
#if ENABLE_CONCAVE_HULL_STATS
InsertMissingAxisCorners(hull, originalPoints, stats);
RemoveDuplicateVertices(hull, stats);
#else
InsertMissingAxisCorners(hull, originalPoints);
RemoveDuplicateVertices(hull);
#endif
}
private static void InsertMissingAxisCorners(
List hull,
List originalPoints
#if ENABLE_CONCAVE_HULL_STATS
,
ConcaveHullRepairStats stats
#endif
)
{
if (hull == null || originalPoints == null || hull.Count < 3)
{
return;
}
using PooledResource> pointSetResource =
Buffers.HashSet.Get(out HashSet pointSet);
pointSet.UnionWith(originalPoints);
using PooledResource> hullSetResource =
Buffers.HashSet.Get(out HashSet hullSet);
hullSet.UnionWith(hull);
using PooledResource> axisPathLease =
Buffers.List.Get(out List axisPathBuffer);
int uniquePointBudget = Math.Max(1, pointSet.Count);
int iterationGuardLimit = Math.Max(
AxisCornerRepairIterationMinimum,
uniquePointBudget * AxisCornerRepairIterationMultiplier
);
int iterationCount = 0;
bool inserted;
do
{
if (iterationCount++ >= iterationGuardLimit)
{
#if ENABLE_CONCAVE_HULL_STATS
throw new InvalidOperationException(
$"InsertMissingAxisCorners exceeded iteration guard limit ({iterationGuardLimit}). Stats => AxisCorners:{stats?.AxisCornerInsertions ?? 0}, AxisPaths:{stats?.AxisPathInsertions ?? 0}, Candidates:{stats?.CandidateConnections ?? 0}."
);
#else
throw new InvalidOperationException(
$"InsertMissingAxisCorners exceeded iteration guard limit ({iterationGuardLimit}). Enable ENABLE_CONCAVE_HULL_STATS for more diagnostics."
);
#endif
}
inserted = false;
int count = hull.Count;
for (int i = 0; i < count; ++i)
{
int nextIndex = (i + 1) % count;
FastVector3Int start = hull[i];
FastVector3Int end = hull[nextIndex];
if (start.x == end.x || start.y == end.y)
{
continue;
}
_ = pointSet.Add(start);
_ = pointSet.Add(end);
#if ENABLE_CONCAVE_HULL_STATS
axisPathBuffer.Clear();
bool foundPath = TryFindAxisPath(
start,
end,
pointSet,
allowStraightAxisFallback: false,
axisPathBuffer,
stats
);
#else
axisPathBuffer.Clear();
bool foundPath = TryFindAxisPath(
start,
end,
pointSet,
allowStraightAxisFallback: false,
axisPathBuffer
);
#endif
if (foundPath && axisPathBuffer.Count > 2)
{
int insertIndex = nextIndex;
bool insertedWaypoint = false;
for (int p = 1; p < axisPathBuffer.Count - 1; ++p)
{
FastVector3Int waypoint = axisPathBuffer[p];
if (!hullSet.Add(waypoint))
{
continue;
}
InsertCorner(hull, insertIndex, waypoint);
++insertIndex;
insertedWaypoint = true;
pointSet.Add(waypoint);
#if ENABLE_CONCAVE_HULL_STATS
stats?.IncrementAxisPathInsertions(1);
#endif
}
if (!insertedWaypoint)
{
continue;
}
inserted = true;
break;
}
FastVector3Int candidateA = new(start.x, end.y, start.z);
FastVector3Int candidateB = new(end.x, start.y, start.z);
if (pointSet.Contains(candidateA) && hullSet.Add(candidateA))
{
InsertCorner(hull, nextIndex, candidateA);
pointSet.Add(candidateA);
#if ENABLE_CONCAVE_HULL_STATS
stats?.IncrementAxisCornerInsertions();
#endif
inserted = true;
break;
}
if (pointSet.Contains(candidateB) && hullSet.Add(candidateB))
{
InsertCorner(hull, nextIndex, candidateB);
pointSet.Add(candidateB);
#if ENABLE_CONCAVE_HULL_STATS
stats?.IncrementAxisCornerInsertions();
#endif
inserted = true;
break;
}
}
} while (inserted);
#if ENABLE_CONCAVE_HULL_STATS
EnsureAxisCornersIncluded(hull, originalPoints, pointSet, stats);
PruneDiagonalOnlyVertices(hull, pointSet, stats);
#else
EnsureAxisCornersIncluded(hull, originalPoints, pointSet);
PruneDiagonalOnlyVertices(hull, pointSet);
#endif
}
private static bool TryFindAxisPath(
FastVector3Int start,
FastVector3Int end,
HashSet pointSet,
bool allowStraightAxisFallback,
List result
#if ENABLE_CONCAVE_HULL_STATS
,
ConcaveHullRepairStats stats
#endif
)
{
if (result == null)
{
throw new ArgumentNullException(nameof(result));
}
result.Clear();
using PooledResource> queueResource =
Buffers.Queue.Get(out Queue frontier);
using PooledResource> parentResource =
DictionaryBuffer.Dictionary.Get(
out Dictionary parents
);
frontier.Enqueue(start);
parents[start] = start;
while (frontier.Count > 0)
{
#if ENABLE_CONCAVE_HULL_STATS
stats?.MaybeRecordFrontierSize(frontier.Count);
#endif
FastVector3Int current = frontier.Dequeue();
if (current == end)
{
break;
}
foreach (FastVector3Int neighbor in EnumerateAxisNeighbors(current, pointSet))
{
#if ENABLE_CONCAVE_HULL_STATS
stats?.IncrementAxisNeighborVisits();
#endif
if (parents.ContainsKey(neighbor))
{
continue;
}
parents[neighbor] = current;
frontier.Enqueue(neighbor);
}
}
if (!parents.ContainsKey(end))
{
if (allowStraightAxisFallback && (start.x == end.x || start.y == end.y))
{
result.Add(start);
result.Add(end);
return true;
}
return false;
}
FastVector3Int cursor = end;
result.Add(cursor);
while (cursor != start)
{
cursor = parents[cursor];
result.Add(cursor);
}
result.Reverse();
return true;
}
private static IEnumerable EnumerateAxisNeighbors(
FastVector3Int origin,
HashSet pointSet
)
{
FastVector3Int right = new(origin.x + 1, origin.y, origin.z);
if (pointSet.Contains(right))
{
yield return right;
}
FastVector3Int left = new(origin.x - 1, origin.y, origin.z);
if (pointSet.Contains(left))
{
yield return left;
}
FastVector3Int up = new(origin.x, origin.y + 1, origin.z);
if (pointSet.Contains(up))
{
yield return up;
}
FastVector3Int down = new(origin.x, origin.y - 1, origin.z);
if (pointSet.Contains(down))
{
yield return down;
}
}
private static void InsertCorner(
List hull,
int insertIndex,
FastVector3Int candidate
)
{
if (insertIndex == hull.Count)
{
hull.Add(candidate);
}
else
{
hull.Insert(insertIndex, candidate);
}
}
private static void EnsureAxisCornersIncluded(
List hull,
List originalPoints,
HashSet pointSet
#if ENABLE_CONCAVE_HULL_STATS
,
ConcaveHullRepairStats stats
#endif
)
{
if (hull == null || originalPoints == null)
{
return;
}
using PooledResource> hullSetResource =
Buffers.HashSet.Get(out HashSet hullSet);
hullSet.UnionWith(hull);
bool added;
do
{
added = false;
foreach (FastVector3Int candidate in originalPoints)
{
if (hullSet.Contains(candidate))
{
continue;
}
if (!HasAxisCornerSupport(candidate, pointSet))
{
continue;
}
#if ENABLE_CONCAVE_HULL_STATS
if (!TryConnectCandidateToHull(candidate, hull, pointSet, hullSet, stats))
#else
if (!TryConnectCandidateToHull(candidate, hull, pointSet, hullSet))
#endif
{
continue;
}
#if ENABLE_CONCAVE_HULL_STATS
stats?.IncrementCandidateConnections();
#endif
added = true;
break;
}
} while (added);
}
private static bool HasAxisCornerSupport(
FastVector3Int candidate,
HashSet points
)
{
if (points == null || points.Count == 0)
{
return false;
}
bool hasX = false;
bool hasY = false;
foreach (FastVector3Int point in points)
{
if (point == candidate)
{
continue;
}
if (!hasX && point.x == candidate.x)
{
hasX = true;
}
if (!hasY && point.y == candidate.y)
{
hasY = true;
}
if (hasX && hasY)
{
return true;
}
}
return false;
}
private static bool TryConnectCandidateToHull(
FastVector3Int candidate,
List hull,
HashSet pointSet,
HashSet hullSet
#if ENABLE_CONCAVE_HULL_STATS
,
ConcaveHullRepairStats stats
#endif
)
{
if (hull == null || hull.Count < 2)
{
return false;
}
int count = hull.Count;
using PooledResource> axisPathLease =
Buffers.List.Get(out List axisPathBuffer);
for (int i = 0; i < count; ++i)
{
FastVector3Int anchor = hull[i];
if (anchor.x != candidate.x && anchor.y != candidate.y)
{
continue;
}
#if ENABLE_CONCAVE_HULL_STATS
axisPathBuffer.Clear();
bool foundPath = TryFindAxisPath(
anchor,
candidate,
pointSet,
allowStraightAxisFallback: true,
axisPathBuffer,
stats
);
#else
axisPathBuffer.Clear();
bool foundPath = TryFindAxisPath(
anchor,
candidate,
pointSet,
allowStraightAxisFallback: true,
axisPathBuffer
);
#endif
if (!foundPath || axisPathBuffer.Count <= 1)
{
continue;
}
#if ENABLE_CONCAVE_HULL_STATS
bool inserted = InsertPathAfterIndex(
hull,
i,
axisPathBuffer,
hullSet,
pointSet,
stats
);
#else
bool inserted = InsertPathAfterIndex(hull, i, axisPathBuffer, hullSet, pointSet);
#endif
if (inserted)
{
return true;
}
}
return false;
}
private static bool InsertPathAfterIndex(
List hull,
int anchorIndex,
List path,
HashSet hullSet,
HashSet pointSet
#if ENABLE_CONCAVE_HULL_STATS
,
ConcaveHullRepairStats stats
#endif
)
{
int insertIndex = anchorIndex + 1;
bool inserted = false;
for (int p = 1; p < path.Count; ++p)
{
FastVector3Int waypoint = path[p];
if (hullSet != null && !hullSet.Add(waypoint))
{
continue;
}
if (insertIndex >= hull.Count)
{
hull.Add(waypoint);
}
else
{
hull.Insert(insertIndex, waypoint);
}
pointSet?.Add(waypoint);
inserted = true;
++insertIndex;
#if ENABLE_CONCAVE_HULL_STATS
if (p < path.Count - 1)
{
stats?.IncrementAxisPathInsertions(1);
}
#endif
}
return inserted;
}
private static bool TryInsertAxisCorner(List hull, FastVector3Int candidate)
{
if (hull == null || hull.Count < 2)
{
return false;
}
int count = hull.Count;
for (int i = 0; i < count; ++i)
{
FastVector3Int prev = hull[i];
FastVector3Int next = hull[(i + 1) % count];
bool prevSharesX = prev.x == candidate.x && IsBetween(candidate.y, prev.y, next.y);
bool prevSharesY = prev.y == candidate.y && IsBetween(candidate.x, prev.x, next.x);
bool nextSharesX = next.x == candidate.x && IsBetween(candidate.y, prev.y, next.y);
bool nextSharesY = next.y == candidate.y && IsBetween(candidate.x, prev.x, next.x);
if ((prevSharesX && nextSharesY) || (prevSharesY && nextSharesX))
{
InsertCorner(hull, (i + 1) % count, candidate);
return true;
}
}
return false;
}
private static bool IsBetween(int value, int boundA, int boundB)
{
return boundA <= boundB
? boundA <= value && value <= boundB
: boundB <= value && value <= boundA;
}
private static bool HasSharedAxisNeighbor(
FastVector3Int candidate,
HashSet points
)
{
if (points == null || points.Count == 0)
{
return false;
}
foreach (FastVector3Int point in points)
{
if (point == candidate)
{
continue;
}
if (point.x == candidate.x || point.y == candidate.y)
{
return true;
}
}
return false;
}
private static void PruneDiagonalOnlyVertices(
List hull,
HashSet originalPoints
#if ENABLE_CONCAVE_HULL_STATS
,
ConcaveHullRepairStats stats
#endif
)
{
if (hull == null || originalPoints == null || originalPoints.Count == 0)
{
return;
}
int minX = int.MaxValue;
int maxX = int.MinValue;
int minY = int.MaxValue;
int maxY = int.MinValue;
foreach (FastVector3Int point in originalPoints)
{
if (point.x < minX)
{
minX = point.x;
}
if (point.x > maxX)
{
maxX = point.x;
}
if (point.y < minY)
{
minY = point.y;
}
if (point.y > maxY)
{
maxY = point.y;
}
}
for (int i = hull.Count - 1; i >= 0; --i)
{
FastVector3Int vertex = hull[i];
if (HasSharedAxisNeighbor(vertex, originalPoints))
{
continue;
}
bool insideX = minX < vertex.x && vertex.x < maxX;
bool insideY = minY < vertex.y && vertex.y < maxY;
if (insideX && insideY)
{
hull.RemoveAt(i);
#if ENABLE_CONCAVE_HULL_STATS
stats?.IncrementDiagonalPruned();
#endif
}
}
}
private static float CalculateAngle(Vector2 lhs, Vector2 rhs)
{
return Mathf.Atan2(rhs.y - lhs.y, rhs.x - lhs.x);
}
private static float AngleDifference(float lhsAngle, float rhsAngle)
{
float delta = lhsAngle - rhsAngle;
float twoPi = Mathf.PI * 2f;
delta %= twoPi;
if (delta < 0f)
{
delta += twoPi;
}
return delta;
}
public static bool Intersects(
Vector2 lhsFrom,
Vector2 lhsTo,
Vector2 rhsFrom,
Vector2 rhsTo
)
{
if (lhsFrom == rhsFrom || lhsFrom == rhsTo || lhsTo == rhsFrom || lhsTo == rhsTo)
{
return false;
}
OrientationType orientation1 = Orientation(lhsFrom, lhsTo, rhsFrom);
OrientationType orientation2 = Orientation(lhsFrom, lhsTo, rhsTo);
OrientationType orientation3 = Orientation(rhsFrom, rhsTo, lhsFrom);
OrientationType orientation4 = Orientation(rhsFrom, rhsTo, lhsTo);
if (orientation1 != orientation2 && orientation3 != orientation4)
{
return true;
}
if (orientation1 == OrientationType.Colinear && LiesOnSegment(lhsFrom, rhsFrom, lhsTo))
{
return true;
}
if (orientation2 == OrientationType.Colinear && LiesOnSegment(lhsFrom, rhsTo, lhsTo))
{
return true;
}
if (orientation3 == OrientationType.Colinear && LiesOnSegment(rhsFrom, lhsFrom, rhsTo))
{
return true;
}
if (orientation4 == OrientationType.Colinear && LiesOnSegment(rhsFrom, lhsTo, rhsTo))
{
return true;
}
return false;
}
public static bool LiesOnSegment(Vector2 p, Vector2 q, Vector2 r)
{
return q.x <= Math.Max(p.x, r.x)
&& Math.Min(p.x, r.x) <= q.x
&& q.y <= Math.Max(p.y, r.y)
&& Math.Min(p.y, r.y) <= q.y;
}
public enum OrientationType
{
Colinear = 0,
Clockwise = 1,
Counterclockwise = 2,
}
public static OrientationType Orientation(Vector2 p, Vector2 q, Vector2 r)
{
float value = (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y);
if (Mathf.Approximately(value, 0))
{
return OrientationType.Colinear;
}
return 0 < value ? OrientationType.Clockwise : OrientationType.Counterclockwise;
}
public static Vector2 Rotate(this Vector2 v, float degrees)
{
float sin = Mathf.Sin(degrees * Mathf.Deg2Rad);
float cos = Mathf.Cos(degrees * Mathf.Deg2Rad);
float tx = v.x;
float ty = v.y;
Vector2 rotatedVector;
rotatedVector.x = cos * tx - sin * ty;
rotatedVector.y = sin * tx + cos * ty;
return rotatedVector;
}
#if ENABLE_CONCAVE_HULL_STATS
public static ConcaveHullRepairStats ProfileConcaveHullRepair(
List hull,
List originalPoints,
ConcaveHullStrategy strategy,
float angleThreshold
)
{
if (hull == null)
{
throw new ArgumentNullException(nameof(hull));
}
#if ENABLE_CONCAVE_HULL_STATS
if (TryGetTrackedHullRepairStats(hull, out ConcaveHullRepairStats cachedStats))
{
return cachedStats;
}
#endif
using PooledResource> workingHullLease =
Buffers.List.Get(out List workingHull);
workingHull.AddRange(hull);
PooledResource> workingOriginalLease = default;
List workingOriginal = null;
if (originalPoints != null)
{
workingOriginalLease = Buffers.List.Get(out workingOriginal);
workingOriginal.AddRange(originalPoints);
}
try
{
ConcaveHullRepairStats stats = new(workingHull.Count, workingOriginal?.Count ?? 0);
MaybeRepairConcaveCorners(
workingHull,
workingOriginal,
strategy,
angleThreshold,
stats
);
stats.MarkFinalHullCount(workingHull.Count);
return stats;
}
finally
{
workingOriginalLease.Dispose();
}
}
#endif
private static void RemoveDuplicateVertices(
List hull
#if ENABLE_CONCAVE_HULL_STATS
,
ConcaveHullRepairStats stats
#endif
)
{
if (hull == null || hull.Count <= 1)
{
#if ENABLE_CONCAVE_HULL_STATS
stats?.MarkFinalHullCount(hull?.Count ?? 0);
#endif
return;
}
using PooledResource> seenResource =
Buffers.HashSet.Get(out HashSet seen);
int index = 0;
while (index < hull.Count)
{
FastVector3Int vertex = hull[index];
if (!seen.Add(vertex))
{
hull.RemoveAt(index);
#if ENABLE_CONCAVE_HULL_STATS
stats?.IncrementDuplicateRemovals();
#endif
continue;
}
++index;
}
#if ENABLE_CONCAVE_HULL_STATS
stats?.MarkFinalHullCount(hull.Count);
#endif
}
}
}