Skip to content

Commit

Permalink
feat(face): Add sub_faces_by_ratio_gridded to Face3D
Browse files Browse the repository at this point in the history
This will be useful for generating skylights.
  • Loading branch information
chriswmackey committed May 4, 2020
1 parent ed708cc commit e401c44
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 23 deletions.
87 changes: 72 additions & 15 deletions ladybug_geometry/geometry3d/face.py
Original file line number Diff line number Diff line change
Expand Up @@ -645,18 +645,7 @@ def is_sub_face(self, face, tolerance, angle_tolerance):
return False

# if it is, convert sub-face to a polygon in this face's plane
verts2d = tuple(self.plane.xyz_to_xy(_v) for _v in face.vertices)
sub_poly = Polygon2D(verts2d)

if not self.has_holes:
return self.polygon2d.is_polygon_inside(sub_poly)
else:
if not self.boundary_polygon2d.is_polygon_inside(sub_poly):
return False
for hole_poly in self.hole_polygon2d:
if not hole_poly.is_polygon_outside(sub_poly):
return False
return True
return self._is_sub_face(face)

def is_point_on_face(self, point, tolerance):
"""Check whether a given point is on this face.
Expand Down Expand Up @@ -890,8 +879,8 @@ def project_point(self, point):
return _plane_int
return None

def get_mesh_grid(self, x_dim, y_dim=None, offset=None, flip=False,
generate_centroids=True):
def mesh_grid(self, x_dim, y_dim=None, offset=None, flip=False,
generate_centroids=True):
"""Get a gridded Mesh3D over this face.
This method generates a mesh grid over the domain of the face
Expand Down Expand Up @@ -1107,7 +1096,8 @@ def countour_fins_by_distance_between(self, distance, depth, offset=0, angle=0,
def sub_faces_by_ratio(self, ratio):
"""Get a list of faces with a combined area equal to the ratio times this face area.
All sub faces will lie inside the boundaries of this face.
All sub faces will lie inside the boundaries of this face and will have
the same normal as this face.
Args:
ratio: A number between 0 and 1 for the ratio between the area of
Expand All @@ -1128,6 +1118,54 @@ def sub_faces_by_ratio(self, ratio):
[pt.scale(scale_factor, _tri_mesh.face_centroids[i]) for pt in _tri])
return [Face3D(_t, self.plane) for _t in _scaled_verts]

def sub_faces_by_ratio_gridded(self, ratio, x_dim, y_dim=None):
"""Get a list of faces with a combined area equal to the ratio times this face area.
All sub faces will lie inside the boundaries of this face and have the same
normal as this face.
Sub faces will be arranged in a grid derived from this face's plane property.
Because the x_dim and y_dim refer to dimensions within the X and Y
coordinate system of this faces's plane, rotating this plane will
result in rotated grid cells.
If the x_dim and/or y_dim are too large for this face, this method will
return essentially the same result as the sub_faces_by_ratio method.
Args:
ratio: A number between 0 and 1 for the ratio between the area of
the sub faces and the area of this face.
x_dim: The x dimension of the grid cells as a number.
y_dim: The y dimension of the grid cells as a number. Default is None,
which will assume the same cell dimension for y as is set for x.
Returns:
A list of Face3D objects for sub faces.
"""
try: # get the gridded mesh derived from this face
grid_mesh = self.mesh_grid(x_dim, y_dim)
except AssertionError: # there are no faces; just return sub_faces_by_ratio
return self.sub_faces_by_ratio(ratio)

# compute the area that each of the mesh faces need to be scaled to
_verts, _faces = grid_mesh.vertices, grid_mesh.faces
_x_dim = _verts[_faces[0][0]].distance_to_point(_verts[_faces[0][1]])
_y_dim = _verts[_faces[0][1]].distance_to_point(_verts[_faces[0][2]])
fac = (self.area * ratio) / (_x_dim * _y_dim * len(_faces))

# if the factor is greater than 1, sub-faces will be overlapping
if fac >= 1:
return self.sub_faces_by_ratio(ratio)
s_fac = fac ** 0.5

# generate the Face3D objects while scaling them to the correct size
sub_faces = []
for face, centr in zip(_faces, grid_mesh.face_centroids):
_f = Face3D(tuple(_verts[i].scale(s_fac, centr) for i in face), self.plane)
if self._is_sub_face(_f): # catch edge cases
sub_faces.append(_f)
return sub_faces

def sub_faces_by_ratio_rectangle(self, ratio, tolerance):
"""Get a list of faces with a combined area equal to the ratio times this face area.
Expand Down Expand Up @@ -1712,6 +1750,25 @@ def _remove_colinear(self, pts_3d, pts_2d, tolerance):
new_vertices.append(pts_3d[i - 1])
return new_vertices

def _is_sub_face(self, face):
"""Check if a face is a sub-face of this face, bypassing coplanar check.
Args:
face: Another face for which sub-face equivalency will be tested.
"""
verts2d = tuple(self.plane.xyz_to_xy(_v) for _v in face.vertices)
sub_poly = Polygon2D(verts2d)

if not self.has_holes:
return self.polygon2d.is_polygon_inside(sub_poly)
else:
if not self.boundary_polygon2d.is_polygon_inside(sub_poly):
return False
for hole_poly in self.hole_polygon2d:
if not hole_poly.is_polygon_outside(sub_poly):
return False
return True

def _vertices_between_points(self, start_pt, end_pt, tolerance):
"""Get the vertices between a start and end point.
Expand Down
24 changes: 19 additions & 5 deletions tests/face3d_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -916,12 +916,12 @@ def test_project_point():
assert face.project_point(pt_4) is None


def test_get_mesh_grid():
"""Test the Face3D get_mesh_grid method."""
def test_mesh_grid():
"""Test the Face3D mesh_grid method."""
pts = (Point3D(0, 0), Point3D(0, 2), Point3D(2, 2), Point3D(2, 0))
plane = Plane(Vector3D(0, 0, 1))
face = Face3D(pts, plane)
mesh = face.get_mesh_grid(0.5)
mesh = face.mesh_grid(0.5)

assert len(mesh.vertices) == 25
assert len(mesh.faces) == 16
Expand All @@ -939,8 +939,8 @@ def test_get_mesh_grid():
assert mesh.face_areas[0] == 0.25
assert len(mesh.face_centroids) == 16

mesh_2 = face.get_mesh_grid(0.5, 0.5, 1, False)
mesh_3 = face.get_mesh_grid(0.5, 0.5, 1, True)
mesh_2 = face.mesh_grid(0.5, 0.5, 1, False)
mesh_3 = face.mesh_grid(0.5, 0.5, 1, True)

assert mesh_2.min.z == pytest.approx(1, rel=1e-2)
assert mesh_2.max.z == pytest.approx(1, rel=1e-2)
Expand Down Expand Up @@ -1100,6 +1100,20 @@ def test_extract_rectangle_complex():
assert len(f1_result[2][0].vertices) == 4


def test_sub_faces_by_ratio_gridded():
"""Test the Face3D sub_faces_by_ratio_gridded method."""
pts_1 = (Point3D(0, 0, 0), Point3D(12, 0, 0), Point3D(12, 0, 12), Point3D(0, 0, 6))
face_1 = Face3D(pts_1)

sub_faces = face_1.sub_faces_by_ratio_gridded(0.4, 2, 2)
assert len(sub_faces) == 24
assert sum([face.area for face in sub_faces]) == pytest.approx(face_1.area * 0.4, rel=1e-3)

sub_faces = face_1.sub_faces_by_ratio_gridded(0.4, 12, 12)
assert len(sub_faces) == 1
assert sum([face.area for face in sub_faces]) == pytest.approx(face_1.area * 0.4, rel=1e-3)


def test_sub_faces_by_ratio_rectangle():
"""Test the Face3D sub_faces_by_ratio_rectangle method."""
pts_1 = (Point3D(0, 0, 0), Point3D(12, 0, 0), Point3D(12, 0, 2), Point3D(0, 0, 3))
Expand Down
2 changes: 1 addition & 1 deletion tests/mesh2d_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ def test_mesh2d_init_from_grid():


def test_triangualted():
"""Test the Mesh2D triangualted method."""
"""Test the Mesh2D triangulated method."""
pts = (Point2D(0, 0), Point2D(0, 2), Point2D(2, 2), Point2D(2, 0), Point2D(4, 0))
mesh = Mesh2D(pts, [(0, 1, 2, 3), (2, 3, 4)], ['red', 'green'])

Expand Down
4 changes: 2 additions & 2 deletions tests/polyface3d_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -501,14 +501,14 @@ def test_min_max_center():
def test_floor_mesh_grid():
"""Test the generation of a mesh grid from the floor of a box."""
polyface = Polyface3D.from_box(5, 10, 3)
floor_grid = polyface.faces[0].get_mesh_grid(1, 1, 1, True)
floor_grid = polyface.faces[0].mesh_grid(1, 1, 1, True)
assert len(floor_grid.faces) == 50

angle = -1 * math.radians(45)
x_axis = Vector3D(1, 0, 0).rotate_xy(angle)
base_plane = Plane(Vector3D(0, 0, 1), Point3D(0, 0, 0), x_axis)
polyface = Polyface3D.from_box(5, 10, 3, base_plane)
floor_grid = polyface.faces[0].get_mesh_grid(1, 1, 1, True)
floor_grid = polyface.faces[0].mesh_grid(1, 1, 1, True)
assert len(floor_grid.faces) == 50


Expand Down

0 comments on commit e401c44

Please sign in to comment.