Source code for honeybee.plus
"""Collection of methods for Honeybee geometry operations in Grasshopper."""
from collections import namedtuple
try:
    import Rhino as rc
except ImportError as e:
    pass
# ------------------------- Required functions -----------------------------------------
#  In order to create a plus library you must overwrite these three methods.
#  The structure of arguments and returns should stay the same.
# --------------------------------------------------------------------------------------
# TODO(someone!): Implement triangulate
[docs]def extract_geometry_points(geometries, meshing_parameters=None):
    """Calculate list of points for a Grasshopper geometry.
    For planar surfaces the length of the list will be only 1. For non-planar
    surfaces or surfaces with internal edges it will be a number of lists.
    Args:
        geometries: List of meshes or Breps
        meshing_parameters: Optional Rhino meshing_parameters. This will only be used if
            the surface is non-planar or has an internal edge and needs to be meshed.
            Default:
                Rhino.Geometry.meshing_parameters.Coarse; SimplePlanes = True for planar
                surfaces; Rhino.Geometry.meshing_parameters.Smooth for non-planar
                surfaces
    Returns:
        A Collection of (geometry, points) in which each geometry is coupled by points.
        For planar surfaces the length of the points list will be only 1. For
        non-planar surfaces, meshes or surfaces with internal edges it will be multiple
        lists.
    """
    if not hasattr(geometries, '__iter__'):
        geometries = (geometries,)
    for geometry in geometries:
        if isinstance(geometry, rc.Geometry.Mesh):
            yield extract_mesh_points((geometry,))
        elif isinstance(geometry, rc.Geometry.Brep):
            yield extract_brep_points(geometry, meshing_parameters)
        else:
            raise TypeError(
                'Input surface should be a Mesh or a Brep not {}.'.format(type(geometry))
            )
[docs]def xyz_to_geometrical_points(xyz_points):
    """convert a sequence of (x, y, z) values to Grasshopper points.
    Input should be list of lists of points.
    """
    for xyz_list in xyz_points:
        for xyz in xyz_list:
            yield rc.Geometry.Point3d(xyz[0], xyz[1], xyz[2])
[docs]def polygon(point_list):
    """Return a polygon from points."""
    return rc.Geometry.Polyline(point_list).ToNurbsCurve()
# ------------------------- End of honeybee[+] methods -----------------------------
# ------------------------------ Utilities -----------------------------------------
[docs]def is_planar(geometry, tol=1e-3):
    """Check if a surface in planar."""
    return geometry.Faces[0].IsPlanar(tol)
[docs]def extract_brep_points(brep, meshing_parameters=None, tol=1e-3):
    """Extract points from Brep."""
    meshing_parameters = meshing_parameters or rc.Geometry.MeshingParameters.Coarse
    for fid in xrange(brep.Faces.Count):
        geometry = brep.Faces[fid].DuplicateFace(False)
        if not brep.Faces[fid].IsPlanar(tol):
            meshes = rc.Geometry.Mesh.CreateFromBrep(geometry, meshing_parameters)
            yield next(extract_mesh_points(meshes))
        else:
            # planar surface
            pts = geometry.DuplicateVertices()
            # sort points anti clockwise
            # this is only important for energy simulation and won't make a difference
            # for Radiance
            # To sort the points we find border of the surface and evaluate points
            # on border and use the parameter value to sort them
            border = rc.Geometry.Curve.JoinCurves(geometry.DuplicateEdgeCurves(True))
            if len(border) > 1:
                # mesh the surface
                meshing_parameters.SimplePlanes = True
                meshes = rc.Geometry.Mesh.CreateFromBrep(geometry, meshing_parameters)
                yield next(extract_mesh_points(meshes))
            else:
                # In some strange cases Rhino returns a single point for the surface
                if len(pts) == 1:
                    pts = (p.Location for p in border[0].Points)
                points_sorted = sorted(pts, key=lambda pt: border[0].ClosestPoint(pt)[1])
                # make sure points are anti clockwise
                if not is_points_sorted_anticlockwise(
                        points_sorted,
                        get_surface_center_pt_and_normal(geometry).normal_vector):
                    points_sorted.reverse()
                # return sorted points
                # Wrap in a list as Honeybee accepts list of list of points
                yield geometry, (points_sorted,)
