Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .idea/.idea.PolygonClipper/.idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions .idea/.idea.PolygonClipper/.idea/encodings.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/.idea.PolygonClipper/.idea/indexLayout.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/.idea.PolygonClipper/.idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

75 changes: 75 additions & 0 deletions src/PolygonClipper/FloatExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

using System;
using System.Runtime.CompilerServices;

namespace PolygonClipper;

/// <summary>
/// Provides extension methods for floating-point numbers.
/// </summary>
internal static class FloatExtensions
{
/// <summary>
/// Returns the next representable double value in the direction of y.
/// </summary>
/// <remarks><see href="https://docs.rs/float_next_after/latest/src/float_next_after/lib.rs.html"/></remarks>
/// <param name="x">The starting floating-point number.</param>
/// <param name="y">The target floating-point number.</param>
/// <returns>The next representable value of x towards y.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double NextAfter(this double x, double y)
{
// Special cases
if (double.IsNaN(x) || double.IsNaN(y))
{
return double.NaN;
}

if (x == y)
{
return y;
}

if (double.IsPositiveInfinity(x))
{
return double.PositiveInfinity;
}

if (double.IsNegativeInfinity(x))
{
return double.NegativeInfinity;
}

// Handle stepping from zero
if (x == 0D)
{
return Math.CopySign(double.Epsilon, y); // Smallest positive subnormal double
}

// Convert double to raw bits
long bits = BitConverter.DoubleToInt64Bits(x);

// Adjust bits to get the next representable value
if ((y > x) == (x > 0D)) // Moving in the same sign direction

Check warning on line 55 in src/PolygonClipper/FloatExtensions.cs

View workflow job for this annotation

GitHub Actions / Build (ubuntu-latest, net9.0, 9.0.x, true, -x64, false)

Check warning on line 55 in src/PolygonClipper/FloatExtensions.cs

View workflow job for this annotation

GitHub Actions / Build (ubuntu-latest, net8.0, 8.0.x, -x64, false)

{
bits++;
}
else
{
bits--;
}

// Convert bits back to double
double next = BitConverter.Int64BitsToDouble(bits);

// Ensure correct handling of signed zeros
if (next == 0D)
{
return Math.CopySign(next, x);
}

return next;
}
}
66 changes: 12 additions & 54 deletions src/PolygonClipper/PolygonClipper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -216,17 +216,15 @@ public Polygon Run()
sweepEvent = sweepEvent.OtherEvent;
int it = sweepEvent.PosSL;
prevEvent = statusLine.Prev(it);

statusLine.RemoveAt(it);

// Shift `next` to account for the removal
nextEvent = statusLine.Next(it - 1);
nextEvent = statusLine.Next(it);

// Check intersection between neighbors
if (prevEvent != null && nextEvent != null)
{
_ = PossibleIntersection(prevEvent, nextEvent, eventQueue);
}

statusLine.RemoveAt(it);
}
}

