Skip to content

Commit

Permalink
fix(face3d): Fix bug in is_centered_adjacent
Browse files Browse the repository at this point in the history
This PR also exposes some of the previously hidden useful functions on the Polygon2D. It also adds a bounding box checking method to the Polyface3D class.
  • Loading branch information
chriswmackey authored and Chris Mackey committed Oct 31, 2019
1 parent bb5aaa1 commit 9ee809e
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 74 deletions.
153 changes: 86 additions & 67 deletions ladybug_geometry/geometry2d/polygon.py
Original file line number Diff line number Diff line change
Expand Up @@ -669,12 +669,95 @@ def intersect_polygon_segments(polygon_list, tolerance):
"""
for i in range(len(polygon_list) - 1):
# No need for j to start at 0 since two polygons are passed
# and they are compared against one other within _intersect_segments.
# and they are compared against one other within intersect_segments.
for j in range(i + 1, len(polygon_list)):
polygon_list[i], polygon_list[j] = \
Polygon2D._intersect_segments(polygon_list[i], polygon_list[j],
tolerance)
Polygon2D.intersect_segments(polygon_list[i], polygon_list[j],
tolerance)
return polygon_list

@staticmethod
def intersect_segments(polygon1, polygon2, tolerance):
"""Intersect the line segments of two Polygon2Ds to ensure matching segments.
Specifically, this method checks two adjacent polygons to see if one contains
a vertex along an edge segment of the other within the given tolerance. If so,
it creates a co-located vertex at that point, partitioning the edge segment
into two edge segments. Point ordering is preserved.
Args:
polygon1: First polygon to check.
polygon2: Second polygon to check.
tolerance: Distance within which two points are considered to be co-located.
Returns:
Two polygon objects with extra vertices inserted if necessary.
"""
polygon1_updates = []
polygon2_updates = []

# Bounding rectangle check
if not Polygon2D.overlapping_bounding_rect(polygon1, polygon2, tolerance):
return polygon1, polygon2 # no overlap

# Test if each point of polygon2 is within the tolerance distance of any segment
# of polygon1. If so, add the closest point on the segment to the polygon1
# update list. And vice versa (testing polygon2 against polygon1).
for i1, seg1 in enumerate(polygon1.segments):
for i2, seg2 in enumerate(polygon2.segments):
# Test polygon1 against polygon2
x = closest_point2d_on_line2d(seg2.p1, seg1)
if all(p.distance_to_point(x) > tolerance for p in polygon1.vertices) \
and x.distance_to_point(seg2.p1) <= tolerance:
polygon1_updates.append([i1, x])
# Test polygon2 against polygon1
y = closest_point2d_on_line2d(seg1.p1, seg2)
if all(p.distance_to_point(y) > tolerance for p in polygon2.vertices) \
and y.distance_to_point(seg1.p1) <= tolerance:
polygon2_updates.append([i2, y])

# Apply any updates to polygon1
poly_points = list(polygon1.vertices)
for update in polygon1_updates[::-1]: # Traverse backwards to preserve order
poly_points.insert(update[0] + 1, update[1])
polygon1 = Polygon2D(poly_points)

# Apply any updates to polygon2
poly_points = list(polygon2.vertices)
for update in polygon2_updates[::-1]: # Traverse backwards to preserve order
poly_points.insert(update[0] + 1, update[1])
polygon2 = Polygon2D(poly_points)

return polygon1, polygon2

@staticmethod
def overlapping_bounding_rect(polygon1, polygon2, tolerance):
"""Check if the bounding rectangles of two polygons overlap within a tolerance.
This is particularly useful as a check before performing computationally intense
processes between two polygons like intersection or checking for adjacency.
Checking the overlap of the bounding boxes is extremely quick given this
method's use of the Separating Axis Theorem.
Args:
polygon1: The first polygon to check.
polygon2: The second polygon to check.
tolerance: Distance within which two points are considered to be co-located.
"""
# Bounding rectangle check using the Separating Axis Theorem
polygon1_width = polygon1.max.x - polygon1.min.x
polygon2_width = polygon2.max.x - polygon2.min.x
dist_btwn_x = abs(polygon1.min.x - polygon2.min.x)
x_gap_btwn_rect = dist_btwn_x - (0.5 * polygon1_width) - (0.5 * polygon2_width)

polygon1_height = polygon1.max.y - polygon1.min.y
polygon2_height = polygon2.max.y - polygon2.min.y
dist_btwn_y = abs(polygon1.min.y - polygon2.min.y)
y_gap_btwn_rect = dist_btwn_y - (0.5 * polygon1_height) - (0.5 * polygon2_height)

if x_gap_btwn_rect > tolerance or y_gap_btwn_rect > tolerance:
return False # no overlap
return True # overlap exists

def _transfer_properties(self, new_polygon):
"""Transfer properties from this polygon to a new polygon.
Expand Down Expand Up @@ -747,70 +830,6 @@ def _are_clockwise(vertices):
_a += vertices[i - 1].x * pt.y - vertices[i - 1].y * pt.x
return _a < 0

