// MIT License - Copyright (c) 2025 wallstop
// Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE
namespace WallstopStudios.UnityHelpers.Core.Helper
{
using System.Collections.Generic;
using UnityEngine;
using WallstopStudios.UnityHelpers.Utils;
///
/// Polyline simplification and distance helpers.
///
public static class LineHelper
{
private static float PerpendicularDistance(
Vector2 point,
Vector2 lineStart,
Vector2 lineEnd
)
{
float xDistance = lineEnd.x - lineStart.x;
float yDistance = lineEnd.y - lineStart.y;
if (Mathf.Approximately(xDistance, 0) && Mathf.Approximately(yDistance, 0))
{
return Vector2.Distance(point, lineStart);
}
float t =
((point.x - lineStart.x) * xDistance + (point.y - lineStart.y) * yDistance)
/ (xDistance * xDistance + yDistance * yDistance);
Vector2 closestPoint = t switch
{
< 0 => lineStart,
> 1 => lineEnd,
_ => new Vector2(lineStart.x + t * xDistance, lineStart.y + t * yDistance),
};
return Vector2.Distance(point, closestPoint);
}
// c# implementation of the Ramer-Douglas-Peucker-Algorithm by Craig Selbert slightly adapted for Unity Vector Types
//https://www.codeproject.com/Articles/18936/A-Csharp-Implementation-of-Douglas-Peucker-Line-Ap
///
/// Douglas–Peucker simplification that preserves extreme points with high precision (double tolerance).
///
/// Input polyline points.
/// Maximum allowable deviation.
/// Optional destination list (reused if provided).
/// Output simplified points (in buffer if provided).
///
///
/// // Keep tighter shape fidelity
/// var precise = LineHelper.SimplifyPrecise(rawPoints, tolerance: 0.01);
///
///
public static List SimplifyPrecise(
List points,
double tolerance,
List buffer = null
)
{
if (points == null || points.Count < 3)
{
return points;
}
int firstPoint = 0;
int lastPoint = points.Count - 1;
while (lastPoint > firstPoint && points[firstPoint] == points[lastPoint])
{
lastPoint--;
}
if (lastPoint <= firstPoint)
{
buffer ??= new List(1);
buffer.Clear();
buffer.Add(points[firstPoint]);
return buffer;
}
using PooledResource> keepersLease = Buffers.List.Get(
out List pointIndexesToKeep
);
pointIndexesToKeep.Add(firstPoint);
pointIndexesToKeep.Add(lastPoint);
DouglasPeuckerReductionRecursive(
points,
firstPoint,
lastPoint,
tolerance,
ref pointIndexesToKeep
);
buffer ??= new List(pointIndexesToKeep.Count);
buffer.Clear();
pointIndexesToKeep.Sort();
for (int i = 0; i < pointIndexesToKeep.Count; ++i)
{
buffer.Add(points[pointIndexesToKeep[i]]);
}
return buffer;
}
private static void DouglasPeuckerReductionRecursive(
List points,
int firstPoint,
int lastPoint,
double tolerance,
ref List pointIndexesToKeep
)
{
do
{
double maxDistance = 0;
int indexFarthest = 0;
for (int index = firstPoint; index < lastPoint; index++)
{
double distance = InternalPerpendicularDistance(
points[firstPoint],
points[lastPoint],
points[index]
);
if (distance > maxDistance)
{
maxDistance = distance;
indexFarthest = index;
}
}
if (maxDistance > tolerance && indexFarthest != 0)
{
//Add the largest point that exceeds the tolerance
pointIndexesToKeep.Add(indexFarthest);
DouglasPeuckerReductionRecursive(
points,
firstPoint,
indexFarthest,
tolerance,
ref pointIndexesToKeep
);
firstPoint = indexFarthest;
continue;
}
break;
} while (true);
return;
static double InternalPerpendicularDistance(
Vector2 point1,
Vector2 point2,
Vector2 point
)
{
double area = System.Math.Abs(
.5f
* (
point1.x * point2.y
+ point2.x * point.y
+ point.x * point1.y
- point2.x * point1.y
- point.x * point2.y
- point1.x * point.y
)
);
double bottom = System.Math.Sqrt(
System.Math.Pow(point1.x - point2.x, 2.0)
+ System.Math.Pow(point1.y - point2.y, 2.0)
);
double height = area / bottom * 2.0;
return height;
}
}
///
/// Fast Douglas–Peucker simplification using float epsilon.
///
/// Input polyline points.
/// Maximum allowable deviation.
/// Optional destination list (reused if provided).
/// Output simplified points (in buffer if provided).
///
///
/// // Faster, good for on-frame simplification
/// var simplified = LineHelper.Simplify(rawPoints, epsilon: 0.1f);
///
///
public static List Simplify(
List points,
float epsilon,
List buffer = null
)
{
int pointCount = points?.Count ?? 0;
buffer ??= new List(pointCount);
buffer.Clear();
if (pointCount > 0 && buffer.Capacity < pointCount)
{
buffer.Capacity = pointCount;
}
if (points == null)
{
return buffer;
}
if (pointCount < 3 || epsilon <= 0)
{
buffer.AddRange(points);
return buffer;
}
SimplifyRecursive(points, 0, pointCount - 1, epsilon, buffer);
buffer.Add(points[pointCount - 1]);
return buffer;
}
private static void SimplifyRecursive(
List points,
int startIndex,
int endIndex,
float epsilon,
List buffer
)
{
if (endIndex <= startIndex + 1)
{
buffer.Add(points[startIndex]);
return;
}
float maxDistance = 0;
int maxIndex = startIndex;
for (int i = startIndex + 1; i < endIndex; ++i)
{
float distance = PerpendicularDistance(
points[i],
points[startIndex],
points[endIndex]
);
if (distance > maxDistance)
{
maxIndex = i;
maxDistance = distance;
}
}
if (maxDistance > epsilon)
{
SimplifyRecursive(points, startIndex, maxIndex, epsilon, buffer);
SimplifyRecursive(points, maxIndex, endIndex, epsilon, buffer);
}
else
{
buffer.Add(points[startIndex]);
}
}
}
}