// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Numerics;
using SixLabors.ImageSharp.Drawing;
namespace SixLabors.ImageSharp.Drawing.Helpers {
///
/// Provides low-level geometry helpers for polygon winding and segment intersection.
///
///
/// Polygon methods expect a closed ring where the first point is repeated as the last point.
/// Orientation signs are defined using world-space math conventions (Y points up):
/// positive signed area is counter-clockwise and negative signed area is clockwise.
/// In screen space (Y points down), the visual winding appears inverted.
///
internal static class PolygonUtilities
{
// Epsilon used for floating-point tolerance. Values within +-Eps are treated as zero.
// This reduces instability when segments are nearly parallel or endpoints are close.
private const float Eps = 1e-3f;
private const float MinusEps = -Eps;
private const float OnePlusEps = 1 + Eps;
///
/// Ensures that a closed polygon ring matches the expected orientation.
///
/// Polygon ring to normalize in place.
///
/// Expected orientation sign:
/// positive for counter-clockwise in world space, negative for clockwise in world space.
///
///
/// The ring is reversed only when its orientation sign disagrees with
/// . Degenerate rings (zero area) are not changed.
///
public static void EnsureOrientation(Span polygon, int expectedOrientation)
{
if (GetPolygonOrientation(polygon) * expectedOrientation < 0)
{
polygon.Reverse();
}
}
///
/// Returns the orientation sign of a closed polygon ring using the shoelace sum.
///
/// Closed polygon ring.
///
/// -1 for clockwise, 1 for counter-clockwise, or 0 for degenerate (zero-area) input.
///
private static int GetPolygonOrientation(ReadOnlySpan polygon)
{
float sum = 0f;
for (int i = 0; i < polygon.Length - 1; ++i)
{
PointF current = polygon[i];
PointF next = polygon[i + 1];
sum += (current.X * next.Y) - (next.X * current.Y);
}
// A tolerant compare could be used here, but edge scanning does not special-case
// zero-area or near-zero-area input, so we keep this strict sign check.
return Math.Sign(sum);
}
///
/// Tests whether two line segments intersect, excluding collinear overlap cases.
///
/// Start point of segment A.
/// End point of segment A.
/// Start point of segment B.
/// End point of segment B.
///
/// Receives the intersection point when an intersection is found.
/// If no intersection is detected, the value is not modified.
///
///
/// when the segments intersect within their extents
/// (including endpoints); otherwise .
///
///
/// This solves the two segment equations in parametric form and accepts values in [0, 1]
/// with an epsilon margin for floating-point tolerance.
/// Parallel and collinear pairs are rejected early (cross product ~= 0).
///
public static bool LineSegmentToLineSegmentIgnoreCollinear(
Vector2 a0,
Vector2 a1,
Vector2 b0,
Vector2 b1,
ref Vector2 intersectionPoint)
{
// Direction vectors of the segments.
float dax = a1.X - a0.X;
float day = a1.Y - a0.Y;
float dbx = b1.X - b0.X;
float dby = b1.Y - b0.Y;
// Cross product of the direction vectors. Near zero means parallel/collinear.
float crossD = (-dbx * day) + (dax * dby);
// Reject parallel and collinear lines. Collinear overlap is intentionally not handled.
if (crossD is > MinusEps and < Eps)
{
return false;
}
// Solve for parameters s and t where:
// a0 + t * (a1 - a0) = b0 + s * (b1 - b0)
float s = ((-day * (a0.X - b0.X)) + (dax * (a0.Y - b0.Y))) / crossD;
float t = ((dbx * (a0.Y - b0.Y)) - (dby * (a0.X - b0.X))) / crossD;
// If both parameters are within [0,1] (with tolerance), the segments intersect.
if (s > MinusEps && s < OnePlusEps && t > MinusEps && t < OnePlusEps)
{
intersectionPoint.X = a0.X + (t * dax);
intersectionPoint.Y = a0.Y + (t * day);
return true;
}
return false;
}
}
}