Expand All @@ -243,7 +241,7 @@ public Polygon Run()
/// <param name="operation">The boolean operation being performed.</param>
/// <param name="result">The resulting polygon if the operation is trivial.</param>
/// <returns>
/// <see langword="true"/> if the operation results in a trivial case due to zero contours;
/// <see langword="true"/> if the operation results in a trivial case due to zero contours;
/// otherwise, <see langword="false"/>.
/// </returns>
private static bool TryTrivialOperationForEmptyPolygons(
Expand Down Expand Up @@ -359,25 +357,13 @@ private static void ProcessSegment(
e1.ContourId = e2.ContourId = contourId;

// Determine which endpoint is the left endpoint
//if (s.Min == s.Source)
//{
// e2.Left = false;
//}
//else if (s.Min == s.Target)
//{
// e1.Left = false;
//}
//else
{
// As a fallback, use the comparator for floating-point precision issues
if (eventQueue.Comparer.Compare(e1, e2) < 0)
{
e2.Left = false;
}
else
{
e1.Left = false;
}
if (eventQueue.Comparer.Compare(e1, e2) < 0)
{
e2.Left = false;
}
else
{
e1.Left = false;
}

min = Vertex.Min(min, s.Min);
Expand Down Expand Up @@ -706,7 +692,7 @@ private static void DivideSegment(
{
// TODO: enabling this line makes a single test issue76.geojson fail.
// The files are different in the two reference repositories but both fail.
// p = new Vertex(NextAfter(p.X, true), p.Y);
p = new Vertex(p.X.NextAfter(double.PositiveInfinity), p.Y);
}

// Create the right event for the left segment (new right endpoint)
Expand Down Expand Up @@ -735,34 +721,6 @@ private static void DivideSegment(
eventQueue.Enqueue(r);
}

/// <summary>
/// Returns the next representable double-precision floating-point value in the given direction.
/// <see href="https://docs.rs/float_next_after/latest/float_next_after/trait.NextAfter.html"/>
/// </summary>
/// <param name="x">The starting double value.</param>
/// <param name="up">If true, moves towards positive infinity; otherwise, towards negative infinity.</param>
/// <returns>The next representable double in the given direction.</returns>
private static double NextAfter(double x, bool up)
{
if (double.IsNaN(x) || x == double.PositiveInfinity || x == double.NegativeInfinity)
{
return x; // NaN and infinity stay the same
}

// Convert double to its IEEE 754 bit representation
long bits = BitConverter.DoubleToInt64Bits(x);
if (up)
{
bits += (bits >= 0) ? 1 : -1; // Increase magnitude
}
else
{
bits += (bits > 0) ? -1 : 1; // Decrease magnitude
}

return BitConverter.Int64BitsToDouble(bits);
}

/// <summary>
/// Connects edges in the result polygon by processing the sweep events
/// and constructing contours for the final result.
Expand Down
32 changes: 31 additions & 1 deletion src/PolygonClipper/PolygonUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,39 @@ internal static class PolygonUtilities
/// <param name="p2">The third point.</param>
/// <returns>The <see cref="double"/> area.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double SignedArea(Vertex p0, Vertex p1, Vertex p2)
public static double SignedArea2(Vertex p0, Vertex p1, Vertex p2)
=> ((p0.X - p2.X) * (p1.Y - p2.Y)) - ((p1.X - p2.X) * (p0.Y - p2.Y));

/// <summary>
/// Computes the robust signed area (actually twice the area) of the triangle defined by the three vertices.
/// The sign indicates the orientation (positive for counterclockwise, negative for clockwise).
/// </summary>
/// <param name="p0">The first point.</param>
/// <param name="p1">The second point.</param>
/// <param name="p2">The third point.</param>
/// <returns>The <see cref="double"/> area.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double SignedArea(Vertex p0, Vertex p1, Vertex p2)
{
// Fast computation in double precision:
double det = ((p1.X - p0.X) * (p2.Y - p0.Y)) - ((p1.Y - p0.Y) * (p2.X - p0.X));

// If the determinant is clearly non-zero, return it.
const double tolerance = 1e-12;
if (Math.Abs(det) > tolerance)
{
return det;
}

// If the value is near zero, recompute using higher-precision arithmetic.
decimal ax = (decimal)p0.X, ay = (decimal)p0.Y;
decimal bx = (decimal)p1.X, by = (decimal)p1.Y;
decimal cx = (decimal)p2.X, cy = (decimal)p2.Y;
decimal detDec = ((bx - ax) * (cy - ay)) - ((by - ay) * (cx - ax));

return (double)detDec;
}

/// <summary>
/// Finds the intersection of two line segments, constraining results to their intersection bounding box.
/// </summary>
Expand Down
68 changes: 42 additions & 26 deletions src/PolygonClipper/SegmentComparer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,46 +52,62 @@ public int Compare(SweepEvent? x, SweepEvent? y)
return x.Point.Y < y.Point.Y ? -1 : 1;
}

// Has the line segment associated to "x" been inserted into the segment after the line
// segment associated to "y"?
// Use the sweep event order to determine the comparison
int compResult = this.eventComparer.Compare(x, y);
if (compResult == 1)
// If `x` and `y` lie on the same side of the reference segment,
// no intersection check is necessary.
if ((area1 > 0) == (area2 > 0))
{
return y.Above(x.Point) ? -1 : 1;
return area1 > 0 ? -1 : 1;
}

// The line segment associated with "y" has been inserted after "x"
return x.Below(y.Point) ? -1 : 1;
}
// If `x` lies on the reference segment, compare based on `y`.
if (area1 == 0)
{
return area2 > 0 ? -1 : 1;
}

// JavaScript comparer is different to C++
if (x.PolygonType == y.PolygonType) // Same polygon
{
Vertex p1 = x.Point;
Vertex p2 = y.Point;
// Form segments from the events.
Segment seg0 = new(x.Point, x.OtherEvent.Point);
Segment seg1 = new(y.Point, y.OtherEvent.Point);

if (p1 == p2) // Points are the same
{
// Compare the other endpoints of the segments
p1 = x.OtherEvent.Point;
p2 = y.OtherEvent.Point;
// Call the provided intersection method.
int interResult = PolygonUtilities.FindIntersection(seg0, seg1, out Vertex pi0, out Vertex _);

if (p1 == p2) // Other endpoints are also the same
if (interResult == 0)
{
// No unique intersection found: decide based on area1.
return (area1 > 0) ? -1 : 1;
}
else if (interResult == 1)
{
// Unique intersection found.
if (pi0 == y.Point)
{
return 0;
return (area2 > 0) ? -1 : 1;
}

return x.ContourId > y.ContourId ? 1 : -1;
return (area1 > 0) ? -1 : 1;
}

// If interResult is neither 0 nor 1, fall through to collinear logic.
}
else // Segments are collinear but belong to separate polygons

// Collinear branch – mimicking the Rust logic:
if (x.PolygonType == y.PolygonType)
{
return x.PolygonType == PolygonType.Subject ? -1 : 1;
// Both segments belong to the same polygon.
if (x.Point == y.Point)
{
// When left endpoints are identical, order by contour id.
return (x.ContourId < y.ContourId) ? -1 : 1;
}

// If left endpoints differ, the Rust version simply returns "less" (i.e. the one inserted earlier).
// Here we mimic that by always returning -1.
return -1;
}

// Fall back to the sweep event comparator for final comparison
return this.eventComparer.Compare(x, y) == 1 ? 1 : -1;
// Segments are collinear but belong to different polygons.
return (x.PolygonType == PolygonType.Subject) ? -1 : 1;
}

/// <inheritdoc/>
Expand Down
35 changes: 35 additions & 0 deletions tests/PolygonClipper.Tests/FloatExtensionsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

using Xunit;

namespace PolygonClipper.Tests;

public class FloatExtensionsTests
{
public static TheoryData<double, double, double> NextAfterTestData => new()
{
{ 0.0, double.PositiveInfinity, double.Epsilon },
{ -0.0, double.PositiveInfinity, double.Epsilon },
{ 0.0, double.NegativeInfinity, -double.Epsilon },
{ -0.0, double.NegativeInfinity, -double.Epsilon },
{ 1.0, double.PositiveInfinity, 1.0000000000000002 },
{ 1.0, double.NegativeInfinity, 0.9999999999999999 },
{ -1.0, double.PositiveInfinity, -0.9999999999999999 },
{ -1.0, double.NegativeInfinity, -1.0000000000000002 },
{ double.MaxValue, double.PositiveInfinity, double.PositiveInfinity },
{ double.MinValue, double.NegativeInfinity, double.NegativeInfinity },
{ double.PositiveInfinity, double.PositiveInfinity, double.PositiveInfinity },
{ double.NegativeInfinity, double.NegativeInfinity, double.NegativeInfinity },
{ double.NaN, double.PositiveInfinity, double.NaN },
{ double.NaN, double.NegativeInfinity, double.NaN },
};

[Theory]
[MemberData(nameof(NextAfterTestData))]
public void NextAfter_ShouldReturnCorrectResult(double input, double target, double expected)
{
double result = input.NextAfter(target);
Assert.Equal(expected, result);
}
}
Loading
Loading