Skip to content

Commit

Permalink
fix(face): Ensure cases of splitting Face3D with line succeed
Browse files Browse the repository at this point in the history
  • Loading branch information
chriswmackey authored and Chris Mackey committed Jul 19, 2024
1 parent 10a2a6e commit bae086f
Show file tree
Hide file tree
Showing 3 changed files with 12 additions and 124 deletions.
3 changes: 1 addition & 2 deletions ladybug_geometry/geometry2d/polygon.py
Original file line number Diff line number Diff line change
Expand Up @@ -1206,8 +1206,7 @@ def distance_from_edge_to_point(self, point):
point: A Point2D object to which the minimum distance will be computed.
Returns:
The distance to the input point. Will be zero if the point is
inside the Polygon2D.
The distance to the input point from the nearest edge.
"""
return min(seg.distance_to_point(point) for seg in self.segments)

Expand Down
98 changes: 11 additions & 87 deletions ladybug_geometry/geometry3d/face.py
Original file line number Diff line number Diff line change
Expand Up @@ -1170,7 +1170,7 @@ def split_with_line(self, line, tolerance):
return None

# get BooleanPolygons of the polygon and the line segment
move_vec = line_2d.v.rotate(math.pi / 2).normalize() * (tolerance / 10)
move_vec = line_2d.v.rotate(math.pi / 2).normalize() * (tolerance / 2)
line_verts = (line_2d.p1, line_2d.p2, line_2d.p2.move(move_vec),
line_2d.p1.move(move_vec))
line_poly = [(pb.BooleanPoint(pt.x, pt.y) for pt in line_verts)]
Expand All @@ -1189,7 +1189,7 @@ def split_with_line(self, line, tolerance):
return None # typically a tolerance issue causing failure

# rebuild the Face3D from the results and return them
return Face3D._from_bool_poly(poly1_result, prim_pl)
return Face3D._from_bool_poly(poly1_result, prim_pl, tolerance)

def split_with_polyline(self, polyline, tolerance):
"""Split this face into two or more Face3D given an open Polyline3D.
Expand Down Expand Up @@ -1243,7 +1243,7 @@ def split_with_polyline(self, polyline, tolerance):
return None

# get BooleanPolygons of the polygon and the polyline
off_p_line = polyline_2d.offset(tolerance / 10)
off_p_line = polyline_2d.offset(tolerance / 2)
P_line_verts = polyline_2d.vertices + tuple(reversed(off_p_line.vertices))
line_poly = [(pb.BooleanPoint(pt.x, pt.y) for pt in P_line_verts)]
face_polys = [(pb.BooleanPoint(pt.x, pt.y) for pt in bnd_poly.vertices)]
Expand All @@ -1261,89 +1261,7 @@ def split_with_polyline(self, polyline, tolerance):
return None # typically a tolerance issue causing failure

# rebuild the Face3D from the results and return them
return Face3D._from_bool_poly(poly1_result, prim_pl)

def split_with_lines(self, lines, tolerance):
"""Split this face into two or more Face3D given multiple LineSegment3D.
Using this method is distinct from looping over the Face3D.split_with_line
in that this method will resolve cases where multiple segments branch out
from nodes in a network of input lines. So, if three line segments
meet at a point in the middle of this Face3D and each extend past the
edges of this Face3D, this method can split the Face3D in 3 parts whereas
looping over the Face3D.split_with_line will not do this given that each
individual segment cannot split the Face3D.
If the input lines together do not intersect this Face3D in a manner
that splits it into two or more pieces, None will be returned.
Args:
lines: A list of LineSegment3D objects in the plane of this Face3D,
which will be used to split it into two or more pieces.
tolerance: The maximum difference between point values for them to be
considered distinct from one another.
Returns:
A list of Face3D for the result of splitting this Face3D with the
input lines. Will be None if the line is not in the plane of the
Face3D or if it does not split the Face3D into two or more pieces.
"""
# first check that the lines are in the plane of the Face3D
rel_line_3ds = []
for line in lines:
if self.plane.distance_to_point(line.p1) <= tolerance or \
self.plane.distance_to_point(line.p1) <= tolerance:
rel_line_3ds.append(line)
if len(rel_line_3ds) == 0:
return None
# extend the endpoints of the lines so that tolerance will split it
ext_rel_line_3ds = []
for line in rel_line_3ds:
tvc = line.v.normalize() * (tolerance / 2)
line = LineSegment3D.from_end_points(line.p1.move(-tvc), line.p2.move(tvc))
ext_rel_line_3ds.append(line)

# change the line and face to be in 2D and check that it can split the Face
prim_pl = self.plane
bnd_poly = self.boundary_polygon2d
rel_line_2ds = []
for line in ext_rel_line_3ds:
line_2d = LineSegment2D.from_end_points(
prim_pl.xyz_to_xy(line.p1), prim_pl.xyz_to_xy(line.p2))
if Polygon2D.overlapping_bounding_rect(bnd_poly, line_2d, tolerance):
rel_line_2ds.append(line_2d)
if len(rel_line_2ds) == 0:
return None

# get BooleanPolygon of the face
face_polys = [(pb.BooleanPoint(pt.x, pt.y) for pt in bnd_poly.vertices)]
if self.has_holes:
for hole in self.hole_polygon2d:
face_polys.append((pb.BooleanPoint(pt.x, pt.y) for pt in hole.vertices))
b_poly1 = pb.BooleanPolygon(face_polys)

