// 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 UnityEngine;
using Utils;
// GridConcaveHull.cs - Strategy dispatcher and Vector2 entry point
// See GeometryConcaveHull.cs for full concave hull architecture documentation
///
/// Strategy dispatcher for concave hull builders. Routes to KNN or EdgeSplit algorithm based on options.
///
public static partial class UnityExtensions
{
private const float Vector2AxisSnapTolerance = 1e-3f;
public static List BuildConcaveHull(
this IReadOnlyCollection points,
ConcaveHullOptions? options = null
)
{
ConcaveHullOptions appliedOptions = options ?? ConcaveHullOptions.Default;
List hull = BuildConcaveHullRaw(points, appliedOptions);
if (
appliedOptions.Strategy == ConcaveHullStrategy.Knn
|| ShouldRepairConcaveCorners(appliedOptions.AngleThreshold)
)
{
hull = RepairVector2Hull(
points,
hull,
appliedOptions.Strategy,
appliedOptions.AngleThreshold
);
}
return hull;
}
private static List BuildConcaveHullRaw(
IReadOnlyCollection points,
ConcaveHullOptions options
)
{
switch (options.Strategy)
{
case ConcaveHullStrategy.Knn:
return points.BuildConcaveHull2(Math.Max(3, options.NearestNeighbors));
case ConcaveHullStrategy.EdgeSplit:
return points.BuildConcaveHull3(
Math.Max(1, options.BucketSize),
options.AngleThreshold
);
default:
throw new InvalidEnumArgumentException(
nameof(options.Strategy),
(int)options.Strategy,
typeof(ConcaveHullStrategy)
);
}
}
private static List RepairVector2Hull(
IReadOnlyCollection originalPoints,
List hull,
ConcaveHullStrategy strategy,
float angleThreshold
)
{
if (hull == null)
{
return new List();
}
if (!AreVector2PointsAxisAligned(originalPoints) || !AreVector2PointsAxisAligned(hull))
{
// Axis-corner repair only applies to lattice-aligned datasets; skip to avoid
// rounding when working with arbitrary floating-point inputs.
return hull;
}
using PooledResource> fastHullResource =
Buffers.List.Get(out List fastHull);
using PooledResource> fastOriginalResource =
Buffers.List.Get(out List fastOriginal);
ConvertVector2CollectionToFastVector3(hull, fastHull);
ConvertVector2CollectionToFastVector3(originalPoints, fastOriginal);
#if ENABLE_CONCAVE_HULL_STATS
ConcaveHullRepairStats repairStats = new(fastHull.Count, fastOriginal.Count);
MaybeRepairConcaveCorners(
fastHull,
fastOriginal,
strategy,
angleThreshold,
repairStats
);
#else
MaybeRepairConcaveCorners(fastHull, fastOriginal, strategy, angleThreshold);
#endif
hull.Clear();
for (int i = 0; i < fastHull.Count; ++i)
{
FastVector3Int vertex = fastHull[i];
hull.Add(new Vector2(vertex.x, vertex.y));
}
return hull;
}
private static void ConvertVector2CollectionToFastVector3(
IEnumerable source,
List destination
)
{
destination.Clear();
foreach (Vector2 point in source)
{
destination.Add(
new FastVector3Int(Mathf.RoundToInt(point.x), Mathf.RoundToInt(point.y), 0)
);
}
}
private static bool AreVector2PointsAxisAligned(IEnumerable source)
{
foreach (Vector2 point in source)
{
if (
!IsApproximatelyInteger(point.x, Vector2AxisSnapTolerance)
|| !IsApproximatelyInteger(point.y, Vector2AxisSnapTolerance)
)
{
return false;
}
}
return true;
}
private static bool IsApproximatelyInteger(float value, float tolerance)
{
return Mathf.Abs(value - Mathf.Round(value)) <= tolerance;
}
}
}