Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support non-rectangular windows and shade meshes #42

Merged
merged 2 commits into from
Nov 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 3 additions & 15 deletions honeybee_idaice/cli/translate.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,6 @@ def translate():
'interior geometries are not perfectly matching one another.',
type=str, default='0.4m', show_default=True
)
@click.option(
'--frame-thickness', '-f', help='Maximum thickness of the window frame. This '
'can include the units of the distance (eg. 4in) or, if no units are provided, '
'the value will be assumed to be in meters (the native units of IDA-ICE). '
'This will be used to join any non-rectangular Apertures together in'
'an attempt to better rectangularize them for IDM.',
type=str, default='0.1m', show_default=True
)
@click.option(
'--name', '-n', help='Deprecated option to set the name of the output file.',
default=None, show_default=True
Expand All @@ -68,8 +60,7 @@ def translate():
'of the translation. By default this will be printed out to stdout.',
type=click.File('w'), default='-', show_default=True)
def model_to_idm(
model_json, wall_thickness, adjacency_distance, frame_thickness,
name, folder, output_file
model_json, wall_thickness, adjacency_distance, name, folder, output_file
):
"""Translate a Model JSON file to an IDA-ICE IDM file.
\b
Expand All @@ -81,7 +72,6 @@ def model_to_idm(
# convert distance strings to floats
wall_thickness = parse_distance_string(str(wall_thickness), 'Meters')
adjacency_distance = parse_distance_string(str(adjacency_distance), 'Meters')
frame_thickness = parse_distance_string(str(frame_thickness), 'Meters')

# translate the Model to IDM
model = Model.from_file(model_json)
Expand All @@ -90,8 +80,7 @@ def model_to_idm(
folder.mkdir(parents=True, exist_ok=True)
model.to_idm(folder.as_posix(), name=name, debug=False,
max_int_wall_thickness=wall_thickness,
max_adjacent_sub_face_dist=adjacency_distance,
max_frame_thickness=frame_thickness)
max_adjacent_sub_face_dist=adjacency_distance)
else:
if output_file.name == '<stdout>': # get a temporary file
out_file = str(uuid.uuid4())[:6]
Expand All @@ -100,8 +89,7 @@ def model_to_idm(
out_folder, out_file = os.path.split(output_file.name)
idm_file = model.to_idm(out_folder, name=out_file, debug=False,
max_int_wall_thickness=wall_thickness,
max_adjacent_sub_face_dist=adjacency_distance,
max_frame_thickness=frame_thickness)
max_adjacent_sub_face_dist=adjacency_distance)
if output_file.name == '<stdout>': # load file contents to stdout
with open(idm_file, 'rb') as of: # IDM can only be read as binary
f_contents = of.read()
Expand Down
97 changes: 80 additions & 17 deletions honeybee_idaice/face.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,31 @@
import math
from typing import Union

from ladybug_geometry.geometry2d import Point2D
from ladybug_geometry.geometry3d import Point3D, Vector3D, Plane
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


def opening_to_idm(
opening: Union[Aperture, Door], ref_plane: Plane,
is_aperture=True, decimal_places: int = 3, angle_tolerance: float = 1.0) -> str:
Expand All @@ -24,42 +44,85 @@ def opening_to_idm(
from the World Z before the opening is treated as being in the
World XY plane. (Default: 1).
"""
# get the name
name = opening.identifier
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
ang_tol = math.radians(angle_tolerance)
vertical = Vector3D(0, 0, 1)
vert_ang = ref_plane.n.angle(vertical)
if vert_ang <= ang_tol or vert_ang >= math.pi - ang_tol:
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)
height = round(max_2d.y - min_2d.y, decimal_places)
width = round(max_2d.x - min_2d.x, decimal_places)
else:
# IDA-ICE looks to apertures from inside the room
opening = opening.geometry.flip()
# get the rectangle in the reference plane
apt_llc = opening.lower_left_corner
apt_urc = opening.upper_right_corner
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)

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}))'
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))'
f' (:RES :N OPENING-SCHEDULE :V ALWAYS_OFF){corners_idm})'

return opening_idm

Expand Down
70 changes: 54 additions & 16 deletions honeybee_idaice/shade.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import List, Union

from honeybee.model import Shade
from honeybee.model import Shade, ShadeMesh
from ladybug_geometry.geometry3d import Point3D, Face3D, Polyface3D, Mesh3D


