honeybee-radiance 1.66.190__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of honeybee-radiance might be problematic. Click here for more details.

Files changed (152) hide show
  1. honeybee_radiance/__init__.py +11 -0
  2. honeybee_radiance/__main__.py +4 -0
  3. honeybee_radiance/_extend_honeybee.py +93 -0
  4. honeybee_radiance/cli/__init__.py +88 -0
  5. honeybee_radiance/cli/dc.py +400 -0
  6. honeybee_radiance/cli/edit.py +529 -0
  7. honeybee_radiance/cli/glare.py +118 -0
  8. honeybee_radiance/cli/grid.py +859 -0
  9. honeybee_radiance/cli/lib.py +458 -0
  10. honeybee_radiance/cli/modifier.py +133 -0
  11. honeybee_radiance/cli/mtx.py +226 -0
  12. honeybee_radiance/cli/multiphase.py +1034 -0
  13. honeybee_radiance/cli/octree.py +640 -0
  14. honeybee_radiance/cli/postprocess.py +1186 -0
  15. honeybee_radiance/cli/raytrace.py +219 -0
  16. honeybee_radiance/cli/rpict.py +125 -0
  17. honeybee_radiance/cli/schedule.py +56 -0
  18. honeybee_radiance/cli/setconfig.py +63 -0
  19. honeybee_radiance/cli/sky.py +545 -0
  20. honeybee_radiance/cli/study.py +66 -0
  21. honeybee_radiance/cli/sunpath.py +331 -0
  22. honeybee_radiance/cli/threephase.py +255 -0
  23. honeybee_radiance/cli/translate.py +400 -0
  24. honeybee_radiance/cli/util.py +121 -0
  25. honeybee_radiance/cli/view.py +261 -0
  26. honeybee_radiance/cli/viewfactor.py +347 -0
  27. honeybee_radiance/config.json +6 -0
  28. honeybee_radiance/config.py +427 -0
  29. honeybee_radiance/dictutil.py +50 -0
  30. honeybee_radiance/dynamic/__init__.py +5 -0
  31. honeybee_radiance/dynamic/group.py +479 -0
  32. honeybee_radiance/dynamic/multiphase.py +557 -0
  33. honeybee_radiance/dynamic/state.py +718 -0
  34. honeybee_radiance/dynamic/stategeo.py +352 -0
  35. honeybee_radiance/geometry/__init__.py +13 -0
  36. honeybee_radiance/geometry/bubble.py +42 -0
  37. honeybee_radiance/geometry/cone.py +215 -0
  38. honeybee_radiance/geometry/cup.py +54 -0
  39. honeybee_radiance/geometry/cylinder.py +197 -0
  40. honeybee_radiance/geometry/geometrybase.py +37 -0
  41. honeybee_radiance/geometry/instance.py +40 -0
  42. honeybee_radiance/geometry/mesh.py +38 -0
  43. honeybee_radiance/geometry/polygon.py +174 -0
  44. honeybee_radiance/geometry/ring.py +214 -0
  45. honeybee_radiance/geometry/source.py +182 -0
  46. honeybee_radiance/geometry/sphere.py +178 -0
  47. honeybee_radiance/geometry/tube.py +46 -0
  48. honeybee_radiance/lib/__init__.py +1 -0
  49. honeybee_radiance/lib/_loadmodifiers.py +72 -0
  50. honeybee_radiance/lib/_loadmodifiersets.py +69 -0
  51. honeybee_radiance/lib/modifiers.py +58 -0
  52. honeybee_radiance/lib/modifiersets.py +63 -0
  53. honeybee_radiance/lightpath.py +204 -0
  54. honeybee_radiance/lightsource/__init__.py +1 -0
  55. honeybee_radiance/lightsource/_gendaylit.py +479 -0
  56. honeybee_radiance/lightsource/dictutil.py +49 -0
  57. honeybee_radiance/lightsource/ground.py +160 -0
  58. honeybee_radiance/lightsource/sky/__init__.py +7 -0
  59. honeybee_radiance/lightsource/sky/_skybase.py +177 -0
  60. honeybee_radiance/lightsource/sky/certainirradiance.py +232 -0
  61. honeybee_radiance/lightsource/sky/cie.py +378 -0
  62. honeybee_radiance/lightsource/sky/climatebased.py +501 -0
  63. honeybee_radiance/lightsource/sky/hemisphere.py +160 -0
  64. honeybee_radiance/lightsource/sky/skydome.py +113 -0
  65. honeybee_radiance/lightsource/sky/skymatrix.py +163 -0
  66. honeybee_radiance/lightsource/sky/strutil.py +34 -0
  67. honeybee_radiance/lightsource/sky/sunmatrix.py +212 -0
  68. honeybee_radiance/lightsource/sunpath.py +247 -0
  69. honeybee_radiance/modifier/__init__.py +3 -0
  70. honeybee_radiance/modifier/material/__init__.py +30 -0
  71. honeybee_radiance/modifier/material/absdf.py +477 -0
  72. honeybee_radiance/modifier/material/antimatter.py +54 -0
  73. honeybee_radiance/modifier/material/ashik2.py +51 -0
  74. honeybee_radiance/modifier/material/brtdfunc.py +81 -0
  75. honeybee_radiance/modifier/material/bsdf.py +292 -0
  76. honeybee_radiance/modifier/material/dielectric.py +53 -0
  77. honeybee_radiance/modifier/material/glass.py +431 -0
  78. honeybee_radiance/modifier/material/glow.py +246 -0
  79. honeybee_radiance/modifier/material/illum.py +51 -0
  80. honeybee_radiance/modifier/material/interface.py +49 -0
  81. honeybee_radiance/modifier/material/light.py +206 -0
  82. honeybee_radiance/modifier/material/materialbase.py +36 -0
  83. honeybee_radiance/modifier/material/metal.py +167 -0
  84. honeybee_radiance/modifier/material/metal2.py +41 -0
  85. honeybee_radiance/modifier/material/metdata.py +41 -0
  86. honeybee_radiance/modifier/material/metfunc.py +41 -0
  87. honeybee_radiance/modifier/material/mirror.py +340 -0
  88. honeybee_radiance/modifier/material/mist.py +86 -0
  89. honeybee_radiance/modifier/material/plasdata.py +58 -0
  90. honeybee_radiance/modifier/material/plasfunc.py +59 -0
  91. honeybee_radiance/modifier/material/plastic.py +354 -0
  92. honeybee_radiance/modifier/material/plastic2.py +58 -0
  93. honeybee_radiance/modifier/material/prism1.py +57 -0
  94. honeybee_radiance/modifier/material/prism2.py +48 -0
  95. honeybee_radiance/modifier/material/spotlight.py +50 -0
  96. honeybee_radiance/modifier/material/trans.py +518 -0
  97. honeybee_radiance/modifier/material/trans2.py +49 -0
  98. honeybee_radiance/modifier/material/transdata.py +50 -0
  99. honeybee_radiance/modifier/material/transfunc.py +53 -0
  100. honeybee_radiance/modifier/mixture/__init__.py +6 -0
  101. honeybee_radiance/modifier/mixture/mixdata.py +49 -0
  102. honeybee_radiance/modifier/mixture/mixfunc.py +54 -0
  103. honeybee_radiance/modifier/mixture/mixpict.py +52 -0
  104. honeybee_radiance/modifier/mixture/mixtext.py +66 -0
  105. honeybee_radiance/modifier/mixture/mixturebase.py +28 -0
  106. honeybee_radiance/modifier/modifierbase.py +40 -0
  107. honeybee_radiance/modifier/pattern/__init__.py +9 -0
  108. honeybee_radiance/modifier/pattern/brightdata.py +49 -0
  109. honeybee_radiance/modifier/pattern/brightfunc.py +47 -0
  110. honeybee_radiance/modifier/pattern/brighttext.py +81 -0
  111. honeybee_radiance/modifier/pattern/colordata.py +56 -0
  112. honeybee_radiance/modifier/pattern/colorfunc.py +47 -0
  113. honeybee_radiance/modifier/pattern/colorpict.py +54 -0
  114. honeybee_radiance/modifier/pattern/colortext.py +73 -0
  115. honeybee_radiance/modifier/pattern/patternbase.py +34 -0
  116. honeybee_radiance/modifier/texture/__init__.py +4 -0
  117. honeybee_radiance/modifier/texture/texdata.py +29 -0
  118. honeybee_radiance/modifier/texture/texfunc.py +26 -0
  119. honeybee_radiance/modifier/texture/texturebase.py +27 -0
  120. honeybee_radiance/modifierset.py +1091 -0
  121. honeybee_radiance/mutil.py +60 -0
  122. honeybee_radiance/postprocess/__init__.py +1 -0
  123. honeybee_radiance/postprocess/annual.py +108 -0
  124. honeybee_radiance/postprocess/annualdaylight.py +425 -0
  125. honeybee_radiance/postprocess/annualglare.py +201 -0
  126. honeybee_radiance/postprocess/annualirradiance.py +187 -0
  127. honeybee_radiance/postprocess/electriclight.py +119 -0
  128. honeybee_radiance/postprocess/en17037.py +261 -0
  129. honeybee_radiance/postprocess/leed.py +304 -0
  130. honeybee_radiance/postprocess/solartracking.py +90 -0
  131. honeybee_radiance/primitive.py +554 -0
  132. honeybee_radiance/properties/__init__.py +1 -0
  133. honeybee_radiance/properties/_base.py +390 -0
  134. honeybee_radiance/properties/aperture.py +197 -0
  135. honeybee_radiance/properties/door.py +198 -0
  136. honeybee_radiance/properties/face.py +123 -0
  137. honeybee_radiance/properties/model.py +1291 -0
  138. honeybee_radiance/properties/room.py +490 -0
  139. honeybee_radiance/properties/shade.py +186 -0
  140. honeybee_radiance/properties/shademesh.py +116 -0
  141. honeybee_radiance/putil.py +44 -0
  142. honeybee_radiance/reader.py +214 -0
  143. honeybee_radiance/sensor.py +166 -0
  144. honeybee_radiance/sensorgrid.py +1008 -0
  145. honeybee_radiance/view.py +1101 -0
  146. honeybee_radiance/writer.py +951 -0
  147. honeybee_radiance-1.66.190.dist-info/METADATA +89 -0
  148. honeybee_radiance-1.66.190.dist-info/RECORD +152 -0
  149. honeybee_radiance-1.66.190.dist-info/WHEEL +5 -0
  150. honeybee_radiance-1.66.190.dist-info/entry_points.txt +2 -0
  151. honeybee_radiance-1.66.190.dist-info/licenses/LICENSE +661 -0
  152. honeybee_radiance-1.66.190.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1101 @@
