Skip to content

Commit

Permalink
fix(polygon2d): Add method to sense whether polygons touch one another
Browse files Browse the repository at this point in the history
I'm also including the bounding rectangle check inside the polygon_relationship function since we pretty much always want to perform this check first to improve the performance.
  • Loading branch information
chriswmackey authored and Chris Mackey committed Apr 3, 2024
1 parent d8b0566 commit dd1d57a
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 16 deletions.
117 changes: 110 additions & 7 deletions ladybug_geometry/geometry2d/polygon.py
Original file line number Diff line number Diff line change
Expand Up @@ -965,7 +965,7 @@ def is_point_inside_bound_rect(self, point, test_vector=Vector2D(1, 0.00001)):
return self.is_point_inside(point, test_vector)

def is_polygon_inside(self, polygon):
""""Test whether another Polygon2D lies completely inside this polygon.
"""Test whether another Polygon2D lies completely inside this polygon.
Args:
polygon: A Polygon2D to test whether it is completely inside this one.
Expand All @@ -984,7 +984,7 @@ def is_polygon_inside(self, polygon):
return True

def is_polygon_outside(self, polygon):
""""Test whether another Polygon2D lies completely outside this polygon.
"""Test whether another Polygon2D lies completely outside this polygon.
Args:
polygon: A Polygon2D to test whether it is completely outside this one.
Expand All @@ -1002,6 +1002,36 @@ def is_polygon_outside(self, polygon):
return False
return True

def does_polygon_touch(self, polygon, tolerance):
"""Test whether another Polygon2D touches, overlaps or is inside this polygon.
Args:
polygon: A Polygon2D to test whether it touches this polygon.
Returns:
A boolean denoting whether the polygon touches (True) or not (False).
"""
# perform a bounding rectangle check to see if the polygons cannot overlap
if not Polygon2D.overlapping_bounding_rect(self, polygon, tolerance):
return False

# first evaluate the point relationships
pt_rels1 = [self.point_relationship(pt, tolerance) for pt in polygon]
if 0 in pt_rels1 or 1 in pt_rels1:
return True # definitely touching polygons
pt_rels2 = [polygon.point_relationship(pt, tolerance) for pt in self]
if 0 in pt_rels2 or 1 in pt_rels2:
return True # definitely touching polygons

# if any of the segments intersect the other polygon, there is overlap
for seg in self.segments:
for _s in polygon.segments:
if does_intersection_exist_line2d(seg, _s):
return True

# we can reliably say that the polygons do not touch
return False

def polygon_relationship(self, polygon, tolerance):
"""Test whether another Polygon2D lies inside, outside or overlaps this one.
Expand All @@ -1027,9 +1057,13 @@ def polygon_relationship(self, polygon, tolerance):
This will be one of the following:
* -1 = Outside this polygon
* 0 = Overlaps (intersects or contains) this polygon
* 0 = Overlaps (intersects) this polygon
* +1 = Inside this polygon
"""
# perform a bounding rectangle check to see if the polygons cannot overlap
if not Polygon2D.overlapping_bounding_rect(self, polygon, tolerance):
return -1

# first evaluate the point relationships to rule out the inside case
pt_rels1 = [self.point_relationship(pt, tolerance) for pt in polygon]
pt_rels2 = [polygon.point_relationship(pt, tolerance) for pt in self]
Expand All @@ -1056,7 +1090,7 @@ def polygon_relationship(self, polygon, tolerance):
return -1

def distance_to_point(self, point):
"""Get the minimum distance between this shape and the input point.
"""Get the minimum distance between this shape and a point.
Points that are inside the Polygon2D will return a distance of zero.
If the distance of an interior point to an edge is needed, the
Expand Down Expand Up @@ -1477,7 +1511,7 @@ def overlapping_bounding_rect(polygon1, polygon2, tolerance):