Expand All @@ -14,13 +14,14 @@ def _vertices_to_idm(vertices: List[Point3D], dec_places: int = 3) -> str:


def _shade_geometry_to_idm(
geometry: Union[Face3D, Polyface3D], name: str, decimal_places: int = 3
geometry: Union[Face3D, Polyface3D, Mesh3D],
name: str, decimal_places: int = 3
):
"""Create an IDM shade block from a Ladybug geometry.

Here is an example:

((AGGREGATE :N "shade1" :T PICT3D)
((AGGREGATE-VTK :N "shade1" :T PICT3D)
(:PAR :N FILE :V "")
(:PAR :N POS :V #(0 0 0.0))
(:PAR :N SHADOWING :V :TRUE)
Expand All @@ -37,6 +38,8 @@ def _shade_geometry_to_idm(

if isinstance(geometry, Face3D):
mesh_3d = geometry.triangulated_mesh3d
elif isinstance(geometry, Mesh3D):
mesh_3d = geometry
else:
# it is a Polyface3D
meshes = [face.triangulated_mesh3d for face in geometry.faces]
Expand All @@ -52,18 +55,19 @@ def _shade_geometry_to_idm(
joined_faces = ' '.join(' '.join(str(f) for f in ff) for ff in faces)

shd_verts = _vertices_to_idm(vertices, decimal_places)
shade = f' ((AGGREGATE :N "{name}" :T PICT3D)\n' \
' (:PAR :N FILE :V "")\n' \
' (:PAR :N SHADOWING :V :TRUE)\n' \
' ((AGGREGATE :N "geom1" :T GEOM3D)\n' \
f' (:PAR :N NPOINTS :V {vertices_count})\n' \
f' (:PAR :N POINTS :DIM ({vertices_count} 3) :V #2A({shd_verts}))\n' \
' (:PAR :N CELLTYPE :V 1)\n' \
f' (:PAR :N NCELLS :V {face_count})\n' \
f' (:PAR :N NVERTICES :DIM ({face_count}) :V #({faces_count}))\n' \
f' (:PAR :N TOTNVERTS :V {total_vertices})\n' \
f' (:PAR :N VERTICES :DIM ({total_vertices}) :V #({joined_faces}))\n' \
' (:PAR :N PROPERTY :V #(1.0 1.0 1.0 0.699999988079071 1.0 1.0 1.0 0.5 1.0 1.0 1.0 0.0 1.0 1.0 1.0 0.0 0.0))))'
shade = f' ((AGGREGATE-VTK :N "{name}" :T PICT3D)\n' \
' (:PAR :N FILE :V "")\n' \
' (:PAR :N SHADOWING :V :TRUE)\n' \
' ((AGGREGATE :N "geom1" :T GEOM3D)\n' \
f' (:PAR :N NPOINTS :V {vertices_count})\n' \
f' (:PAR :N POINTS :DIM ({vertices_count} 3) :V #2A({shd_verts}))\n' \
' (:PAR :N CELLTYPE :V 1)\n' \
f' (:PAR :N NCELLS :V {face_count})\n' \
f' (:PAR :N NVERTICES :DIM ({face_count}) :V #({faces_count}))\n' \
f' (:PAR :N TOTNVERTS :V {total_vertices})\n' \
f' (:PAR :N VERTICES :DIM ({total_vertices}) :V #({joined_faces}))\n' \
' (:PAR :N PROPERTY :V #(1.0 1.0 1.0 0.699999988079071 1.0 1.0 1.0 0.5 1.0 1.0 1.0 0.0 1.0 1.0 1.0 0.0 0.0)))\n' \
f' )'

return shade

Expand Down Expand Up @@ -141,4 +145,38 @@ def shades_to_idm(shades: List[Shade], tolerance: float, decimal_places: int = 3
for shades in filtered_groups.values()]
)

return f'((AGGREGATE :N ARCDATA)\n{single_shades}\n{group_shades})'
return f'{single_shades}\n{group_shades}\n'


def shade_meshes_to_idm(shades: List[ShadeMesh], tolerance: float, decimal_places: int = 3):
"""Convert a list of Shades to a IDM string.

Args:
shades: A list of Honeybee ShadeMeshes.
tolerance: The maximum difference between X, Y, and Z values at which point
vertices are considered distinct from one another.
decimal_places: An integer for the number of decimal places to which
coordinate values will be rounded. (Default: 3).

Returns:
A formatted string that represents this shade in IDM format.

