Skip to content

Commit bae086f

Browse files
chriswmackeyChris Mackey
authored and
Chris Mackey
committed
fix(face): Ensure cases of splitting Face3D with line succeed
1 parent 10a2a6e commit bae086f

File tree

3 files changed

+12
-124
lines changed

3 files changed

+12
-124
lines changed

ladybug_geometry/geometry2d/polygon.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -1206,8 +1206,7 @@ def distance_from_edge_to_point(self, point):
12061206
point: A Point2D object to which the minimum distance will be computed.
12071207
12081208
Returns:
1209-
The distance to the input point. Will be zero if the point is
1210-
inside the Polygon2D.
1209+
The distance to the input point from the nearest edge.
12111210
"""
12121211
return min(seg.distance_to_point(point) for seg in self.segments)
12131212

ladybug_geometry/geometry3d/face.py

+11-87
Original file line numberDiff line numberDiff line change
@@ -1170,7 +1170,7 @@ def split_with_line(self, line, tolerance):
11701170
return None
11711171

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

11911191
# rebuild the Face3D from the results and return them
1192-
return Face3D._from_bool_poly(poly1_result, prim_pl)
1192+
return Face3D._from_bool_poly(poly1_result, prim_pl, tolerance)
11931193

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

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

12631263
# rebuild the Face3D from the results and return them
1264-
return Face3D._from_bool_poly(poly1_result, prim_pl)
1265-
1266-
def split_with_lines(self, lines, tolerance):
1267-
"""Split this face into two or more Face3D given multiple LineSegment3D.
1268-
1269-
Using this method is distinct from looping over the Face3D.split_with_line
1270-
in that this method will resolve cases where multiple segments branch out
1271-
from nodes in a network of input lines. So, if three line segments
1272-
meet at a point in the middle of this Face3D and each extend past the
1273-
edges of this Face3D, this method can split the Face3D in 3 parts whereas
1274-
looping over the Face3D.split_with_line will not do this given that each
1275-
individual segment cannot split the Face3D.
1276-
1277-
If the input lines together do not intersect this Face3D in a manner
1278-
that splits it into two or more pieces, None will be returned.
1279-
1280-
Args:
1281-
lines: A list of LineSegment3D objects in the plane of this Face3D,
1282-
which will be used to split it into two or more pieces.
1283-
tolerance: The maximum difference between point values for them to be
1284-
considered distinct from one another.
1285-
1286-
Returns:
1287-
A list of Face3D for the result of splitting this Face3D with the
1288-
input lines. Will be None if the line is not in the plane of the
1289-
Face3D or if it does not split the Face3D into two or more pieces.
1290-
"""
1291-
# first check that the lines are in the plane of the Face3D
1292-
rel_line_3ds = []
1293-
for line in lines:
1294-
if self.plane.distance_to_point(line.p1) <= tolerance or \
1295-
self.plane.distance_to_point(line.p1) <= tolerance:
1296-
rel_line_3ds.append(line)
1297-
if len(rel_line_3ds) == 0:
1298-
return None
1299-
# extend the endpoints of the lines so that tolerance will split it
1300-
ext_rel_line_3ds = []
1301-
for line in rel_line_3ds:
1302-
tvc = line.v.normalize() * (tolerance / 2)
1303-
line = LineSegment3D.from_end_points(line.p1.move(-tvc), line.p2.move(tvc))
1304-
ext_rel_line_3ds.append(line)
1305-
1306-
# change the line and face to be in 2D and check that it can split the Face
1307-
prim_pl = self.plane
1308-
bnd_poly = self.boundary_polygon2d
1309-
rel_line_2ds = []
1310-
for line in ext_rel_line_3ds:
1311-
line_2d = LineSegment2D.from_end_points(
1312-
prim_pl.xyz_to_xy(line.p1), prim_pl.xyz_to_xy(line.p2))
1313-
if Polygon2D.overlapping_bounding_rect(bnd_poly, line_2d, tolerance):
1314-
rel_line_2ds.append(line_2d)
1315-
if len(rel_line_2ds) == 0:
1316-
return None
1317-
1318-
# get BooleanPolygon of the face
1319-
face_polys = [(pb.BooleanPoint(pt.x, pt.y) for pt in bnd_poly.vertices)]
1320-
if self.has_holes:
1321-
for hole in self.hole_polygon2d:
1322-
face_polys.append((pb.BooleanPoint(pt.x, pt.y) for pt in hole.vertices))
1323-
b_poly1 = pb.BooleanPolygon(face_polys)
1324-
1325-
# loop through the segments and split the faces' boolean polygon
1326-
int_tol = tolerance / 100000
1327-
for line_2d in rel_line_2ds:
1328-
move_vec1 = line_2d.v.rotate(math.pi / 2) * (tolerance / 20)
1329-
move_vec2 = move_vec1.reverse()
1330-
line_verts = (line_2d.p1.move(move_vec1), line_2d.p2.move(move_vec1),
1331-
line_2d.p2.move(move_vec2), line_2d.p1.move(move_vec2))
1332-
line_poly = [(pb.BooleanPoint(pt.x, pt.y) for pt in line_verts)]
1333-
b_poly2 = pb.BooleanPolygon(line_poly)
1334-
try:
1335-
b_poly1 = pb.difference(b_poly1, b_poly2, int_tol)
1336-
except Exception:
1337-
return None # typically a tolerance issue causing failure
1338-
1339-
# rebuild the Face3D from the results and clean up the result
1340-
split_result = Face3D._from_bool_poly(b_poly1, prim_pl)
1341-
if len(split_result) == 1: # nothing was split
1342-
return None # return None as the result is probably less clean than input
1343-
final_result = []
1344-
for face in split_result:
1345-
final_result.append(face.remove_duplicate_vertices(tolerance))
1346-
return final_result
1264+
return Face3D._from_bool_poly(poly1_result, prim_pl, tolerance)
13471265

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

24862404
@staticmethod
2487-
def _from_bool_poly(bool_polygon, plane):
2405+
def _from_bool_poly(bool_polygon, plane, snap_tolerance=None):
24882406
"""Get a list of Face3D from a BooleanPolygon.
24892407
24902408
This method will automatically check whether any of the regions is meant
@@ -2493,6 +2411,9 @@ def _from_bool_poly(bool_polygon, plane):
24932411
Args:
24942412
bool_polygon: A BooleanPolygon to be interpreted to Face3D.
24952413
plane: The Plane in which the resulting Face3Ds exist.
2414+
snap_tolerance: An optional tolerance value to be used to snap the
2415+
polygons together before turning them into Face3D. If None,
2416+
no snapping will occur.
24962417
"""
24972418
# serialize the BooleanPolygon into Polygon2D
24982419
polys = [Polygon2D(tuple(Point2D(pt.x, pt.y) for pt in new_poly))
@@ -2502,6 +2423,9 @@ def _from_bool_poly(bool_polygon, plane):
25022423
if len(polys) == 1:
25032424
verts_3d = tuple(plane.xy_to_xyz(pt) for pt in polys[0].vertices)
25042425
return [Face3D(verts_3d, plane)]
2426+
# snap the polygons together if requested
2427+
if snap_tolerance is not None:
2428+
polys = Polygon2D.snap_polygons(polys, snap_tolerance)
25052429
# sort the polygons by area and check if any are inside the others
25062430
polys.sort(key=lambda x: x.area, reverse=True)
25072431
poly_groups = [[polys[0]]]

tests/face3d_test.py

-35
Original file line numberDiff line numberDiff line change
@@ -1174,41 +1174,6 @@ def test_split_with_polyline():
11741174
assert int_result is None
11751175

11761176

1177-
def test_split_with_lines():
1178-
"""Test the split_with_line method."""
1179-
f_pts = (Point3D(0, 0, 2), Point3D(2, 0, 2), Point3D(2, 2, 2), Point3D(0, 2, 2))
1180-
face = Face3D(f_pts)
1181-
1182-
l_pts1 = (Point3D(1, -1, 2), Point3D(1, 1, 2))
1183-
line1 = LineSegment3D.from_end_points(*l_pts1)
1184-
l_pts2 = (Point3D(-1, 1, 2), Point3D(1, 1, 2))
1185-
line2 = LineSegment3D.from_end_points(*l_pts2)
1186-
l_pts3 = (Point3D(1, 1, 2), Point3D(3, 3, 2))
1187-
line3 = LineSegment3D.from_end_points(*l_pts3)
1188-
all_lines = [line1, line2, line3]
1189-
int_result = face.split_with_lines(all_lines, 0.01)
1190-
1191-
assert len(int_result) == 3
1192-
for int_f in int_result:
1193-
assert int_f.area == pytest.approx(face.area * 0.25, rel=1e-2) or \
1194-
int_f.area == pytest.approx(face.area * 0.375, rel=1e-2)
1195-
1196-
l_pts1 = (Point3D(1, -1, 2), Point3D(1, 1, 2))
1197-
line1 = LineSegment3D.from_end_points(*l_pts1)
1198-
l_pts2 = (Point3D(-1, 1, 2), Point3D(1, 1, 2))
1199-
line2 = LineSegment3D.from_end_points(*l_pts2)
1200-
l_pts3 = (Point3D(1, 1, 2), Point3D(3, 1, 2))
1201-
line3 = LineSegment3D.from_end_points(*l_pts3)
1202-
l_pts4 = (Point3D(1, 1, 2), Point3D(1, 3, 2))
1203-
line4 = LineSegment3D.from_end_points(*l_pts4)
1204-
all_lines = [line1, line2, line3, line4]
1205-
int_result = face.split_with_lines(all_lines, 0.01)
1206-
1207-
assert len(int_result) == 4
1208-
for int_f in int_result:
1209-
assert int_f.area == pytest.approx(face.area * 0.25, rel=1e-2)
1210-
1211-
12121177
def test_intersect_line_ray():
12131178
"""Test the Face3D intersect_line_ray method."""
12141179
pts = (Point3D(0, 0, 2), Point3D(2, 0, 2), Point3D(2, 1, 2), Point3D(1, 1, 2),

0 commit comments

Comments
 (0)