"""Functions to translate entire Ladybug core objects to Rhino geometries.
The methods here are intended to help translate groups of geometry that are commonly
generated by several objects in Ladybug core (ie. legends, compasses, etc.)
"""
import math
import copy
from .config import current_tolerance
from .fromgeometry import from_point2d, from_vector2d, from_ray2d, from_linesegment2d, \
from_arc2d, from_polyline2d, from_polygon2d, from_mesh2d, \
from_point3d, from_vector3d, from_ray3d, from_linesegment3d, from_arc3d, \
from_plane, from_polyline3d, from_mesh3d, from_face3d, from_polyface3d, \
from_sphere, from_cone, from_cylinder
from .text import text_objects
try:
from ladybug_geometry.geometry2d import Vector2D, Point2D, Ray2D, LineSegment2D, \
Arc2D, Polyline2D, Polygon2D, Mesh2D
from ladybug_geometry.geometry3d import Vector3D, Point3D, Ray3D, LineSegment3D, \
Arc3D, Polyline3D, Plane, Mesh3D, Face3D, Polyface3D, Sphere, Cone, Cylinder
except ImportError as e:
raise ImportError("Failed to import ladybug_geometry.\n{}".format(e))
try:
import Rhino.Geometry as rg
except ImportError as e:
raise ImportError("Failed to import Rhino document attributes.\n{}".format(e))
[docs]
def legend_objects(legend):
"""Translate a Ladybug Legend object into Grasshopper geometry.
Args:
legend: A Ladybug Legend object to be converted to Rhino geometry.
Returns:
A list of Rhino geometries in the following order.
- legend_mesh -- A colored mesh for the legend.
- legend_title -- A bake-able text object for the legend title.
- legend_text -- Bake-able text objects for the rest of the legend text.
"""
_height = legend.legend_parameters.text_height
_font = legend.legend_parameters.font
legend_mesh = from_mesh3d(legend.segment_mesh)
legend_title = text_objects(legend.title, legend.title_location, _height, _font)
if legend.legend_parameters.continuous_legend is False:
legend_text = [text_objects(txt, loc, _height, _font, 0, 5) for txt, loc in
zip(legend.segment_text, legend.segment_text_location)]
elif legend.legend_parameters.vertical is True:
legend_text = [text_objects(txt, loc, _height, _font, 0, 3) for txt, loc in
zip(legend.segment_text, legend.segment_text_location)]
else:
legend_text = [text_objects(txt, loc, _height, _font, 1, 5) for txt, loc in
zip(legend.segment_text, legend.segment_text_location)]
return [legend_mesh] + [legend_title] + legend_text
[docs]
def compass_objects(compass, z=0, custom_angles=None, projection=None, font='Arial'):
"""Translate a Ladybug Compass object into Grasshopper geometry.
Args:
compass: A Ladybug Compass object to be converted to Rhino geometry.
z: A number for the Z-coordinate to be used in translation. (Default: 0)
custom_angles: An array of numbers between 0 and 360 to be used to
generate custom angle labels around the compass.
projection: Text for the name of the projection to use from the sky
dome hemisphere to the 2D plane. If None, no altitude circles o
labels will be drawn (Default: None). Choose from the following:
* Orthographic
* Stereographic
font: Optional text for the font to be used in creating the text.
(Default: 'Arial')
Returns:
A list of Rhino geometries in the following order.
- all_boundary_circles -- Three Circle objects for the compass boundary.
- major_azimuth_ticks -- Line objects for the major azimuth labels.
- major_azimuth_text -- Bake-able text objects for the major azimuth labels.
- minor_azimuth_ticks -- Line objects for the minor azimuth labels
(if applicable).
- minor_azimuth_text -- Bake-able text objects for the minor azimuth
labels (if applicable).
- altitude_circles -- Circle objects for the altitude labels.
- altitude_text -- Bake-able text objects for the altitude labels.
"""
# set default variables based on the compass properties
maj_txt = compass.radius / 20
min_txt = maj_txt / 2
xaxis = Vector3D(1, 0, 0).rotate_xy(math.radians(compass.north_angle))
result = [] # list to hold all of the returned objects
for circle in compass.all_boundary_circles:
result.append(from_arc2d(circle, z))
# generate the labels and tick marks for the azimuths
if custom_angles is None:
for line in compass.major_azimuth_ticks:
result.append(from_linesegment2d(line, z))
for txt, pt in zip(compass.MAJOR_TEXT, compass.major_azimuth_points):
txt_pln = Plane(o=Point3D(pt.x, pt.y, z), x=xaxis)
result.append(text_objects(txt, txt_pln, maj_txt, font, 1, 3))
for line in compass.minor_azimuth_ticks:
result.append(from_linesegment2d(line, z))
for txt, pt in zip(compass.MINOR_TEXT, compass.minor_azimuth_points):
txt_pln = Plane(o=Point3D(pt.x, pt.y, z), x=xaxis)
result.append(text_objects(txt, txt_pln, min_txt, font, 1, 3))
else:
for line in compass.ticks_from_angles(custom_angles):
result.append(from_linesegment2d(line, z))
for txt, pt in zip(
custom_angles, compass.label_points_from_angles(custom_angles)):
txt_pln = Plane(o=Point3D(pt.x, pt.y, z), x=xaxis)
result.append(text_objects(str(txt), txt_pln, maj_txt, font, 1, 3))
# generate the labels and tick marks for the altitudes
if projection is not None:
if projection.title() == 'Orthographic':
for circle in compass.orthographic_altitude_circles:
result.append(from_arc2d(circle, z))
for txt, pt in zip(compass.ALTITUDES, compass.orthographic_altitude_points):
txt_pln = Plane(o=Point3D(pt.x, pt.y, z), x=xaxis)
result.append(text_objects(str(txt), txt_pln, min_txt, font, 1, 0))
elif projection.title() == 'Stereographic':
for circle in compass.stereographic_altitude_circles:
result.append(from_arc2d(circle, z))
for txt, pt in zip(compass.ALTITUDES, compass.stereographic_altitude_points):
txt_pln = Plane(o=Point3D(pt.x, pt.y, z), x=xaxis)
result.append(text_objects(str(txt), txt_pln, min_txt, font, 1, 0))
return result
[docs]
def from_geometry(geometry):
"""Generic geometry translation function that works for any ladybug-geometry object.
This is only recommended for cases where an input geometry stream can contain
a variety of different objects. When the geometry type is know, it can be
significantly faster to use the dedicated geometry translator.
Args:
geometry: Any 2D or 3D ladybug-geometry object.
"""
if isinstance(geometry, Point3D):
return from_point3d(geometry)
elif isinstance(geometry, Vector3D):
return from_vector3d(geometry)
elif isinstance(geometry, Ray3D):
return from_ray3d(geometry)
elif isinstance(geometry, LineSegment3D):
return from_linesegment3d(geometry)
elif isinstance(geometry, Plane):
return from_plane(geometry)
elif isinstance(geometry, Arc3D):
return from_arc3d(geometry)
elif isinstance(geometry, Polyline3D):
return from_polyline3d(geometry)
elif isinstance(geometry, Mesh3D):
return from_mesh3d(geometry)
elif isinstance(geometry, Face3D):
return from_face3d(geometry)
elif isinstance(geometry, Polyface3D):
return from_polyface3d(geometry)
elif isinstance(geometry, Sphere):
return from_sphere(geometry)
elif isinstance(geometry, Cone):
return from_cone(geometry)
elif isinstance(geometry, Cylinder):
return from_cylinder(geometry)
elif isinstance(geometry, Point2D):
return from_point2d(geometry)
elif isinstance(geometry, Vector2D):
return from_vector2d(geometry)
elif isinstance(geometry, Ray2D):
return from_ray2d(geometry)
elif isinstance(geometry, LineSegment2D):
return from_linesegment2d(geometry)
elif isinstance(geometry, Arc2D):
return from_arc2d(geometry)
elif isinstance(geometry, Polygon2D):
return from_polygon2d(geometry)
elif isinstance(geometry, Polyline2D):
return from_polyline2d(geometry)
elif isinstance(geometry, Mesh2D):
return from_mesh2d(geometry)
[docs]
def luminaire_objects(luminaire, scale=1):
"""Translate a Honeybee Luminaire object into Rhino geometry.
Args:
luminaire: A Honeybee Luminaire object to be converted to Rhino geometry.
scale: A number for the scale factor to be used in translation. (Default: 1)
Returns:
A list of Rhino geometries in the following order.
- lum_web -- Rhino geometry representing the candela distribution.
- lum_poly -- Rhino geometry representing the luminaire opening.
- lum_axes -- Line objects for the C0 and G0 axes of the luminaire.
"""
luminaire.parse_photometric_data()
luminaire_web = luminaire_web_to_rhino_breps(luminaire, normalize=True)
lum_web = place_luminaire_from_object(luminaire, luminaire_web, scale)
luminaire_poly = create_luminaire_brep(luminaire)
lum_poly = place_luminaire_from_object(luminaire, luminaire_poly, scale)
luminaire_axes = create_luminaire_axes(luminaire)
lum_axes = place_luminaire_from_object(luminaire, luminaire_axes, scale)
return lum_web, lum_poly, lum_axes
[docs]
def luminaire_web_to_rhino_breps(luminaire, normalize=True):
"""Generate geometric representation of the candela distribution of a luminaire.
Args:
luminaire: A Honeybee Luminaire.
normalize: Set to True to Normalize candela values.
Returns:
List[Rhino.Geometry.Brep]
"""
# Ensure photometry is parsed
luminaire._ensure_parsed()
# Expand symmetry
horz_deg, candelas = luminaire._expand_horizontal_angles(
luminaire.horizontal_angles,
luminaire.candela_values
)
vert_deg = luminaire.vertical_angles
# Convert to radians
horz = [math.radians(h) for h in horz_deg]
vert = [math.radians(v) for v in vert_deg]
# Normalize candela
if normalize:
max_cd = luminaire.max_candela or 1.0
candelas = [
[v / max_cd for v in row]
for row in candelas
]
# Scale = max luminous dimension
mul3d = max(abs(luminaire.width_m), abs(luminaire.length_m))
# Create vertical-angle curves
curves = []
for h_idx, h_ang in enumerate(horz):
pts = []
for v_idx, v_ang in enumerate(vert):
cd = mul3d * candelas[h_idx][v_idx]
x = cd * math.sin(v_ang) * math.cos(h_ang)
y = cd * math.sin(v_ang) * math.sin(h_ang)
z = -cd * math.cos(v_ang)
pts.append(rg.Point3d(x, y, z))
curves.append(rg.PolyCurve.CreateControlPointCurve(pts))
# Create edge surfaces between curves
breps = []
for i in range(len(curves) - 1):
b = rg.Brep.CreateEdgeSurface([curves[i], curves[i + 1]])
if b:
breps.append(b)
return breps
[docs]
def create_luminaire_brep(luminaire):
"""Create geometric representation of the luminous opening of a Luminaire.
Args:
luminaire: A Honeybee Luminaire.
Returns:
List[Rhino.Geometry.Brep]
"""
w = luminaire.width_m
l = luminaire.length_m
h = luminaire.height_m
plane = rg.Plane.WorldXY
origin = rg.Point3d.Origin
# Implies that the luminous opening is a point
if round(w, 2) == 0 and round(l, 2) == 0 and round(h, 2) == 0:
return []
# Implies that luminous opening is rectangular
elif w > 0 and l > 0 and round(h, 2) == 0:
corner_a = rg.Point3d(-l/2, -w/2, 0)
corner_b = rg.Point3d(l/2, w/2, 0)
lum_rect = rg.Rectangle3d(plane, corner_a, corner_b).ToNurbsCurve()
lum_poly = rg.Brep.CreatePlanarBreps([lum_rect])[0]
# Implies that luminous opening is rectangular with luminous sides
elif w > 0 and l > 0 and h> 0:
x_interval = rg.Interval(-l/2, l/2)
y_interval = rg.Interval(-w/2, w/2)
z_interval = rg.Interval(-h/2, h/2)
lum_poly = rg.Box(plane, x_interval, y_interval, z_interval)
# Implies that the luminous opening is a circle
elif w < 0 and l < 0 and round(l, 2) == round(w, 2) and round(h, 2) == 0:
lum_circ = rg.Circle(plane ,origin, abs(-w/2)).ToNurbsCurve()
lum_poly = rg.Brep.CreatePlanarBreps([lum_circ])[0]
elif w < 0 and round(l, 2) == 0 and round(h, 2) == 0:
lum_circ = rg.Circle(plane, origin, abs(-w/2)).ToNurbsCurve()
lum_poly = rg.Brep.CreatePlanarBreps([lum_circ])[0]
# Implies that the luminous opening is an ellipse
elif w < 0 and l < 0 and round(l, 2) != round(w, 2) and round(h, 2) == 0:
lum_ellip = rg.Ellipse(plane, abs(-w/2), abs(-l/2)).ToNurbsCurve()
lum_poly = rg.Brep.CreatePlanarBreps([lum_ellip])[0]
# Implies the luminous opening is a vertical cylinder
elif w < 0 and l < 0 and h > 0 and round(l, 2) == round(w, 2):
lum_circ = rg.Circle(plane, origin, abs(-w/2))
lum_poly = rg.Cylinder(lum_circ, h).ToBrep(True, True)
# Implies the luminous opening is a vertical elliptcal cylinder
elif w < 0 and l < 0 and h > 0 and round(l, 2) != round(w, 2):
lum_circ = rg.Circle(plane, origin, 1)
lum_poly = rg.Cylinder(lum_circ, 1).ToNurbsSurface()
transf = rg.Transform.Scale(plane, abs(w/2), abs(l/2), abs(h))
lum_poly.Transform(transf)
lum_poly = lum_poly.ToBrep().CapPlanarHoles(current_tolerance())
elif w < 0 and l < 0 and h < 0 and round(l, 2) == round(w, 2) and round(w, 2) == round(h, 2):
lum_poly = rg.Sphere(rg.Point3d(0, 0, abs(w/2)), abs(w/2))
# Implies the luminous opening is an ellipsoid
elif w < 0 and l < 0 and h < 0:
lum_poly = rg.Sphere(rg.Point3d(0, 0, abs(w/2)), 1).ToNurbsSurface()
transf = rg.Transform.Scale(
rg.Plane(rg.Point3d(0, 0, abs(w/2)), rg.Vector3d.ZAxis), abs(w/2), abs(l/2), abs(h/2))
lum_poly.Transform(transf)
# Implies the luminous opening is a horizontal cylinder
elif w < 0 and l > 0 and h < 0 and round(w, 2) == round(h, 2):
lum_circ = rg.Circle(rg.Plane.WorldYZ,rg.Point3d((-l/2), 0, abs(-w/2)), abs(-w/2))
lum_poly = rg.Cylinder(lum_circ, l).ToBrep(True, True)
# Implies the luminous opening is a horizontal elliptical cylinder
elif w < 0 and l > 0 and h < 0 and round(w, 2) != round(h, 2):
cent_pt = rg.Point3d((h/ 2), 0, abs(h/ 2))
lum_circ = rg.Circle(rg.Plane.WorldYZ, cent_pt, 1)
lum_poly = rg.Cylinder(lum_circ, 1).ToNurbsSurface()
transf = rg.Transform.Scale(rg.Plane(cent_pt,rg.Vector3d.ZAxis), abs(l), abs(w/2), abs(h/2))
lum_poly.Transform(transf)
lum_poly = lum_poly.ToBrep().CapPlanarHoles(current_tolerance())
# Implies the luminous opening is a horizontal cylinder
elif w > 0 and l < 0 and h < 0 and round(l, 2) == round(h, 2):
lum_circ = rg.Circle(rg.Plane.WorldZX, rg.Point3d(0, (-w/2), abs(-l/2)), abs(-l/2))
lum_poly = rg.Cylinder(lum_circ, w).ToBrep(True, True)
# Implies the luminous opening is a horizontal elliptical cylinder
elif w > 0 and l < 0 and h < 0 and round(l, 2) != round(h, 2):
cent_pt = rg.Point3d(0, (-w/2), abs(h/2))
lum_circ = rg.Circle(rg.Plane.WorldZX, cent_pt, 1)
lum_poly = rg.Cylinder(lum_circ, 1).ToNurbsSurface()
transf = rg.Transform.Scale(
rg.Plane(cent_pt, rg.Vector3d.ZAxis), abs(l/2), abs(w), abs(h/2))
lum_poly.Transform(transf)
lum_poly = lum_poly.ToBrep().CapPlanarHoles(current_tolerance())
# Implies the luminous opening is a vertical circle
elif w < 0 and round(l) == 0 and h < 0 and round(w, 2) == round(h, 2):
lum_circ = rg.Circle(rg.Plane.WorldYZ, origin, abs(w/2)).ToNurbsCurve()
lum_poly = rg.Brep.CreatePlanarBreps([lum_circ])[0]
# Implies the luminous opening is a vertical ellipse
elif w < 0 and round(l) == 0 and h < 0 and round(w, 2) != round(h, 2):
lum_ellip = rg.Ellipse(rg.Plane.WorldYZ, abs(w/2), abs(h/2)).ToNurbsCurve()
lum_poly = rg.Brep.CreatePlanarBreps([lum_circ])[0]
return lum_poly
[docs]
def create_luminaire_axes(luminaire):
"""Draw the C0-G0 axes for a Luminaire according to IES LM-63.
Args:
luminaire: A Honeybee Luminaire.
Returns:
[C0 axis, G0 axis]
"""
# Ensure photometry is parsed
luminaire.parse_photometric_data()
# Dimensions
width = luminaire.width_m
length = luminaire.length_m
# IES rule: circular luminaires
# width < 0, length == 0: use width magnitude
if abs(length) < 1e-6 and width < 0:
length = abs(width)
# Fallback if dimensions are zero
if abs(length) < 1e-6:
length = 0.5 # default
origin = rg.Point3d(0, 0, 0)
# C0 axis
c0_axis = rg.Line(
origin,
rg.Point3d(1.2 * length / 2.0, 0, 0)
)
# G0 axis
g0_axis = rg.Line(
origin,
rg.Point3d(0, 0, -2.0 * length / 2.0)
)
return [c0_axis, g0_axis]
[docs]
def place_luminaire_from_object(luminaire, luminaire_web, scale):
"""Take a Luminaire object, place its geometry at all points in its LuminaireZone,
applying spin, tilt, and rotation.
Args:
luminaire: A Honeybee Luminaire.
luminaire_web: Geometric representation of the candela distribution of
of the luminaire.
scale: Scalar value to scale the geometry.
Returns:
List[Rhino.Geometry.Brep]
"""
if luminaire.luminaire_zone is None:
return [luminaire_web]
luminaire_zone = luminaire.luminaire_zone
geometry = []
for instance in luminaire_zone.instances:
geo = transform_geometry(
luminaire_web,
spin=instance.spin,
tilt=instance.tilt,
rotation=instance.rotation,
translation=instance.point,
scale=scale
)
geometry.append(geo)
return geometry