diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e1cd132c..aa315074e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,7 +58,7 @@ - `Polyline.Frames` now works correctly with `startSetbackDistance` and `endSetbackDistance` parameters. - `Polygon.Frames` now works correctly with `startSetbackDistance` and `endSetbackDistance` parameters. - `BoundedCurve.ToPolyline` now works correctly for `EllipticalArc` class. - +- `Polygon.Contains3D` now performs edge intersection test. ### Changed - `GltfExtensions.UseReferencedContentExtension` is now true by default. diff --git a/Elements/src/Geometry/Polygon.cs b/Elements/src/Geometry/Polygon.cs index 1f92b8a98..7522b0757 100644 --- a/Elements/src/Geometry/Polygon.cs +++ b/Elements/src/Geometry/Polygon.cs @@ -555,8 +555,7 @@ public bool Intersects(Plane plane, out List results, bool distinct = t // Projects non-flat containment request into XY plane and returns the answer for this projection internal bool Contains3D(Vector3 location, out Containment containment) { - // Test that the test point is in the same plane - // as the polygon. + // Test that the test point is in the same plane as the polygon. var transformTo3D = Vertices.ToTransform(); if (!location.DistanceTo(transformTo3D.XY()).ApproximatelyEquals(0)) { @@ -564,14 +563,13 @@ internal bool Contains3D(Vector3 location, out Containment containment) return false; } - var is3D = Vertices.Any(vertex => vertex.Z != 0); + var is3D = Vertices.Any(vertex => !vertex.Z.ApproximatelyEquals(0)); if (!is3D) { return Contains(Edges(), location, out containment); } - var transformToGround = new Transform(transformTo3D); - transformToGround.Invert(); + var transformToGround = transformTo3D.Inverted(); var groundSegments = Edges(transformToGround); var groundLocation = transformToGround.OfPoint(location); return Contains(groundSegments, groundLocation, out containment); @@ -579,11 +577,13 @@ internal bool Contains3D(Vector3 location, out Containment containment) internal bool Contains3D(Polygon polygon) { - return polygon.Vertices.All(v => this.Contains(v, out _)); + return Contains3D(polygon, out _); } // Adapted from https://stackoverflow.com/questions/46144205/point-in-polygon-using-winding-number/46144206 - internal static bool Contains(IEnumerable<(Vector3 from, Vector3 to)> edges, Vector3 location, out Containment containment) + internal static bool Contains(IEnumerable<(Vector3 from, Vector3 to)> edges, + Vector3 location, + out Containment containment) { int windingNumber = 0; @@ -628,6 +628,70 @@ internal static bool Contains(IEnumerable<(Vector3 from, Vector3 to)> edges, Vec return result; } + internal static bool Contains(IEnumerable<(Vector3 from, Vector3 to)> edges1, + IEnumerable<(Vector3 from, Vector3 to)> edges2, + out Containment containment) + { + containment = Containment.Inside; + + // If an edge crosses without being fully overlapping, the polygon is only partially covered. + foreach (var edge1 in edges1) + { + foreach (var edge2 in edges2) + { + var direction1 = Line.Direction(edge1.from, edge1.to); + var direction2 = Line.Direction(edge2.from, edge2.to); + if (Line.Intersects2d(edge1.from, edge1.to, edge2.from, edge2.to) && + !direction1.IsParallelTo(direction2)) + { + containment = Containment.Outside; + return false; + } + } + } + + var allInside = true; + foreach (var vertex in edges2.Select(e => e.from)) + { + Contains(edges1, vertex, out var vertexContainment); + if (vertexContainment == Containment.Outside) + { + containment = Containment.Outside; + return false; + } + + if (vertexContainment > containment) + { + containment = vertexContainment; + } + + if (vertexContainment != Containment.Inside) + { + allInside = false; + } + } + + // If all vertices of the polygon are inside this polygon then there is full coverage since no edges cross. + if (allInside) + { + return true; + } + + // If some edges are partially shared (!allInside) then we must still make sure that none of this.Vertices are inside the given polygon. + // The above two checks aren't sufficient in cases like two almost identical polygons, but with an extra vertex on an edge of this polygon that's pulled into the other polygon. + foreach (var vertex in edges1.Select(e => e.from)) + { + Contains(edges2, vertex, out var otherContainment); + if (otherContainment == Containment.Inside) + { + containment = Containment.Outside; + return false; + } + } + + return true; + } + #region WindingNumberCalcs private static int Wind(Vector3 location, (Vector3 from, Vector3 to) edge, Position position) { @@ -755,21 +819,24 @@ public bool Covers(Vector3 vector) /// Returns false if any part of the polygon is entirely outside of this polygon. public bool Contains3D(Polygon polygon, out Containment containment) { - containment = Containment.Inside; - foreach (var v in polygon.Vertices) + // Test that the test polygon is in the same plane as this. + var transformTo3D = Vertices.ToTransform(); + if (polygon.Vertices.Any(v => !v.DistanceTo(transformTo3D.XY()).ApproximatelyEquals(0))) { - Contains3D(v, out var foundContainment); - if (foundContainment == Containment.Outside) - { - containment = foundContainment; - return false; - } - if (foundContainment > containment) - { - containment = foundContainment; - } + containment = Containment.Outside; + return false; } - return true; + + var is3D = Vertices.Any(vertex => !vertex.Z.ApproximatelyEquals(0)); + if (!is3D) + { + return Contains(Edges(), polygon.Edges(), out containment); + } + + var transformToGround = transformTo3D.Inverted(); + var edges0 = Edges(transformToGround); + var edges1 = polygon.Edges(transformToGround); + return Contains(edges0, edges1, out containment); } /// @@ -786,54 +853,7 @@ public bool Covers(Polygon polygon) return false; } - // If an edge crosses without being fully overlapping, the polygon is only partially covered. - foreach (var edge1 in Edges()) - { - foreach (var edge2 in polygon.Edges()) - { - var direction1 = Line.Direction(edge1.from, edge1.to); - var direction2 = Line.Direction(edge2.from, edge2.to); - if (Line.Intersects2d(edge1.from, edge1.to, edge2.from, edge2.to) && - !direction1.IsParallelTo(direction2) && - !direction1.IsParallelTo(direction2.Negate())) - { - return false; - } - } - } - - var allInside = true; - foreach (var vertex in polygon.Vertices) - { - Contains(Edges(), vertex, out Containment containment); - if (containment == Containment.Outside) - { - return false; - } - if (containment != Containment.Inside) - { - allInside = false; - } - } - - // If all vertices of the polygon are inside this polygon then there is full coverage since no edges cross. - if (allInside) - { - return true; - } - - // If some edges are partially shared (!allInside) then we must still make sure that none of this.Vertices are inside the given polygon. - // The above two checks aren't sufficient in cases like two almost identical polygons, but with an extra vertex on an edge of this polygon that's pulled into the other polygon. - foreach (var vertex in Vertices) - { - Contains(polygon.Edges(), vertex, out Containment containment); - if (containment == Containment.Inside) - { - return false; - } - } - - return true; + return Contains(Edges(), polygon.Edges(), out _); } /// diff --git a/Elements/test/PolygonTests.cs b/Elements/test/PolygonTests.cs index 959199c26..e6d1e9d79 100644 --- a/Elements/test/PolygonTests.cs +++ b/Elements/test/PolygonTests.cs @@ -240,6 +240,32 @@ public void Covers() Assert.True(tp1.Contains3D(tp3, out var c3)); } + [Fact] + public void Contains3d() + { + var p0 = new Polygon(new Vector3[] + { + (0, 0, 2), + (0, 0, 10), + (2, 0, 10), + (2, 0, 5), + (8, 0, 5), + (8, 0, 10), + (10, 0, 10), + (10, 0, 2) + }); + + var p1 = new Polygon(new Vector3[] + { + (1, 0, 6), + (1, 0, 8), + (9.5, 0, 7.5), + (9, 0, 6) + }); + + Assert.False(p0.Contains3D(p1)); + } + [Fact] public void Disjoint() {