def test_polyface3d_init_from_faces_tolerance(): """Test the initialization of Polyface3D from_faces with a tolerance.""" pts_1 = [Point3D(0, 0, 0), Point3D(0, 2, 0), Point3D(2, 2, 0), Point3D(2, 0, 0)] pts_2 = [Point3D(0, 0, 0), Point3D(0, 0, 2), Point3D(0, 2, 2), Point3D(0, 2, 0)] pts_3 = [Point3D(0, 0, 0), Point3D(2, 0, 0), Point3D(2, 0, 2), Point3D(0, 0, 2)] pts_4 = [Point3D(2, 2, 0), Point3D(0, 2, 0), Point3D(0, 2, 2), Point3D(2, 2, 2)] pts_5 = [Point3D(2, 2, 0), Point3D(2, 0, 0), Point3D(2, 0, 2), Point3D(2, 2, 2)] pts_6 = [Point3D(0, 0, 2), Point3D(0, 2, 2), Point3D(2, 2, 2), Point3D(2, 0, 2)] pts_7 = [Point3D(0, 0, 2), Point3D(0, 2, 2), Point3D(2, 2, 2), Point3D(2, 0, 2.0001)] face_1 = Face3D(pts_1) face_2 = Face3D(pts_2) face_3 = Face3D(pts_3) face_4 = Face3D(pts_4) face_5 = Face3D(pts_5) face_6 = Face3D(pts_6) face_7 = Face3D(pts_7) polyface_1 = Polyface3D.from_faces( [face_1, face_2, face_3, face_4, face_5, face_6], 0.001) polyface_2 = Polyface3D.from_faces( [face_1, face_2, face_3, face_4, face_5, face_7], 0.001) polyface_3 = Polyface3D.from_faces( [face_1, face_2, face_3, face_4, face_5, face_7], 0.000001) assert polyface_1.is_solid assert polyface_2.is_solid assert not polyface_3.is_solid
def test_polyface3d_init_from_faces_coplanar(): """Test the initialization of Polyface3D from_faces with two coplanar faces.""" # this is an important case that must be solved # can be done by iterating through naked edges and finding colinear ones pts_1 = [Point3D(0, 0, 0), Point3D(0, 2, 0), Point3D(2, 2, 0), Point3D(2, 0, 0)] pts_2 = [Point3D(0, 0, 0), Point3D(0, 0, 2), Point3D(0, 2, 2), Point3D(0, 2, 0)] pts_3 = [Point3D(0, 0, 0), Point3D(2, 0, 0), Point3D(2, 0, 2), Point3D(0, 0, 2)] pts_4 = [Point3D(2, 2, 0), Point3D(0, 2, 0), Point3D(0, 2, 2), Point3D(2, 2, 2)] pts_5 = [Point3D(2, 2, 0), Point3D(2, 0, 0), Point3D(2, 0, 2), Point3D(2, 2, 2)] pts_6 = [Point3D(0, 0, 2), Point3D(0, 1, 2), Point3D(2, 1, 2), Point3D(2, 0, 2)] pts_7 = [Point3D(0, 1, 2), Point3D(0, 2, 2), Point3D(2, 2, 2), Point3D(2, 1, 2)] face_1 = Face3D(pts_1) face_2 = Face3D(pts_2) face_3 = Face3D(pts_3) face_4 = Face3D(pts_4) face_5 = Face3D(pts_5) face_6 = Face3D(pts_6) face_7 = Face3D(pts_7) polyface = Polyface3D.from_faces( [face_1, face_2, face_3, face_4, face_5, face_6, face_7], 0.01) polyface_2 = Polyface3D.from_faces( [face_1, face_2, face_3, face_4, face_5, face_7], 0.01) assert not polyface.is_solid assert len(polyface.naked_edges) != 0 new_polyface = polyface.merge_overlapping_edges(0.0001, 0.0001) assert new_polyface.is_solid assert len(new_polyface.naked_edges) == 0 assert len(new_polyface.internal_edges) == 13 new_polyface_2 = polyface_2.merge_overlapping_edges(0.0001, 0.0001) assert not new_polyface_2.is_solid assert len(new_polyface_2.naked_edges) != 0
def __init__(self, identifier, faces, tolerance=0, angle_tolerance=0): """Initialize Room.""" _BaseWithShade.__init__(self, identifier) # process the identifier # process the zone volume geometry if not isinstance(faces, tuple): faces = tuple(faces) for face in faces: assert isinstance(face, Face), \ 'Expected honeybee Face. Got {}'.format(type(face)) face._parent = self if tolerance == 0: self._faces = faces self._geometry = None # calculated later from faces or added by classmethods else: # try to get a closed volume between the faces room_polyface = Polyface3D.from_faces( tuple(face.geometry for face in faces), tolerance) if not room_polyface.is_solid and angle_tolerance != 0: ang_tol = math.radians(angle_tolerance) 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): faces[i]._geometry = correct_face3d self._faces = faces self._geometry = room_polyface self._multiplier = 1 # default value that can be overridden later self._story = None # default value that can be overridden later self._properties = RoomProperties(self) # properties for extensions
def check_solid(self, tolerance=0.01, angle_tolerance=1, raise_exception=True): """Check whether the Room is a closed solid to within the input tolerances. Args: tolerance: tolerance: The maximum difference between x, y, and z values at which face vertices are considered equivalent. This is used in determining whether the faces form a closed volume. Default: 0.01, suitable for objects in meters. angle_tolerance: The max angle difference in degrees that vertices are allowed to differ from one another in order to consider them colinear. Default: 1 degree. raise_exception: Boolean to note whether a ValueError should be raised if the room geometry does not form a closed solid. """ if self._geometry is not None and self.geometry.is_solid: return True face_geometries = tuple(face.geometry for face in self._faces) self._geometry = Polyface3D.from_faces(face_geometries, tolerance) if self.geometry.is_solid: return True ang_tol = math.radians(angle_tolerance) self._geometry = self.geometry.merge_overlapping_edges( tolerance, ang_tol) if self.geometry.is_solid: return True if raise_exception: raise ValueError( 'Room "{}" is not closed to within {} tolerance and {} angle ' 'tolerance.'.format(self.display_name, tolerance, angle_tolerance)) return False
def geometry(self): """Get a ladybug_geometry Polyface3D object representing the room.""" if self._geometry is None: self._geometry = Polyface3D.from_faces( tuple(face.geometry for face in self._faces), 0) # use 0 tolerance return self._geometry
def to_polyface3d(geo, meshing_parameters=None): """A Ladybug Polyface3D object from a Rhino Brep. Args: geo: A Rhino Brep, Surface or Mesh that will be converted into a single Ladybug Polyface3D. meshing_parameters: Optional Rhino Meshing Parameters to describe how curved faces should be converted into planar elements. If None, Rhino's Default Meshing Parameters will be used. """ mesh_par = meshing_parameters or rg.MeshingParameters.Default # default if not isinstance( geo, rg.Mesh) and _planar.has_curved_face(geo): # keep solidity return Polyface3D.from_faces(_planar.curved_solid_faces(geo, mesh_par), tolerance) return Polyface3D.from_faces(to_face3d(geo, mesh_par), tolerance)
def test_polyface3d_to_from_dict_with_overlap(): """Test the to/from dict of Polyface3D objects with overlapping edges.""" pts_1 = [Point3D(0, 0, 0), Point3D(0, 2, 0), Point3D(2, 2, 0), Point3D(2, 0, 0)] pts_2 = [Point3D(0, 0, 0), Point3D(0, 0, 2), Point3D(0, 2, 2), Point3D(0, 2, 0)] pts_3 = [Point3D(0, 0, 0), Point3D(2, 0, 0), Point3D(2, 0, 2), Point3D(0, 0, 2)] pts_4 = [Point3D(2, 2, 0), Point3D(0, 2, 0), Point3D(0, 2, 2), Point3D(2, 2, 2)] pts_5 = [Point3D(2, 2, 0), Point3D(2, 0, 0), Point3D(2, 0, 2), Point3D(2, 2, 2)] pts_6 = [Point3D(0, 0, 2), Point3D(0, 1, 2), Point3D(2, 1, 2), Point3D(2, 0, 2)] pts_7 = [Point3D(0, 1, 2), Point3D(0, 2, 2), Point3D(2, 2, 2), Point3D(2, 1, 2)] face_1 = Face3D(pts_1) face_2 = Face3D(pts_2) face_3 = Face3D(pts_3) face_4 = Face3D(pts_4) face_5 = Face3D(pts_5) face_6 = Face3D(pts_6) face_7 = Face3D(pts_7) polyface = Polyface3D.from_faces( [face_1, face_2, face_3, face_4, face_5, face_6, face_7], 0.01) new_polyface = polyface.merge_overlapping_edges(0.0001, 0.0001) assert new_polyface.is_solid assert len(new_polyface.naked_edges) == 0 assert len(new_polyface.internal_edges) == 13 polyface_dict = new_polyface.to_dict() dict_polyface = Polyface3D.from_dict(polyface_dict) assert isinstance(dict_polyface, Polyface3D) assert dict_polyface.to_dict() == polyface_dict assert dict_polyface.is_solid assert len(dict_polyface.naked_edges) == 0 assert len(dict_polyface.internal_edges) == 13
def test_polyface3d_init_from_faces_open(): """Test the initialization of Polyface3D from_faces with an open object.""" pts_1 = [Point3D(0, 0, 0), Point3D(0, 2, 0), Point3D(2, 2, 0), Point3D(2, 0, 0)] pts_2 = [Point3D(0, 0, 0), Point3D(0, 0, 2), Point3D(0, 2, 2), Point3D(0, 2, 0)] pts_3 = [Point3D(0, 0, 0), Point3D(2, 0, 0), Point3D(2, 0, 2), Point3D(0, 0, 2)] pts_4 = [Point3D(2, 2, 0), Point3D(0, 2, 0), Point3D(0, 2, 2), Point3D(2, 2, 2)] pts_5 = [Point3D(2, 2, 0), Point3D(2, 0, 0), Point3D(2, 0, 2), Point3D(2, 2, 2)] face_1 = Face3D(pts_1) face_2 = Face3D(pts_2) face_3 = Face3D(pts_3) face_4 = Face3D(pts_4) face_5 = Face3D(pts_5) polyface = Polyface3D.from_faces([face_1, face_2, face_3, face_4, face_5], 0.01) assert len(polyface.vertices) == 8 assert len(polyface.face_indices) == 5 assert len(polyface.faces) == 5 assert len(polyface.edge_indices) == 12 assert len(polyface.edges) == 12 assert len(polyface.naked_edges) == 4 assert len(polyface.non_manifold_edges) == 0 assert len(polyface.internal_edges) == 8 assert polyface.area == 20 assert polyface.volume == 0 assert polyface.is_solid is False for face in polyface.faces: assert face.area == 4 assert face.is_clockwise is False
def __init__(self, name, faces, tolerance=None, angle_tolerance=None): """A volume enclosed by faces, representing a single room or space. Note that, if None is input for tolerance and angle_tolerance, no checks will be performed to determine whether the room is a closed volume and no attempt will be made to flip faces in the event that they are not facing outward from the room volume. As such, an input tolerance of None is intended for workflows where the solidity of the room volume has been evaluated elsewhere. Args: name: Room name. Must be < 100 characters. faces: A list or tuple of honeybee Face objects that together form the closed volume of a room. tolerance: The maximum difference between x, y, and z values at which vertices of adjacent faces are considered equivalent. This is used in determining whether the faces form a closed volume. Default is None, which makes no attempt to evaluate whether the Room volume is closed. angle_tolerance: The max angle difference in degrees that vertices are allowed to differ from one another in order to consider them colinear. Default is None, which makes no attempt to evaluate whether the Room volume is closed. """ _BaseWithShade.__init__(self, name) # process the name # process the zone volume geometry if not isinstance(faces, tuple): faces = tuple(faces) for face in faces: assert isinstance(face, Face), \ 'Expected honeybee Face. Got {}'.format(type(face)) face._parent = self if tolerance is None: self._faces = faces self._geometry = None # calculated later from faces or added by classmethods else: # try to get a closed volume between the faces room_polyface = Polyface3D.from_faces( tuple(face.geometry for face in faces), tolerance) if not room_polyface.is_solid and angle_tolerance is not None: ang_tol = math.radians(angle_tolerance) 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): faces[i]._geometry = correct_face3d self._faces = faces self._geometry = room_polyface self._multiplier = 1 # default value that can be overridden later self._properties = RoomProperties(self) # properties for extensions
def floor_geometry(self, tolerance=0.01): """Get a ladybug_geometry Polyface3D object representing the floor plate. Args: tolerance: The minimum distance between points at which they are not considered touching. Default: 0.01, suitable for objects in meters. """ story_height = self.floor_height room_floors = [] for room in self.room_2ds: diff = story_height - room.floor_height if abs(diff) <= tolerance: room_floors.append(room.floor_geometry) else: room_floors.append(room.floor_geometry.move(Vector3D(0, 0, diff))) # TODO: consider returning a list of polyfaces if input rooms are disjointed return Polyface3D.from_faces(room_floors, tolerance)
def remove_colinear_vertices_envelope(self, tolerance=0.01): """Remove colinear and duplicate vertices from this object's Faces and Sub-faces. Note that this does not affect any assigned Shades. Args: tolerance: The minimum distance between a vertex and the boundary segments at which point the vertex is considered colinear. Default: 0.01, suitable for objects in meters. """ for face in self._faces: face.remove_colinear_vertices(tolerance) for ap in face._apertures: ap.remove_colinear_vertices(tolerance) for dr in face._doors: dr.remove_colinear_vertices(tolerance) if self._geometry is not None: self._geometry = Polyface3D.from_faces( tuple(face.geometry for face in self._faces), tolerance)
def test_polyface3d_init_from_faces_solid(): """Test the initialization of Polyface3D from_faces with a solid.""" pts_1 = [Point3D(0, 0, 0), Point3D(0, 2, 0), Point3D(2, 2, 0), Point3D(2, 0, 0)] pts_2 = [Point3D(0, 0, 0), Point3D(0, 0, 2), Point3D(0, 2, 2), Point3D(0, 2, 0)] pts_3 = [Point3D(0, 0, 0), Point3D(2, 0, 0), Point3D(2, 0, 2), Point3D(0, 0, 2)] pts_4 = [Point3D(2, 2, 0), Point3D(0, 2, 0), Point3D(0, 2, 2), Point3D(2, 2, 2)] pts_5 = [Point3D(2, 2, 0), Point3D(2, 0, 0), Point3D(2, 0, 2), Point3D(2, 2, 2)] pts_6 = [Point3D(0, 0, 2), Point3D(0, 2, 2), Point3D(2, 2, 2), Point3D(2, 0, 2)] face_1 = Face3D(pts_1) face_2 = Face3D(pts_2) face_3 = Face3D(pts_3) face_4 = Face3D(pts_4) face_5 = Face3D(pts_5) face_6 = Face3D(pts_6) polyface = Polyface3D.from_faces( [face_1, face_2, face_3, face_4, face_5, face_6], 0.01) assert len(polyface.vertices) == 8 assert len(polyface.face_indices) == 6 assert len(polyface.faces) == 6 assert len(polyface.edge_indices) == 12 assert len(polyface.edges) == 12 assert len(polyface.naked_edges) == 0 assert len(polyface.non_manifold_edges) == 0 assert len(polyface.internal_edges) == 12 assert polyface.area == 24 assert polyface.volume == 8 assert polyface.is_solid for face in polyface.faces: assert face.area == 4 assert face.is_clockwise is False assert polyface.faces[0].normal == Vector3D(0, 0, -1) assert polyface.faces[1].normal == Vector3D(-1, 0, 0) assert polyface.faces[2].normal == Vector3D(0, -1, 0) assert polyface.faces[3].normal == Vector3D(0, 1, 0) assert polyface.faces[4].normal == Vector3D(1, 0, 0) assert polyface.faces[5].normal == Vector3D(0, 0, 1)
def test_is_solid_with_hole(): """Test the is_solid property for a polyface with a hole. This ensures that the is_solid property still works where the Euler characteristic fails. """ pts_1 = [Point3D(0, 0, 2), Point3D(0, 0, 0), Point3D(4, 0, 0), Point3D(4, 0, 2)] pts_2 = [Point3D(4, 0, 2), Point3D(4, 0, 0), Point3D(4, 4, 0), Point3D(4, 4, 2)] pts_3 = [Point3D(4, 4, 2), Point3D(4, 4, 0), Point3D(0, 4, 0), Point3D(0, 4, 2)] pts_4 = [Point3D(0, 4, 2), Point3D(0, 4, 0), Point3D(0, 0, 0), Point3D(0, 0, 2)] pts_5 = [Point3D(0, 0, 0), Point3D(0, 4, 0), Point3D(1, 3, 1), Point3D(1, 1, 1)] pts_6 = [Point3D(4, 0, 0), Point3D(0, 0, 0), Point3D(1, 1, 1), Point3D(3, 1, 1)] pts_7 = [Point3D(4, 4, 0), Point3D(4, 0, 0), Point3D(3, 1, 1), Point3D(3, 3, 1)] pts_8 = [Point3D(0, 4, 0), Point3D(4, 4, 0), Point3D(3, 3, 1), Point3D(1, 3, 1)] pts_9 = [Point3D(1, 1, 1), Point3D(1, 3, 1), Point3D(0, 4, 2), Point3D(0, 0, 2)] pts_10 = [Point3D(3, 1, 1), Point3D(1, 1, 1), Point3D(0, 0, 2), Point3D(4, 0, 2)] pts_11 = [Point3D(3, 3, 1), Point3D(3, 1, 1), Point3D(4, 0, 2), Point3D(4, 4, 2)] pts_12 = [Point3D(1, 3, 1), Point3D(3, 3, 1), Point3D(4, 4, 2), Point3D(0, 4, 2)] face_1 = Face3D(pts_1) face_2 = Face3D(pts_2) face_3 = Face3D(pts_3) face_4 = Face3D(pts_4) face_5 = Face3D(pts_5) face_6 = Face3D(pts_6) face_7 = Face3D(pts_7) face_8 = Face3D(pts_8) face_9 = Face3D(pts_9) face_10 = Face3D(pts_10) face_11 = Face3D(pts_11) face_12 = Face3D(pts_12) polyface = Polyface3D.from_faces([face_1, face_2, face_3, face_4, face_5, face_6, face_7, face_8, face_9, face_10, face_11, face_12], 0.01) assert len(polyface.faces) + len(polyface.vertices) - len(polyface.edges) != 2 assert polyface.area == pytest.approx(65.941125, rel=1e-3) assert polyface.volume == pytest.approx(13.333333, rel=1e-3) assert polyface.is_solid
def geometry(self): """A ladybug_geometry Polyface3D object representing the room.""" if self._geometry is None: self._geometry = Polyface3D.from_faces( tuple(face.geometry for face in self._faces)) return self._geometry