# loop through the segments and split the faces' boolean polygon
int_tol = tolerance / 100000
for line_2d in rel_line_2ds:
move_vec1 = line_2d.v.rotate(math.pi / 2) * (tolerance / 20)
move_vec2 = move_vec1.reverse()
line_verts = (line_2d.p1.move(move_vec1), line_2d.p2.move(move_vec1),
line_2d.p2.move(move_vec2), line_2d.p1.move(move_vec2))
line_poly = [(pb.BooleanPoint(pt.x, pt.y) for pt in line_verts)]
b_poly2 = pb.BooleanPolygon(line_poly)
try:
b_poly1 = pb.difference(b_poly1, b_poly2, int_tol)
except Exception:
return None # typically a tolerance issue causing failure

# rebuild the Face3D from the results and clean up the result
split_result = Face3D._from_bool_poly(b_poly1, prim_pl)
if len(split_result) == 1: # nothing was split
return None # return None as the result is probably less clean than input
final_result = []
for face in split_result:
final_result.append(face.remove_duplicate_vertices(tolerance))
return final_result
return Face3D._from_bool_poly(poly1_result, prim_pl, tolerance)

def intersect_line_ray(self, line_ray):
"""Get the intersection between this face and the input LineSegment3D or Ray3D.
Expand Down Expand Up @@ -2484,7 +2402,7 @@ def coplanar_union_all(faces, tolerance, angle_tolerance):
return union_faces

@staticmethod
def _from_bool_poly(bool_polygon, plane):
def _from_bool_poly(bool_polygon, plane, snap_tolerance=None):
"""Get a list of Face3D from a BooleanPolygon.
This method will automatically check whether any of the regions is meant
Expand All @@ -2493,6 +2411,9 @@ def _from_bool_poly(bool_polygon, plane):
Args:
bool_polygon: A BooleanPolygon to be interpreted to Face3D.
plane: The Plane in which the resulting Face3Ds exist.
snap_tolerance: An optional tolerance value to be used to snap the
polygons together before turning them into Face3D. If None,
no snapping will occur.
"""
# serialize the BooleanPolygon into Polygon2D
polys = [Polygon2D(tuple(Point2D(pt.x, pt.y) for pt in new_poly))
Expand All @@ -2502,6 +2423,9 @@ def _from_bool_poly(bool_polygon, plane):
if len(polys) == 1:
verts_3d = tuple(plane.xy_to_xyz(pt) for pt in polys[0].vertices)
return [Face3D(verts_3d, plane)]
# snap the polygons together if requested
if snap_tolerance is not None:
polys = Polygon2D.snap_polygons(polys, snap_tolerance)
# sort the polygons by area and check if any are inside the others
polys.sort(key=lambda x: x.area, reverse=True)
poly_groups = [[polys[0]]]
Expand Down
35 changes: 0 additions & 35 deletions tests/face3d_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1174,41 +1174,6 @@ def test_split_with_polyline():
assert int_result is None


def test_split_with_lines():
"""Test the split_with_line method."""
f_pts = (Point3D(0, 0, 2), Point3D(2, 0, 2), Point3D(2, 2, 2), Point3D(0, 2, 2))
face = Face3D(f_pts)

l_pts1 = (Point3D(1, -1, 2), Point3D(1, 1, 2))
line1 = LineSegment3D.from_end_points(*l_pts1)
l_pts2 = (Point3D(-1, 1, 2), Point3D(1, 1, 2))
line2 = LineSegment3D.from_end_points(*l_pts2)
l_pts3 = (Point3D(1, 1, 2), Point3D(3, 3, 2))
line3 = LineSegment3D.from_end_points(*l_pts3)
all_lines = [line1, line2, line3]
int_result = face.split_with_lines(all_lines, 0.01)

assert len(int_result) == 3
for int_f in int_result:
assert int_f.area == pytest.approx(face.area * 0.25, rel=1e-2) or \
int_f.area == pytest.approx(face.area * 0.375, rel=1e-2)

l_pts1 = (Point3D(1, -1, 2), Point3D(1, 1, 2))
line1 = LineSegment3D.from_end_points(*l_pts1)
l_pts2 = (Point3D(-1, 1, 2), Point3D(1, 1, 2))
line2 = LineSegment3D.from_end_points(*l_pts2)
l_pts3 = (Point3D(1, 1, 2), Point3D(3, 1, 2))
line3 = LineSegment3D.from_end_points(*l_pts3)
l_pts4 = (Point3D(1, 1, 2), Point3D(1, 3, 2))
line4 = LineSegment3D.from_end_points(*l_pts4)
all_lines = [line1, line2, line3, line4]
int_result = face.split_with_lines(all_lines, 0.01)

assert len(int_result) == 4
for int_f in int_result:
assert int_f.area == pytest.approx(face.area * 0.25, rel=1e-2)


def test_intersect_line_ray():
"""Test the Face3D intersect_line_ray method."""
pts = (Point3D(0, 0, 2), Point3D(2, 0, 2), Point3D(2, 1, 2), Point3D(1, 1, 2),
Expand Down

0 comments on commit bae086f

Please sign in to comment.