Source code for honeybee_radiance.view

# coding=utf-8
u"""Create a Radiance view."""
from __future__ import division

from .lightpath import light_path_from_room

from honeybee_radiance_command.options import TupleOption, \
    StringOptionJoined, NumericOption
import honeybee.typing as typing
import ladybug_geometry.geometry3d.pointvector as pv
from ladybug_geometry.geometry3d.plane import Plane
import ladybug.futil as futil

import math
import os
import re
import collections


[docs]class View(object): u"""A Radiance view. Args: identifier: Text string for a unique View ID. Must not contain spaces or special characters. This will be used to identify the object across a model and in the exported Radiance files. position: Set the view position (-vp) to (x, y, z). This is the focal point of a perspective view or the center of a parallel projection. Default: (0, 0, 0) direction: Set the view direction (-vd) vector to (x, y, z). The length of this vector indicates the focal distance as needed by the pixel depth of field (-pd) in rpict. Default: (0, 0, 1) up_vector: Set the view up (-vu) vector (vertical direction) to (x, y, z) default: (0, 1, 0). type: A single character for the view type (-vt). Choose from the following. * v - Perspective * h - Hemispherical fisheye * l - Parallel * c - Cylindrical panorama * a - Angular fisheye * s - Planisphere [stereographic] projection For more detailed description about view types check rpict manual page: (http://radsite.lbl.gov/radiance/man_html/rpict.1.html) h_size: Set the view horizontal size (-vh). For a perspective projection (including fisheye views), val is the horizontal field of view (in degrees). For a parallel projection, val is the view width in world coordinates. v_size: Set the view vertical size (-vv). For a perspective projection (including fisheye views), val is the horizontal field of view (in degrees). For a parallel projection, val is the view width in world coordinates. shift: Set the view shift (-vs). This is the amount the actual image will be shifted to the right of the specified view. This option is useful for generating skewed perspectives or rendering an image a piece at a time. A value of 1 means that the rendered image starts just to the right of the normal view. A value of -1 would be to the left. Larger or fractional values are permitted as well. lift: Set the view lift (-vl) to a value. This is the amount the actual image will be lifted up from the specified view. Properties: * identifier * display_name * position * direction * up_vector * type * h_size * v_size * shift * lift * room_identifier * light_path * group_identifier * full_identifier Usage: .. code-block:: python v = View() # add a fore clip v.fore_clip = 100 print(v) > -vtv -vp 0.000 0.000 0.000 -vd 0.000 0.000 1.000 -vu 0.000 1.000 0.000 -vh 60.000 -vv 60.000 -vo 100.000 # split the view into a view grid gridViews = v.grid(2, 2, 600, 600) for g in gridViews: print(g) > -vtv -vp 0.000 0.000 0.000 -vd 0.000 0.000 1.000 -vu 0.000 1.000 0.000 -vh 29.341 -vv 32.204 -vs -0.500 -vl -0.500 -vo 100.000 > -vtv -vp 0.000 0.000 0.000 -vd 0.000 0.000 1.000 -vu 0.000 1.000 0.000 -vh 29.341 -vv 32.204 -vs 0.500 -vl -0.500 -vo 100.000 > -vtv -vp 0.000 0.000 0.000 -vd 0.000 0.000 1.000 -vu 0.000 1.000 0.000 -vh 29.341 -vv 32.204 -vs -0.500 -vl 0.500 -vo 100.000 > -vtv -vp 0.000 0.000 0.000 -vd 0.000 0.000 1.000 -vu 0.000 1.000 0.000 -vh 29.341 -vv 32.204 -vs 0.500 -vl 0.500 -vo 100.000 """ __slots__ = ('_identifier', '_display_name', '_position', '_direction', '_up_vector', '_h_size', '_v_size', '_shift', '_lift', '_type', '_fore_clip', '_aft_clip', '_room_identifier', '_light_path', '_group_identifier') def __init__(self, identifier, position=None, direction=None, up_vector=None, type='v', h_size=60, v_size=60, shift=None, lift=None): u"""Create a view.""" self.identifier = identifier self._display_name = None self._position = TupleOption( 'vp', 'view position', position if position is not None else (0, 0, 0) ) self._direction = TupleOption( 'vd', 'view direction', direction if direction is not None else (0, 0, 1) ) self._up_vector = TupleOption( 'vu', 'view up vector', up_vector if up_vector is not None else (0, 1, 0) ) self._h_size = NumericOption('vh', 'view horizontal size', h_size, min_value=0) self._v_size = NumericOption('vv', 'view vertical size', v_size, min_value=0) self._shift = NumericOption('vs', 'view shift', shift) self._lift = NumericOption('vl', 'view lift', lift) self._type = StringOptionJoined( 'vt', 'view type', type, valid_values=['v', 'h', 'l', 'c', 'a', 's'] ) # set for_clip to None self._fore_clip = NumericOption('vo', 'view fore clip') self._aft_clip = NumericOption('va', 'view aft clip') self._room_identifier = None self._group_identifier = None self._light_path = None self._check_size_and_type() @property def identifier(self): """Get or set a text string for a unique View identifier.""" return self._identifier @identifier.setter def identifier(self, n): self._identifier = typing.valid_rad_string(n, 'view identifier') @property def display_name(self): """Get or set a string for the object name without any character restrictions. If not set, this will be equal to the identifier. """ if self._display_name is None: return self._identifier return self._display_name @display_name.setter def display_name(self, value): try: self._display_name = str(value) except UnicodeEncodeError: # Python 2 machine lacking the character set self._display_name = value # keep it as unicode @property def is_fisheye(self): """Check if the view type is one of the fisheye views.""" return self.type in ('h', 'a', 's') @property def type(self): """Set and get view type (-vt) to one of the choices below. * v - Perspective (v) * h - Hemispherical fisheye (h) * l - Parallel (l) * c - Cylindrical panorama (c) * a - Angular fisheye (a) * s - Planisphere [stereographic] projection (s) """ return self._type.value @property def vt(self): """View type as a string in radiance format.""" return self._type.to_radiance() @type.setter def type(self, value): self._type.value = value[-1:] # this will handle both vtv and v inputs self._check_size_and_type() @property def position(self): """Set the view position (-vp) to (x, y, z). This is the focal point of a perspective view or the center of a parallel projection. Default: (0, 0, 0) """ return self._position.value @property def vp(self): """View point / position as a string in radiance format.""" return self._position.to_radiance() @position.setter def position(self, value): self._position.value = value @property def direction(self): """Set the view direction (-vd) vector to (x, y, z). The length of this vector indicates the focal distance as needed by the pixel depth of field (-pd) in rpict. Default: (0, 0, 1) """ return self._direction.value @property def vd(self): """View direction as a string in radiance format.""" return self._direction.to_radiance() @direction.setter def direction(self, value): self._direction.value = value @property def up_vector(self): """Set and get the view up (-vu) vector (vertical direction) to (x, y, z) Default: (0, 1, 0). """ return self._up_vector.value @property def vu(self): """View up as a string in radiance format.""" return self._up_vector.to_radiance() @up_vector.setter def up_vector(self, value): self._up_vector.value = value @property def h_size(self): """Set the view horizontal size (-vh). For a perspective projection (including fisheye views), this is the horizontal field of view (in degrees). For a parallel projection, this is the view width in world coordinates. """ return self._h_size.value @property def vh(self): """View horizontal size as a string in radiance format.""" return self._h_size.to_radiance() @h_size.setter def h_size(self, value): self._h_size.value = value if value is not None else 60 self._check_size_and_type() @property def v_size(self): """Set the view vertical size (-vv). For a perspective projection (including fisheye views), this is the horizontal field of view (in degrees). For a parallel projection, this is the view width in world coordinates. """ return self._v_size.value @property def vv(self): """View vertical size as a string in radiance format.""" return self._v_size.to_radiance() @v_size.setter def v_size(self, value): self._v_size.value = value if value is not None else 60 self._check_size_and_type() @property def shift(self): """Set the view shift (-vs). This is the amount the actual image will be shifted to the right of the specified view. This option is useful for generating skewed perspectives or rendering an image a piece at a time. A value of 1 means that the rendered image starts just to the right of the normal view. A value of -1 would be to the left. Larger or fractional values are permitted as well. """ return self._shift.value @property def vs(self): """View shift as a string in radiance format.""" return self._shift.to_radiance() @shift.setter def shift(self, value): self._shift.value = value @property def lift(self): """Set the view lift (-vl) to a value. This is the amount the actual image will be lifted up from the specified view. """ return self._lift.value @property def vl(self): """View lift as a string in radiance format.""" return self._lift.to_radiance() @lift.setter def lift(self, value): self._lift.value = value @property def fore_clip(self): """View fore clip (-vo) at a distance from the view point. The plane will be perpendicular to the view direction for perspective and parallel view types. For fisheye view types, the clipping plane is actually a clipping sphere, centered on the view point with radius val. Objects in front of this imaginary surface will not be visible. This may be useful for seeing through walls (to get a longer perspective from an exterior view point) or for incremental rendering. A value of zero implies no foreground clipping. A negative value produces some interesting effects, since it creates an inverted image for objects behind the viewpoint. """ return self._fore_clip.value @property def vo(self): """View fore clip as a string in radiance format.""" return self._fore_clip.to_radiance() @fore_clip.setter def fore_clip(self, distance): self._fore_clip.value = distance @property def aft_clip(self): """View aft clip (-va) at a distance from the view point. Set the view aft clipping plane at a distance of val from the view point. Like the view fore plane, it will be perpendicular to the view direction for perspective and parallel view types. For fisheye view types, the clipping plane is actually a clipping sphere, centered on the view point with radius val. Objects behind this imaginary surface will not be visible. A value of zero means no aft clipping, and is the only way to see infinitely distant objects such as the sky. """ return self._aft_clip.value @property def va(self): """View aft clip as a string in radiance format.""" return self._aft_clip.to_radiance() @aft_clip.setter def aft_clip(self, distance): self._aft_clip.value = distance @property def room_identifier(self): """Get or set text for the Room identifier to which this View belongs. This will be used in the info_dict method to narrow down the number of aperture groups that have to be run with this view. If None, the view will be run with all aperture groups in the model. """ return self._room_identifier @room_identifier.setter def room_identifier(self, n): self._room_identifier = typing.valid_string(n) @property def group_identifier(self): """Get or set text for the group identifier to which this View belongs. This will be used in the write to radiance folder method to write all the views with the same group identifier under the same subfolder. You may use / in name to identify nested view groups. For example floor_1/living_room create a view under living_room/floor_1 subfolder. If None, the view will be written to the root of views folder. """ return self._group_identifier @group_identifier.setter def group_identifier(self, identifier_key): if identifier_key is not None: identifier_key = \ '/'.join( typing.valid_rad_string(key, 'view group identifier') for key in identifier_key.split('/') ) self._group_identifier = identifier_key @property def full_identifier(self): """Get full identifier for view. For a view with group identifier it will be group_identifier/identifier """ return self.identifier if not self.group_identifier \ else '%s/%s' % (self.group_identifier, self.identifier) @property def light_path(self): """Get or set list of lists for the light path from the view to the sky. Each sub-list contains identifiers of aperture groups through which light passes. (eg. [['SouthWindow1'], ['__static_apertures__', 'NorthWindow2']]). Setting this property will override any auto-calculation of the light path from the model upon export to the simulation. """ return self._light_path @light_path.setter def light_path(self, l_path): if l_path is not None: assert isinstance(l_path, (tuple, list)), 'Expected list or tuple for ' \ 'light_path. Got {}.'.format(type(l_path)) for ap_list in l_path: assert isinstance(ap_list, (tuple, list)), 'Expected list or tuple ' \ 'for light_path sub-list. Got {}.'.format(type(ap_list)) for ap in ap_list: assert isinstance(ap, str), 'Expected text for light_path ' \ 'aperture group identifier. Got {}.'.format(type(ap)) self._light_path = l_path
[docs] def standardize_fisheye(self): """Automatically set view size to 180 degrees if the view type is a fisheye. Alternatively it sets the view size to 360 degrees if both the view type is angular fisheye and either the horizontal or vertical view size is 360 degrees. """ if self.type in ('h', 's'): if self.h_size != 180: self.h_size = 180 if self.v_size != 180: self.v_size = 180 if self.type in ('a'): if self.h_size == 360 or self.v_size == 360: self.h_size = self.v_size = 360 else: if self.h_size != 180: self.h_size = 180 if self.v_size != 180: self.v_size = 180
def _check_size_and_type(self): """Check to be sure the view size and type are compatible.""" if self.type == 'v': assert self.h_size < 180, \ '\n{} is an invalid horizontal view size for Perspective view.\n' \ 'The size should be smaller than 180.'.format(self.h_size) assert self.v_size < 180, \ '\n{} is an invalid vertical view size for Perspective view.\n' \ 'The size should be smaller than 180.'.format(self.v_size)
[docs] @classmethod def from_dict(cls, view_dict): """Create a view from a dictionary in the following format. .. code-block:: python { 'type': 'View', 'identifier': str, # View identifier "display_name": str, # View display name 'position': [], # list with position value 'direction': [], # list with direction value 'up_vector': [], # list with up_vector value 'h_size': number, # h_size.value 'v_size': number, # v_size value 'shift': number, # shift value 'lift': number, # lift value 'view_type': number, # view_type value 'fore_clip': number, # fore_clip value 'aft_clip': number, # aft_clip value 'room_identifier': str, # optional room identifier 'light_path': [] # optional list of lists for light path } """ assert view_dict['type'] == 'View', \ 'Expected View dictionary. Got {}.'.format(view_dict['type']) view_type = view_dict['view_type'][-1:] if 'view_type' in view_dict else 'v' view = cls( identifier=view_dict['identifier'], position=view_dict['position'], direction=view_dict['direction'], up_vector=view_dict['up_vector'], type=view_type, h_size=view_dict['h_size'], v_size=view_dict['v_size'], shift=view_dict['shift'], lift=view_dict['lift'], ) if 'fore_clip' in view_dict: view.fore_clip = view_dict['fore_clip'] if 'aft_clip' in view_dict: view.aft_clip = view_dict['aft_clip'] if 'display_name' in view_dict and view_dict['display_name'] is not None: view.display_name = view_dict['display_name'] if 'room_identifier' in view_dict and view_dict['room_identifier'] is not None: view.room_identifier = view_dict['room_identifier'] if 'light_path' in view_dict and view_dict['light_path'] is not None: view.light_path = view_dict['light_path'] if 'group_identifier' in view_dict and view_dict['group_identifier'] is not None: view_dict.group_identifier = view_dict['group_identifier'] return view
[docs] @classmethod def from_string(cls, identifier, view_string): """Create a view object from a string. This method is similar to from_string method for radiance parameters with the difference that all the parameters that are not related to view will be ignored. """ mapper = { 'identifier': identifier, 'vp': 'position', 'vd': 'direction', 'vu': 'up_vector', 'vh': 'h_size', 'vv': 'v_size', 'vs': 'shift', 'vl': 'lift', 'vo': 'fore_clip', 'va': 'aft_clip' } base = { 'type': 'View', 'identifier': identifier, 'position': None, 'direction': None, 'up_vector': None, 'h_size': None, 'v_size': None, 'shift': None, 'lift': None, 'view_type': None, 'fore_clip': None, 'aft_clip': None } # parse the string here options = cls._parse_radiance_options(view_string) for opt, value in options.items(): if opt in mapper: base[mapper[opt]] = value elif opt[:2] == 'vt': base['view_type'] = opt else: print('%s is not a view parameter and is ignored.' % opt) return cls.from_dict(base)
[docs] @classmethod def from_file(cls, file_path, identifier=None): """Create view from a view file. Args: file_path: Full path to view file. identifier: Optional ext string for a unique View ID. Must not contain spaces or special characters. This will be used to identify the object across a model and in the exported Radiance files. If None, this will be set to file name. (Default: None) """ if not os.path.isfile(file_path): raise IOError("Can't find {}.".format(file_path)) identifier = identifier or os.path.split(os.path.splitext(file_path)[0])[-1] with open(file_path, 'r') as input_data: view_string = str(input_data.read()).rstrip() assert view_string[:3] == 'rvu', \ 'View file must start with rvu not %s' % view_string[:3] return cls.from_string(identifier, view_string)
[docs] @classmethod def from_grid(cls, grid, identifier='from_grid'): """Create view from a grid of views. Generally the grid argument should be the views generated by the grid method. Args: grid: A list of subviews. If only a single view is given, this view will be returned. The views can be either class instances of View, strings or .unf files. If strings are used, the views will be created by the from_string method. If .unf files are used, the views will be created by the from_string method using the view found in the Radiance header. identifier: Text string for a unique View ID. Must not contain spaces or special characters. This will be used to identify the object across a model and in the exported Radiance files. If None, this will be set to 'from_grid'. (Default: 'from_grid') """ if not isinstance(grid, (list, tuple)): grid = [grid] views = [] # check if grid argument views are valid for c, view in enumerate(grid): if isinstance(view, View): views.append(view) elif view.endswith('.unf'): try: f = open(view, 'r', encoding='utf-8', errors='ignore') except Exception: f = open(view, 'r') try: for line in f: if not line.strip(): break else: low_line = line.strip().lower() if low_line.startswith('view='): print(low_line) views.append(cls.from_string('view_%04d' % c, low_line)) except Exception: raise ValueError('Failed to find view in Radiance header.') finally: f.close() elif isinstance(view, str): views.append(cls.from_string('view_%04d' % c, view)) else: raise ValueError( 'Expected Honeybee Radiance View, string or .unf file.' 'Got: {}'.format(type(view)) ) # if only a single (valid) view is given, then return the view if len(views) == 1: return views[0] _type = set() _view_point = set() _view_direction = set() _up_direction = set() _vh = set() _vv = set() _x_div_count = set() _y_div_count = set() # check if type, view point, view direction, and up direction are equal in views # all unique values are collected, all except -vh and -vv must be the same for _view in views: _type.add(_view.type) _view_point.add(_view.position) _view_direction.add(_view.direction) _up_direction.add(_view.up_vector) _vh.add(_view.h_size) _vv.add(_view.v_size) _x_div_count.add(_view.shift) _y_div_count.add(_view.lift) if len(_type) > 1: raise ValueError('All subviews must have the same view type.') if len(_view_point) > 1: raise ValueError('All subviews must have the same view point.') if len(_view_direction) > 1: raise ValueError('All subviews must have the same view direction.') if len(_up_direction) > 1: raise ValueError('All subviews must have the same up direction.') if len(_vh) > 1: raise ValueError('All subviews must have the same horizontal view size.') if len(_vv) > 1: raise ValueError('All subviews must have the same vertical view size.') # find the grid dimensions (x_count, y_count) x_div_count = len(_x_div_count) y_div_count = len(_y_div_count) # get the horizontal and vertical view size from the subviews _vh = list(_vh)[0] _vv = list(_vv)[0] # create instance of view view = cls( identifier=identifier, position=list(_view_point)[0], direction=list(_view_direction)[0], up_vector=list(_up_direction)[0], type=list(_type)[0]) PI = math.pi # find the horizontal and vertical size if view.type == 'l' or view.type == 'a': # parallel view (vtl) or angular fisheye (vta) h_size = _vh * x_div_count v_size = _vv * y_div_count elif view.type == 'v': # perspective (vtv) pi2 = (2. * 180. / PI) h_size = pi2 * math.tan(_vh / (2. * 180. / PI)) * x_div_count v_size = pi2 * math.atan(math.tan(_vv / (2. * 180. / PI)) * y_div_count) elif view.type == 'h': # hemispherical fisheye (vth) pi2 = (2. * 180. / PI) h_size = pi2 * math.asin(math.sin(_vh / (2. * 180. / PI)) * x_div_count) v_size = pi2 * math.asin(math.sin(_vv / (2. * 180. / PI)) * y_div_count) else: raise ValueError( 'Grid views are not supported for %s.' % view.type) # round the number to avoid cases like 59.99999999999999 when should be 60 h_size = round(h_size, 10) v_size = round(v_size, 10) # update horizontal and vertical view size view.h_size = h_size view.v_size = v_size return view
[docs] def dimension(self, x_res=None, y_res=None): """Get dimensions for this view as '-x %d -y %d [-ld-]'. This method is same as vwrays -d. Default values for x_res and y_res are set to match Radiance defaults. """ x, y = self.dimension_x_y(x_res, y_res) return '-x %d -y %d -ld%s' % (x, y, '-' if (self.vo + self.va == '') else '+')
[docs] def dimension_x_y(self, x_res=None, y_res=None): """Get dimensions for this view as x, y. Default values for x_res and y_res are set to match Radiance defaults. """ # radiance default is 512 x_res = int(x_res) if x_res is not None else 512 y_res = int(y_res) if y_res is not None else 512 if self.is_fisheye: return min(x_res, y_res), min(x_res, y_res) vh = self.h_size vv = self.v_size if self.type == 'v': hv_ratio = math.tan(math.radians(vh) / 2.0) / \ math.tan(math.radians(vv) / 2.0) else: hv_ratio = vh / vv # radiance keeps the largest max size and tries to scale the other size # to fit the aspect ratio. In case the size doesn't match it reverses # the process. if y_res <= x_res: new_x = int(round(hv_ratio * y_res)) if new_x <= x_res: return new_x, y_res else: new_y = int(round(x_res / hv_ratio)) return x_res, new_y else: new_y = int(round(x_res / hv_ratio)) if new_y <= y_res: return x_res, new_y else: new_x = int(round(hv_ratio * y_res)) return new_x, y_res
[docs] def grid(self, x_div_count=1, y_div_count=1): """Break-down the view into a grid of views based on x and y grid count. Views will be returned row by row from right to left. Args: x_div_count: Set number of divisions in x direction (Default: 1). y_div_count: Set number of divisions in y direction (Default: 1). Returns: A tuple of views. Views are sorted row by row from right to left. """ PI = math.pi try: x_div_count = abs(x_div_count) y_div_count = abs(y_div_count) except TypeError as e: raise ValueError("Division count should be a number.\n%s" % str(e)) assert x_div_count * y_div_count != 0, "Division count should be larger than 0." if x_div_count == y_div_count == 1: return [self] _views = list(range(x_div_count * y_div_count)) if self.type in ('l', 'a', 'c'): # parallel view (vtl) or angular fisheye (vta) _vh = self.h_size / x_div_count _vv = self.v_size / y_div_count elif self.type == 'v': # perspective (vtv) pi2 = (2. * 180. / PI) _vh = pi2 * math.atan(((PI / 180. / 2.) * self.h_size) / x_div_count) _vv = pi2 * math.atan(math.tan((PI / 180. / 2.) * self.v_size) / y_div_count) elif self.type == 's': # planisphere (stereographic) pi2 = (2. * 180. / PI * 2) _vh = pi2 * math.atan(math.sin((PI / 180. / 2.) * self.h_size) / x_div_count) _vv = pi2 * math.atan(math.sin((PI / 180. / 2.) * self.v_size) / y_div_count) elif self.type in ('h'): # hemispherical fish eye pi2 = (2. * 180. / PI) _vh = pi2 * math.asin(math.sin((PI / 180. / 2.) * self.h_size) / x_div_count) _vv = pi2 * math.asin(math.sin((PI / 180. / 2.) * self.v_size) / y_div_count) else: print("Grid views are not supported for %s." % self.type) return [self] # create a set of new views for view_count in range(len(_views)): # calculate view shift and view lift if x_div_count == 1: _vs = 0 else: _vs = (((view_count % x_div_count) / (x_div_count - 1)) - 0.5) \ * (x_div_count - 1) if y_div_count == 1: _vl = 0 else: _vl = ((int(view_count % y_div_count) / (y_div_count - 1)) - 0.5) \ * (y_div_count - 1) # create a copy from the current view _n_view = self.duplicate() _n_view.identifier = '%s_%d' % (self.identifier, view_count) # update parameters _n_view.h_size = _vh _n_view.v_size = _vv _n_view.shift = _vs _n_view.lift = _vl _n_view._fore_clip = self._fore_clip _n_view._aft_clip = self._aft_clip try: _n_view.display_name = '%s_%d' % (self.display_name, view_count) except UnicodeEncodeError: # character no found on machine pass # add the new view to views list _views[view_count] = _n_view return _views
[docs] def to_radiance(self): """Return full Radiance definition as a string.""" # create base information of view view_options = ' '.join(( self.vt, self.vp, self.vd, self.vu, self.vh, self.vv, self.vs, self.vl, self.vo, self.va )) return ' '.join(view_options.split()) # remove white spaces
[docs] def info_dict(self, model=None): """Get a dictionary with information about the View. This can be written as a JSON into a model radiance folder to narrow down the number of aperture groups that have to be run with this view. Args: model: A honeybee Model object which will be used to identify the aperture groups that will be run with this view. Default: None. """ base = {} if self._light_path: base['light_path'] = self._light_path elif model and self._room_identifier: # auto-calculate the light path base['light_path'] = light_path_from_room(model, self._room_identifier) if self._group_identifier: base['group_identifier'] = self._group_identifier return base
[docs] def to_dict(self): """Translate view to a dictionary.""" base = { 'type': 'View', 'identifier': self.identifier, 'position': self.position, 'direction': self.direction, 'up_vector': self.up_vector, 'h_size': self.h_size, 'v_size': self.v_size, 'shift': self.shift, 'lift': self.lift, 'view_type': self.type, 'fore_clip': self.fore_clip, 'aft_clip': self.aft_clip } if self._display_name is not None: base['display_name'] = self.display_name if self._room_identifier is not None: base['room_identifier'] = self.room_identifier if self._light_path is not None: base['light_path'] = self.light_path if self._group_identifier is not None: base['group_identifier'] = self.group_identifier return base
[docs] def to_file(self, folder, file_name=None, mkdir=False): """Save view to a file. Args: folder: Target folder. file_name: Optional file name without extension (Default: self.identifier). mkdir: A boolean to indicate if the folder should be created in case it doesn't exist already (Default: False). Returns: Full path to newly created file. """ identifier = file_name or self.identifier + '.vf' if not (identifier.endswith('.vf') or identifier.endswith('.unf')): identifier += '.vf' # add rvu before the view itself content = 'rvu ' + self.to_radiance() return futil.write_to_file_by_name(folder, identifier, content, mkdir)
[docs] def move(self, moving_vec): """Move this view along a vector. Args: moving_vec: A ladybug_geometry Vector3D with the direction and distance to move the view. """ position = pv.Point3D(*self.position) self.position = tuple(position.move(moving_vec))
[docs] def rotate(self, axis, angle, origin=None): """Rotate this view by a certain angle around an axis and origin. Args: axis: Rotation axis as a Vector3D. If None, self.up_vector will be used. angle: An angle for rotation in degrees. origin: A ladybug_geometry Point3D for the origin around which the object will be rotated. If None, self.position is used. (Default: None). """ view_up_vector = pv.Vector3D(*self.up_vector) view_position = pv.Point3D(*self.position) view_direction = pv.Vector3D(*self.direction) view_plane = Plane(n=view_up_vector, o=view_position, x=view_direction) axis = axis if axis is not None else view_up_vector position = origin if origin is not None else view_position rotated_plane = view_plane.rotate(axis, math.radians(angle), position) self._apply_plane_properties(rotated_plane, view_direction, view_up_vector)
[docs] def rotate_xy(self, angle, origin=None): """Rotate this view 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. If None, self.position is used. (Default: None). """ view_up_vector = pv.Vector3D(*self.up_vector) view_position = pv.Point3D(*self.position) view_direction = pv.Vector3D(*self.direction) view_plane = Plane(n=view_up_vector, o=view_position, x=view_direction) position = origin if origin is not None else view_position rotated_plane = view_plane.rotate_xy(math.radians(angle), position) self._apply_plane_properties(rotated_plane, view_direction, view_up_vector)
[docs] def reflect(self, plane): """Reflect this view across a plane. Args: plane: A ladybug_geometry Plane across which the object will be reflected. """ view_up_vector = pv.Vector3D(*self.up_vector) view_position = pv.Point3D(*self.position) view_direction = pv.Vector3D(*self.direction) view_plane = Plane(n=view_up_vector, o=view_position, x=view_direction) ref_plane = view_plane.reflect(plane.n, plane.o) self._apply_plane_properties(ref_plane, view_direction, view_up_vector)
[docs] def scale(self, factor, origin=None): """Scale this view 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). """ view_position = pv.Point3D(*self.position) self.position = view_position.scale(factor, origin) self.direction = pv.Vector3D(*self.direction) * factor self.up_vector = pv.Vector3D(*self.up_vector) * factor
def _apply_plane_properties(self, plane, view_direction, view_up_vector): """Re-set the position, direction and up_vector from a Plane. This method also ensures that the magnitude of the vectors is unchanged (since all Plane objects will have unitized vectors). """ self.position = plane.o self.direction = plane.x * view_direction.magnitude self.up_vector = plane.n * view_up_vector.magnitude @staticmethod def _parse_radiance_options(string): """Parse a radiance option string (e.g. '-ab 4 -ad 256'). The string should start with a '-' otherwise it will be trimmed to the first '-' in the string. """ try: index = string.index('-') except ValueError: if not ' '.join(string.split()).replace('"', '').replace("'", '').strip(): return {} raise ValueError( 'Invalid Radiance options string input. ' 'Failed to find - in input string.' ) _rad_opt_pattern = r'-[a-zA-Z]+' _rad_opt_compiled_pattern = re.compile(_rad_opt_pattern) sub_string = ' '.join(string[index:].split()) value = re.split(_rad_opt_compiled_pattern, sub_string)[1:] key = re.findall(_rad_opt_pattern, sub_string) options = collections.OrderedDict() for k, v in zip(key, value): values = v.split() count = len(values) if count == 0: values = '' elif count == 1: values = values[0] options[k[1:]] = values return options
[docs] def duplicate(self): """Get a copy of this object.""" return self.__copy__()
def __copy__(self): new_obj = View( self.identifier, position=self.position, direction=self.direction, up_vector=self.up_vector, type=self.type, h_size=self.h_size, v_size=self.v_size, shift=self.shift, lift=self.lift) new_obj._display_name = self._display_name new_obj._room_identifier = self._room_identifier new_obj._light_path = self._light_path return new_obj
[docs] def ToString(self): """Overwrite .NET ToString.""" return self.__repr__()
def __key(self): """A tuple based on the object properties, useful for hashing.""" return (self.identifier, hash(self.position), hash(self.direction), hash(self.up_vector), self.type, self.h_size, self.v_size, self.shift, self.lift, self._display_name, self._room_identifier) def __hash__(self): return hash(self.__key()) def __eq__(self, other): return isinstance(other, View) and self.__key() == other.__key() and \ self.light_path == other.light_path def __ne__(self, other): return not self.__eq__(other) def __repr__(self): """View representation.""" return self.to_radiance()