Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add edge intersection test to Contains3D #1047

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
156 changes: 88 additions & 68 deletions Elements/src/Geometry/Polygon.cs
Original file line number Diff line number Diff line change
Expand Up @@ -555,35 +555,35 @@ public bool Intersects(Plane plane, out List<Vector3> 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))
{
containment = Containment.Outside;
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);
}

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;

Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -755,21 +819,24 @@ public bool Covers(Vector3 vector)
/// <returns>Returns false if any part of the polygon is entirely outside of this polygon.</returns>
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);
}

/// <summary>
Expand All @@ -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 _);
}

/// <summary>
Expand Down
26 changes: 26 additions & 0 deletions Elements/test/PolygonTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
Loading