Skip to content

Commit

Permalink
fix(room): Add a method to split Room Faces through holes
Browse files Browse the repository at this point in the history
  • Loading branch information
chriswmackey committed Dec 6, 2024
1 parent b33f7a9 commit 734a6a4
Showing 1 changed file with 101 additions and 1 deletion.
102 changes: 101 additions & 1 deletion honeybee/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -1216,6 +1216,106 @@ def clean_envelope(self, adjacency_dict, tolerance=0.01):
tuple(face.geometry for face in self._faces), tolerance)
return adj_dict

def split_through_holes(self, tolerance=0.01, angle_tolerance=1):
"""Split any Faces of this Room with holes such that they no longer have holes.
This method is useful for destination engines that cannot support holes
either through dedicated hole loops that are separate from the boundary
loop or as a single collapsed list of vertices that winds inward to cut
out the holes.
This method attempts to preserve as many properties as possible for the
split Faces, including all extension attributes and sub-faces (as long
as they don't fall in the path of the intersection).
Args:
tolerance: The minimum difference between the coordinate values of two
faces at which they can be considered adjacent. Default: 0.01,
suitable for objects in meters.
angle_tolerance: The max angle in degrees that the plane normals can
differ from one another in order for them to be considered
coplanar. (Default: 1 degree).
Returns:
A list containing only the new Faces that were created as part of the
splitting process. These new Faces will have as many properties of the
original Face assigned to them as possible but they will not have a
Surface boundary condition if the original Face had one. Having just
the new Faces here can be used in operations like setting new Surface
boundary conditions.
"""
# make a dictionary of all face geometry to be split
geo_dict = {f.identifier: [f.geometry] for f in self.faces}

# loop through the faces and split them
ang_tol = math.radians(angle_tolerance)
for face_1 in self.faces:
if face_1.geometry.has_holes:
new_geo = []
f_split = face_1.geometry.split_through_holes()
for sp_g in f_split:
try:
sp_g = sp_g.remove_colinear_vertices(tolerance)
new_geo.append(sp_g)
except AssertionError: # degenerate geometry to ignore
pass
geo_dict[face_1.identifier] = new_geo

# use the split geometry to remake this room's faces
all_faces, new_faces = [], []
for face in self.faces:
int_faces = geo_dict[face.identifier]
if len(int_faces) == 1: # just use the old Face object
all_faces.append(face)
else: # make new Face objects
new_bc = face.boundary_condition \
if not isinstance(face.boundary_condition, Surface) \
else boundary_conditions.outdoors
new_aps = [ap.duplicate() for ap in face.apertures]
new_drs = [dr.duplicate() for dr in face.doors]
for x, nf_geo in enumerate(int_faces):
new_id = '{}_{}'.format(face.identifier, x)
new_face = Face(new_id, nf_geo, face.type, new_bc)
new_face._display_name = face._display_name
new_face._user_data = None if face.user_data is None \
else face.user_data.copy()
for ap in new_aps:
if nf_geo.is_sub_face(ap.geometry, tolerance, ang_tol):
new_face.add_aperture(ap)
for dr in new_drs:
if nf_geo.is_sub_face(dr.geometry, tolerance, ang_tol):
new_face.add_door(dr)
if x == 0:
face._duplicate_child_shades(new_face)
new_face._parent = face._parent
new_face._properties._duplicate_extension_attr(face._properties)
new_faces.append(new_face)
all_faces.append(new_face)
if len(new_faces) == 0:
return new_faces # nothing has been split

# make a new polyface from the updated faces
room_polyface = Polyface3D.from_faces(
tuple(face.geometry for face in all_faces), tolerance)
if not room_polyface.is_solid:
room_polyface = room_polyface.merge_overlapping_edges(tolerance, ang_tol)
# replace honeybee face geometry with versions that are facing outwards
if room_polyface.is_solid:
for i, correct_face3d in enumerate(room_polyface.faces):
face = all_faces[i]
norm_init = face._geometry.normal
face._geometry = correct_face3d
if face.has_sub_faces: # flip sub-faces to align with parent Face
if norm_init.angle(face._geometry.normal) > (math.pi / 2):
for ap in face._apertures:
ap._geometry = ap._geometry.flip()
for dr in face._doors:
dr._geometry = dr._geometry.flip()
# reset the faces and geometry of the room with the new faces
self._faces = tuple(all_faces)
self._geometry = room_polyface
return new_faces

def is_geo_equivalent(self, room, tolerance=0.01):
"""Get a boolean for whether this object is geometrically equivalent to another.
Expand Down Expand Up @@ -1686,7 +1786,7 @@ def coplanar_split(self, geometry, tolerance=0.01, angle_tolerance=1):
continue # no overlap in bounding box; intersection impossible
s_geos = s_geo.faces if isinstance(s_geo, Polyface3D) else [s_geo]
for face_1 in self.faces:
for face_2 in s_geos:
for face_2 in s_geos:
if not face_1.geometry.plane.is_coplanar_tolerance(
face_2.plane, tolerance, ang_tol):
continue # not coplanar; intersection impossible
Expand Down

0 comments on commit 734a6a4

Please sign in to comment.