Source code for honeybee.shademesh

# coding: utf-8
"""Honeybee ShadeMesh."""
from __future__ import division
import math

from ladybug_geometry.geometry3d import Mesh3D, Face3D
from ladybug.color import Color

from ._base import _Base
from .typing import clean_string
from .properties import ShadeMeshProperties
import honeybee.writer.shademesh as writer


[docs] class ShadeMesh(_Base): """A single planar shade. Args: identifier: Text string for a unique Shade ID. Must be < 100 characters and not contain any spaces or special characters. geometry: A ladybug-geometry Mesh3D. is_detached: Boolean to note whether this object is detached from other geometry. Cases where this should be True include shade representing surrounding buildings or context. (Default: True). Properties: * identifier * display_name * is_detached * geometry * vertices * faces * center * area * min * max * type_color * bc_color * user_data """ __slots__ = ('_geometry', '_is_detached') TYPE_COLORS = { False: Color(120, 75, 190), True: Color(80, 50, 128) } BC_COLOR = Color(120, 75, 190) def __init__(self, identifier, geometry, is_detached=True): """A single planar shade.""" _Base.__init__(self, identifier) # process the identifier # process the geometry and basic properties assert isinstance(geometry, Mesh3D), \ 'Expected ladybug_geometry Mesh3D. Got {}'.format(type(geometry)) self._geometry = geometry self.is_detached = is_detached # initialize properties for extensions self._properties = ShadeMeshProperties(self)
[docs] @classmethod def from_dict(cls, data): """Initialize an ShadeMesh from a dictionary. Args: data: A dictionary representation of an ShadeMesh object. """ try: # check the type of dictionary assert data['type'] == 'ShadeMesh', 'Expected ShadeMesh dictionary. ' \ 'Got {}.'.format(data['type']) is_detached = data['is_detached'] if 'is_detached' in data else True shade = cls( data['identifier'], Mesh3D.from_dict(data['geometry']), is_detached) if 'display_name' in data and data['display_name'] is not None: shade.display_name = data['display_name'] if 'user_data' in data and data['user_data'] is not None: shade.user_data = data['user_data'] if data['properties']['type'] == 'ShadeMeshProperties': shade.properties._load_extension_attr_from_dict(data['properties']) return shade except Exception as e: cls._from_dict_error_message(data, e)
@property def is_detached(self): """Get or set a boolean for whether this object is detached from other geometry. """ return self._is_detached @is_detached.setter def is_detached(self, value): try: self._is_detached = bool(value) except TypeError: raise TypeError( 'Expected boolean for ShadeMesh.is_detached. Got {}.'.format(value)) @property def geometry(self): """Get a ladybug_geometry Mesh3D object representing the Shade.""" return self._geometry @property def vertices(self): """Get a tuple of ladybug_geometry Point3D for the vertices of the mesh.""" return self._geometry.vertices @property def faces(self): """Get a tuple of tuples for the faces of the mesh.""" return self._geometry.faces @property def center(self): """Get a ladybug_geometry Point3D for the center of the shade. Note that this is the center of the bounding box around this geometry and not the area or volume centroid. """ return self._geometry.center @property def area(self): """Get the surface area of the shade mesh.""" return self._geometry.area @property def min(self): """Get a Point3D for the minimum of the bounding box around the object.""" return self._geometry.min @property def max(self): """Get a Point3D for the maximum of the bounding box around the object.""" return self._geometry.max @property def type_color(self): """Get a Color to be used in visualizations by type.""" return self.TYPE_COLORS[self.is_detached] @property def bc_color(self): """Get a Color to be used in visualizations by boundary condition.""" return self.BC_COLOR
[docs] def add_prefix(self, prefix): """Change the identifier of this object by inserting a prefix. This is particularly useful in workflows where you duplicate and edit a starting object and then want to combine it with the original object into one Model (like making a model of repeated rooms) since all objects within a Model must have unique identifiers. Args: prefix: Text that will be inserted at the start of this object's identifier and display_name. It is recommended that this prefix be short to avoid maxing out the 100 allowable characters for honeybee identifiers. """ self._identifier = clean_string('{}_{}'.format(prefix, self.identifier)) self.display_name = '{}_{}'.format(prefix, self.display_name) self.properties.add_prefix(prefix)
[docs] def move(self, moving_vec): """Move this Shade along a vector. Args: moving_vec: A ladybug_geometry Vector3D with the direction and distance to move the face. """ self._geometry = self.geometry.move(moving_vec) self.properties.move(moving_vec)
[docs] def rotate(self, axis, angle, origin): """Rotate this Shade by a certain angle around an axis and origin. Args: axis: A ladybug_geometry Vector3D axis representing the axis of rotation. angle: An angle for rotation in degrees. origin: A ladybug_geometry Point3D for the origin around which the object will be rotated. """ self._geometry = self.geometry.rotate(axis, math.radians(angle), origin) self.properties.rotate(axis, angle, origin)
[docs] def rotate_xy(self, angle, origin): """Rotate this Shade counterclockwise in the world XY plane by a certain angle. Args: angle: An angle in degrees. origin: A ladybug_geometry Point3D for the origin around which the object will be rotated. """ self._geometry = self.geometry.rotate_xy(math.radians(angle), origin) self.properties.rotate_xy(angle, origin)
[docs] def reflect(self, plane): """Reflect this Shade across a plane. Args: plane: A ladybug_geometry Plane across which the object will be reflected. """ self._geometry = self.geometry.reflect(plane.n, plane.o) self.properties.reflect(plane)
[docs] def scale(self, factor, origin=None): """Scale this Shade by a factor from an origin point. Args: factor: A number representing how much the object should be scaled. origin: A ladybug_geometry Point3D representing the origin from which to scale. If None, it will be scaled from the World origin (0, 0, 0). """ self._geometry = self.geometry.scale(factor, origin) self.properties.scale(factor, origin)
[docs] def triangulate_and_remove_degenerate_faces(self, tolerance=0.01): """Triangulate non-planar faces in the mesh and remove all degenerate faces. This is helpful for certain geometry interfaces that require perfectly planar geometry without duplicate or colinear vertices. 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. """ new_faces, verts = [], self.geometry.vertices for shd in self.faces: shd_verts = [verts[v] for v in shd] shf = Face3D(shd_verts) if len(shd_verts) == 4 and not \ shf.check_planar(tolerance, raise_exception=False): shades = ((shd[0], shd[1], shd[2]), (shd[2], shd[3], shd[0])) for shade in shades: shd_verts = [verts[v] for v in shade] shade_face = Face3D(shd_verts) try: shade_face.remove_colinear_vertices(tolerance) except AssertionError: continue # degenerate face to remove new_faces.append(shade) else: try: new_face = shf.remove_colinear_vertices(tolerance) except AssertionError: continue # degenerate face to remove if len(new_face.vertices) == len(shd): new_faces.append(shd) else: # quad face with duplicate or colinear verts new_sh = tuple(shd[shd_verts.index(v)] for v in new_face.vertices) new_faces.append(new_sh) self._geometry = Mesh3D(verts, new_faces)
[docs] def is_geo_equivalent(self, shade_mesh, tolerance=0.01): """Get a boolean for whether this object is geometrically equivalent to another. The total number of vertices and the ordering of these vertices can be different but the geometries must share the same center point and be next to one another to within the tolerance. Args: shade_mesh: Another ShadeMesh for which geometric equivalency will be tested. tolerance: The minimum difference between the coordinate values of two vertices at which they can be considered geometrically equivalent. Returns: True if geometrically equivalent. False if not geometrically equivalent. """ meta_1 = (self.display_name, self.is_detached) meta_2 = (shade_mesh.display_name, shade_mesh.is_detached) if meta_1 != meta_2: return False if len(self.geometry.vertices) != len(shade_mesh.geometry.vertices): return False if len(self.geometry.faces) != len(shade_mesh.geometry.faces): return False return all(pt.is_equivalent(o_pt, tolerance) for pt, o_pt in zip(self.geometry.vertices, shade_mesh.geometry.vertices))
[docs] def display_dict(self): """Get a list of DisplayMesh3D dictionaries for visualizing the object.""" return [self._display_mesh(self.geometry, self.type_color)]
@property def to(self): """ShadeMesh writer object. Use this method to access Writer class to write the shade in different formats. Usage: .. code-block:: python shade_mesh.to.idf(shade) -> idf string. shade_mesh.to.radiance(shade) -> Radiance string. """ return writer
[docs] def to_dict(self, abridged=False, included_prop=None): """Return Shade as a dictionary. Args: abridged: Boolean to note whether the extension properties of the object (ie. modifiers, transmittance schedule) should be included in detail (False) or just referenced by identifier (True). Default: False. included_prop: List of properties to filter keys that must be included in output dictionary. For example ['energy'] will include 'energy' key if available in properties to_dict. By default all the keys will be included. To exclude all the keys from extensions use an empty list. """ base = {'type': 'ShadeMesh'} base['identifier'] = self.identifier base['display_name'] = self.display_name base['properties'] = self.properties.to_dict(abridged, included_prop) base['geometry'] = self._geometry.to_dict() if not self.is_detached: base['is_detached'] = self.is_detached if self.user_data is not None: base['user_data'] = self.user_data return base
@staticmethod def _display_mesh(mesh3d, color): """Create a DisplayMesh3D dictionary from a Mesh3D and color.""" return { 'type': 'DisplayMesh3D', 'geometry': mesh3d.to_dict(), 'color': color.to_dict(), 'display_mode': 'SurfaceWithEdges' } def __copy__(self): new_shade = ShadeMesh(self.identifier, self.geometry, self.is_detached) new_shade._display_name = self._display_name new_shade._user_data = None if self.user_data is None else self.user_data.copy() new_shade._properties._duplicate_extension_attr(self._properties) return new_shade def __repr__(self): return 'ShadeMesh: %s' % self.display_name