@staticmethod
def group_by_overlap(polygons, tolerance):
"""Group Polygon2Ds that overlap one another greater than the tolerance.
"""Group Polygon2Ds that overlap one another within the tolerance.
This is useful as a pre-step before running Polygon2D.boolean_union_all()
in order to assess whether unionizing is necessary and to ensure that
Expand All @@ -1495,8 +1529,9 @@ def group_by_overlap(polygons, tolerance):
A list of lists where each sub-list represents a group of polygons
that all overlap with one another.
"""
# sort the polygons by area to ensure larger ones grab smaller ones
# sort the polygons by area to help larger ones grab smaller ones
polygons = list(sorted(polygons, key=lambda x: x.area, reverse=True))

# loop through the polygons and check to see if it overlaps with the others
grouped_polys = [[polygons[0]]]
for poly in polygons[1:]:
Expand All @@ -1511,7 +1546,8 @@ def group_by_overlap(polygons, tolerance):
break
if not group_found: # the polygon does not overlap with any of the others
grouped_polys.append([poly]) # make a new group for the polygon
# if some groups were found, do several passes to merge groups

# if some groups were found, recursively merge groups together
old_group_len = len(polygons)
while len(grouped_polys) != old_group_len:
new_groups, g_to_remove = grouped_polys[:], []
Expand All @@ -1532,6 +1568,64 @@ def group_by_overlap(polygons, tolerance):
grouped_polys = new_groups
return grouped_polys

@staticmethod
def group_by_touching(polygons, tolerance):
"""Group Polygon2Ds that touch or overlap one another within the tolerance.
This is useful to group geometries together before extracting a bounding
rectangle or convex hull around multiple polygons.
This method will return the minimal number of polygon groups
thanks to a recursive check of whether groups can be merged.
Args:
polygons: A list of Polygon2D to be grouped by their touching.
tolerance: The minimum distance from the edge of a neighboring polygon
at which a point is considered to touch that polygon.
Returns:
A list of lists where each sub-list represents a group of polygons
that all touch or overlap with one another.
"""
# sort the polygons by area to help larger ones grab smaller ones
polygons = list(sorted(polygons, key=lambda x: x.area, reverse=True))

# loop through the polygons and check to see if it overlaps with the others
grouped_polys = [[polygons[0]]]
for poly in polygons[1:]:
group_found = False
for poly_group in grouped_polys:
for oth_poly in poly_group:
if poly.does_polygon_touch(oth_poly, tolerance):
poly_group.append(poly)
group_found = True
break
if group_found:
break
if not group_found: # the polygon does not touch any of the others
grouped_polys.append([poly]) # make a new group for the polygon

# if some groups were found, recursively merge groups together
old_group_len = len(polygons)
while len(grouped_polys) != old_group_len:
new_groups, g_to_remove = grouped_polys[:], []
for i, group_1 in enumerate(grouped_polys):
try:
for j, group_2 in enumerate(grouped_polys[i + 1:]):
if Polygon2D._groups_touch(group_1, group_2, tolerance):
new_groups[i] = new_groups[i] + group_2
g_to_remove.append(i + j + 1)
except IndexError:
pass # we have reached the end of the list of polygons
if len(g_to_remove) != 0:
g_to_remove = list(set(g_to_remove))
g_to_remove.sort()
for ri in reversed(g_to_remove):
new_groups.pop(ri)
old_group_len = len(grouped_polys)
grouped_polys = new_groups
return grouped_polys

@staticmethod
def _groups_overlap(group_1, group_2, tolerance):
"""Evaluate whether two groups of Polygons overlap with one another."""
Expand All @@ -1540,6 +1634,15 @@ def _groups_overlap(group_1, group_2, tolerance):
if poly_1.polygon_relationship(poly_2, tolerance) >= 0:
return True
return False

@staticmethod
def _groups_touch(group_1, group_2, tolerance):
"""Evaluate whether two groups of Polygons touch with one another."""
for poly_1 in group_1:
for poly_2 in group_2:
if poly_1.does_polygon_touch(poly_2, tolerance):
return True
return False

@staticmethod
def joined_intersected_boundary(polygons, tolerance):
Expand Down
39 changes: 30 additions & 9 deletions ladybug_geometry/geometry3d/face.py
Original file line number Diff line number Diff line change
Expand Up @@ -1905,8 +1905,6 @@ def coplanar_difference(self, faces, tolerance, angle_tolerance):
continue
# test whether the two polygons have any overlap in 2D space
f2_poly = Polygon2D(tuple(prim_pl.xyz_to_xy(pt) for pt in face2.boundary))
if not Polygon2D.overlapping_bounding_rect(f1_poly, f2_poly, tolerance):
continue
if f1_poly.polygon_relationship(f2_poly, tolerance) == -1:
continue
# snap the polygons to one another to avoid tolerance issues
Expand Down Expand Up @@ -1963,8 +1961,6 @@ def coplanar_union(face1, face2, tolerance, angle_tolerance):
# test whether the two polygons have any overlap in 2D space
f1_poly = face1.boundary_polygon2d
f2_poly = Polygon2D(tuple(prim_pl.xyz_to_xy(pt) for pt in face2.boundary))
if not Polygon2D.overlapping_bounding_rect(f1_poly, f2_poly, tolerance):
return None
if f1_poly.polygon_relationship(f2_poly, tolerance) == -1:
return None
# snap the polygons to one another to avoid tolerance issues
Expand Down Expand Up @@ -2022,8 +2018,6 @@ def coplanar_intersection(face1, face2, tolerance, angle_tolerance):
# test whether the two polygons have any overlap in 2D space
f1_poly = face1.boundary_polygon2d
f2_poly = Polygon2D(tuple(prim_pl.xyz_to_xy(pt) for pt in face2.boundary))
if not Polygon2D.overlapping_bounding_rect(f1_poly, f2_poly, tolerance):
return None
if f1_poly.polygon_relationship(f2_poly, tolerance) == -1:
return None
# snap the polygons to one another to avoid tolerance issues
Expand Down Expand Up @@ -2084,8 +2078,6 @@ def coplanar_split(face1, face2, tolerance, angle_tolerance):
# test whether the two polygons have any overlap in 2D space
f1_poly = face1.boundary_polygon2d
f2_poly = Polygon2D(tuple(prim_pl.xyz_to_xy(pt) for pt in face2.boundary))
if not Polygon2D.overlapping_bounding_rect(f1_poly, f2_poly, tolerance):
return [face1], [face2]
if f1_poly.polygon_relationship(f2_poly, tolerance) == -1:
return [face1], [face2]
# snap the polygons to one another to avoid tolerance issues
Expand Down Expand Up @@ -2176,9 +2168,12 @@ def group_by_coplanar_overlap(faces, tolerance):
"""Group coplanar Face3Ds depending on whether they overlap one another.
This is useful as a pre-step before running Face3D.coplanar_union()
in order to assess whether unionizing is necessary and to ensure that
in order to assess whether union-ing is necessary and to ensure that
it is only performed among the necessary groups of faces.
This method will return the minimal number of overlapping polygon groups
thanks to a recursive check of whether groups can be merged.
Args:
faces: A list of Face3D to be grouped by their overlapping.
tolerance: The minimum distance from the edge of a neighboring Face3D
Expand All @@ -2194,6 +2189,7 @@ def group_by_coplanar_overlap(faces, tolerance):
r_plane = faces[0].plane
polygons = [Polygon2D([r_plane.xyz_to_xy(pt) for pt in face.vertices])
for face in faces]