[docs]def extract_mesh_points(meshes):
    """Extract points from a mesh."""
    for mesh in meshes:
        yield mesh, tuple(
            tuple(mesh.Vertices[face[i]] for i in range(4))
            if face.IsQuad else
            tuple(mesh.Vertices[face[i]] for i in range(3))
            for face in mesh.Faces
        )
[docs]def vectors_cross_product(vector1, vector2):
    """Calculate cross product of two vectors."""
    return vector1.X * vector2.X + \
        vector1.Y * vector2.Y + vector1.Z * vector2.Z
[docs]def is_points_sorted_anticlockwise(sorted_points, normal):
    """Check if an ordered list of points are anti-clockwise."""
    vector0 = rc.Geometry.Vector3d(sorted_points[1] - sorted_points[0])
    vector1 = rc.Geometry.Vector3d(sorted_points[-1] - sorted_points[0])
    pts_normal = rc.Geometry.Vector3d.CrossProduct(vector0, vector1)
    # in case points are anti-clockwise then normals should be parallel
    if vectors_cross_product(pts_normal, normal) > 0:
        return True
    else:
        return False
[docs]def get_surface_center_pt_and_normal(geometry):
    """Calculate center point and normal for a hb_surface.
    Args:
        hb_surface: A Honeybee surface
    Returns:
        Returns a tuple as (center_pt, normal_vector)
    """
    brep_face = geometry.Faces[0]
    if brep_face.IsPlanar and brep_face.IsSurface:
        u_domain = brep_face.Domain(0)
        v_domain = brep_face.Domain(1)
        center_u = (u_domain.Min + u_domain.Max) / 2
        center_v = (v_domain.Min + v_domain.Max) / 2
        center_pt = brep_face.PointAt(center_u, center_v)
        normal_vector = brep_face.NormalAt(center_u, center_v)
    else:
        centroid = rc.Geometry.AreaMassProperties.Compute(brep_face).Centroid
        uv = brep_face.ClosestPoint(centroid)
        center_pt = brep_face.PointAt(uv[1], uv[2])
        normal_vector = brep_face.NormalAt(uv[1], uv[2])
    SurfaceData = namedtuple('SurfaceData', 'center_pt normal_vector')
    return SurfaceData(center_pt, normal_vector)
[docs]def check_planarity(hb_surface, tolerance=1e-3):
    """Check planarity of a hb_surface.
    Args:
        hb_surface: A Honeybee surface
        tolerance: A float number as tolerance (Default: 1e-3)
    Returns:
        True is the surface is planar, otherwise return False.
    """
    try:
        return hb_surface.geometry.Faces[0].is_planar(tolerance)
    except AttributeError as e:
        raise TypeError("Input is not a hb_surface: %s" % str(e))
[docs]def check_for_internal_edge(hb_surface):
    """Check if the surface has an internal edge.
    For surfaces with internal edge surfaces needs to be meshed to extract the points.
    Args:
        hb_surface: A Honeybee surface
    Returns:
        True is the surface has an internal edge, otherwise return False.
    """
    # I believe there should be a direct method in RhinoCommon to indicate if a
    # surface is an open brep but since I couldn't find it I'm using this method
    # if Surface has no intenal edges the length of joined border is 1
    try:
        edges = hb_surface.geometry.DuplicateEdgeCurves(True)
    except AttributeError as e:
        raise TypeError("Input is not a hb_surface: %s" % str(e))
    else:
        border = rc.Geometry.Curve.JoinCurves(edges)
        if len(border) > 1:
            return True
        else:
            return False