"""
if not shades:
return ''

shade_idms = []
for shade in shades:
shade.triangulate_and_remove_degenerate_faces(tolerance)
name = '_'.join(
(
' '.join(shade.display_name.split()),
shade.identifier.replace('Shade_', '')
)
)

shade_idms.append(
_shade_geometry_to_idm(shade.geometry, name, decimal_places)
)

return '\n'.join(shade_idms)
2 changes: 0 additions & 2 deletions honeybee_idaice/templates/building.idm
Original file line number Diff line number Diff line change
Expand Up @@ -725,8 +725,6 @@
(:PAR :N TRADER :V "Facility")
(:PAR :N METER-ROLE :V ENVIRONMENT)
(:PAR :N COLOR :V #S(RGB RED 33 GREEN 186 BLUE 200)))
((SITE-OBJECT :N SITE)
(:PAR :N SITE-AREA :V #(-100.0 -80.0 150.0 100.0)))
((SIMULATION_DATA :N SIMULATION_DATA)
((SIMULATION_PHASE :N STARTUP-PHASE)
(:PAR :N FROM-TIME :V 3911846400)
Expand Down
31 changes: 19 additions & 12 deletions honeybee_idaice/writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from .archive import zip_folder_to_idm
from .bldgbody import section_to_idm, MAX_FLOOR_ELEVATION_DIFFERENCE, \
IDA_ICE_BUILDING_BODY_TOL
from .shade import shades_to_idm
from .shade import shades_to_idm, shade_meshes_to_idm
from .face import face_to_idm, opening_to_idm, face_reference_plane


Expand Down Expand Up @@ -390,7 +390,7 @@ def prepare_folder(bldg_name: str, out_folder: str) -> List[pathlib.Path]:
def model_to_idm(
model: Model, out_folder: pathlib.Path, name: str = None,
max_int_wall_thickness: float = 0.40, max_adjacent_sub_face_dist: float = 0.40,
max_frame_thickness: float = 0.1, debug: bool = False):
debug: bool = False):
"""Translate a Honeybee model to an IDM file.

Args:
Expand All @@ -412,9 +412,6 @@ def model_to_idm(
set this to zero (like some cases of max_int_wall_thickness),
particularly when the adjacent interior geometries are not matching
one another. (Default: 0.40).
max_frame_thickness: Maximum thickness of the window frame in meters.
This will be used to join any non-rectangular Apertures together in
an attempt to better rectangularize them for IDM. (Default: 0.1).
debug: Set to True to not to delete the IDM folder before zipping it into a
single file.
"""
Expand All @@ -436,10 +433,6 @@ def model_to_idm(
for room in model.rooms:
room.merge_coplanar_faces(
model.tolerance, model.angle_tolerance, orthogonal_only=True)
# convert all apertures to be rectangular, using the model tolerances
ap_dist = max_frame_thickness if max_frame_thickness > model.tolerance \
else model.tolerance
model.rectangularize_apertures(max_separation=ap_dist, resolve_adjacency=False)

# edit the model display_names and add user_data to help with the translation
adj_dist = max_adjacent_sub_face_dist \
Expand Down Expand Up @@ -479,6 +472,23 @@ def model_to_idm(
line = line[1:]
bldg.write(line)

# site object
site_idm = '((SITE-OBJECT :N SITE)\n' \
' (:PAR :N SITE-AREA :V #(-100.0 -80.0 150.0 100.0))\n'
bldg.write(site_idm)
has_shade = model.shade_meshes or model.shades
if has_shade:
bldg.write(' ((AGGREGATE :N ARCDATA)\n')
# add shades to building if any
shades_idm = shades_to_idm(model.shades, model.tolerance, dec_count)
bldg.write(shades_idm)
shades_idm = shade_meshes_to_idm(model.shade_meshes, model.tolerance, dec_count)
bldg.write(shades_idm)
# end of site object
if has_shade:
bldg.write('))\n')
else:
bldg.write(')\n')
# create a building sections/bodies for the building
sections = section_to_idm(
model, max_int_wall_thickness=max_int_wall_thickness,
Expand All @@ -490,9 +500,6 @@ def model_to_idm(
for room in model.rooms:
bldg.write(f'((CE-ZONE :N "{room.display_name}" :T ZONE))\n')

# add shades to building
shades_idm = shades_to_idm(model.shades, model.tolerance, dec_count)
bldg.write(shades_idm)
bldg.write(f'\n;[end of {bldg_name}.idm]\n')

# copy all the template files
Expand Down
Loading