# loop through the polygons and check to see if it overlaps with the others
grouped_polys, grouped_faces = [[polygons[0]]], [[faces[0]]]
for poly, face in zip(polygons[1:], faces[1:]):
Expand All @@ -2210,6 +2206,31 @@ def group_by_coplanar_overlap(faces, tolerance):
if not group_found: # the polygon does not overlap with any of the others
grouped_polys.append([poly]) # make a new group for the polygon
grouped_faces.append([face]) # make a new group for the face

# if some groups were found, recursively merge groups together
old_group_len = len(polygons)
while len(grouped_polys) != old_group_len:
new_poly_groups, new_face_groups = grouped_polys[:], grouped_faces[:]
g_to_remove = []
for i, group_1 in enumerate(grouped_polys):
try:
zip_obj = zip(grouped_polys[i + 1:], grouped_faces[i + 1:])
for j, (group_2, f2) in enumerate(zip_obj):
if Polygon2D._groups_overlap(group_1, group_2, tolerance):
new_poly_groups[i] = new_poly_groups[i] + group_2
new_face_groups[i] = new_face_groups[i] + f2
g_to_remove.append(i + j + 1)
except IndexError:
pass # we have reached the end of the list of polygons
if len(g_to_remove) != 0:
g_to_remove = list(set(g_to_remove))
g_to_remove.sort()
for ri in reversed(g_to_remove):
new_poly_groups.pop(ri)
new_face_groups.pop(ri)
old_group_len = len(grouped_polys)
grouped_polys = new_poly_groups
grouped_faces = new_face_groups
return grouped_faces

@staticmethod
Expand Down
Loading

0 comments on commit dd1d57a

Please sign in to comment.