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