From 734a6a4eef25a056834e18a25d3f66dc484ada24 Mon Sep 17 00:00:00 2001 From: Chris Mackey Date: Fri, 6 Dec 2024 15:00:31 -0800 Subject: [PATCH] fix(room): Add a method to split Room Faces through holes --- honeybee/room.py | 102 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 101 insertions(+), 1 deletion(-) diff --git a/honeybee/room.py b/honeybee/room.py index fef8004c..1e2f23e7 100644 --- a/honeybee/room.py +++ b/honeybee/room.py @@ -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. @@ -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