# coding: utf-8
"""Dragonfly Context Shade."""
from __future__ import division
import re
import math
from ladybug_geometry.geometry3d import Point3D, LineSegment3D, Polyline3D, Plane
from ._base import _Base
from .search import get_attr_nested
from .properties import BoundaryProperties
import fairyfly.writer.boundary as writer
[docs]
class Boundary(_Base):
"""A Context Shade object defined by an array of Face3Ds and/or Mesh3Ds.
Args:
geometry: An array of ladybug_geometry LineSegment3D objects
that together represent a type of boundary in a construction detail.
identifier: Text string for a unique Boundary ID. Must be a UUID in the
format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx. If None, a UUID will
automatically be generated. (Default: None).
Properties:
* identifier
* display_name
* therm_uuid
* full_id
* parent
* has_parent
* geometry
* vertices
* length
* min
* max
* center
* user_data
"""
__slots__ = ('_geometry', '_parent')
def __init__(self, geometry, identifier=None):
"""Initialize Boundary."""
_Base.__init__(self, identifier) # process the identifier
# process the geometry
if not isinstance(geometry, tuple):
geometry = tuple(geometry)
assert len(geometry) > 0, 'Boundary must have at least one geometry.'
for l_geo in geometry:
assert isinstance(l_geo, LineSegment3D), 'Expected ladybug_geometry ' \
'LineSegment3D. Got {}'.format(type(l_geo))
self._geometry = geometry
self._parent = None # _parent will be set when Boundary is added to an object
self._properties = BoundaryProperties(self) # properties for extensions
[docs]
@classmethod
def from_dict(cls, data):
"""Initialize an Boundary from a dictionary.
Args:
data: A dictionary representation of an Boundary object.
"""
# check the type of dictionary
assert data['type'] == 'Boundary', 'Expected Boundary dictionary. ' \
'Got {}.'.format(data['type'])
# serialize the geometry
geometry = []
for l_geo in data['geometry']:
if l_geo['type'] == 'LineSegment3D':
geometry.append(LineSegment3D.from_dict(l_geo))
else: # it is a polyline
verts = tuple(Point3D.from_array(pt) for pt in data['vertices'])
if len(verts) == 2:
geometry.append(LineSegment3D.from_end_points(*l_geo))
else:
poly_geo = Polyline3D(verts)
geometry.extend(poly_geo.segments)
# create the Boundary
bound = cls(geometry, data['identifier'])
if 'display_name' in data and data['display_name'] is not None:
bound.display_name = data['display_name']
if 'user_data' in data and data['user_data'] is not None:
bound.user_data = data['user_data']
if data['properties']['type'] == 'BoundaryProperties':
bound.properties._load_extension_attr_from_dict(data['properties'])
return bound
[docs]
@classmethod
def from_vertices(cls, vertices, identifier=None):
"""Create a Boundary from vertices with each vertex as an iterable of 3 floats.
Args:
vertices: A list of lists where each sub-list represents a line segment
or polyline with 2 or more vertices. Each vertex is represented
as an iterable of three (x, y, z) floats.
identifier: Text string for a unique Shape ID. Must be a UUID in the
format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx. If None, a UUID will
automatically be generated. (Default: None).
"""
geometry = []
for l_geo in vertices:
verts = tuple(Point3D.from_array(pt) for pt in l_geo)
if len(verts) == 2:
geometry.append(LineSegment3D.from_end_points(*verts))
else:
poly_geo = Polyline3D(verts)
geometry.extend(poly_geo.segments)
return cls(geometry, identifier)
@property
def parent(self):
"""Get the parent object if assigned. None if not assigned.
The parent object is typically a GlazingSystem.
"""
return self._parent
@property
def has_parent(self):
"""Get a boolean noting whether this Shape has a parent object."""
return self._parent is not None
@property
def geometry(self):
"""Get a tuple of LineSegment3D objects that represent the boundary."""
return self._geometry
@property
def vertices(self):
"""Get a list of vertices for the boundary."""
return tuple(pt for geo in self._geometry for pt in geo.vertices)
@property
def length(self):
"""Get a number for the total length of the Boundary."""
return sum([geo.length for geo in self._geometry])
@property
def min(self):
"""Get a Point3D for the minimum of the bounding box around the object."""
return self._calculate_min(self._geometry)
@property
def max(self):
"""Get a Point3D for the maximum of the bounding box around the object."""
return self._calculate_max(self._geometry)
@property
def center(self):
"""A Point3D for the center of the bounding box around the object."""
mn, mx = self.min, self.max
return Point3D((mn.x + mx.x) / 2, (mn.y + mx.y) / 2, (mn.z + mx.z) / 2)
[docs]
def rename_by_attribute(self, format_str='{display_name} - {length}'):
"""Set the display name of this Boundary using a format string with attributes.
Args:
format_str: Text string for the pattern with which the Boundary will be
renamed. Any property on this class may be used and each
property should be put in curly brackets. Nested properties
can be specified by using "." to denote nesting levels
(eg. properties.energy.construction.display_name). Functions that
return string outputs can also be passed here as long as these
functions defaults specified for all arguments.
"""
matches = re.findall(r'{([^}]*)}', format_str)
attributes = [get_attr_nested(self, m, decimal_count=2) for m in matches]
for attr_name, attr_val in zip(matches, attributes):
format_str = format_str.replace('{{{}}}'.format(attr_name), attr_val)
self.display_name = format_str
return format_str
[docs]
def move(self, moving_vec):
"""Move this Boundary along a vector.
Args:
moving_vec: A ladybug_geometry Vector3D with the direction and distance
to move the object.
"""
self._geometry = tuple(l_geo.move(moving_vec) for l_geo in self._geometry)
self.properties.move(moving_vec)
[docs]
def rotate(self, axis, angle, origin):
"""Rotate this Shape 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 = tuple(l_geo.rotate(axis, math.radians(angle), origin)
for l_geo in self._geometry)
self.properties.rotate(axis, angle, origin)
[docs]
def rotate_xy(self, angle, origin):
"""Rotate this Boundary counterclockwise in the 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 = tuple(l_geo.rotate_xy(math.radians(angle), origin)
for l_geo in self._geometry)
self.properties.rotate_xy(angle, origin)
[docs]
def reflect(self, plane):
"""Reflect this Boundary across a plane.
Args:
plane: A ladybug_geometry Plane across which the object will be reflected.
"""
self._geometry = tuple(l_geo.reflect(plane.n, plane.o)
for l_geo in self._geometry)
self.properties.reflect(plane)
[docs]
def scale(self, factor, origin=None):
"""Scale this Boundary 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 = tuple(l_geo.scale(factor, origin)
for l_geo in self._geometry)
self.properties.scale(factor, origin)
[docs]
def check_planar(self, tolerance=0.01, raise_exception=True, detailed=False):
"""Check whether all of the Boundary's vertices lie within the same plane.
Args:
tolerance: The minimum distance between a given vertex and a the
object's plane at which the vertex is said to lie in the plane.
Default: 0.01, suitable for objects in millimeters.
raise_exception: Boolean to note whether an ValueError should be
raised if a vertex does not lie within the object's plane.
detailed: Boolean for whether the returned object is a detailed list of
dicts with error info or a string with a message. (Default: False).
Returns:
A string with the message or a list with a dictionary if detailed is True.
"""
# collect all of the unique points
pts = []
for seg in self.geometry:
for pt in seg.vertices:
for o_pt in pts:
if pt.is_equivalent(o_pt, tolerance):
break
else: # the point is unique
pts.append(pt)
# evaluate the points in relation to their plane
if len(pts) > 3:
plane = Plane.from_three_points(*pts[:3])
for _v in pts[3:]:
if plane.distance_to_point(_v) >= tolerance:
g_ms = 'Vertex {} does not lie in the same plane.\nDistance ' \
'to plane is {}'.format(_v, plane.distance_to_point(_v))
msg = 'Boundary "{}" is not planar.\n{}'.format(self.full_id, g_ms)
full_msg = self._validation_message(
msg, raise_exception, detailed, '200101',
error_type='Non-Planar Geometry')
if detailed: # add the out-of-plane point to helper_geometry
full_msg[0]['helper_geometry'] = [_v.to_dict()]
return full_msg
return [] if detailed else ''
[docs]
def to_dict(self, abridged=False, included_prop=None):
"""Return Boundary as a dictionary.
Args:
abridged: Boolean to note whether the extension properties of the
object (ie. materials, 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 ['therm'] will include 'therm' 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': 'Boundary'}
base['identifier'] = self.identifier
base['geometry'] = [l_geo.to_dict() for l_geo in self._geometry]
base['properties'] = self.properties.to_dict(abridged, included_prop)
if self._display_name is not None:
base['display_name'] = self.display_name
if self.user_data is not None:
base['user_data'] = self.user_data
return base
@property
def to(self):
"""Boundary writer object.
Use this method to access Writer class to write the context in other formats.
"""
return writer
def __copy__(self):
new_shd = Boundary(self._geometry, self.identifier)
new_shd._display_name = self._display_name
new_shd._user_data = None if self.user_data is None else self.user_data.copy()
new_shd._properties._duplicate_extension_attr(self._properties)
return new_shd
def __len__(self):
return len(self._geometry)
def __getitem__(self, key):
return self._geometry[key]
def __iter__(self):
return iter(self._geometry)
def __repr__(self):
return 'Boundary: %s' % self.display_name