@staticmethod
def _intersect_segments(polygon1, polygon2, tolerance):
"""Intersect the line segments of two Polygon2Ds to ensure matching segments.
Specifically, this method checks two adjacent polygons to see if one contains
a vertex along an edge segment of the other within the given tolerance. If so,
it creates a co-located vertex at that point, partitioning the edge segment
into two edge segments. Point ordering is preserved.
Args:
polygon1: First polygon to check.
polygon2: Second polygon to check.
tolerance: Distance within which two points are considered to be co-located.
Returns:
Two polygon objects with extra vertices inserted if necessary.
"""
polygon1_updates = []
polygon2_updates = []

# Bounding rectangle check using the Separating Axis Theorem
polygon1_width = polygon1.max.x - polygon1.min.x
polygon2_width = polygon2.max.x - polygon2.min.x
dist_btwn_x = abs(polygon1.min.x - polygon2.min.x)
x_gap_btwn_rect = dist_btwn_x - (0.5 * polygon1_width) - (0.5 * polygon2_width)

polygon2_height = polygon2.max.y - polygon2.min.y
polygon1_height = polygon1.max.y - polygon1.min.y
dist_btwn_y = abs(polygon1.min.y - polygon2.min.y)
y_gap_btwn_rect = dist_btwn_y - (0.5 * polygon1_height) - (0.5 * polygon2_height)

if x_gap_btwn_rect > tolerance or y_gap_btwn_rect > tolerance: # no overlap
return polygon1, polygon2

# Test if each point of polygon2 is within the tolerance distance of any segment
# of polygon1. If so, add the closest point on the segment to the polygon1
# update list. And vice versa (testing polygon2 against polygon1).
for i1, seg1 in enumerate(polygon1.segments):
for i2, seg2 in enumerate(polygon2.segments):
# Test polygon1 against polygon2
x = closest_point2d_on_line2d(seg2.p1, seg1)
if all(p.distance_to_point(x) > tolerance for p in polygon1.vertices) \
and x.distance_to_point(seg2.p1) <= tolerance:
polygon1_updates.append([i1, x])
# Test polygon2 against polygon1
y = closest_point2d_on_line2d(seg1.p1, seg2)
if all(p.distance_to_point(y) > tolerance for p in polygon2.vertices) \
and y.distance_to_point(seg1.p1) <= tolerance:
polygon2_updates.append([i2, y])

# Apply any updates to polygon1
poly_points = list(polygon1.vertices)
for update in polygon1_updates[::-1]: # Traverse backwards to preserve order
poly_points.insert(update[0] + 1, update[1])
polygon1 = Polygon2D(poly_points)

# Apply any updates to polygon2
poly_points = list(polygon2.vertices)
for update in polygon2_updates[::-1]: # Traverse backwards to preserve order
poly_points.insert(update[0] + 1, update[1])
polygon2 = Polygon2D(poly_points)

return polygon1, polygon2