1
+ # coding=utf-8
2
+ u"""Create a Radiance view."""
3
+ from __future__ import division
4
+
5
+ from .lightpath import light_path_from_room
6
+
7
+ from honeybee_radiance_command.options import TupleOption, \
8
+ StringOptionJoined, NumericOption
9
+ import honeybee.typing as typing
10
+ import ladybug_geometry.geometry3d.pointvector as pv
11
+ from ladybug_geometry.geometry3d.plane import Plane
12
+ import ladybug.futil as futil
13
+
14
+ import math
15
+ import os
16
+ import re
17
+ import collections
18
+
19
+
20
+ class View(object):
21
+ u"""A Radiance view.
22
+
23
+ Args:
24
+ identifier: Text string for a unique View ID. Must not contain spaces
25
+ or special characters. This will be used to identify the object across
26
+ a model and in the exported Radiance files.
27
+ position: Set the view position (-vp) to (x, y, z). This is the focal
28
+ point of a perspective view or the center of a parallel projection.
29
+ Default: (0, 0, 0)
30
+ direction: Set the view direction (-vd) vector to (x, y, z). The
31
+ length of this vector indicates the focal distance as needed by
32
+ the pixel depth of field (-pd) in rpict. Default: (0, 0, 1)
33
+ up_vector: Set the view up (-vu) vector (vertical direction) to
34
+ (x, y, z) default: (0, 1, 0).
35
+ type: A single character for the view type (-vt). Choose from the following.
36
+
37
+ * v - Perspective
38
+ * h - Hemispherical fisheye
39
+ * l - Parallel
40
+ * c - Cylindrical panorama
41
+ * a - Angular fisheye
42
+ * s - Planisphere [stereographic] projection
43
+
44
+ For more detailed description about view types check rpict manual
45
+ page: (http://radsite.lbl.gov/radiance/man_html/rpict.1.html)
46
+ h_size: Set the view horizontal size (-vh). For a perspective
47
+ projection (including fisheye views), val is the horizontal field
48
+ of view (in degrees). For a parallel projection, val is the view
49
+ width in world coordinates.
50
+ v_size: Set the view vertical size (-vv). For a perspective
51
+ projection (including fisheye views), val is the horizontal field
52
+ of view (in degrees). For a parallel projection, val is the view
53
+ width in world coordinates.
54
+ shift: Set the view shift (-vs). This is the amount the actual
55
+ image will be shifted to the right of the specified view. This
56
+ option is useful for generating skewed perspectives or rendering
57
+ an image a piece at a time. A value of 1 means that the rendered
58
+ image starts just to the right of the normal view. A value of -1
59
+ would be to the left. Larger or fractional values are permitted
60
+ as well.
61
+ lift: Set the view lift (-vl) to a value. This is the amount the
62
+ actual image will be lifted up from the specified view.
63
+
64
+ Properties:
65
+ * identifier
66
+ * display_name
67
+ * position
68
+ * direction
69
+ * up_vector
70
+ * type
71
+ * h_size
72
+ * v_size
73
+ * shift
74
+ * lift
75
+ * room_identifier
76
+ * light_path
77
+ * group_identifier
78
+ * full_identifier
79
+
80
+ Usage:
81
+
82
+ .. code-block:: python
83
+
84
+ v = View()
85
+ # add a fore clip
86
+ v.fore_clip = 100
87
+ print(v)
88
+
89
+ > -vtv -vp 0.000 0.000 0.000 -vd 0.000 0.000 1.000 -vu 0.000 1.000
90
+ 0.000 -vh 60.000 -vv 60.000 -vo 100.000
91
+
92
+ # split the view into a view grid
93
+ gridViews = v.grid(2, 2, 600, 600)
94
+ for g in gridViews:
95
+ print(g)
96
+
97
+ > -vtv -vp 0.000 0.000 0.000 -vd 0.000 0.000 1.000 -vu 0.000 1.000
98
+ 0.000 -vh 29.341 -vv 32.204 -vs -0.500 -vl -0.500 -vo 100.000
99
+
100
+ > -vtv -vp 0.000 0.000 0.000 -vd 0.000 0.000 1.000 -vu 0.000 1.000
101
+ 0.000 -vh 29.341 -vv 32.204 -vs 0.500 -vl -0.500 -vo 100.000
102
+
103
+ > -vtv -vp 0.000 0.000 0.000 -vd 0.000 0.000 1.000 -vu 0.000 1.000
104
+ 0.000 -vh 29.341 -vv 32.204 -vs -0.500 -vl 0.500 -vo 100.000
105
+
106
+ > -vtv -vp 0.000 0.000 0.000 -vd 0.000 0.000 1.000 -vu 0.000 1.000
107
+ 0.000 -vh 29.341 -vv 32.204 -vs 0.500 -vl 0.500 -vo 100.000
108
+ """
109
+
110
+ __slots__ = ('_identifier', '_display_name', '_position', '_direction',
111
+ '_up_vector', '_h_size', '_v_size', '_shift', '_lift',
112
+ '_type', '_fore_clip', '_aft_clip', '_room_identifier',
113
+ '_light_path', '_group_identifier')
114
+
115
+ def __init__(self, identifier, position=None, direction=None, up_vector=None,
116
+ type='v', h_size=60, v_size=60, shift=None, lift=None):
117
+ u"""Create a view."""
118
+ self.identifier = identifier
119
+ self._display_name = None
120
+ self._position = TupleOption(
121
+ 'vp', 'view position', position if position is not None else (0, 0, 0)
122
+ )
123
+ self._direction = TupleOption(
124
+ 'vd', 'view direction', direction if direction is not None else (0, 0, 1)
125
+ )
126
+ self._up_vector = TupleOption(
127
+ 'vu', 'view up vector', up_vector if up_vector is not None else (0, 1, 0)
128
+ )
129
+ self._h_size = NumericOption('vh', 'view horizontal size', h_size, min_value=0)
130
+ self._v_size = NumericOption('vv', 'view vertical size', v_size, min_value=0)
131
+ self._shift = NumericOption('vs', 'view shift', shift)
132
+ self._lift = NumericOption('vl', 'view lift', lift)
133
+ self._type = StringOptionJoined(
134
+ 'vt', 'view type', type, valid_values=['v', 'h', 'l', 'c', 'a', 's']
135
+ )
136
+ # set for_clip to None
137
+ self._fore_clip = NumericOption('vo', 'view fore clip')
138
+ self._aft_clip = NumericOption('va', 'view aft clip')
139
+
140
+ self._room_identifier = None
141
+ self._group_identifier = None
142
+ self._light_path = None
143
+ self._check_size_and_type()
144
+
145
+ @property
146
+ def identifier(self):
147
+ """Get or set a text string for a unique View identifier."""
148
+ return self._identifier
149
+
150
+ @identifier.setter
151
+ def identifier(self, n):
152
+ self._identifier = typing.valid_rad_string(n, 'view identifier')
153
+
154
+ @property
155
+ def display_name(self):
156
+ """Get or set a string for the object name without any character restrictions.
157
+
158
+ If not set, this will be equal to the identifier.
159
+ """
160
+ if self._display_name is None:
161
+ return self._identifier
162
+ return self._display_name
163
+
164
+ @display_name.setter
165
+ def display_name(self, value):
166
+ try:
167
+ self._display_name = str(value)
168
+ except UnicodeEncodeError: # Python 2 machine lacking the character set
169
+ self._display_name = value # keep it as unicode
170
+
171
+ @property
172
+ def is_fisheye(self):
173
+ """Check if the view type is one of the fisheye views."""
174
+ return self.type in ('h', 'a', 's')
175
+
176
+ @property
177
+ def type(self):
178
+ """Set and get view type (-vt) to one of the choices below.
179
+
180
+ * v - Perspective (v)
181
+ * h - Hemispherical fisheye (h)
182
+ * l - Parallel (l)
183
+ * c - Cylindrical panorama (c)
184
+ * a - Angular fisheye (a)
185
+ * s - Planisphere [stereographic] projection (s)
186
+ """
187
+ return self._type.value
188
+
189
+ @property
190
+ def vt(self):
191
+ """View type as a string in radiance format."""
192
+ return self._type.to_radiance()
193
+
194
+ @type.setter
195
+ def type(self, value):
196
+ self._type.value = value[-1:] # this will handle both vtv and v inputs
197
+ self._check_size_and_type()
198
+
199
+ @property
200
+ def position(self):
201
+ """Set the view position (-vp) to (x, y, z).
202
+
203
+ This is the focal point of a perspective view or the center of a parallel
204
+ projection. Default: (0, 0, 0)
205
+ """
206
+ return self._position.value
207
+
208
+ @property
209
+ def vp(self):
210
+ """View point / position as a string in radiance format."""
211
+ return self._position.to_radiance()
212
+
213
+ @position.setter
214
+ def position(self, value):
215
+ self._position.value = value
216
+
217
+ @property
218
+ def direction(self):
219
+ """Set the view direction (-vd) vector to (x, y, z).
220
+
221
+ The length of this vector indicates the focal distance as needed by
222
+ the pixel depth of field (-pd) in rpict. Default: (0, 0, 1)
223
+ """
224
+ return self._direction.value
225
+
226
+ @property
227
+ def vd(self):
228
+ """View direction as a string in radiance format."""
229
+ return self._direction.to_radiance()
230
+
231
+ @direction.setter
232
+ def direction(self, value):
233
+ self._direction.value = value
234
+
235
+ @property
236
+ def up_vector(self):
237
+ """Set and get the view up (-vu) vector (vertical direction) to (x, y, z)
238
+
239
+ Default: (0, 1, 0).
240
+ """
241
+ return self._up_vector.value
242
+
243
+ @property
244
+ def vu(self):
245
+ """View up as a string in radiance format."""
246
+ return self._up_vector.to_radiance()
247
+
248
+ @up_vector.setter
249
+ def up_vector(self, value):
250
+ self._up_vector.value = value
251
+
252
+ @property
253
+ def h_size(self):
254
+ """Set the view horizontal size (-vh).
255
+
256
+ For a perspective projection (including fisheye views), this is the horizontal
257
+ field of view (in degrees). For a parallel projection, this is the view
258
+ width in world coordinates.
259
+ """
260
+ return self._h_size.value
261
+
262
+ @property
263
+ def vh(self):
264
+ """View horizontal size as a string in radiance format."""
265
+ return self._h_size.to_radiance()
266
+
267
+ @h_size.setter
268
+ def h_size(self, value):
269
+ self._h_size.value = value if value is not None else 60
270
+ self._check_size_and_type()
271
+
272
+ @property
273
+ def v_size(self):
274
+ """Set the view vertical size (-vv).
275
+
276
+ For a perspective projection (including fisheye views), this is the horizontal
277
+ field of view (in degrees). For a parallel projection, this is the view width in
278
+ world coordinates.
279
+ """
280
+ return self._v_size.value
281
+
282
+ @property
283
+ def vv(self):
284
+ """View vertical size as a string in radiance format."""
285
+ return self._v_size.to_radiance()
286
+
287
+ @v_size.setter
288
+ def v_size(self, value):
289
+ self._v_size.value = value if value is not None else 60
290
+ self._check_size_and_type()
291
+
292
+ @property
293
+ def shift(self):
294
+ """Set the view shift (-vs).
295
+
296
+ This is the amount the actual image will be shifted to the right of the specified
297
+ view. This option is useful for generating skewed perspectives or rendering an
298
+ image a piece at a time. A value of 1 means that the rendered image starts just
299
+ to the right of the normal view. A value of -1 would be to the left. Larger or
300
+ fractional values are permitted as well.
301
+ """
302
+ return self._shift.value
303
+
304
+ @property
305
+ def vs(self):
306
+ """View shift as a string in radiance format."""
307
+ return self._shift.to_radiance()
308
+
309
+ @shift.setter
310
+ def shift(self, value):
311
+ self._shift.value = value
312
+
313
+ @property
314
+ def lift(self):
315
+ """Set the view lift (-vl) to a value.
316
+
317
+ This is the amount the actual image will be lifted up from the specified view.
318
+ """
319
+ return self._lift.value
320
+
321
+ @property
322
+ def vl(self):
323
+ """View lift as a string in radiance format."""
324
+ return self._lift.to_radiance()
325
+
326
+ @lift.setter
327
+ def lift(self, value):
328
+ self._lift.value = value
329
+
330
+ @property
331
+ def fore_clip(self):
332
+ """View fore clip (-vo) at a distance from the view point.
333
+
334
+ The plane will be perpendicular to the view direction for perspective
335
+ and parallel view types. For fisheye view types, the clipping plane is
336
+ actually a clipping sphere, centered on the view point with radius val.
337
+ Objects in front of this imaginary surface will not be visible. This may
338
+ be useful for seeing through walls (to get a longer perspective from an
339
+ exterior view point) or for incremental rendering. A value of zero implies
340
+ no foreground clipping. A negative value produces some interesting effects,
341
+ since it creates an inverted image for objects behind the viewpoint.
342
+ """
343
+ return self._fore_clip.value
344
+
345
+ @property
346
+ def vo(self):
347
+ """View fore clip as a string in radiance format."""
348
+ return self._fore_clip.to_radiance()
349
+
350
+ @fore_clip.setter
351
+ def fore_clip(self, distance):
352
+ self._fore_clip.value = distance
353
+
354
+ @property
355
+ def aft_clip(self):
356
+ """View aft clip (-va) at a distance from the view point.
357
+
358
+ Set the view aft clipping plane at a distance of val from the view point. Like
359
+ the view fore plane, it will be perpendicular to the view direction for
360
+ perspective and parallel view types. For fisheye view types, the clipping plane
361
+ is actually a clipping sphere, centered on the view point with radius val.
362
+ Objects behind this imaginary surface will not be visible. A value of zero means
363
+ no aft clipping, and is the only way to see infinitely distant objects such as
364
+ the sky.
365
+ """
366
+ return self._aft_clip.value
367
+
368
+ @property
369
+ def va(self):
370
+ """View aft clip as a string in radiance format."""
371
+ return self._aft_clip.to_radiance()
372
+
373
+ @aft_clip.setter
374
+ def aft_clip(self, distance):
375
+ self._aft_clip.value = distance
376
+
377
+ @property
378
+ def room_identifier(self):
379
+ """Get or set text for the Room identifier to which this View belongs.
380
+
381
+ This will be used in the info_dict method to narrow down the
382
+ number of aperture groups that have to be run with this view. If None,
383
+ the view will be run with all aperture groups in the model.
384
+ """
385
+ return self._room_identifier
386
+
387
+ @room_identifier.setter
388
+ def room_identifier(self, n):
389
+ self._room_identifier = typing.valid_string(n)
390
+
391
+ @property
392
+ def group_identifier(self):
393
+ """Get or set text for the group identifier to which this View belongs.
394
+
395
+ This will be used in the write to radiance folder method to write all the views
396
+ with the same group identifier under the same subfolder.
397
+
398
+ You may use / in name to identify nested view groups. For example
399
+ floor_1/living_room create a view under living_room/floor_1 subfolder.
400
+
401
+ If None, the view will be written to the root of views folder.
402
+ """
403
+ return self._group_identifier
404
+
405
+ @group_identifier.setter
406
+ def group_identifier(self, identifier_key):
407
+ if identifier_key is not None:
408
+ identifier_key = \
409
+ '/'.join(
410
+ typing.valid_rad_string(key, 'view group identifier')
411
+ for key in identifier_key.split('/')
412
+ )
413
+ self._group_identifier = identifier_key
414
+
415
+ @property
416
+ def full_identifier(self):
417
+ """Get full identifier for view.
418
+
419
+ For a view with group identifier it will be group_identifier/identifier
420
+ """
421
+ return self.identifier if not self.group_identifier \
422
+ else '%s/%s' % (self.group_identifier, self.identifier)
423
+
424
+ @property
425
+ def light_path(self):
426
+ """Get or set list of lists for the light path from the view to the sky.
427
+
428
+ Each sub-list contains identifiers of aperture groups through which light
429
+ passes. (eg. [['SouthWindow1'], ['__static_apertures__', 'NorthWindow2']]).
430
+ Setting this property will override any auto-calculation of the light
431
+ path from the model upon export to the simulation.
432
+ """
433
+ return self._light_path
434
+
435
+ @light_path.setter
436
+ def light_path(self, l_path):
437
+ if l_path is not None:
438
+ assert isinstance(l_path, (tuple, list)), 'Expected list or tuple for ' \
439
+ 'light_path. Got {}.'.format(type(l_path))
440
+ for ap_list in l_path:
441
+ assert isinstance(ap_list, (tuple, list)), 'Expected list or tuple ' \
442
+ 'for light_path sub-list. Got {}.'.format(type(ap_list))
443
+ for ap in ap_list:
444
+ assert isinstance(ap, str), 'Expected text for light_path ' \
445
+ 'aperture group identifier. Got {}.'.format(type(ap))
446
+ self._light_path = l_path
447
+
448
+ def standardize_fisheye(self):
449
+ """Automatically set view size to 180 degrees if the view type is a fisheye.
450
+
451
+ Alternatively it sets the view size to 360 degrees if both the view type is
452
+ angular fisheye and either the horizontal or vertical view size is 360 degrees.
453
+ """
454
+ if self.type in ('h', 's'):
455
+ if self.h_size != 180:
456
+ self.h_size = 180
457
+ if self.v_size != 180:
458
+ self.v_size = 180
459
+ if self.type in ('a'):
460
+ if self.h_size == 360 or self.v_size == 360:
461
+ self.h_size = self.v_size = 360
462
+ else:
463
+ if self.h_size != 180:
464
+ self.h_size = 180
465
+ if self.v_size != 180:
466
+ self.v_size = 180
467
+
468
+ def _check_size_and_type(self):
469
+ """Check to be sure the view size and type are compatible."""
470
+ if self.type == 'v':
471
+ assert self.h_size < 180, \
472
+ '\n{} is an invalid horizontal view size for Perspective view.\n' \
473
+ 'The size should be smaller than 180.'.format(self.h_size)
474
+ assert self.v_size < 180, \
475
+ '\n{} is an invalid vertical view size for Perspective view.\n' \
476
+ 'The size should be smaller than 180.'.format(self.v_size)
477
+
478
+ @classmethod
479
+ def from_dict(cls, view_dict):
480
+ """Create a view from a dictionary in the following format.
481
+
482
+ .. code-block:: python
483
+
484
+ {
485
+ 'type': 'View',
486
+ 'identifier': str, # View identifier
487
+ "display_name": str, # View display name
488
+ 'position': [], # list with position value
489
+ 'direction': [], # list with direction value
490
+ 'up_vector': [], # list with up_vector value
491
+ 'h_size': number, # h_size.value
492
+ 'v_size': number, # v_size value
493
+ 'shift': number, # shift value
494
+ 'lift': number, # lift value
495
+ 'view_type': number, # view_type value
496
+ 'fore_clip': number, # fore_clip value
497
+ 'aft_clip': number, # aft_clip value
498
+ 'room_identifier': str, # optional room identifier
499
+ 'light_path': [] # optional list of lists for light path
500
+ }
501
+ """
502
+ assert view_dict['type'] == 'View', \
503
+ 'Expected View dictionary. Got {}.'.format(view_dict['type'])
504
+
505
+ view_type = view_dict['view_type'][-1:] if 'view_type' in view_dict else 'v'
506
+ view = cls(
507
+ identifier=view_dict['identifier'],
508
+ position=view_dict['position'],
509
+ direction=view_dict['direction'],
510
+ up_vector=view_dict['up_vector'],
511
+ type=view_type,
512
+ h_size=view_dict['h_size'],
513
+ v_size=view_dict['v_size'],
514
+ shift=view_dict['shift'],
515
+ lift=view_dict['lift'],
516
+ )
517
+
518
+ if 'fore_clip' in view_dict:
519
+ view.fore_clip = view_dict['fore_clip']
520
+ if 'aft_clip' in view_dict:
521
+ view.aft_clip = view_dict['aft_clip']
522
+ if 'display_name' in view_dict and view_dict['display_name'] is not None:
523
+ view.display_name = view_dict['display_name']
524
+ if 'room_identifier' in view_dict and view_dict['room_identifier'] is not None:
525
+ view.room_identifier = view_dict['room_identifier']
526
+ if 'light_path' in view_dict and view_dict['light_path'] is not None:
527
+ view.light_path = view_dict['light_path']
528
+ if 'group_identifier' in view_dict and view_dict['group_identifier'] is not None:
529
+ view_dict.group_identifier = view_dict['group_identifier']
530
+ return view
531
+
532
+ @classmethod
533
+ def from_string(cls, identifier, view_string):
534
+ """Create a view object from a string.
535
+
536
+ This method is similar to from_string method for radiance parameters with the
537
+ difference that all the parameters that are not related to view will be ignored.
538
+ """
539
+ mapper = {
540
+ 'identifier': identifier, 'vp': 'position', 'vd': 'direction',
541
+ 'vu': 'up_vector', 'vh': 'h_size', 'vv': 'v_size', 'vs': 'shift',
542
+ 'vl': 'lift', 'vo': 'fore_clip', 'va': 'aft_clip'
543
+ }
544
+
545
+ base = {
546
+ 'type': 'View',
547
+ 'identifier': identifier,
548
+ 'position': None,
549
+ 'direction': None,
550
+ 'up_vector': None,
551
+ 'h_size': None,
552
+ 'v_size': None,
553
+ 'shift': None,
554
+ 'lift': None,
555
+ 'view_type': None,
556
+ 'fore_clip': None,
557
+ 'aft_clip': None
558
+ }
559
+
560
+ # parse the string here
561
+ options = cls._parse_radiance_options(view_string)
562
+
563
+ for opt, value in options.items():
564
+ if opt in mapper:
565
+ base[mapper[opt]] = value
566
+ elif opt[:2] == 'vt':
567
+ base['view_type'] = opt
568
+ else:
569
+ print('%s is not a view parameter and is ignored.' % opt)
570
+
571
+ return cls.from_dict(base)
572
+
573
+ @classmethod
574
+ def from_file(cls, file_path, identifier=None):
575
+ """Create view from a view file.
576
+
577
+ Args:
578
+ file_path: Full path to view file.
579
+ identifier: Optional ext string for a unique View ID. Must not contain spaces
580
+ or special characters. This will be used to identify the object across
581
+ a model and in the exported Radiance files. If None, this will be set
582
+ to file name. (Default: None)
583
+ """
584
+
585
+ if not os.path.isfile(file_path):
586
+ raise IOError("Can't find {}.".format(file_path))
587
+ identifier = identifier or os.path.split(os.path.splitext(file_path)[0])[-1]
588
+
589
+ with open(file_path, 'r') as input_data:
590
+ view_string = str(input_data.read()).rstrip()
591
+
592
+ assert view_string[:3] == 'rvu', \
593
+ 'View file must start with rvu not %s' % view_string[:3]
594
+ return cls.from_string(identifier, view_string)
595
+
596
+ @classmethod
597
+ def from_grid(cls, grid, identifier='from_grid'):
598
+ """Create view from a grid of views.
599
+ Generally the grid argument should be the views generated by the grid method.
600
+ Args:
601
+ grid: A list of subviews. If only a single view is given, this view will be
602
+ returned. The views can be either class instances of View, strings or
603
+ .unf files. If strings are used, the views will be created by the
604
+ from_string method. If .unf files are used, the views will be created by
605
+ the from_string method using the view found in the Radiance header.
606
+ identifier: Text string for a unique View ID. Must not contain spaces
607
+ or special characters. This will be used to identify the object across
608
+ a model and in the exported Radiance files. If None, this will be set
609
+ to 'from_grid'. (Default: 'from_grid')
610
+ """
611
+ if not isinstance(grid, (list, tuple)):
612
+ grid = [grid]
613
+
614
+ views = []
615
+ # check if grid argument views are valid
616
+ for c, view in enumerate(grid):
617
+ if isinstance(view, View):
618
+ views.append(view)
619
+ elif view.endswith('.unf'):
620
+ try:
621
+ f = open(view, 'r', encoding='utf-8', errors='ignore')
622
+ except Exception:
623
+ f = open(view, 'r')
624
+ try:
625
+ for line in f:
626
+ if not line.strip():
627
+ break
628
+ else:
629
+ low_line = line.strip().lower()
630
+ if low_line.startswith('view='):
631
+ print(low_line)
632
+ views.append(cls.from_string('view_%04d' % c, low_line))
633
+ except Exception:
634
+ raise ValueError('Failed to find view in Radiance header.')
635
+ finally:
636
+ f.close()
637
+ elif isinstance(view, str):
638
+ views.append(cls.from_string('view_%04d' % c, view))
639
+ else:
640
+ raise ValueError(
641
+ 'Expected Honeybee Radiance View, string or .unf file.'
642
+ 'Got: {}'.format(type(view))
643
+ )
644
+
645
+ # if only a single (valid) view is given, then return the view
646
+ if len(views) == 1:
647
+ return views[0]
648
+
649
+ _type = set()
650
+ _view_point = set()
651
+ _view_direction = set()
652
+ _up_direction = set()
653
+ _vh = set()
654
+ _vv = set()
655
+ _x_div_count = set()
656
+ _y_div_count = set()
657
+ # check if type, view point, view direction, and up direction are equal in views
658
+ # all unique values are collected, all except -vh and -vv must be the same
659
+ for _view in views:
660
+ _type.add(_view.type)
661
+ _view_point.add(_view.position)
662
+ _view_direction.add(_view.direction)
663
+ _up_direction.add(_view.up_vector)
664
+ _vh.add(_view.h_size)
665
+ _vv.add(_view.v_size)
666
+ _x_div_count.add(_view.shift)
667
+ _y_div_count.add(_view.lift)
668
+
669
+ if len(_type) > 1:
670
+ raise ValueError('All subviews must have the same view type.')
671
+ if len(_view_point) > 1:
672
+ raise ValueError('All subviews must have the same view point.')
673
+ if len(_view_direction) > 1:
674
+ raise ValueError('All subviews must have the same view direction.')
675
+ if len(_up_direction) > 1:
676
+ raise ValueError('All subviews must have the same up direction.')
677
+ if len(_vh) > 1:
678
+ raise ValueError('All subviews must have the same horizontal view size.')
679
+ if len(_vv) > 1:
680
+ raise ValueError('All subviews must have the same vertical view size.')
681
+
682
+ # find the grid dimensions (x_count, y_count)
683
+ x_div_count = len(_x_div_count)
684
+ y_div_count = len(_y_div_count)
685
+
686
+ # get the horizontal and vertical view size from the subviews
687
+ _vh = list(_vh)[0]
688
+ _vv = list(_vv)[0]
689
+
690
+ # create instance of view
691
+ view = cls(
692
+ identifier=identifier,
693
+ position=list(_view_point)[0],
694
+ direction=list(_view_direction)[0],
695
+ up_vector=list(_up_direction)[0],
696
+ type=list(_type)[0])
697
+
698
+ PI = math.pi
699
+ # find the horizontal and vertical size
700
+ if view.type == 'l' or view.type == 'a':
701
+ # parallel view (vtl) or angular fisheye (vta)
702
+ h_size = _vh * x_div_count
703
+ v_size = _vv * y_div_count
704
+
705
+ elif view.type == 'v':
706
+ # perspective (vtv)
707
+ pi2 = (2. * 180. / PI)
708
+ h_size = pi2 * math.tan(_vh / (2. * 180. / PI)) * x_div_count
709
+ v_size = pi2 * math.atan(math.tan(_vv / (2. * 180. / PI)) * y_div_count)
710
+
711
+ elif view.type == 'h':
712
+ # hemispherical fisheye (vth)
713
+ pi2 = (2. * 180. / PI)
714
+ h_size = pi2 * math.asin(math.sin(_vh / (2. * 180. / PI)) * x_div_count)
715
+ v_size = pi2 * math.asin(math.sin(_vv / (2. * 180. / PI)) * y_div_count)
716
+
717
+ else:
718
+ raise ValueError(
719
+ 'Grid views are not supported for %s.' % view.type)
720
+
721
+ # round the number to avoid cases like 59.99999999999999 when should be 60
722
+ h_size = round(h_size, 10)
723
+ v_size = round(v_size, 10)
724
+
725
+ # update horizontal and vertical view size
726
+ view.h_size = h_size
727
+ view.v_size = v_size
728
+
729
+ return view
730
+
731
+ def dimension(self, x_res=None, y_res=None):
732
+ """Get dimensions for this view as '-x %d -y %d [-ld-]'.
733
+
734
+ This method is same as vwrays -d. Default values for x_res and y_res are set to
735
+ match Radiance defaults.
736
+ """
737
+ x, y = self.dimension_x_y(x_res, y_res)
738
+ return '-x %d -y %d -ld%s' % (x, y, '-' if (self.vo + self.va == '') else '+')
739
+
740
+ def dimension_x_y(self, x_res=None, y_res=None):
741
+ """Get dimensions for this view as x, y.
742
+
743
+ Default values for x_res and y_res are set to match Radiance defaults.
744
+ """
745
+ # radiance default is 512
746
+ x_res = int(x_res) if x_res is not None else 512
747
+ y_res = int(y_res) if y_res is not None else 512
748
+
749
+ if self.is_fisheye:
750
+ return min(x_res, y_res), min(x_res, y_res)
751
+
752
+ vh = self.h_size
753
+ vv = self.v_size
754
+
755
+ if self.type == 'v':
756
+ hv_ratio = math.tan(math.radians(vh) / 2.0) / \
757
+ math.tan(math.radians(vv) / 2.0)
758
+ else:
759
+ hv_ratio = vh / vv
760
+
761
+ # radiance keeps the largest max size and tries to scale the other size
762
+ # to fit the aspect ratio. In case the size doesn't match it reverses
763
+ # the process.
764
+ if y_res <= x_res:
765
+ new_x = int(round(hv_ratio * y_res))
766
+ if new_x <= x_res:
767
+ return new_x, y_res
768
+ else:
769
+ new_y = int(round(x_res / hv_ratio))
770
+ return x_res, new_y
771
+ else:
772
+ new_y = int(round(x_res / hv_ratio))
773
+ if new_y <= y_res:
774
+ return x_res, new_y
775
+ else:
776
+ new_x = int(round(hv_ratio * y_res))
777
+ return new_x, y_res
778
+
779
+ def grid(self, x_div_count=1, y_div_count=1):
780
+ """Break-down the view into a grid of views based on x and y grid count.
781
+
782
+ Views will be returned row by row from right to left.
783
+
784
+ Args:
785
+ x_div_count: Set number of divisions in x direction (Default: 1).
786
+ y_div_count: Set number of divisions in y direction (Default: 1).
787
+
788
+ Returns:
789
+ A tuple of views. Views are sorted row by row from right to left.
790
+ """
791
+ PI = math.pi
792
+ try:
793
+ x_div_count = abs(x_div_count)
794
+ y_div_count = abs(y_div_count)
795
+ except TypeError as e:
796
+ raise ValueError("Division count should be a number.\n%s" % str(e))
797
+
798
+ assert x_div_count * y_div_count != 0, "Division count should be larger than 0."
799
+
800
+ if x_div_count == y_div_count == 1:
801
+ return [self]
802
+
803
+ _views = list(range(x_div_count * y_div_count))
804
+
805
+ if self.type in ('l', 'a', 'c'):
806
+ # parallel view (vtl) or angular fisheye (vta)
807
+ _vh = self.h_size / x_div_count
808
+ _vv = self.v_size / y_div_count
809
+
810
+ elif self.type == 'v':
811
+ # perspective (vtv)
812
+ pi2 = (2. * 180. / PI)
813
+ _vh = pi2 * math.atan(((PI / 180. / 2.) * self.h_size) / x_div_count)
814
+ _vv = pi2 * math.atan(math.tan((PI / 180. / 2.) * self.v_size) / y_div_count)
815
+
816
+ elif self.type == 's':
817
+ # planisphere (stereographic)
818
+ pi2 = (2. * 180. / PI * 2)
819
+ _vh = pi2 * math.atan(math.sin((PI / 180. / 2.) * self.h_size) / x_div_count)
820
+ _vv = pi2 * math.atan(math.sin((PI / 180. / 2.) * self.v_size) / y_div_count)
821
+
822
+ elif self.type in ('h'):
823
+ # hemispherical fish eye
824
+ pi2 = (2. * 180. / PI)
825
+ _vh = pi2 * math.asin(math.sin((PI / 180. / 2.) * self.h_size) / x_div_count)
826
+ _vv = pi2 * math.asin(math.sin((PI / 180. / 2.) * self.v_size) / y_div_count)
827
+
828
+ else:
829
+ print("Grid views are not supported for %s." % self.type)
830
+ return [self]
831
+
832
+ # create a set of new views
833
+ for view_count in range(len(_views)):
834
+ # calculate view shift and view lift
835
+ if x_div_count == 1:
836
+ _vs = 0
837
+ else:
838
+ _vs = (((view_count % x_div_count) / (x_div_count - 1)) - 0.5) \
839
+ * (x_div_count - 1)
840
+
841
+ if y_div_count == 1:
842
+ _vl = 0
843
+ else:
844
+ _vl = ((int(view_count % y_div_count) / (y_div_count - 1)) - 0.5) \
845
+ * (y_div_count - 1)
846
+
847
+ # create a copy from the current view
848
+ _n_view = self.duplicate()
849
+ _n_view.identifier = '%s_%d' % (self.identifier, view_count)
850
+ # update parameters
851
+ _n_view.h_size = _vh
852
+ _n_view.v_size = _vv
853
+ _n_view.shift = _vs
854
+ _n_view.lift = _vl
855
+ _n_view._fore_clip = self._fore_clip
856
+ _n_view._aft_clip = self._aft_clip
857
+ try:
858
+ _n_view.display_name = '%s_%d' % (self.display_name, view_count)
859
+ except UnicodeEncodeError: # character no found on machine
860
+ pass
861
+
862
+ # add the new view to views list
863
+ _views[view_count] = _n_view
864
+
865
+ return _views
866
+
867
+ def to_radiance(self):
868
+ """Return full Radiance definition as a string."""
869
+ # create base information of view
870
+ view_options = ' '.join((
871
+ self.vt, self.vp, self.vd, self.vu,
872
+ self.vh, self.vv, self.vs, self.vl,
873
+ self.vo, self.va
874
+ ))
875
+
876
+ return ' '.join(view_options.split()) # remove white spaces
877
+
878
+ def info_dict(self, model=None):
879
+ """Get a dictionary with information about the View.
880
+
881
+ This can be written as a JSON into a model radiance folder to narrow
882
+ down the number of aperture groups that have to be run with this view.
883
+
884
+ Args:
885
+ model: A honeybee Model object which will be used to identify
886
+ the aperture groups that will be run with this view. Default: None.
887
+ """
888
+ base = {}
889
+ if self._light_path:
890
+ base['light_path'] = self._light_path
891
+ elif model and self._room_identifier: # auto-calculate the light path
892
+ base['light_path'] = light_path_from_room(model, self._room_identifier)
893
+
894
+ if self._group_identifier:
895
+ base['group_identifier'] = self._group_identifier
896
+
897
+ return base
898
+
899
+ def to_dict(self):
900
+ """Translate view to a dictionary."""
901
+ base = {
902
+ 'type': 'View',
903
+ 'identifier': self.identifier,
904
+ 'position': self.position,
905
+ 'direction': self.direction,
906
+ 'up_vector': self.up_vector,
907
+ 'h_size': self.h_size,
908
+ 'v_size': self.v_size,
909
+ 'shift': self.shift,
910
+ 'lift': self.lift,
911
+ 'view_type': self.type,
912
+ 'fore_clip': self.fore_clip,
913
+ 'aft_clip': self.aft_clip
914
+ }
915
+ if self._display_name is not None:
916
+ base['display_name'] = self.display_name
917
+ if self._room_identifier is not None:
918
+ base['room_identifier'] = self.room_identifier
919
+ if self._light_path is not None:
920
+ base['light_path'] = self.light_path
921
+ if self._group_identifier is not None:
922
+ base['group_identifier'] = self.group_identifier
923
+ return base
924
+
925
+ def to_file(self, folder, file_name=None, mkdir=False):
926
+ """Save view to a file.
927
+
928
+ Args:
929
+ folder: Target folder.
930
+ file_name: Optional file name without extension (Default: self.identifier).
931
+ mkdir: A boolean to indicate if the folder should be created in case it
932
+ doesn't exist already (Default: False).
933
+
934
+ Returns:
935
+ Full path to newly created file.
936
+ """
937
+
938
+ identifier = file_name or self.identifier + '.vf'
939
+ if not (identifier.endswith('.vf') or identifier.endswith('.unf')):
940
+ identifier += '.vf'
941
+ # add rvu before the view itself
942
+ content = 'rvu ' + self.to_radiance()
943
+ return futil.write_to_file_by_name(folder, identifier, content, mkdir)
944
+
945
+ def move(self, moving_vec):
946
+ """Move this view along a vector.
947
+
948
+ Args:
949
+ moving_vec: A ladybug_geometry Vector3D with the direction and distance
950
+ to move the view.
951
+ """
952
+ position = pv.Point3D(*self.position)
953
+ self.position = tuple(position.move(moving_vec))
954
+
955
+ def rotate(self, axis, angle, origin=None):
956
+ """Rotate this view by a certain angle around an axis and origin.
957
+
958
+ Args:
959
+ axis: Rotation axis as a Vector3D. If None, self.up_vector will be used.
960
+ angle: An angle for rotation in degrees.
961
+ origin: A ladybug_geometry Point3D for the origin around which the
962
+ object will be rotated. If None, self.position is used. (Default: None).
963
+ """
964
+ view_up_vector = pv.Vector3D(*self.up_vector)
965
+ view_position = pv.Point3D(*self.position)
966
+ view_direction = pv.Vector3D(*self.direction)
967
+ view_plane = Plane(n=view_up_vector, o=view_position, x=view_direction)
968
+ axis = axis if axis is not None else view_up_vector
969
+ position = origin if origin is not None else view_position
970
+
971
+ rotated_plane = view_plane.rotate(axis, math.radians(angle), position)
972
+ self._apply_plane_properties(rotated_plane, view_direction, view_up_vector)
973
+
974
+ def rotate_xy(self, angle, origin=None):
975
+ """Rotate this view counterclockwise in the world XY plane by a certain angle.
976
+
977
+ Args:
978
+ angle: An angle in degrees.
979
+ origin: A ladybug_geometry Point3D for the origin around which the
980
+ object will be rotated. If None, self.position is used. (Default: None).
981
+ """
982
+ view_up_vector = pv.Vector3D(*self.up_vector)
983
+ view_position = pv.Point3D(*self.position)
984
+ view_direction = pv.Vector3D(*self.direction)
985
+ view_plane = Plane(n=view_up_vector, o=view_position, x=view_direction)
986
+ position = origin if origin is not None else view_position
987
+
988
+ rotated_plane = view_plane.rotate_xy(math.radians(angle), position)
989
+ self._apply_plane_properties(rotated_plane, view_direction, view_up_vector)
990
+
991
+ def reflect(self, plane):
992
+ """Reflect this view across a plane.
993
+
994
+ Args:
995
+ plane: A ladybug_geometry Plane across which the object will
996
+ be reflected.
997
+ """
998
+ view_up_vector = pv.Vector3D(*self.up_vector)
999
+ view_position = pv.Point3D(*self.position)
1000
+ view_direction = pv.Vector3D(*self.direction)
1001
+ view_plane = Plane(n=view_up_vector, o=view_position, x=view_direction)
1002
+
1003
+ ref_plane = view_plane.reflect(plane.n, plane.o)
1004
+ self._apply_plane_properties(ref_plane, view_direction, view_up_vector)
1005
+
1006
+ def scale(self, factor, origin=None):
1007
+ """Scale this view by a factor from an origin point.
1008
+
1009
+ Args:
1010
+ factor: A number representing how much the object should be scaled.
1011
+ origin: A ladybug_geometry Point3D representing the origin from which
1012
+ to scale. If None, it will be scaled from the World origin (0, 0, 0).
1013
+ """
1014
+ view_position = pv.Point3D(*self.position)
1015
+ self.position = view_position.scale(factor, origin)
1016
+ self.direction = pv.Vector3D(*self.direction) * factor
1017
+ self.up_vector = pv.Vector3D(*self.up_vector) * factor
1018
+
1019
+ def _apply_plane_properties(self, plane, view_direction, view_up_vector):
1020
+ """Re-set the position, direction and up_vector from a Plane.
1021
+
1022
+ This method also ensures that the magnitude of the vectors is unchanged
1023
+ (since all Plane objects will have unitized vectors).
1024
+ """
1025
+ self.position = plane.o
1026
+ self.direction = plane.x * view_direction.magnitude
1027
+ self.up_vector = plane.n * view_up_vector.magnitude
1028
+
1029
+ @staticmethod
1030
+ def _parse_radiance_options(string):
1031
+ """Parse a radiance option string (e.g. '-ab 4 -ad 256').
1032
+
1033
+ The string should start with a '-' otherwise it will be trimmed to the
1034
+ first '-' in the string.
1035
+ """
1036
+ try:
1037
+ index = string.index('-')
1038
+ except ValueError:
1039
+ if not ' '.join(string.split()).replace('"', '').replace("'", '').strip():
1040
+ return {}
1041
+ raise ValueError(
1042
+ 'Invalid Radiance options string input. '
1043
+ 'Failed to find - in input string.'
1044
+ )
1045
+
1046
+ _rad_opt_pattern = r'-[a-zA-Z]+'
1047
+ _rad_opt_compiled_pattern = re.compile(_rad_opt_pattern)
1048
+ sub_string = ' '.join(string[index:].split())
1049
+ value = re.split(_rad_opt_compiled_pattern, sub_string)[1:]
1050
+ key = re.findall(_rad_opt_pattern, sub_string)
1051
+
1052
+ options = collections.OrderedDict()
1053
+ for k, v in zip(key, value):
1054
+ values = v.split()
1055
+ count = len(values)
1056
+ if count == 0:
1057
+ values = ''
1058
+ elif count == 1:
1059
+ values = values[0]
1060
+ options[k[1:]] = values
1061
+
1062
+ return options
1063
+
1064
+ def duplicate(self):
1065
+ """Get a copy of this object."""
1066
+ return self.__copy__()
1067
+
1068
+ def __copy__(self):
1069
+ new_obj = View(
1070
+ self.identifier, position=self.position, direction=self.direction,
1071
+ up_vector=self.up_vector, type=self.type, h_size=self.h_size,
1072
+ v_size=self.v_size, shift=self.shift, lift=self.lift)
1073
+ new_obj._display_name = self._display_name
1074
+ new_obj._room_identifier = self._room_identifier
1075
+ new_obj._light_path = self._light_path
1076
+ return new_obj
1077
+
1078
+ def ToString(self):
1079
+ """Overwrite .NET ToString."""
1080
+ return self.__repr__()
1081
+
1082
+ def __key(self):
1083
+ """A tuple based on the object properties, useful for hashing."""
1084
+ return (self.identifier, hash(self.position), hash(self.direction),
1085
+ hash(self.up_vector), self.type, self.h_size,
1086
+ self.v_size, self.shift, self.lift, self._display_name,
1087
+ self._room_identifier)
1088
+
1089
+ def __hash__(self):
1090
+ return hash(self.__key())
1091
+
1092
+ def __eq__(self, other):
1093
+ return isinstance(other, View) and self.__key() == other.__key() and \
1094
+ self.light_path == other.light_path
1095
+
1096
+ def __ne__(self, other):
1097
+ return not self.__eq__(other)
1098
+
1099
+ def __repr__(self):
1100
+ """View representation."""
1101
+ return self.to_radiance()