Source code for honeybee_idaice.face

import math
from typing import Union

from ladybug_geometry.geometry2d import Point2D, Polygon2D, Vector2D
from ladybug_geometry.geometry3d import Point3D, Vector3D, Plane, Face3D
from honeybee.model import Face, Aperture, Door


def _is_straight_rectangle(opening_poly: Polygon2D, ang_tol):
    """Check if the window is rectangular and in parallel to XY axis."""
    if len(opening_poly.vertices) != 4:
        return False
    if not opening_poly.is_rectangle(ang_tol):
        return False
    x_axis_2d = Vector2D(1, 0)
    first_edge = opening_poly[1] - opening_poly[0]
    first_edge_ang = x_axis_2d.angle(first_edge)
    # technically we should not need to check for both angles but
    # from the tests that I (mostapha) have seen the sorted coordinates
    # are not always sorted from lower-left and counter clockwise.
    if not (
        first_edge_ang <= ang_tol
        or abs(first_edge_ang - math.pi) <= ang_tol
    ):
        return False
    return True


[docs] def opening_to_idm( opening: Union[Aperture, Door], ref_plane: Plane, is_aperture=True, decimal_places: int = 3, angle_tolerance: float = 1.0) -> str: """Translate a HBJSON aperture or Door to an IDM Window. Args: opening: A Honeybee Aperture or Door to be translated to IDM. ref_plane: A ladybug-geometry Plane object for the reference Plane of the parent geometry. This plane should be pointing inwards towards the Room volume. is_aperture: A boolean to note whether the opening is an Aperture or a Door. (Default: True). decimal_places: An integer for the number of decimal places to which coordinate values will be rounded. (Default: 3). angle_tolerance: The max angle in degrees that opening normal can differ from the World Z before the opening is treated as being in the World XY plane. (Default: 1). """ ang_tol = math.radians(angle_tolerance) # IDA-ICE looks to apertures from inside the room opening_geo = opening.geometry.flip() corners_idm = '' ver_count = len(opening_geo.vertices) # rectangle based on opening reference plane apt_llc = opening_geo.lower_left_corner apt_urc = opening_geo.upper_right_corner def opening_corners_to_idm( opening_geo: Face3D, ref_plane: Plane, min_2d: Point2D, ang_tol: float, is_horizontal): # calculate 2D polygon opening_poly = Polygon2D( [ ref_plane.xyz_to_xy(v) for v in opening_geo.lower_left_counter_clockwise_vertices ] ) if _is_straight_rectangle(opening_poly, ang_tol): # no need to use corners method return '' min_2d_apt = ref_plane.xyz_to_xy(opening_geo.flip().lower_left_corner) if is_horizontal and min_2d.y + min_2d_apt.y < 0.001: corners = ' '.join( f'({round(v.x - min_2d.x, decimal_places)} ' f'{round(-v.y - min_2d.y, decimal_places)})' for v in opening_poly.vertices ) else: corners = ' '.join( f'({round(v.x - min_2d.x, decimal_places)} ' f'{round(v.y - min_2d.y, decimal_places)})' for v in opening_poly.vertices ) corners_idm = '\n ((AGGREGATE :N SHAPE :T SHAPE2D)\n' \ f' (:PAR :N NCORN :V {ver_count} :S (:DEFAULT NIL 2))\n' \ f' (:PAR :N CORNERS :DIM ({ver_count} 2) :V #2A({corners}))\n' \ f' (:PAR :N CONTOURS :V NIL))' return corners_idm # if the aperture is horizontal, use the world XY vertical = Vector3D(0, 0, 1) vert_ang = ref_plane.n.angle(vertical) is_horizontal = vert_ang <= ang_tol or vert_ang >= math.pi - ang_tol if is_horizontal: # horizontal aperture min_2d = Point2D(opening.min.x - ref_plane.o.x, opening.min.y - ref_plane.o.y) max_2d = Point2D(opening.max.x - ref_plane.o.x, opening.max.y - ref_plane.o.y) else: min_2d = ref_plane.xyz_to_xy(apt_llc) max_2d = ref_plane.xyz_to_xy(apt_urc) height = round(max_2d.y - min_2d.y, decimal_places) width = round(max_2d.x - min_2d.x, decimal_places) name = opening.identifier corners_idm = opening_corners_to_idm( opening_geo, ref_plane, min_2d, ang_tol, is_horizontal ) if is_aperture: opening_idm = f'\n ((CE-WINDOW :N "{name}" :T WINDOW)\n' \ f' (:PAR :N X :V {round(min_2d.x, decimal_places)})\n' \ f' (:PAR :N Y :V {round(min_2d.y, decimal_places)})\n' \ f' (:PAR :N DX :V {width})\n' \ f' (:PAR :N DY :V {height}){corners_idm})' else: opening_idm = f'\n ((OPENING :N "{name}" :T OPENING)\n' \ f' (:PAR :N X :V {round(min_2d.x, decimal_places)})\n' \ f' (:PAR :N Y :V {round(min_2d.y, decimal_places)})\n' \ f' (:PAR :N DX :V {width})\n' \ f' (:PAR :N DY :V {height})\n' \ f' (:RES :N OPENING-SCHEDULE :V ALWAYS_OFF){corners_idm})' return opening_idm
[docs] def face_to_idm( face: Face, origin: Point3D, index: int, angle_tolerance: float = 1.0, decimal_places: int = 3 ): """Translate a HBJSON face to an IDM ENCLOSING-ELEMENT. Args: face: A Honeybee Face to be translated to IDM. origin: A Point3D for the origin of the parent Room. index: An integer for the index of the Face in the parent Room. The index starts from 1 for Walls, -1000 for ceilings and -2000 from floors. angle_tolerance: The max angle in degrees that Face normal can differ from the World Z before the Face is treated as being in the World XY plane. (Default: 1). decimal_places: An integer for the number of decimal places to which coordinate values will be rounded. (Default: 3). """ # translate the vertices of the the Face into IDM format _face_mapper = { 'RoofCeiling': 'CEILING', 'Floor': 'FLOOR', 'Wall': 'WALL' } name = face.identifier type_ = _face_mapper[str(face.type)] geometry = face.geometry holes = geometry.holes bv = list(geometry.boundary) if not holes: contours = [bv] count = len(bv) contours_formatted = '' else: contours = [bv] + [list(h) for h in holes] count = sum(len(c) for c in contours) contours_formatted = ' '.join(str(len(c)) for c in contours) dpl = decimal_places verts_idm = ' '.join(( f'({round(v.x - origin.x, dpl)} ' f'{round(v.y - origin.y, dpl)} ' f'{round(v.z - origin.z, dpl)})' for vertices in contours for v in vertices )) # compute the reference plane of the Face if it has Apertures or doors ref_plane = face_reference_plane(face, angle_tolerance) \ if face.has_sub_faces else None # add apertures windows = [''] for aperture in face.apertures: if aperture.user_data and aperture.user_data.get('_idm_ignore', False): continue windows.append(opening_to_idm(aperture, ref_plane, decimal_places=dpl)) windows = ''.join(windows) # add doors doors = [''] for door in face.doors: if door.user_data and door.user_data.get('_idm_ignore', False): continue is_aperture = True if door.is_glass else False doors.append(opening_to_idm(door, ref_plane, is_aperture, decimal_places=dpl)) doors = ''.join(doors) face = f'((ENCLOSING-ELEMENT :N "{name}" :T {type_} :INDEX {index})\n' \ f' ((AGGREGATE :N GEOMETRY)\n' \ f' (:PAR :N CORNERS :DIM ({count} 3) :SP ({count} 3) :V #2A({verts_idm}))\n' \ f' (:PAR :N CONTOURS :V ({contours_formatted}))\n' \ f' (:PAR :N SLOPE :V {round(face.altitude + 90, 2)})){windows}\n{doors})' return face
[docs] def face_reference_plane(face: Face, angle_tolerance: float = 1.0): """Get a reference plane that is used to translate openings for a Face. This plane will point inwards to the Room geometry and start in the lower left corner of the Face. Args: face: A Face from which the reference plane will be derived. angle_tolerance: The max angle in degrees that Face normal can differ from the World Z before the Face is treated as being in the World XY plane. (Default: 1). """ # IDA-ICE looks to apertures from inside the room parent = face.geometry.flip() parent_llc = parent.lower_left_corner rel_plane = parent.plane # use the XY plane if the Face is perfectly horizontal ang_tol = math.radians(angle_tolerance) vertical = Vector3D(0, 0, 1) vert_ang = rel_plane.n.angle(vertical) if vert_ang <= ang_tol or vert_ang >= math.pi - ang_tol: parent_llc = parent.min proj_x = Vector3D(1, 0, 0) else: proj_y = Vector3D(0, 0, 1).project(rel_plane.n) proj_x = proj_y.rotate(rel_plane.n, math.pi / -2) return Plane(rel_plane.n, parent_llc, proj_x)