def __copy__(self):
_new_poly = Polygon2D(self.vertices)
return _new_poly
Expand Down
6 changes: 3 additions & 3 deletions ladybug_geometry/geometry3d/face.py
Original file line number Diff line number Diff line change
Expand Up @@ -609,13 +609,13 @@ def is_centered_adjacent(self, face, tolerance):
return False
if not abs(self.area - face.area) <= tolerance: # area check
return False
# construct a ray using this face's normal and a point on this face
# construct a ray using this face's normal and a point just behind this face
v1 = self.boundary[-1] - self.boundary[0]
v2 = self.boundary[1] - self.boundary[0]
move_vec = Vector3D(
move_vec = Vector3D( # vector moving from the edge towards the center of Face
(v1.x + v2.x / 2), (v1.y + v2.y / 2), (v1.z + v2.z / 2)).normalize()
move_vec = move_vec * (tolerance + 0.00001)
point_on_face = self.boundary[0] + move_vec
point_on_face = self.boundary[0] + move_vec - (self.normal * tolerance)
test_ray = Ray3D(point_on_face, self.normal)
# shoot ray from this face to the other to verify adjacency
if face.intersect_line_ray(test_ray):
Expand Down
35 changes: 35 additions & 0 deletions ladybug_geometry/geometry3d/polyface.py
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,41 @@ def intersect_plane(self, plane):
_inters.extend(_int)
return _inters

@staticmethod
def overlapping_bounding_boxes(polyface1, polyface2, tolerance):
"""Check if the bounding boxes of two polyfaces overlap within a tolerance.
This is particularly useful as a check before performing computationally
intenseprocesses between two polyfaces like intersection or checking for
adjacency. Checking the overlap of the bounding boxes is extremely quick
given this method's use of the Separating Axis Theorem.
Args:
polyface1: The first polyface to check.
polyface2: The second polyface to check.
tolerance: Distance within which two points are considered to be co-located.
"""
# Bounding box check using the Separating Axis Theorem
polyf1_width = polyface1.max.x - polyface1.min.x
polyf2_width = polyface2.max.x - polyface2.min.x
dist_btwn_x = abs(polyface1.min.x - polyface2.min.x)
x_gap_btwn_box = dist_btwn_x - (0.5 * polyf1_width) - (0.5 * polyf2_width)

polyf1_depth = polyface1.max.y - polyface1.min.y
polyf2_depth = polyface2.max.y - polyface2.min.y
dist_btwn_y = abs(polyface1.min.y - polyface2.min.y)
y_gap_btwn_box = dist_btwn_y - (0.5 * polyf1_depth) - (0.5 * polyf2_depth)

polyf1_height = polyface1.max.z - polyface1.min.z
polyf2_height = polyface2.max.z - polyface2.min.z
dist_btwn_z = abs(polyface1.min.z - polyface2.min.z)
z_gap_btwn_box = dist_btwn_z - (0.5 * polyf1_height) - (0.5 * polyf2_height)

if x_gap_btwn_box > tolerance or y_gap_btwn_box > tolerance or \
z_gap_btwn_box > tolerance:
return False # no overlap
return True # overlap exists

@staticmethod
def get_outward_faces(faces, tolerance=0):
"""Get faces that are all pointing outward from a list of faces together forming a solid.
Expand Down
14 changes: 14 additions & 0 deletions tests/polyface3d_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -706,3 +706,17 @@ def test_is_point_inside():
assert polyface.is_point_inside(Point3D(4, 1, 1)) is False
assert polyface.is_point_inside(Point3D(-1, 1, 1)) is False
assert polyface.is_point_inside(Point3D(4, 4, 4)) is False


def test_overlapping_bounding_boxes():
"""Test the Polyface3D overlapping_bounding_boxes method."""
polyface_1 = Polyface3D.from_box(1, 1, 1, Plane(o=Point3D(1, 1, 2)))
polyface_2 = Polyface3D.from_box(1, 1, 1, Plane(o=Point3D(1, 1, 1)))
polyface_3 = Polyface3D.from_box(1, 1, 1, Plane(o=Point3D(2, 1, 2)))
polyface_4 = Polyface3D.from_box(1, 1, 1, Plane(o=Point3D(1, 2, 2)))
polyface_5 = Polyface3D.from_box(1, 1, 1, Plane(o=Point3D(0, 0, 0)))

assert Polyface3D.overlapping_bounding_boxes(polyface_1, polyface_2, 0.01)
assert Polyface3D.overlapping_bounding_boxes(polyface_1, polyface_3, 0.01)
assert Polyface3D.overlapping_bounding_boxes(polyface_1, polyface_4, 0.01)
assert not Polyface3D.overlapping_bounding_boxes(polyface_1, polyface_5, 0.01)
8 changes: 4 additions & 4 deletions tests/polygon2d_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,7 @@ def test_intersect_segments():
pts1 = (Point2D(0, 2), Point2D(3, 2), Point2D(3, 4), Point2D(0, 4))
polygon1 = Polygon2D(pts1)

polygon0, polygon1 = Polygon2D._intersect_segments(polygon0, polygon1, tolerance)
polygon0, polygon1 = Polygon2D.intersect_segments(polygon0, polygon1, tolerance)

# Extra vertex added to polygon0, as expected
assert len(polygon0.segments) == 5
Expand All @@ -530,14 +530,14 @@ def test_intersect_segments_zero_tolerance():
pts1 = (Point2D(0, 2), Point2D(3, 2), Point2D(3, 4), Point2D(0, 4))
polygon1 = Polygon2D(pts1)

polygon2, polygon3 = Polygon2D._intersect_segments(polygon0, polygon1, 0)
polygon2, polygon3 = Polygon2D.intersect_segments(polygon0, polygon1, 0)

assert len(polygon2.segments) == 4 # No new points
assert all([polygon0.vertices[i] == polygon2.vertices[i] for i in range(len(polygon0.vertices))])
assert len(polygon3.segments) == 4 # No new points
assert all([polygon1.vertices[i] == polygon3.vertices[i] for i in range(len(polygon1.vertices))])

polygon2, polygon3 = Polygon2D._intersect_segments(polygon0, polygon1, 0.02)
polygon2, polygon3 = Polygon2D.intersect_segments(polygon0, polygon1, 0.02)

assert len(polygon2.segments) == 5 # Intersection within tolerance
assert len(polygon3.segments) == 5 # Intersection within tolerance
Expand All @@ -550,7 +550,7 @@ def test_intersect_segments_with_colinear_edges():
pts1 = (Point2D(0, 2), Point2D(3, 2), Point2D(3, 4), Point2D(0, 4))
polygon1 = Polygon2D(pts1)

polygon0, polygon1 = Polygon2D._intersect_segments(polygon0, polygon1, 0)
polygon0, polygon1 = Polygon2D.intersect_segments(polygon0, polygon1, 0)

# Extra vertex added to polygon0, as expected
assert len(polygon0.segments) == 5
Expand Down

0 comments on commit 9ee809e

Please sign in to comment.