From b4fd024980f8296d0d750bcd0367c1e01449f38a Mon Sep 17 00:00:00 2001 From: Twinki Date: Sun, 31 Dec 2023 22:10:51 -0500 Subject: [PATCH] Fix winding number logic --- src/PolyZone.Tests/Shapes/PolygonTests.cs | 87 +++++++++++++++++----- src/PolyZone/Shapes/Interfaces/IPolygon.cs | 2 +- src/PolyZone/Shapes/Polygon.cs | 30 +++++--- 3 files changed, 86 insertions(+), 33 deletions(-) diff --git a/src/PolyZone.Tests/Shapes/PolygonTests.cs b/src/PolyZone.Tests/Shapes/PolygonTests.cs index b4fc797..92b283d 100644 --- a/src/PolyZone.Tests/Shapes/PolygonTests.cs +++ b/src/PolyZone.Tests/Shapes/PolygonTests.cs @@ -1,7 +1,13 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; using CitizenFX.Core; using FluentAssertions; +using FluentAssertions.Execution; +using FluentAssertions.Primitives; using PolyZone.Shapes; +using Xunit.Abstractions; // ReSharper disable ArrangeObjectCreationWhenTypeNotEvident @@ -9,36 +15,77 @@ namespace PolyZone.Tests.Shapes; public class PolygonTests { + private readonly ITestOutputHelper _testOutputHelper; + + public PolygonTests(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + } + [Fact] public void Polygon_A_IsInside_ShouldPassTest() { - Vector2 fPointOutside = new() { X = -5.120505f, Y = 1.105695f }; // F - Vector2 gPointInside = new() { X = -5.1205f, Y = 1.10569f }; // G - - Vector2 iPointOutside = new() { X = -6.1472f, Y = -0.6536f }; // I - Vector2 hPointInside = new() { X = -6.1476f, Y = -0.6536f }; // H + var insidePoints = new[] + { + new Vector2 { X = 9.11417f, Y = 4.19913f }, // H (9.11417,4.19913) + new Vector2 { X = 9.08464f, Y = 4.1949f }, // I (9.08464,4.1949) + new Vector2 { X = 9.04999f, Y = 4.19522f }, // J (9.04999,4.19522) + new Vector2 { X = 8.79974f, Y = 4.15845f }, // K (8.79974,4.15845) + new Vector2 { X = -1f, Y = 3f }, // L (-1.22244,2.8594) + new Vector2 { X = -3.44097f, Y = 0.18371f }, // M (-3.44097,0.18371) + new Vector2 { X = -3.26505f, Y = -4.55097f }, // N (-3.26505,-4.55097) + new Vector2 { X = -2f, Y = -8f }, // O (-3,-8) + new Vector2 { X = 2.26106f, Y = -9.30121f }, // P (2.26106,-9.30121) + }; - IReadOnlyList points = + List points = [ - new() { X = 7.00776f, Y = 1.24414f }, // A - new() { X = -5.46056f, Y = 1.10181f }, // B - new() { X = -6.99775f, Y = -2.82657f }, // C - new() { X = -0.30813f, Y = -2.6273f }, // D - new() { X = -7.39628f, Y = -0.57772f }, // E - new() { X = -5.1175f, Y = 1.10583f }, // F + new() { X = 9.12f, Y = 4.2f }, // A (9.12,4.2) + new() { X = -3.98f, Y = 3.32f }, // B (-3.98,3.32) + new() { X = -4.56966f, Y = -3.5142f }, // C (-4.56966,-3.5142) + new() { X = -3.56008f, Y = -9.12483f }, // D (-3.56008,-9.12483) + new() { X = 3.75525f, Y = -9.88615f }, // E (3.75525,-9.88615) + new() { X = -2.36844f, Y = -2.83563f }, // F (-2.36844,-2.83563) + new() { X = -0.5f, Y = 2.06f } // G (-0.5,2.06) ]; - + var polygon = new Polygon(points); - polygon.IsInside(fPointOutside).Should().BeFalse(); - polygon.IsInside(gPointInside).Should().BeTrue(); - - polygon.IsInside(iPointOutside).Should().BeFalse(); - polygon.IsInside(hPointInside).Should().BeTrue(); - + foreach (var point in insidePoints) + { + var b = polygon.IsInside(point); + + _testOutputHelper.WriteLine(b.ToString()); + + //point.Should().BeInside(polygon); + } var distance = polygon.DistanceTo(new() { X = -1f, Y = 3.6f }); distance.Should().BeGreaterThan(0); } } + +public static class Vector2Extensions +{ + public static Vector2Assertions Should(this Vector2 instance) + { + return new Vector2Assertions(instance); + } +} + +public class Vector2Assertions(Vector2 instance) : ReferenceTypeAssertions(instance) +{ + private readonly Vector2 _instance = instance; + protected override string Identifier => "directory"; + + public AndConstraint BeInside(Polygon polygon, string because = "", params object[] becauseArgs) + { + Execute.Assertion + .BecauseOf(because, becauseArgs) + .ForCondition(!polygon.IsInside(_instance)) + .FailWith($"Expected point to be inside polygon, point was { _instance.X }, { _instance.Y }"); + + return new AndConstraint(this); + } +} diff --git a/src/PolyZone/Shapes/Interfaces/IPolygon.cs b/src/PolyZone/Shapes/Interfaces/IPolygon.cs index f7b1f50..6d5606c 100644 --- a/src/PolyZone/Shapes/Interfaces/IPolygon.cs +++ b/src/PolyZone/Shapes/Interfaces/IPolygon.cs @@ -4,6 +4,6 @@ namespace PolyZone.Shapes.Interfaces; public interface IPolygon { - bool IsInside(in Vector2 point); + bool IsInside(Vector2 point); float DistanceTo(in Vector2 point); } diff --git a/src/PolyZone/Shapes/Polygon.cs b/src/PolyZone/Shapes/Polygon.cs index 7d3ff7d..44060b9 100644 --- a/src/PolyZone/Shapes/Polygon.cs +++ b/src/PolyZone/Shapes/Polygon.cs @@ -15,35 +15,41 @@ public class Polygon(IReadOnlyList points) : IPolygon { public readonly IReadOnlyList _points = points; - public bool IsInside(in Vector2 point) => IsInside(point, _points); + public bool IsInside(Vector2 point) => IsInside(point, _points); public float DistanceTo(in Vector2 point) => DistanceTo(point, _points); private static bool IsInside(in Vector2 point, IReadOnlyList polygon) { var windingNumber = 0; - - for (var i = 0; i < polygon.Count - 1; i++) + + // loop through all edges of the polygon (considering the last edge connecting the last and first vertices) + for (var i = 0; i < polygon.Count; i++) { + // edge from polygon[i] to polygon[(i + 1) % polygon.Count] + var nextIndex = (i + 1) % polygon.Count; + + // start polygon[i].Y <= point.Y if (polygon[i].Y <= point.Y) { - if (polygon[i + 1].Y > point.Y && IsLeft(polygon[i], polygon[i + 1], point) > 0) + // an upward crossing + if (polygon[nextIndex].Y > point.Y && IsLeft(polygon[i], polygon[nextIndex], point) > 0) { - windingNumber++; + // P left of edge + ++windingNumber; // have a valid up intersect } } - else + // start polygon[i].Y > point.Y (no test needed) + else if (polygon[nextIndex].Y <= point.Y && IsLeft(polygon[i], polygon[nextIndex], point) < 0) { - if (polygon[i + 1].Y <= point.Y && IsLeft(polygon[i], polygon[i + 1], point) < 0) - { - windingNumber--; - } + // a downward crossing, P right of edge + --windingNumber; // have a valid down intersect } } return windingNumber != 0; } - + private static float DistanceTo(in Vector2 point, in IReadOnlyList polygon) { var minDistance = float.MaxValue; @@ -82,7 +88,7 @@ private static Vector2 ClosestPointOnLineSegment(Vector2 p, Vector2 a, Vector2 b _ => new Vector2 { X = a.X + t * ab.X, Y = a.Y + t * ab.Y } }; } - + private static float IsLeft(Vector2 a, Vector2 b, Vector2 c) { return (b.X - a.X) * (c.Y - a.Y) - (c.X - a.X) * (b.Y - a.Y);