// 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 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).
///
public static partial class UnityExtensions
{
private const float ConvexHullRelationEpsilon = 1e-5f;
private const double ConvexHullOrientationEpsilon = 1e-8d;
private static readonly Comparison Vector2LexicographicalComparison = (lhs, rhs) =>
{
int cmp = lhs.x.CompareTo(rhs.x);
return cmp != 0 ? cmp : lhs.y.CompareTo(rhs.y);
};
private static double ComputeAreaTolerance(Vector2 a, Vector2 b, Vector2 c)
{
double maxComponent = Math.Max(
Math.Max(
Math.Max(Math.Abs(a.x), Math.Abs(a.y)),
Math.Max(Math.Abs(b.x), Math.Abs(b.y))
),
Math.Max(Math.Abs(c.x), Math.Abs(c.y))
);
double scale = Math.Max(1d, maxComponent);
return ConvexHullRelationEpsilon * scale * scale;
}
private static bool AreApproximatelyColinear(Vector2 a, Vector2 b, Vector2 c)
{
double cross = Geometry.IsAPointLeftOfVectorOrOnTheLineDouble(a, b, c);
double tolerance = ComputeAreaTolerance(a, b, c);
return Math.Abs(cross) <= tolerance;
}
private static bool AreVector2PointsEquivalent(Vector2 lhs, Vector2 rhs)
{
return Mathf.Abs(lhs.x - rhs.x) <= ConvexHullRelationEpsilon
&& Mathf.Abs(lhs.y - rhs.y) <= ConvexHullRelationEpsilon;
}
private static void DeduplicateSortedVector2(List points)
{
if (points == null || points.Count <= 1)
{
return;
}
int writeIndex = 1;
for (int readIndex = 1; readIndex < points.Count; ++readIndex)
{
Vector2 previous = points[writeIndex - 1];
Vector2 current = points[readIndex];
if (!AreVector2PointsEquivalent(previous, current))
{
if (writeIndex != readIndex)
{
points[writeIndex] = current;
}
++writeIndex;
}
}
if (writeIndex < points.Count)
{
points.RemoveRange(writeIndex, points.Count - writeIndex);
}
}
private static void DeduplicateSortedVector3Int(List points)
{
if (points == null || points.Count <= 1)
{
return;
}
int writeIndex = 1;
for (int readIndex = 1; readIndex < points.Count; ++readIndex)
{
Vector3Int previous = points[writeIndex - 1];
Vector3Int current = points[readIndex];
if (previous != current)
{
if (writeIndex != readIndex)
{
points[writeIndex] = current;
}
++writeIndex;
}
}
if (writeIndex < points.Count)
{
points.RemoveRange(writeIndex, points.Count - writeIndex);
}
}
private static void DeduplicateSortedFastVector3Int(List points)
{
if (points == null || points.Count <= 1)
{
return;
}
int writeIndex = 1;
for (int readIndex = 1; readIndex < points.Count; ++readIndex)
{
FastVector3Int previous = points[writeIndex - 1];
FastVector3Int current = points[readIndex];
if (previous != current)
{
if (writeIndex != readIndex)
{
points[writeIndex] = current;
}
++writeIndex;
}
}
if (writeIndex < points.Count)
{
points.RemoveRange(writeIndex, points.Count - writeIndex);
}
}
public enum ConvexHullAlgorithm
{
[Obsolete("Do not use default value; specify an algorithm explicitly.")]
Unknown = 0,
MonotoneChain = 1,
Jarvis = 2,
}
private static int FindLowestGridPointIndex(List points, Grid grid)
{
if (points == null || points.Count == 0)
{
return -1;
}
if (grid == null)
{
throw new ArgumentNullException(nameof(grid));
}
int lowestIndex = -1;
float lowestY = float.MaxValue;
for (int i = 0; i < points.Count; ++i)
{
Vector2 candidateWorld = grid.CellToWorld(points[i]);
if (lowestIndex < 0 || candidateWorld.y < lowestY)
{
lowestIndex = i;
lowestY = candidateWorld.y;
}
}
return lowestIndex;
}
private static bool TryBuildGridConcaveHullAttempt(
List points,
List hull,
Grid grid,
bool[] availability,
List neighborIndices,
float[] neighborDistances,
Vector2[] worldPositions,
int nearestNeighbors,
int firstIndex,
FastVector3Int firstPoint,
int maxSteps
)
{
int totalPoints = points.Count;
int availableCount = ResetAvailabilityFlags(availability, totalPoints, firstIndex);
int step = 2;
float previousAngle = 0f;
Vector2 currentWorld = worldPositions[firstIndex];
bool includeFirstCandidate = false;
while (availableCount > 0)
{
if (!includeFirstCandidate && step == 5)
{
includeFirstCandidate = true;
}
FillNearestNeighborIndices(
availability,
neighborIndices,
neighborDistances,
worldPositions,
totalPoints,
nearestNeighbors,
currentWorld,
includeFirstCandidate,
firstIndex
);
if (neighborIndices.Count == 0)
{
break;
}
SortByRightHandTurnIndices(
neighborIndices,
worldPositions,
currentWorld,
previousAngle
);
bool intersects = true;
int neighborOffset = -1;
while (intersects && neighborOffset < neighborIndices.Count - 1)
{
++neighborOffset;
int candidateIndex = neighborIndices[neighborOffset];
FastVector3Int candidate = points[candidateIndex];
int lastPoint = candidate == firstPoint ? 1 : 0;
int j = 2;
intersects = false;
Vector2 lhsTo = worldPositions[candidateIndex];
while (!intersects && j < hull.Count - lastPoint)
{
Vector2 lhsFrom = grid.CellToWorld(hull[step - 2]);
Vector2 rhsFrom = grid.CellToWorld(hull[step - 2 - j]);
Vector2 rhsTo = grid.CellToWorld(hull[step - 1 - j]);
intersects = Intersects(lhsFrom, lhsTo, rhsFrom, rhsTo);
++j;
}
}
if (intersects)
{
return RemainingPointsInside(points, availability, hull, grid);
}
int nextIndex = neighborIndices[neighborOffset];
if (nextIndex == firstIndex)
{
break;
}
FastVector3Int nextPoint = points[nextIndex];
hull.Add(nextPoint);
if (availability[nextIndex])
{
availability[nextIndex] = false;
--availableCount;
}
currentWorld = worldPositions[nextIndex];
previousAngle = CalculateAngle(
grid.CellToWorld(hull[step - 1]),
grid.CellToWorld(hull[step - 2])
);
++step;
if (step > maxSteps)
{
break;
}
}
return RemainingPointsInside(points, availability, hull, grid);
}
private static void FillNearestNeighborIndices(
bool[] availability,
List neighborIndices,
float[] neighborDistances,
Vector2[] worldPositions,
int totalPoints,
int nearestNeighbors,
Vector2 currentWorld,
bool includeFirstCandidate,
int firstIndex
)
{
neighborIndices.Clear();
if (nearestNeighbors <= 0 || worldPositions == null || totalPoints <= 0)
{
return;
}
int storedCount = 0;
for (int i = 0; i < totalPoints; ++i)
{
if (!availability[i])
{
continue;
}
Vector2 candidateWorld = worldPositions[i];
float distance = (candidateWorld - currentWorld).sqrMagnitude;
InsertNeighborCandidate(
neighborIndices,
neighborDistances,
i,
distance,
ref storedCount,
nearestNeighbors
);
}
if (includeFirstCandidate)
{
Vector2 firstWorld = worldPositions[firstIndex];
float firstDistance = (firstWorld - currentWorld).sqrMagnitude;
InsertNeighborCandidate(
neighborIndices,
neighborDistances,
firstIndex,
firstDistance,
ref storedCount,
nearestNeighbors
);
}
}
private static void SortByRightHandTurnIndices(
List neighborIndices,
Vector2[] worldPositions,
Vector2 currentWorld,
float previousAngle
)
{
int count = neighborIndices.Count;
if (count <= 1)
{
return;
}
using PooledArray angleBufferResource = SystemArrayPool.Get(
count,
out float[] angles
);
for (int i = 0; i < count; ++i)
{
Vector2 candidatePoint = worldPositions[neighborIndices[i]];
float candidateAngle = CalculateAngle(currentWorld, candidatePoint);
angles[i] = -AngleDifference(previousAngle, candidateAngle);
}
SelectionSort(neighborIndices, angles, count);
}
private static bool RemainingPointsInside(
List points,
bool[] availability,
List hull,
Grid grid
)
{
for (int i = 0; i < points.Count; ++i)
{
if (!availability[i])
{
continue;
}
if (!IsPositionInside(hull, points[i], grid))
{
return false;
}
}
return true;
}
private static int FindLowestPointIndex(List points)
{
if (points == null || points.Count == 0)
{
return -1;
}
int lowestIndex = -1;
float lowestY = float.MaxValue;
Vector2 lowestPoint = default;
for (int i = 0; i < points.Count; ++i)
{
Vector2 candidate = points[i];
if (
lowestIndex < 0
|| candidate.y < lowestY
|| (Mathf.Approximately(candidate.y, lowestY) && candidate.x < lowestPoint.x)
)
{
lowestIndex = i;
lowestY = candidate.y;
lowestPoint = candidate;
}
}
return lowestIndex;
}
private static bool TryBuildConcaveHull2Attempt(
List points,
List hull,
bool[] availability,
List neighborIndices,
float[] neighborDistances,
int nearestNeighbors,
int firstIndex,
Vector2 firstPoint,
int maxSteps
)
{
int totalPoints = points.Count;
int availableCount = ResetAvailabilityFlags(availability, totalPoints, firstIndex);
int step = 2;
float previousAngle = 0f;
Vector2 currentPoint = firstPoint;
bool includeFirstCandidate = false;
while (availableCount > 0)
{
if (!includeFirstCandidate && step == 5)
{
includeFirstCandidate = true;
}
FillNearestNeighborIndices(
points,
availability,
neighborIndices,
neighborDistances,
nearestNeighbors,
currentPoint,
includeFirstCandidate,
firstIndex
);
if (neighborIndices.Count == 0)
{
break;
}
SortByRightHandTurnIndices(neighborIndices, points, currentPoint, previousAngle);
bool intersects = true;
int neighborOffset = -1;
while (intersects && neighborOffset < neighborIndices.Count - 1)
{
++neighborOffset;
Vector2 candidate = points[neighborIndices[neighborOffset]];
int lastPoint = candidate == firstPoint ? 1 : 0;
int j = 2;
intersects = false;
Vector2 lhsTo = candidate;
while (!intersects && j < hull.Count - lastPoint)
{
Vector2 lhsFrom = hull[step - 2];
Vector2 rhsFrom = hull[step - 2 - j];
Vector2 rhsTo = hull[step - 1 - j];
intersects = Intersects(lhsFrom, lhsTo, rhsFrom, rhsTo);
++j;
}
}
if (intersects)
{
return RemainingPointsInside(points, availability, hull);
}
int nextIndex = neighborIndices[neighborOffset];
if (nextIndex == firstIndex)
{
break;
}
Vector2 nextPoint = points[nextIndex];
hull.Add(nextPoint);
if (availability[nextIndex])
{
availability[nextIndex] = false;
--availableCount;
}
currentPoint = nextPoint;
previousAngle = CalculateAngle(hull[step - 1], hull[step - 2]);
++step;
if (step > maxSteps)
{
break;
}
}
return RemainingPointsInside(points, availability, hull);
}
private static void FillNearestNeighborIndices(
List points,
bool[] availability,
List neighborIndices,
float[] neighborDistances,
int nearestNeighbors,
Vector2 currentPoint,
bool includeFirstCandidate,
int firstIndex
)
{
neighborIndices.Clear();
if (nearestNeighbors <= 0 || points.Count == 0)
{
return;
}
int storedCount = 0;
int totalPoints = points.Count;
for (int i = 0; i < totalPoints; ++i)
{
if (!availability[i])
{
continue;
}
Vector2 candidate = points[i];
float distance = (candidate - currentPoint).sqrMagnitude;
InsertNeighborCandidate(
neighborIndices,
neighborDistances,
i,
distance,
ref storedCount,
nearestNeighbors
);
}
if (includeFirstCandidate)
{
Vector2 firstPoint = points[firstIndex];
float firstDistance = (firstPoint - currentPoint).sqrMagnitude;
InsertNeighborCandidate(
neighborIndices,
neighborDistances,
firstIndex,
firstDistance,
ref storedCount,
nearestNeighbors
);
}
}
private static void SortByRightHandTurnIndices(
List neighborIndices,
List points,
Vector2 currentPoint,
float previousAngle
)
{
int count = neighborIndices.Count;
if (count <= 1)
{
return;
}
using PooledArray angleBufferResource = SystemArrayPool.Get(
count,
out float[] angles
);
for (int i = 0; i < count; ++i)
{
Vector2 candidatePoint = points[neighborIndices[i]];
float candidateAngle = CalculateAngle(currentPoint, candidatePoint);
angles[i] = -AngleDifference(previousAngle, candidateAngle);
}
SelectionSort(neighborIndices, angles, count);
}
private static bool RemainingPointsInside(
List points,
bool[] availability,
List hull
)
{
for (int i = 0; i < points.Count; ++i)
{
if (!availability[i])
{
continue;
}
if (!IsPositionInside(hull, points[i]))
{
return false;
}
}
return true;
}
private static int ResetAvailabilityFlags(
bool[] availability,
int totalPoints,
int skipIndex
)
{
if (availability == null)
{
return 0;
}
int availableCount = 0;
for (int i = 0; i < totalPoints; ++i)
{
bool available = i != skipIndex;
availability[i] = available;
if (available)
{
++availableCount;
}
}
return availableCount;
}
internal static List BuildConvexHullJarvisFallback(
List points,
List hull,
bool includeColinearPoints,
List scratchIndices,
float[] scratchDistances,
bool[] membershipFlags
)
{
hull ??= new List();
hull.Clear();
int pointCount = points?.Count ?? 0;
if (pointCount == 0 || points == null)
{
return hull;
}
if (pointCount <= 2)
{
hull.AddRange(points);
return hull;
}
ResetBooleanFlags(membershipFlags, pointCount);
int startIndex = FindLowestPointIndex(points);
if (startIndex < 0)
{
hull.AddRange(points);
return hull;
}
int currentIndex = startIndex;
int guard = 0;
int guardMax = Math.Max(8, pointCount * 8);
do
{
Vector2 current = points[currentIndex];
hull.Add(current);
if (membershipFlags != null && membershipFlags.Length > currentIndex)
{
membershipFlags[currentIndex] = true;
}
if (!includeColinearPoints)
{
TrimTailColinear(hull);
}
int candidateIndex = -1;
for (int i = 0; i < pointCount; ++i)
{
if (i == currentIndex)
{
continue;
}
candidateIndex = i;
break;
}
if (candidateIndex < 0)
{
break;
}
for (int i = 0; i < pointCount; ++i)
{
if (i == currentIndex || i == candidateIndex)
{
continue;
}
Vector2 candidate = points[candidateIndex];
Vector2 point = points[i];
float relation = Geometry.IsAPointLeftOfVectorOrOnTheLine(
current,
candidate,
point
);
if (relation > ConvexHullRelationEpsilon)
{
candidateIndex = i;
continue;
}
if (Mathf.Abs(relation) <= ConvexHullRelationEpsilon)
{
float candidateDistance = (candidate - current).sqrMagnitude;
float pointDistance = (point - current).sqrMagnitude;
if (pointDistance > candidateDistance)
{
candidateIndex = i;
}
}
}
if (includeColinearPoints && scratchIndices != null)
{
scratchIndices.Clear();
for (int i = 0; i < pointCount; ++i)
{
if (i == currentIndex || i == candidateIndex)
{
continue;
}
float relation = Geometry.IsAPointLeftOfVectorOrOnTheLine(
current,
points[candidateIndex],
points[i]
);
if (Mathf.Abs(relation) <= ConvexHullRelationEpsilon)
{
scratchIndices.Add(i);
}
}
SortIndicesByDistance(points, current, scratchIndices, scratchDistances);
if (scratchIndices.Count > 0)
{
foreach (int index in scratchIndices)
{
if (membershipFlags != null && membershipFlags[index])
{
continue;
}
hull.Add(points[index]);
if (membershipFlags != null && membershipFlags.Length > index)
{
membershipFlags[index] = true;
}
}
}
}
currentIndex = candidateIndex;
if (++guard > guardMax)
{
break;
}
} while (currentIndex != startIndex);
if (!includeColinearPoints && hull.Count > 2)
{
PruneColinearOnHull(hull);
}
return hull;
}
internal static List BuildGridConvexHullJarvisFallback(
List points,
Vector2[] worldPositions,
List hull,
bool includeColinearPoints,
List scratchIndices,
float[] scratchDistances,
bool[] membershipFlags
)
{
hull ??= new List();
hull.Clear();
int pointCount = points?.Count ?? 0;
if (pointCount == 0 || points == null)
{
return hull;
}
if (pointCount <= 2)
{
hull.AddRange(points);
return hull;
}
ResetBooleanFlags(membershipFlags, pointCount);
int startIndex = FindLowestWorldPositionIndex(worldPositions, pointCount);
if (startIndex < 0)
{
hull.AddRange(points);
return hull;
}
int currentIndex = startIndex;
int guard = 0;
int guardMax = Math.Max(8, pointCount * 8);
do
{
hull.Add(points[currentIndex]);
if (membershipFlags != null && membershipFlags.Length > currentIndex)
{
membershipFlags[currentIndex] = true;
}
if (!includeColinearPoints)
{
TrimTailColinear(hull);
}
int candidateIndex = -1;
for (int i = 0; i < pointCount; ++i)
{
if (i == currentIndex)
{
continue;
}
candidateIndex = i;
break;
}
if (candidateIndex < 0)
{
break;
}
for (int i = 0; i < pointCount; ++i)
{
if (i == currentIndex || i == candidateIndex)
{
continue;
}
Vector2 candidate = worldPositions[candidateIndex];
Vector2 point = worldPositions[i];
float relation = Geometry.IsAPointLeftOfVectorOrOnTheLine(
worldPositions[currentIndex],
candidate,
point
);
if (relation > ConvexHullRelationEpsilon)
{
candidateIndex = i;
continue;
}
if (Mathf.Abs(relation) <= ConvexHullRelationEpsilon)
{
float candidateDistance = (
candidate - worldPositions[currentIndex]
).sqrMagnitude;
float pointDistance = (point - worldPositions[currentIndex]).sqrMagnitude;
if (pointDistance > candidateDistance)
{
candidateIndex = i;
}
}
}
if (includeColinearPoints && scratchIndices != null)
{
scratchIndices.Clear();
for (int i = 0; i < pointCount; ++i)
{
if (i == currentIndex || i == candidateIndex)
{
continue;
}
float relation = Geometry.IsAPointLeftOfVectorOrOnTheLine(
worldPositions[currentIndex],
worldPositions[candidateIndex],
worldPositions[i]
);
if (Mathf.Abs(relation) <= ConvexHullRelationEpsilon)
{
scratchIndices.Add(i);
}
}
SortIndicesByDistance(
worldPositions,
worldPositions[currentIndex],
scratchIndices,
scratchDistances
);
if (scratchIndices.Count > 0)
{
foreach (int index in scratchIndices)
{
if (membershipFlags != null && membershipFlags[index])
{
continue;
}
hull.Add(points[index]);
if (membershipFlags != null && membershipFlags.Length > index)
{
membershipFlags[index] = true;
}
}
}
}
currentIndex = candidateIndex;
if (++guard > guardMax)
{
break;
}
} while (currentIndex != startIndex);
if (!includeColinearPoints && hull.Count > 2)
{
PruneColinearOnHull(hull);
}
return hull;
}
private static int FindLowestWorldPositionIndex(Vector2[] worldPositions, int count)
{
if (worldPositions == null || count <= 0)
{
return -1;
}
int lowestIndex = -1;
float lowestY = float.MaxValue;
float lowestX = float.MaxValue;
for (int i = 0; i < count; ++i)
{
Vector2 candidate = worldPositions[i];
if (
lowestIndex < 0
|| candidate.y < lowestY
|| (Mathf.Approximately(candidate.y, lowestY) && candidate.x < lowestX)
)
{
lowestIndex = i;
lowestY = candidate.y;
lowestX = candidate.x;
}
}
return lowestIndex;
}
private static void ResetBooleanFlags(bool[] flags, int count)
{
if (flags == null || count <= 0)
{
return;
}
int length = Math.Min(flags.Length, count);
Array.Clear(flags, 0, length);
}
private static void SortIndicesByDistance(
List points,
Vector2 origin,
List indices,
float[] scratchDistances
)
{
if (indices == null || scratchDistances == null)
{
return;
}
int count = indices.Count;
if (count <= 1)
{
return;
}
for (int i = 0; i < count; ++i)
{
int pointIndex = indices[i];
scratchDistances[i] = (points[pointIndex] - origin).sqrMagnitude;
}
SelectionSort(indices, scratchDistances, count);
}
private static void SortIndicesByDistance(
Vector2[] worldPositions,
Vector2 origin,
List indices,
float[] scratchDistances
)
{
if (worldPositions == null || indices == null || scratchDistances == null)
{
return;
}
int count = indices.Count;
if (count <= 1)
{
return;
}
for (int i = 0; i < count; ++i)
{
int pointIndex = indices[i];
scratchDistances[i] = (worldPositions[pointIndex] - origin).sqrMagnitude;
}
SelectionSort(indices, scratchDistances, count);
}
private static void InsertNeighborCandidate(
List neighborIndices,
float[] neighborDistances,
int candidateIndex,
float candidateDistance,
ref int storedCount,
int maximumCount
)
{
if (maximumCount <= 0)
{
return;
}
if (neighborIndices == null || neighborDistances == null)
{
return;
}
if (neighborDistances.Length < maximumCount)
{
throw new ArgumentException(
"Neighbor distance buffer must be at least as large as maximumCount.",
nameof(neighborDistances)
);
}
if (storedCount < maximumCount)
{
neighborIndices.Add(candidateIndex);
neighborDistances[storedCount] = candidateDistance;
int insertPosition = storedCount;
while (
insertPosition > 0 && candidateDistance < neighborDistances[insertPosition - 1]
)
{
neighborDistances[insertPosition] = neighborDistances[insertPosition - 1];
neighborIndices[insertPosition] = neighborIndices[insertPosition - 1];
--insertPosition;
}
neighborDistances[insertPosition] = candidateDistance;
neighborIndices[insertPosition] = candidateIndex;
++storedCount;
return;
}
if (candidateDistance >= neighborDistances[storedCount - 1])
{
return;
}
int replacePosition = storedCount - 1;
while (
replacePosition > 0 && candidateDistance < neighborDistances[replacePosition - 1]
)
{
neighborDistances[replacePosition] = neighborDistances[replacePosition - 1];
neighborIndices[replacePosition] = neighborIndices[replacePosition - 1];
--replacePosition;
}
neighborDistances[replacePosition] = candidateDistance;
neighborIndices[replacePosition] = candidateIndex;
}
private static void PopulateVectorBuffers(
IEnumerable source,
List vectorPoints,
Dictionary mapping,
out int fallbackZ
)
{
if (vectorPoints == null)
{
throw new ArgumentNullException(nameof(vectorPoints));
}
if (mapping == null)
{
throw new ArgumentNullException(nameof(mapping));
}
vectorPoints.Clear();
mapping.Clear();
fallbackZ = 0;
bool fallbackAssigned = false;
if (source is ICollection collection)
{
if (vectorPoints.Capacity < collection.Count)
{
vectorPoints.Capacity = collection.Count;
}
}
foreach (FastVector3Int point in source)
{
Vector2 vectorPoint = new(point.x, point.y);
vectorPoints.Add(vectorPoint);
mapping.TryAdd(vectorPoint, point);
if (fallbackAssigned)
{
continue;
}
fallbackZ = point.z;
fallbackAssigned = true;
}
}
private static List ConvertVector2HullToFastVector3(
IList vectorHull,
Dictionary mapping,
int fallbackZ
)
{
if (vectorHull == null)
{
return new List();
}
List converted = new(vectorHull.Count);
for (int i = 0; i < vectorHull.Count; ++i)
{
Vector2 vertex = vectorHull[i];
if (!mapping.TryGetValue(vertex, out FastVector3Int point))
{
Vector2 rounded = new(Mathf.Round(vertex.x), Mathf.Round(vertex.y));
if (!mapping.TryGetValue(rounded, out point))
{
point = new FastVector3Int(
Mathf.RoundToInt(vertex.x),
Mathf.RoundToInt(vertex.y),
fallbackZ
);
}
}
converted.Add(point);
}
return converted;
}
}
}