// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; namespace SixLabors.PolygonClipper { /// /// Provides utility methods for performing geometric calculations related to polygons, such as calculating signed areas /// and finding intersections of line segments. /// internal static class PolygonUtilities { /// /// Returns the signed area of a triangle. /// /// The first point. /// The second point. /// The third point. /// The area. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double SignedArea(in Vertex p0, in Vertex p1, in Vertex p2) => Vertex.Cross(p0 - p2, p1 - p2); /// /// Finds the intersection of two line segments, constraining results to their intersection bounding box. /// /// The first segment. /// The second segment. /// The first intersection point. /// The second intersection point (if overlap occurs). /// /// An indicating the number of intersection points: /// - Returns 0 if there is no intersection. /// - Returns 1 if the segments intersect at a single point. /// - Returns 2 if the segments overlap. /// public static int FindIntersection(in Segment seg0, in Segment seg1, out Vertex pi0, out Vertex pi1) { pi0 = default; pi1 = default; if (!TryGetIntersectionBoundingBox(seg0.Source, seg0.Target, seg1.Source, seg1.Target, out Box2? bbox)) { return 0; } int interResult = FindIntersectionImpl(seg0, seg1, out pi0, out pi1); if (interResult == 1) { pi0 = ConstrainToBoundingBox(pi0, bbox.Value); } else if (interResult == 2) { pi0 = ConstrainToBoundingBox(pi0, bbox.Value); pi1 = ConstrainToBoundingBox(pi1, bbox.Value); } return interResult; } /// /// Finds the intersection of two line segments. /// /// The first line segment. /// The second line segment. /// /// The first intersection point (if any). If the segments intersect at a single point, this will contain the intersection point. /// If the segments overlap, this will contain the start of the overlapping segment. /// /// /// The second intersection point (if any). If the segments overlap, this will contain the end of the overlapping segment. /// /// /// An indicating the number of intersection points: /// - Returns 0 if there is no intersection. /// - Returns 1 if the segments intersect at a single point. /// - Returns 2 if the segments overlap. /// private static int FindIntersectionImpl(in Segment seg0, in Segment seg1, out Vertex pi0, out Vertex pi1) { pi0 = default; pi1 = default; Vertex a1 = seg0.Source; Vertex a2 = seg1.Source; Vertex va = seg0.Target - a1; Vertex vb = seg1.Target - a2; Vertex e = a2 - a1; double kross = Vertex.Cross(va, vb); double sqrKross = kross * kross; double sqrLenA = Vertex.Dot(va, va); if (sqrKross > 0) { // Lines of the segments are not parallel double s = Vertex.Cross(e, vb) / kross; if (s is < 0 or > 1) { return 0; } double t = Vertex.Cross(e, va) / kross; if (t is < 0 or > 1) { return 0; } // If s or t is exactly 0 or 1, the intersection is on an endpoint if (s is 0 or 1) { // On an endpoint of line segment a pi0 = MidPoint(a1, s, va); return 1; } if (t is 0 or 1) { // On an endpoint of line segment b pi0 = MidPoint(a2, t, vb); return 1; } // Intersection of lines is a point on each segment pi0 = a1 + (s * va); return 1; } // Lines are parallel; check if they are collinear kross = Vertex.Cross(e, va); sqrKross = kross * kross; if (sqrKross > 0) { // Lines of the segments are different return 0; } // Segments are collinear, check for overlap double sa = Vertex.Dot(va, e) / sqrLenA; double sb = sa + (Vertex.Dot(va, vb) / sqrLenA); double smin = Math.Min(sa, sb); double smax = Math.Max(sa, sb); if (smin <= 1 && smax >= 0) { if (smin == 1) { pi0 = MidPoint(a1, smin, va); return 1; } if (smax == 0) { pi0 = MidPoint(a1, smax, va); return 1; } pi0 = MidPoint(a1, Math.Max(smin, 0), va); pi1 = MidPoint(a1, Math.Min(smax, 1), va); return 2; } return 0; } /// /// Computes the bounding box of the intersection area of two line segments. /// /// The first point of the first segment. /// The second point of the first segment. /// The first point of the second segment. /// The second point of the second segment. /// The intersection bounding box if one exists, otherwise null. /// /// if the segments intersect; otherwise, . /// private static bool TryGetIntersectionBoundingBox( in Vertex a1, in Vertex a2, in Vertex b1, in Vertex b2, [NotNullWhen(true)] out Box2? result) { Vertex minA = Vertex.Min(a1, a2); Vertex maxA = Vertex.Max(a1, a2); Vertex minB = Vertex.Min(b1, b2); Vertex maxB = Vertex.Max(b1, b2); Vertex interMin = Vertex.Max(minA, minB); Vertex interMax = Vertex.Min(maxA, maxB); if (interMin.X <= interMax.X && interMin.Y <= interMax.Y) { result = new Box2(interMin, interMax); return true; } result = null; return false; } /// /// Constrains a point to the given bounding box. /// /// The point to constrain. /// The bounding box. /// The constrained point. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Vertex ConstrainToBoundingBox(in Vertex p, in Box2 bbox) => Vertex.Min(Vertex.Max(p, bbox.Min), bbox.Max); /// /// Computes the point at a given fractional distance adouble a directed line segment. /// /// The starting vertex of the segment. /// The scalar factor representing the fractional distance adouble the segment. /// The direction vector of the segment. /// The interpolated vertex at the given fractional distance. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vertex MidPoint(in Vertex p, double s, in Vertex d) => p + (s * d); /// /// Returns the dot product of the vectors AB and BC. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double Dot(in Vertex a, in Vertex b, in Vertex c) => Vertex.Dot(b - a, c - b); /// /// Returns the cross product of the vectors AB and BC. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double Cross(in Vertex a, in Vertex b, in Vertex c) => Vertex.Cross(b - a, c - b); /// /// Returns the sign of the cross product of the vectors AB and BC. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int CrossSign(in Vertex a, in Vertex b, in Vertex c) { double crossValueInt = Cross(a, b, c); if (crossValueInt == 0) { return 0; } return crossValueInt > 0 ? 1 : -1; } /// /// Returns true when three vertices are collinear. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsCollinear(in Vertex a, in Vertex shared, in Vertex b) => CrossSign(a, shared, b) == 0; /// /// Computes the signed area of a contour. /// public static double Area(List path) { int count = path.Count; if (count < 3) { return 0D; } double area = 0; Vertex prev = path[count - 1]; for (int i = 0; i < count; i++) { Vertex current = path[i]; area += (prev.Y + current.Y) * (prev.X - current.X); prev = current; } return area * 0.5D; } /// /// Computes the squared perpendicular distance from a point to a line segment. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double PerpendicularDistanceSquared(in Vertex point, in Vertex line1, in Vertex line2) { Vertex toPoint = point - line1; Vertex direction = line2 - line1; double lengthSquared = Vertex.Dot(direction, direction); if (lengthSquared == 0D) { return 0D; } double cross = Vertex.Cross(toPoint, direction); return (cross * cross) / lengthSquared; } /// /// Finds the intersection of two line segments, including endpoints. /// public static bool TryGetLineIntersection( in Vertex a1, in Vertex a2, in Vertex b1, in Vertex b2, out Vertex intersection) { double dy1 = a2.Y - a1.Y; double dx1 = a2.X - a1.X; double dy2 = b2.Y - b1.Y; double dx2 = b2.X - b1.X; double det = (dy1 * dx2) - (dy2 * dx1); if (det == 0D) { intersection = default; return false; } double t = (((a1.X - b1.X) * dy2) - ((a1.Y - b1.Y) * dx2)) / det; if (t <= 0D) { intersection = a1; return true; } if (t >= 1D) { intersection = a2; return true; } intersection = new Vertex(a1.X + (t * dx1), a1.Y + (t * dy1)); return true; } /// /// Projects a point onto a segment and returns the closest point. /// public static Vertex ClosestPointOnSegment(in Vertex point, in Vertex seg1, in Vertex seg2) { if (seg1 == seg2) { return seg1; } double dx = seg2.X - seg1.X; double dy = seg2.Y - seg1.Y; double q = (((point.X - seg1.X) * dx) + ((point.Y - seg1.Y) * dy)) / ((dx * dx) + (dy * dy)); // Clamp to segment bounds so we always return the closest point on the finite segment. q = Math.Clamp(q, 0D, 1D); return new Vertex(seg1.X + (q * dx), seg1.Y + (q * dy)); } /// /// Returns true when two segments intersect. /// public static bool SegmentsIntersect(in Vertex a1, in Vertex a2, in Vertex b1, in Vertex b2, bool inclusive = false) { // Uses cross-product tests to solve a1 + d1 * t == b1 + d2 * u. // cp is the denominator (cross of directions); cp == 0 means parallel/collinear. Vertex d1 = a2 - a1; Vertex d2 = b2 - b1; double cp = Vertex.Cross(d2, d1); if (cp == 0) { return false; } if (inclusive) { // Inclusive mode allows intersections at endpoints. double t = Vertex.Cross(a1 - b1, d2); if (t == 0) { return true; } if (t > 0) { if (cp < 0 || t > cp) { // t outside [0, cp] once sign is normalized. return false; } } else if (cp > 0 || t < cp) { return false; } t = Vertex.Cross(a1 - b1, d1); if (t == 0) { return true; } if (t > 0) { // t within bounds for the second segment. return cp > 0 && t <= cp; } return cp < 0 && t >= cp; } // Exclusive mode requires the intersection to be strictly inside both segments. double t2 = Vertex.Cross(a1 - b1, d2); if (t2 == 0) { return false; } if (t2 > 0) { if (cp < 0 || t2 >= cp) { // Reject if t2 is outside the open interval. return false; } } else if (cp > 0 || t2 <= cp) { return false; } t2 = Vertex.Cross(a1 - b1, d1); if (t2 == 0) { return false; } if (t2 > 0) { // Both parameters are inside open intervals. return cp > 0 && t2 < cp; } return cp < 0 && t2 > cp; } /// /// Computes the bounding box of a contour. /// public static Box2 GetBounds(List path) { if (path.Count == 0) { return default; } double minX = double.MaxValue; double minY = double.MaxValue; double maxX = double.MinValue; double maxY = double.MinValue; for (int i = 0; i < path.Count; i++) { Vertex pt = path[i]; if (pt.X < minX) { minX = pt.X; } if (pt.X > maxX) { maxX = pt.X; } if (pt.Y < minY) { minY = pt.Y; } if (pt.Y > maxY) { maxY = pt.Y; } } if (minX == double.MaxValue) { return default; } return new Box2(new Vertex(minX, minY), new Vertex(maxX, maxY)); } /// /// Returns the midpoint of a contour's bounding box. /// private static Vertex GetBoundsMidPoint(List path) => GetBounds(path).MidPoint(); /// /// Determines whether a point is inside a contour. /// public static PointInPolygonResult PointInPolygon(in Vertex point, List polygon) { int len = polygon.Count; int start = 0; if (len < 3) { return PointInPolygonResult.Outside; } while (start < len && polygon[start].Y == point.Y) { start++; } if (start == len) { return PointInPolygonResult.Outside; } bool isAbove = polygon[start].Y < point.Y; bool startingAbove = isAbove; int val = 0; int i = start + 1; int end = len; while (true) { if (i == end) { if (end == 0 || start == 0) { break; } end = start; i = 0; } if (isAbove) { while (i < end && polygon[i].Y < point.Y) { i++; } } else { while (i < end && polygon[i].Y > point.Y) { i++; } } if (i == end) { continue; } Vertex curr = polygon[i]; Vertex prev = i > 0 ? polygon[i - 1] : polygon[len - 1]; if (curr.Y == point.Y) { if (curr.X == point.X || (curr.Y == prev.Y && ((point.X < prev.X) != (point.X < curr.X)))) { return PointInPolygonResult.On; } i++; if (i == start) { break; } continue; } if (point.X < curr.X && point.X < prev.X) { // no-op } else if (point.X > prev.X && point.X > curr.X) { val = 1 - val; } else { int cps2 = CrossSign(prev, curr, point); if (cps2 == 0) { return PointInPolygonResult.On; } if ((cps2 < 0) == isAbove) { val = 1 - val; } } isAbove = !isAbove; i++; } if (isAbove == startingAbove) { return val == 0 ? PointInPolygonResult.Outside : PointInPolygonResult.Inside; } if (i == len) { i = 0; } int cps = i == 0 ? CrossSign(polygon[len - 1], polygon[0], point) : CrossSign(polygon[i - 1], polygon[i], point); if (cps == 0) { return PointInPolygonResult.On; } if ((cps < 0) == isAbove) { val = 1 - val; } return val == 0 ? PointInPolygonResult.Outside : PointInPolygonResult.Inside; } /// /// Returns true if the outer contour contains the inner contour. /// private static bool PathContainsPath(List inner, List outer) { PointInPolygonResult pip = PointInPolygonResult.On; for (int i = 0; i < inner.Count; i++) { switch (PointInPolygon(inner[i], outer)) { case PointInPolygonResult.Outside: if (pip == PointInPolygonResult.Outside) { return false; } pip = PointInPolygonResult.Outside; break; case PointInPolygonResult.Inside: if (pip == PointInPolygonResult.Inside) { return true; } pip = PointInPolygonResult.Inside; break; default: break; } } Vertex midpoint = GetBoundsMidPoint(inner); return PointInPolygon(midpoint, outer) != PointInPolygonResult.Outside; } /// /// Returns true if the outer contour contains the inner contour. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool Path2ContainsPath1(List inner, List outer) => PathContainsPath(inner, outer); /// /// Finds the intersection of two line segments, constraining results to their intersection bounding box. /// /// The first point of the first segment. /// The second point of the first segment. /// The first point of the second segment. /// The second point of the second segment. /// The first intersection point. /// The second intersection point (if overlap occurs). /// /// An indicating the number of intersection points: /// - Returns 0 if there is no intersection. /// - Returns 1 if the segments intersect at a single point. /// - Returns 2 if the segments overlap. /// public static int FindIntersection(in Vertex a1, in Vertex a2, in Vertex b1, in Vertex b2, out Vertex pi0, out Vertex pi1) { pi0 = default; pi1 = default; if (!TryGetIntersectionBoundingBox(a1, a2, b1, b2, out Box2 bbox)) { return 0; } int interResult = FindIntersectionImpl(a1, a2, b1, b2, out pi0, out pi1); if (interResult == 1) { pi0 = ConstrainToBoundingBox(pi0, bbox); } else if (interResult == 2) { pi0 = ConstrainToBoundingBox(pi0, bbox); pi1 = ConstrainToBoundingBox(pi1, bbox); } return interResult; } /// /// Finds the intersection of two line segments. /// /// The first point of the first segment. /// The second point of the first segment. /// The first point of the second segment. /// The second point of the second segment. /// /// The first intersection point (if any). If the segments intersect at a single point, this will contain the intersection point. /// If the segments overlap, this will contain the start of the overlapping segment. /// /// /// The second intersection point (if any). If the segments overlap, this will contain the end of the overlapping segment. /// /// /// An indicating the number of intersection points: /// - Returns 0 if there is no intersection. /// - Returns 1 if the segments intersect at a single point. /// - Returns 2 if the segments overlap. /// private static int FindIntersectionImpl(in Vertex a1, in Vertex a2, in Vertex b1, in Vertex b2, out Vertex pi0, out Vertex pi1) { pi0 = default; pi1 = default; Vertex va = a2 - a1; Vertex vb = b2 - b1; Vertex e = b1 - a1; double kross = Vertex.Cross(va, vb); double sqrKross = kross * kross; double sqrLenA = Vertex.Dot(va, va); if (sqrKross > 0D) { // Lines of the segments are not parallel. double s = Vertex.Cross(e, vb) / kross; if (s is < 0D or > 1D) { return 0; } double t = Vertex.Cross(e, va) / kross; if (t is < 0D or > 1D) { return 0; } // If s or t is exactly 0 or 1, the intersection is on an endpoint. if (s is 0D or 1D) { // On an endpoint of segment a. pi0 = MidPoint(a1, s, va); return 1; } if (t is 0D or 1D) { // On an endpoint of segment b. pi0 = MidPoint(a2, t, vb); return 1; } // Intersection of lines is a point on each segment. pi0 = MidPoint(a1, s, va); return 1; } // Lines are parallel; check if they are collinear. kross = Vertex.Cross(e, va); sqrKross = kross * kross; if (sqrKross > 0D) { // Parallel but not collinear. return 0; } if (sqrLenA == 0D) { return 0; } // Segments are collinear, check 1D overlap in segment-a parameter space. double sa = Vertex.Dot(va, e) / sqrLenA; double sb = sa + (Vertex.Dot(va, vb) / sqrLenA); double smin = Math.Min(sa, sb); double smax = Math.Max(sa, sb); if (smin <= 1D && smax >= 0D) { if (smin == 1D) { pi0 = MidPoint(a1, smin, va); return 1; } if (smax == 0D) { pi0 = MidPoint(a1, smax, va); return 1; } pi0 = MidPoint(a1, Math.Max(smin, 0D), va); pi1 = MidPoint(a1, Math.Min(smax, 1D), va); return pi0 == pi1 ? 1 : 2; } return 0; } /// /// Computes the bounding box of the intersection area of two line segments. /// /// The first point of the first segment. /// The second point of the first segment. /// The first point of the second segment. /// The second point of the second segment. /// The intersection bounding box if one exists, otherwise null. /// /// if the segments intersect; otherwise, . /// private static bool TryGetIntersectionBoundingBox( in Vertex a1, in Vertex a2, in Vertex b1, in Vertex b2, out Box2 result) { Vertex minA = Vertex.Min(a1, a2); Vertex maxA = Vertex.Max(a1, a2); Vertex minB = Vertex.Min(b1, b2); Vertex maxB = Vertex.Max(b1, b2); Vertex interMin = Vertex.Max(minA, minB); Vertex interMax = Vertex.Min(maxA, maxB); if (interMin.X <= interMax.X && interMin.Y <= interMax.Y) { result = new Box2(interMin, interMax); return true; } result = default; return false; } } }