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