Source code for ladybug_rhino.fromobjects

"""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
[docs] def transform_geometry(geometry, spin=0, tilt=0, rotation=0, translation=None, scale=1.0): """Transform a geometry or list of geometries based on spin, tilt, rotation, translation, scale. Args: geometry: Rhino geometry to transform. spin: Number for the spin (Z-axis) in degrees. tilt: Number for the tilt (Y-axis) in degrees. rotation: Number for the rotation (Z-axis) in degrees. translation: A Ladybug Vector3D or Point3D, or a tuple of three numbers for the translation in the X, Y, and Z directions. If None, no translation is applied. scale: Number for the scale factor. Returns: Transformed geometry or list of geometries. """ if not isinstance(geometry, list): geometry = [geometry] geometry = [copy.deepcopy(g) for g in geometry] norm_vec = rg.Vector3d(0, 0, 1) plane = rg.Plane(rg.Point3d(0, 0, 0), norm_vec) origin = rg.Point3d(0, 0, 0) # Scale if scale != 1.0: s = rg.Transform.Scale(plane, scale, scale, scale) for g in geometry: g.Transform(s) # Spin (Z-axis) if spin != 0: t = rg.Transform.Rotation(math.radians(spin), rg.Vector3d.ZAxis, origin) for g in geometry: g.Transform(t) # Tilt (Y-axis) if tilt != 0: t = rg.Transform.Rotation(math.radians(tilt), rg.Vector3d.YAxis, origin) for g in geometry: g.Transform(t) # Rotate (Z-axis) if rotation != 0: t = rg.Transform.Rotation(math.radians(rotation), rg.Vector3d.ZAxis, origin) for g in geometry: g.Transform(t) # Translate x, y, z = translation t = rg.Transform.Translation(x, y, z) for g in geometry: g.Transform(t) return geometry if len(geometry) > 1 else geometry[0]